diff --git a/.claude-plugin/marketplace.json b/.claude-plugin/marketplace.json new file mode 100644 index 0000000..7799283 --- /dev/null +++ b/.claude-plugin/marketplace.json @@ -0,0 +1,12 @@ +{ + "name": "fiscalapi", + "description": "Plugins de FiscalAPI para agentes de código.", + "owner": { "name": "FiscalAPI" }, + "plugins": [ + { + "name": "fiscalapi", + "source": "./", + "description": "Skills en español para integrar FiscalAPI (CFDI 4.0) en tu ERP, POS o SaaS." + } + ] +} diff --git a/.claude-plugin/plugin.json b/.claude-plugin/plugin.json new file mode 100644 index 0000000..f5f1739 --- /dev/null +++ b/.claude-plugin/plugin.json @@ -0,0 +1,10 @@ +{ + "name": "fiscalapi", + "version": "0.1.0", + "description": "Skills en español para integrar FiscalAPI (CFDI 4.0) en tu ERP, POS o SaaS: primer timbre en pruebas, complementos, ciclo de vida de la factura y diagnóstico de errores del SAT.", + "author": { "name": "FiscalAPI" }, + "homepage": "https://docs.fiscalapi.com", + "repository": "https://github.com/FiscalAPI/fiscalapi-ai", + "license": "MPL-2.0", + "keywords": ["fiscalapi", "cfdi", "sat", "facturacion-electronica", "mexico"] +} diff --git a/.cursor-plugin/plugin.json b/.cursor-plugin/plugin.json new file mode 100644 index 0000000..f5f1739 --- /dev/null +++ b/.cursor-plugin/plugin.json @@ -0,0 +1,10 @@ +{ + "name": "fiscalapi", + "version": "0.1.0", + "description": "Skills en español para integrar FiscalAPI (CFDI 4.0) en tu ERP, POS o SaaS: primer timbre en pruebas, complementos, ciclo de vida de la factura y diagnóstico de errores del SAT.", + "author": { "name": "FiscalAPI" }, + "homepage": "https://docs.fiscalapi.com", + "repository": "https://github.com/FiscalAPI/fiscalapi-ai", + "license": "MPL-2.0", + "keywords": ["fiscalapi", "cfdi", "sat", "facturacion-electronica", "mexico"] +} diff --git a/.github/workflows/check.yml b/.github/workflows/check.yml new file mode 100644 index 0000000..c8499ae --- /dev/null +++ b/.github/workflows/check.yml @@ -0,0 +1,33 @@ +name: Check + +# En cada PR y cada lunes: las docs cambian aunque este repo no, y un enlace roto deja a una skill sin su fuente. +on: + pull_request: + schedule: + - cron: '0 14 * * 1' + workflow_dispatch: + +permissions: + contents: read + +jobs: + check: + runs-on: ubuntu-latest + steps: + - name: Checkout + uses: actions/checkout@fbc6f3992d24b796d5a048ff273f7fcc4a7b6c09 # v5.1.0 + + - name: Setup Node + uses: actions/setup-node@949feb2413d6458794dcd2491c4babbbce0c15c1 # v7.1.0 + with: + node-version: 24 + + - name: Frontmatter y enlaces de las skills + run: node scripts/check-skills.mjs + + - name: Manifiestos de Claude Code, Codex y Copilot + run: npx -y @anthropic-ai/claude-code@2.1.293 plugin validate . --strict + + # Cursor no lee .claude-plugin: su manifiesto es una copia exacta del de Claude Code. + - name: Manifiesto de Cursor + run: cmp .claude-plugin/plugin.json .cursor-plugin/plugin.json diff --git a/LICENSE b/LICENSE new file mode 100644 index 0000000..a612ad9 --- /dev/null +++ b/LICENSE @@ -0,0 +1,373 @@ +Mozilla Public License Version 2.0 +================================== + +1. Definitions +-------------- + +1.1. "Contributor" + means each individual or legal entity that creates, contributes to + the creation of, or owns Covered Software. + +1.2. "Contributor Version" + means the combination of the Contributions of others (if any) used + by a Contributor and that particular Contributor's Contribution. + +1.3. "Contribution" + means Covered Software of a particular Contributor. + +1.4. "Covered Software" + means Source Code Form to which the initial Contributor has attached + the notice in Exhibit A, the Executable Form of such Source Code + Form, and Modifications of such Source Code Form, in each case + including portions thereof. + +1.5. "Incompatible With Secondary Licenses" + means + + (a) that the initial Contributor has attached the notice described + in Exhibit B to the Covered Software; or + + (b) that the Covered Software was made available under the terms of + version 1.1 or earlier of the License, but not also under the + terms of a Secondary License. + +1.6. "Executable Form" + means any form of the work other than Source Code Form. + +1.7. "Larger Work" + means a work that combines Covered Software with other material, in + a separate file or files, that is not Covered Software. + +1.8. "License" + means this document. + +1.9. "Licensable" + means having the right to grant, to the maximum extent possible, + whether at the time of the initial grant or subsequently, any and + all of the rights conveyed by this License. + +1.10. "Modifications" + means any of the following: + + (a) any file in Source Code Form that results from an addition to, + deletion from, or modification of the contents of Covered + Software; or + + (b) any new file in Source Code Form that contains any Covered + Software. + +1.11. "Patent Claims" of a Contributor + means any patent claim(s), including without limitation, method, + process, and apparatus claims, in any patent Licensable by such + Contributor that would be infringed, but for the grant of the + License, by the making, using, selling, offering for sale, having + made, import, or transfer of either its Contributions or its + Contributor Version. + +1.12. "Secondary License" + means either the GNU General Public License, Version 2.0, the GNU + Lesser General Public License, Version 2.1, the GNU Affero General + Public License, Version 3.0, or any later versions of those + licenses. + +1.13. "Source Code Form" + means the form of the work preferred for making modifications. + +1.14. "You" (or "Your") + means an individual or a legal entity exercising rights under this + License. For legal entities, "You" includes any entity that + controls, is controlled by, or is under common control with You. For + purposes of this definition, "control" means (a) the power, direct + or indirect, to cause the direction or management of such entity, + whether by contract or otherwise, or (b) ownership of more than + fifty percent (50%) of the outstanding shares or beneficial + ownership of such entity. + +2. License Grants and Conditions +-------------------------------- + +2.1. Grants + +Each Contributor hereby grants You a world-wide, royalty-free, +non-exclusive license: + +(a) under intellectual property rights (other than patent or trademark) + Licensable by such Contributor to use, reproduce, make available, + modify, display, perform, distribute, and otherwise exploit its + Contributions, either on an unmodified basis, with Modifications, or + as part of a Larger Work; and + +(b) under Patent Claims of such Contributor to make, use, sell, offer + for sale, have made, import, and otherwise transfer either its + Contributions or its Contributor Version. + +2.2. Effective Date + +The licenses granted in Section 2.1 with respect to any Contribution +become effective for each Contribution on the date the Contributor first +distributes such Contribution. + +2.3. Limitations on Grant Scope + +The licenses granted in this Section 2 are the only rights granted under +this License. No additional rights or licenses will be implied from the +distribution or licensing of Covered Software under this License. +Notwithstanding Section 2.1(b) above, no patent license is granted by a +Contributor: + +(a) for any code that a Contributor has removed from Covered Software; + or + +(b) for infringements caused by: (i) Your and any other third party's + modifications of Covered Software, or (ii) the combination of its + Contributions with other software (except as part of its Contributor + Version); or + +(c) under Patent Claims infringed by Covered Software in the absence of + its Contributions. + +This License does not grant any rights in the trademarks, service marks, +or logos of any Contributor (except as may be necessary to comply with +the notice requirements in Section 3.4). + +2.4. Subsequent Licenses + +No Contributor makes additional grants as a result of Your choice to +distribute the Covered Software under a subsequent version of this +License (see Section 10.2) or under the terms of a Secondary License (if +permitted under the terms of Section 3.3). + +2.5. Representation + +Each Contributor represents that the Contributor believes its +Contributions are its original creation(s) or it has sufficient rights +to grant the rights to its Contributions conveyed by this License. + +2.6. Fair Use + +This License is not intended to limit any rights You have under +applicable copyright doctrines of fair use, fair dealing, or other +equivalents. + +2.7. Conditions + +Sections 3.1, 3.2, 3.3, and 3.4 are conditions of the licenses granted +in Section 2.1. + +3. Responsibilities +------------------- + +3.1. Distribution of Source Form + +All distribution of Covered Software in Source Code Form, including any +Modifications that You create or to which You contribute, must be under +the terms of this License. You must inform recipients that the Source +Code Form of the Covered Software is governed by the terms of this +License, and how they can obtain a copy of this License. You may not +attempt to alter or restrict the recipients' rights in the Source Code +Form. + +3.2. Distribution of Executable Form + +If You distribute Covered Software in Executable Form then: + +(a) such Covered Software must also be made available in Source Code + Form, as described in Section 3.1, and You must inform recipients of + the Executable Form how they can obtain a copy of such Source Code + Form by reasonable means in a timely manner, at a charge no more + than the cost of distribution to the recipient; and + +(b) You may distribute such Executable Form under the terms of this + License, or sublicense it under different terms, provided that the + license for the Executable Form does not attempt to limit or alter + the recipients' rights in the Source Code Form under this License. + +3.3. Distribution of a Larger Work + +You may create and distribute a Larger Work under terms of Your choice, +provided that You also comply with the requirements of this License for +the Covered Software. If the Larger Work is a combination of Covered +Software with a work governed by one or more Secondary Licenses, and the +Covered Software is not Incompatible With Secondary Licenses, this +License permits You to additionally distribute such Covered Software +under the terms of such Secondary License(s), so that the recipient of +the Larger Work may, at their option, further distribute the Covered +Software under the terms of either this License or such Secondary +License(s). + +3.4. Notices + +You may not remove or alter the substance of any license notices +(including copyright notices, patent notices, disclaimers of warranty, +or limitations of liability) contained within the Source Code Form of +the Covered Software, except that You may alter any license notices to +the extent required to remedy known factual inaccuracies. + +3.5. Application of Additional Terms + +You may choose to offer, and to charge a fee for, warranty, support, +indemnity or liability obligations to one or more recipients of Covered +Software. However, You may do so only on Your own behalf, and not on +behalf of any Contributor. You must make it absolutely clear that any +such warranty, support, indemnity, or liability obligation is offered by +You alone, and You hereby agree to indemnify every Contributor for any +liability incurred by such Contributor as a result of warranty, support, +indemnity or liability terms You offer. You may include additional +disclaimers of warranty and limitations of liability specific to any +jurisdiction. + +4. Inability to Comply Due to Statute or Regulation +--------------------------------------------------- + +If it is impossible for You to comply with any of the terms of this +License with respect to some or all of the Covered Software due to +statute, judicial order, or regulation then You must: (a) comply with +the terms of this License to the maximum extent possible; and (b) +describe the limitations and the code they affect. Such description must +be placed in a text file included with all distributions of the Covered +Software under this License. Except to the extent prohibited by statute +or regulation, such description must be sufficiently detailed for a +recipient of ordinary skill to be able to understand it. + +5. Termination +-------------- + +5.1. The rights granted under this License will terminate automatically +if You fail to comply with any of its terms. However, if You become +compliant, then the rights granted under this License from a particular +Contributor are reinstated (a) provisionally, unless and until such +Contributor explicitly and finally terminates Your grants, and (b) on an +ongoing basis, if such Contributor fails to notify You of the +non-compliance by some reasonable means prior to 60 days after You have +come back into compliance. Moreover, Your grants from a particular +Contributor are reinstated on an ongoing basis if such Contributor +notifies You of the non-compliance by some reasonable means, this is the +first time You have received notice of non-compliance with this License +from such Contributor, and You become compliant prior to 30 days after +Your receipt of the notice. + +5.2. If You initiate litigation against any entity by asserting a patent +infringement claim (excluding declaratory judgment actions, +counter-claims, and cross-claims) alleging that a Contributor Version +directly or indirectly infringes any patent, then the rights granted to +You by any and all Contributors for the Covered Software under Section +2.1 of this License shall terminate. + +5.3. In the event of termination under Sections 5.1 or 5.2 above, all +end user license agreements (excluding distributors and resellers) which +have been validly granted by You or Your distributors under this License +prior to termination shall survive termination. + +************************************************************************ +* * +* 6. Disclaimer of Warranty * +* ------------------------- * +* * +* Covered Software is provided under this License on an "as is" * +* basis, without warranty of any kind, either expressed, implied, or * +* statutory, including, without limitation, warranties that the * +* Covered Software is free of defects, merchantable, fit for a * +* particular purpose or non-infringing. The entire risk as to the * +* quality and performance of the Covered Software is with You. * +* Should any Covered Software prove defective in any respect, You * +* (not any Contributor) assume the cost of any necessary servicing, * +* repair, or correction. This disclaimer of warranty constitutes an * +* essential part of this License. No use of any Covered Software is * +* authorized under this License except under this disclaimer. * +* * +************************************************************************ + +************************************************************************ +* * +* 7. Limitation of Liability * +* -------------------------- * +* * +* Under no circumstances and under no legal theory, whether tort * +* (including negligence), contract, or otherwise, shall any * +* Contributor, or anyone who distributes Covered Software as * +* permitted above, be liable to You for any direct, indirect, * +* special, incidental, or consequential damages of any character * +* including, without limitation, damages for lost profits, loss of * +* goodwill, work stoppage, computer failure or malfunction, or any * +* and all other commercial damages or losses, even if such party * +* shall have been informed of the possibility of such damages. This * +* limitation of liability shall not apply to liability for death or * +* personal injury resulting from such party's negligence to the * +* extent applicable law prohibits such limitation. Some * +* jurisdictions do not allow the exclusion or limitation of * +* incidental or consequential damages, so this exclusion and * +* limitation may not apply to You. * +* * +************************************************************************ + +8. Litigation +------------- + +Any litigation relating to this License may be brought only in the +courts of a jurisdiction where the defendant maintains its principal +place of business and such litigation shall be governed by laws of that +jurisdiction, without reference to its conflict-of-law provisions. +Nothing in this Section shall prevent a party's ability to bring +cross-claims or counter-claims. + +9. Miscellaneous +---------------- + +This License represents the complete agreement concerning the subject +matter hereof. If any provision of this License is held to be +unenforceable, such provision shall be reformed only to the extent +necessary to make it enforceable. Any law or regulation which provides +that the language of a contract shall be construed against the drafter +shall not be used to construe this License against a Contributor. + +10. Versions of the License +--------------------------- + +10.1. New Versions + +Mozilla Foundation is the license steward. Except as provided in Section +10.3, no one other than the license steward has the right to modify or +publish new versions of this License. Each version will be given a +distinguishing version number. + +10.2. Effect of New Versions + +You may distribute the Covered Software under the terms of the version +of the License under which You originally received the Covered Software, +or under the terms of any subsequent version published by the license +steward. + +10.3. Modified Versions + +If you create software not governed by this License, and you want to +create a new license for such software, you may create and use a +modified version of this License if you rename the license and remove +any references to the name of the license steward (except to note that +such modified license differs from this License). + +10.4. Distributing Source Code Form that is Incompatible With Secondary +Licenses + +If You choose to distribute Source Code Form that is Incompatible With +Secondary Licenses under the terms of this version of the License, the +notice described in Exhibit B of this License must be attached. + +Exhibit A - Source Code Form License Notice +------------------------------------------- + + This Source Code Form is subject to the terms of the Mozilla Public + License, v. 2.0. If a copy of the MPL was not distributed with this + file, You can obtain one at http://mozilla.org/MPL/2.0/. + +If it is not possible or desirable to put the notice in a particular +file, then You may include the notice in a location (such as a LICENSE +file in a relevant directory) where a recipient would be likely to look +for such a notice. + +You may add additional accurate notices of copyright ownership. + +Exhibit B - "Incompatible With Secondary Licenses" Notice +--------------------------------------------------------- + + This Source Code Form is "Incompatible With Secondary Licenses", as + defined by the Mozilla Public License, v. 2.0. diff --git a/README.md b/README.md index 246ea17..8b87812 100644 --- a/README.md +++ b/README.md @@ -1 +1,57 @@ # fiscalapi-ai + +Skills en español para integrar [FiscalAPI](https://docs.fiscalapi.com) (CFDI 4.0) en tu ERP, POS o SaaS desde tu agente de código: Claude Code, Codex, Cursor o GitHub Copilot. + +Las skills no copian la documentación: te llevan a la sección exacta de [docs.fiscalapi.com](https://docs.fiscalapi.com) y te ayudan a decidir qué endpoint, complemento o catálogo usar. + +## Instalación + +### Claude Code + +``` +/plugin install fiscalapi --marketplace FiscalAPI/fiscalapi-ai +``` + +O desde la terminal: + +``` +claude plugin marketplace add FiscalAPI/fiscalapi-ai +claude plugin install fiscalapi@fiscalapi +``` + +### Codex + +``` +codex plugin marketplace add FiscalAPI/fiscalapi-ai +codex plugin add fiscalapi@fiscalapi +``` + +### GitHub Copilot + +En Copilot CLI: + +``` +copilot plugin marketplace add FiscalAPI/fiscalapi-ai +copilot plugin install fiscalapi@fiscalapi +``` + +En VS Code, agrega el marketplace a tu configuración: + +```json +{ + "chat.plugins.enabled": true, + "chat.plugins.marketplaces": ["FiscalAPI/fiscalapi-ai"] +} +``` + +### Cursor + +Clona el repo en la carpeta de plugins locales y recarga la ventana (**Developer: Reload Window**): + +``` +git clone https://github.com/FiscalAPI/fiscalapi-ai ~/.cursor/plugins/local/fiscalapi +``` + +## Licencia + +[MPL-2.0](LICENSE) diff --git a/commands/explicar-error.md b/commands/explicar-error.md new file mode 100644 index 0000000..bcfc7db --- /dev/null +++ b/commands/explicar-error.md @@ -0,0 +1,14 @@ +--- +description: Explica un error de la API de FiscalAPI (un código como CFDI40147 o la respuesta JSON completa) y propone la corrección. +argument-hint: +--- + +Explica este error de FiscalAPI siguiendo la skill `fiscalapi-diagnostico`: + +$ARGUMENTS + +1. Clasifícalo con el árbol de decisión de la skill: validación de FiscalAPI, rechazo del PAC o del SAT, error del servidor u otro 4xx. +2. Abre la página o sección enlazada que le corresponde y localiza la fila del código o la regla del campo. +3. Responde con el tipo de error, la causa según esa fila o sección (con su enlace) y el cambio concreto que hay que hacer. + +Si solo recibiste un código, sin respuesta, explícalo con su fila de la tabla y pide la respuesta completa solo si hace falta para proponer el cambio. diff --git a/commands/primer-timbre.md b/commands/primer-timbre.md new file mode 100644 index 0000000..a3dff5c --- /dev/null +++ b/commands/primer-timbre.md @@ -0,0 +1,17 @@ +--- +description: Timbra tu primera factura de ingreso en el ambiente de pruebas de FiscalAPI con el SDK de tu proyecto, de punta a punta, y devuelve su UUID. +argument-hint: "[lenguaje, si el proyecto todavía no tiene uno]" +--- + +Timbra una factura de ingreso de prueba en este proyecto siguiendo la skill `fiscalapi-integracion` (lee su sección de SDK y `references/sdks.md` antes de escribir código). + +$ARGUMENTS + +1. **Llaves.** Lee `FISCALAPI_API_KEY` y `FISCALAPI_TENANT` de las variables de entorno, y `FISCALAPI_API_URL` (si no existe, usa `https://test.fiscalapi.com`). Si falta la llave o el tenant, detente y explica cómo obtenerlas con el paso 1 de la skill. Nunca escribas las llaves en el código ni en un archivo, y nunca uses `https://live.fiscalapi.com`. +2. **Lenguaje y SDK.** Detecta el lenguaje con la tabla de la skill (o usa el que te indiquen arriba; si el proyecto está vacío y no te indican uno, pregunta cuál usar). Instala el SDK oficial con el comando de `references/sdks.md`. +3. **Certificados de prueba.** Descarga el zip de [Certificados de prueba](https://docs.fiscalapi.com/testing-data/certificados-de-prueba.md) en una carpeta que no se versione (agrégala al `.gitignore`). Del emisor `FUNK671228PH6`, toma el `.cer` y el `.key` de la subcarpeta `CSD_*` (no los de la FIEL) y conviértelos a base64 al ejecutar; la contraseña es `12345678a`. +4. **Factura.** Arma una factura de ingreso por valores con el cuerpo de ejemplo de [Crear factura ingreso](https://docs.fiscalapi.com/invoices-by-value/crear-factura-ingreso.md): emisor `FUNK671228PH6` (régimen 621), receptor `EKU9003173C9` (régimen 601, uso G01, CP 42501), un concepto con IVA 16 % que conserve todos los campos del concepto del ejemplo (por valores, `itemSku` es obligatorio), y la fecha actual como texto `yyyy-MM-ddTHH:mm:ss`. Usa los nombres del SDK, no los del README si `references/sdks.md` dice que el README falla. +5. **Timbra una sola vez.** Si `succeeded` es verdadero, imprime el `id` y el `uuid` de `data`. Si no, interpreta la respuesta con la skill `fiscalapi-diagnostico`, corrige y vuelve a intentar. Ante un 5xx o un timeout, no reintentes: sigue la regla de la skill. +6. **Comprueba.** Consulta la factura por su `id` con el SDK ([Obtener factura por ID](https://docs.fiscalapi.com/invoices-by-value/obtener-factura-por-id.md)) y confirma que trae el mismo `uuid`. + +Responde con el `uuid`, el `id`, los archivos que creaste y el comando para volver a correr el programa. diff --git a/commands/revisar-integracion.md b/commands/revisar-integracion.md new file mode 100644 index 0000000..8c1ac72 --- /dev/null +++ b/commands/revisar-integracion.md @@ -0,0 +1,19 @@ +--- +description: Revisa una integración existente con FiscalAPI y reporta los defectos que causan problemas en producción, con la línea y la corrección de cada uno. +argument-hint: "[carpeta o archivos a revisar; por omisión, todo el proyecto]" +--- + +Revisa la integración con FiscalAPI de este proyecto, siguiendo las skills `fiscalapi-integracion` y `fiscalapi-ciclo-de-vida`. + +$ARGUMENTS + +Busca en el código (excluye `node_modules`, `bin`, `obj`, `vendor` y dependencias) estos defectos, en este orden: + +1. **Llaves en el código o en archivos versionados.** Valores con forma de API key de FiscalAPI (`sk_live_`, `sk_test_` o `sk_development_` seguidos de un GUID con guiones bajos), secretos de webhook (`whsec_`), o una API key o tenant escritos como texto en lugar de leerse del entorno o de un gestor de secretos. Revisa también `.env` versionados y archivos de configuración. Una llave `sk_live_` es crítica: indica rotarla en el dashboard. +2. **Peticiones sin `X-TIME-ZONE`.** Llamadas HTTP directas a FiscalAPI que mandan `X-API-KEY` pero no `X-TIME-ZONE`, o un cliente del SDK sin zona horaria cuando el sistema no trabaja en hora de México. +3. **Reintentos a ciegas tras un 5xx o un timeout al timbrar.** Un ciclo de reintento, una librería de retry o un `catch` que vuelve a llamar `POST /api/v4/invoices` (o `invoices.create`) sin consultar antes si la factura quedó timbrada. Un 5xx o un timeout al timbrar no se reintenta. +4. **Webhooks sin verificar la firma.** Un endpoint que recibe eventos de FiscalAPI y procesa el cuerpo sin verificar `webhook-signature` con el secreto, o que verifica sobre el JSON ya parseado en lugar del cuerpo crudo. + +Además, si los ves, repórtalos: la URL de producción (`live.fiscalapi.com`) en código de pruebas, `succeeded` sin revisar en la respuesta, y una cancelación marcada como definitiva con la sola respuesta del DELETE. + +Responde con una tabla: defecto, archivo y línea, por qué es un problema (con el enlace a la sección de las docs que aplique) y la corrección concreta. Si no encuentras un defecto de la lista, dilo explícitamente. No modifiques archivos: solo reporta. diff --git a/docs/ai-integracion/README.md b/docs/ai-integracion/README.md new file mode 100644 index 0000000..7eab265 --- /dev/null +++ b/docs/ai-integracion/README.md @@ -0,0 +1,66 @@ +# Integración asistida por IA + +## Objetivo +Que un desarrollador integre FiscalAPI en su ERP, POS o SaaS desde Claude Code, Codex, Cursor o Copilot, y logre su primer timbre correcto en test al primer intento. + +Para eso el feature publica: +- skills en español; +- un plugin para los 4 hosts; +- un MCP stdio opcional; +- los cimientos que esos tres necesitan: OpenAPI público y docs por sección. + +**Cómo se sabe que quedó:** +- Las evals muestran mejora con las skills frente a sin ellas. +- Baja el tiempo al primer timbre en test (métrica norte en `requisitos.md`). + +**Ramas:** `feat/ai-integracion` en cada repo que toque (fiscalapi-ai, fapi-back, fapi-docs, fapi-reusables), siempre desde `main`. + +**Fuera de alcance:** MCP hosteado, sandbox sin "compra", CSD de prueba propios y el top de rechazos del PAC en Seq (MED-03, D-20). Ver `requisitos.md`. + +## Cómo trabajar +Sigue el ciclo de `fapi-core` (sección 1). Particularidades de este feature: +- **El tablero vive aquí, en fiscalapi-ai,** porque el feature se escribe casi todo en este repo (decisión del usuario). El código de las tareas `BE-*` va en fapi-back y el de las `DOC-*` en fapi-docs o fapi-reusables; la fila de esas tareas se cierra con `@`. +- **Restricción dura: cero mantenimiento adicional para FiscalAPI (D-02).** Si una tarea agrega algo que hay que actualizar a mano en paralelo a las docs o al código, está mal planteada. +- **fiscalapi-ai es público.** Nada llega a `main` sin el PR que fusiona el usuario. Publicar en npm o en los marketplaces es un paso del usuario (U-04). +- **Todo en español:** skills, descripciones de herramientas del MCP y docs (D-04). +- **Entorno local:** fapi-back y fapi-docs se trabajan en los worktrees `../fapi-back-ai` y `../fapi-docs-ai` (rama `feat/ai-integracion`, ya con `npm ci` en fapi-docs-ai), para no tocar la rama `feat/localization` de sus working trees principales. fapi-reusables sigue en `feat/auto-invoice` con cambios del usuario; lo del feature va en el worktree `../fapi-reusables-ai` (rama `feat/ai-integracion` desde `origin/main`). fapi-docs tiene `user.name` local "Fiscalapi": todo commit ahí lleva `-c user.name=mendozagit -c user.email=mendoza.git@gmail.com`. +- **Reanudar:** relee este tablero y vuelve a lanzar `/loop` con el prompt del feature; toma la siguiente fila según `fapi-core`. Nada queda a medias fuera de git. + +## Tablero +Estado ∈ `pendiente` | `en curso` | `hecho` | `bloqueado`. En `bloqueado`, el motivo o la pregunta va en las Notas de la tarea. + +La siguiente tarea (global o de un área) es la primera fila `pendiente` (de esa área) cuyas dependencias sean `—` o `hecho`. El orden de las filas es la prioridad. + +| ID | Tarea | Área | Depende de | Estado | Commit | +|---|---|---|---|---|---| +| MED-01 | Medir mezcla de tipos y complementos en el restore de prod | medicion | — | hecho | fiscalapi-ai@4f6664c | +| MED-02 | Medir embudo y tiempo al primer timbre en el restore de Test | medicion | — | hecho | fiscalapi-ai@a5f9586 | +| BE-01 | Publicar la superficie de 100 acciones con `[PublicApi]` | backend | — | hecho | fapi-back@8d31a9aa | +| BE-02 | Agregar `operationId` estable y metadatos del documento | backend | BE-01 | hecho | fapi-back@8194141e | +| BE-03 | Servir `/api/openapi/v4.json` anónimo en test y live | backend | BE-02 | hecho | fapi-back@3ab2a369 | +| BE-04 | Agregar el trinquete de `EndpointSummary` y cubrir facturas | backend | BE-01 | hecho | fapi-back@2bd55554 | +| BE-05 | Completar `EndpointSummary` en el resto de la superficie | backend | BE-04 | hecho | fapi-back@bc22dcec | +| DOC-01 | Generar `.md` por sección H2 en el build | docs | — | hecho | fapi-docs@b97ad82 | +| DOC-02 | Generar la página de errores de Pagos | docs | — | hecho | fapi-docs@816f3a4 | +| DOC-03 | Corregir el enlace del SDK de Node en `llms.txt` | docs | — | hecho | fapi-docs@b5eec29 | +| DOC-04 | Generar la página de errores de Comercio Exterior | docs | — | hecho | fapi-docs@73d326f, fapi-reusables@3be8d36 | +| SK-01 | Armar el esqueleto del repo, los manifiestos y el CI de enlaces | skills | — | hecho | fiscalapi-ai@6caa2c4 | +| SK-10 | Validar en el check semanal los enlaces "Docs:" del OpenAPI | skills | SK-01, BE-05 | hecho | fiscalapi-ai@b38786b | +| SK-02 | Escribir `fiscalapi-integracion` y `primer-timbre` | skills | SK-01, DOC-01 | hecho | fiscalapi-ai@05135bf | +| SK-03 | Escribir `fiscalapi-diagnostico` y `explicar-error` | skills | SK-01, DOC-02 | hecho | fiscalapi-ai@bce6b59 | +| SK-04 | Escribir `fiscalapi-ingreso-egreso` | skills | SK-02, MED-01 | hecho | fiscalapi-ai@1bcaf7d | +| SK-05 | Escribir `fiscalapi-pagos` | skills | SK-02, MED-01 | hecho | fiscalapi-ai@557c1f6 | +| SK-06 | Escribir `fiscalapi-ciclo-de-vida` y `revisar-integracion` | skills | SK-02 | hecho | fiscalapi-ai@e5d6690 | +| SK-07 | Escribir `fiscalapi-nomina` | skills | SK-02, MED-01 | hecho | fiscalapi-ai@51cbb20 | +| SK-08 | Escribir `fiscalapi-carta-porte` | skills | SK-02, MED-01 | hecho | fiscalapi-ai@9849def | +| SK-09 | Escribir `fiscalapi-comercio-exterior` | skills | SK-02, MED-01 | hecho | fiscalapi-ai@8cb8c77 | +| MCP-01 | Crear el paquete stdio con `listar_operaciones` y `obtener_operacion` | mcp | SK-01, BE-03 | hecho | fiscalapi-ai@6e0c755 | +| MCP-02 | Agregar `buscar_docs` con índice en español | mcp | MCP-01, DOC-01 | hecho | fiscalapi-ai@3594080 | +| EV-01 | Montar el harness de evals con los casos de ingreso y pagos | evals | SK-05, MCP-02 | pendiente | | +| EV-02 | Agregar casos de nómina, carta porte y comercio exterior | evals | EV-01, SK-09 | pendiente | | +| EV-03 | Correr las 3 variantes y reportar | evals | EV-02 | pendiente | | +| U-02 | Confirmar el nombre y el scope del paquete npm (D-14) | usuario | — | hecho | | +| MCP-03 | Agregar el workflow de build y publicación por hash | mcp | MCP-02, U-02 | pendiente | | +| U-03 | Importar `/api/openapi/v4.json` al workspace de Postman | usuario | BE-03 | pendiente | | +| U-04 | Decidir la publicación en npm y en los marketplaces con el reporte de EV-03 | usuario | EV-03, MCP-03 | pendiente | | +| CIERRE | Suite de fapi-back, instalar en los 4 hosts y borrador de notas | cierre | todas | pendiente | | diff --git a/docs/ai-integracion/backend.md b/docs/ai-integracion/backend.md new file mode 100644 index 0000000..97bd7ff --- /dev/null +++ b/docs/ai-integracion/backend.md @@ -0,0 +1,115 @@ +# Integración asistida por IA: backend (fapi-back) + +El código de estas tareas va en fapi-back, en la rama `feat/ai-integracion` desde `main`. La fila del tablero se cierra con `fapi-back@`. Antes de usar cualquier API de `Microsoft.AspNetCore.OpenApi`, se consulta la documentación oficial de ASP.NET Core 10 (MCP microsoft-learn), sin responder de memoria. + +### BE-01 — Publicar la superficie de 100 acciones con `[PublicApi]` +**Objetivo:** que el documento `v4` contenga exactamente las 100 acciones de D-05 y nada más, protegido por una prueba de equivalencia con la colección. +**Depende de:** — +**Skills:** `fapi-new-feature`, `dotnet-aspnetcore:dotnet-webapi`, `dotnet-test:run-tests`, `fapi-e2e-validate`. +**Modelo sugerido (opcional):** opus. Toca qué se expone públicamente (seguridad). +**Contexto:** D-05, D-12, RF-01, RNF-03. Hoy el documento se registra en `Infrastructure/DependencyInjection` con un `BuildServiceProvider` intermedio y un documento por versión (incluido el grupo v5 de `Controllers/V5/ValuesController`). +**Alcance:** +- Atributo `PublicApiAttribute` (metadata de endpoint) en las 100 acciones. Se aplica por acción, nunca por controller: en los mismos controllers hay 52 acciones fuera de la colección. +- Registro explícito del documento `v4` con `ShouldInclude` = tiene `PublicApiAttribute`. Se quita el `BuildServiceProvider` intermedio. Si se conserva un documento completo para Scalar en Development, se decide aquí y se documenta como D-NN. +- Prueba de equivalencia en WebApi.Tests: + - lee los `*.request.yaml` de `postman/collections/Fiscalapi API Reference`; + - normaliza MÉTODO+ruta (ids y valores → `{param}`; pdf y xml del portal → `{format}`); + - resta la lista explícita de las 6 exclusiones de D-05, con su motivo en el código; + - exige igualdad con las operaciones del documento `v4`. + + El parser YAML lo decide la tarea: se reutiliza uno de la solución si existe. +**Consumidores afectados:** Scalar en Development (deja de ver lo no público si no se conserva un documento completo); `grep -rn "AddOpenApi\|MapOpenApi\|MapScalarApiReference" WebApi.*`. +**Criterios de aceptación (todos con `→ verificar`):** +- El documento local trae 100 operaciones → verificar: `curl -s http://localhost:5000/api/openapi/v4.json | jq '[.paths[]|keys[]]|length'`, una vez que BE-03 mapee la ruta; antes, desde la prueba. +- Nada interno aparece (Tenants, TestAccounts, Checkouts, Impersonation, register, login, `load`) → verificar: la prueba de equivalencia, más `jq '.paths|keys'` revisado a mano. +- La prueba falla si se quita un `[PublicApi]` o una exclusión → verificar: quitar uno, correr la prueba y verla en rojo; luego restaurar. +**Pruebas focalizadas:** sí, por contrato y exposición. `dotnet test WebApi.Tests/WebApi.Tests.csproj --filter "FullyQualifiedName~OpenApi"`. +**Fuera de alcance:** `operationId` y metadatos (BE-02); servir la ruta (BE-03); resúmenes (BE-04 y BE-05). +**Notas:** +- fapi-back está en el worktree `fapi-back-ai` (rama `feat/ai-integracion`): el working tree `fapi-back` lo ocupa `feat/localization`. Un solo documento `v4`, también para Scalar (D-17). +- `PublicApiSurfaceTests` resuelve cada request contra todas las acciones v4 (la más literal gana): `tickets/{id}` no tiene `:guid`, así que comparar solo contra el documento no distinguía `tickets/load`. +- Pendiente de la revisión, fuera de alcance: RNF-03 depende de la colección (no hay prueba propia de esquemas de auth), y las anotaciones OpenAPI de acciones internas ya no llegan a ningún documento. + +### BE-02 — Agregar `operationId` estable y metadatos del documento +**Objetivo:** que cada operación tenga un `operationId` estable y que el documento declare servidores reales, autenticación y descripción. +**Depende de:** BE-01. +**Skills:** `dotnet-aspnetcore:dotnet-webapi`, `dotnet-test:run-tests`. +**Modelo sugerido (opcional):** opus. +**Contexto:** D-06, D-13, RF-01. +**Alcance:** +- `operationId` en las 100 acciones con el mecanismo de nombre de endpoint de ASP.NET Core 10, en camelCase y en inglés (`createInvoice`, `cancelInvoice`, …). La convención queda en D-13 al cerrar. +- Transformer de documento: + - `info` con título, versión de la API y la `description` tomada del spec de Postman (`postman/specs/Fiscalapi Specification`, solo como fuente del texto); + - `servers` con `https://test.fiscalapi.com` y `https://live.fiscalapi.com`; + - esquemas de seguridad `apiKey` en header para `X-API-KEY` y `X-TENANT-KEY`, aplicados a las operaciones no anónimas; + - parámetro de header opcional `X-TIME-ZONE`. +- Prueba: los `operationId` son únicos y no vacíos, y las operaciones de `portal/*` no exigen seguridad. +**Textos visibles (propuesta al usuario):** el `info.description` final; se le muestra al usuario antes del commit (contrato público). +**Criterios de aceptación (todos con `→ verificar`):** +- 100 `operationId` únicos → verificar: `jq '[.paths[][]|.operationId]|unique|length'` igual a 100. +- Hay servidores y esquemas de seguridad → verificar: `jq '.servers, .components.securitySchemes'`. +- El documento pasa un lint de OpenAPI 3.1 → verificar: `npx @redocly/cli@ lint` sobre el JSON descargado. +**Pruebas focalizadas:** sí (contrato). `--filter "FullyQualifiedName~OpenApi"`. +**Fuera de alcance:** resúmenes por operación (BE-04 y BE-05). +**Notas:** +- **Textos visibles** aprobados por el usuario (2026-10-15): `title` "Fiscalapi API", `version` "4.0" y como `description` el texto del spec de Postman (párrafo de la API, "Modos de operación" y los enlaces a credenciales y datos de prueba) sin la sección "Configura un environment", más la línea "Ambientes: `https://test.fiscalapi.com` (pruebas) y `https://live.fiscalapi.com` (producción)". +- La fuente (`postman/specs/Fiscalapi Specification`) solo existe sin rastrear en el working tree `fapi-back`, no en `main`: el texto aprobado se copia al transformer. +- Hecho: el operationId va en `[PublicApi("…")]` (D-13). `X-TIME-ZONE` lleva una descripción redactada desde `TimezoneMiddleware` (texto visible asumido; se revisa con BE-04). Redocly 2.57.0: válido salvo `operation-summary` (BE-04/05). +- Pendiente opcional: ninguna prueba fija los 100 nombres; renombrar uno no falla. La descripción aprobada dice que toda petición usa los dos headers, pero el portal es anónimo. + +### BE-03 — Servir `/api/openapi/v4.json` anónimo en test y live +**Objetivo:** que cualquier agente o herramienta descargue el contrato sin credenciales y sin tocar nginx. +**Depende de:** BE-02. +**Skills:** `dotnet-aspnetcore:dotnet-webapi`, `fapi-release` (verifica que no haya pasos manuales de deploy), `fapi-e2e-validate`. +**Modelo sugerido (opcional):** opus. Expone una ruta anónima en producción. +**Contexto:** D-16. Hoy `MapOpenApi` y Scalar solo se registran en Development (`WebConfigurePipeline`). +**Alcance:** +- `MapOpenApi("/api/openapi/{documentName}.json")` en todos los ambientes, anónimo y con caché HTTP. Scalar sigue solo en Development. +- Verificar que el middleware de autenticación y de tenant (`X-TENANT-KEY` obligatorio) no rechaza esta ruta. +**Criterios de aceptación (todos con `→ verificar`):** +- Responde 200 sin headers de auth en local con `ASPNETCORE_ENVIRONMENT=Test` → verificar: `curl -si http://localhost:5000/api/openapi/v4.json | head -1`. +- No existe ruta para el documento v5 ni para uno interno → verificar: `curl -si …/api/openapi/v5.json` devuelve 404. +- Tras el deploy del usuario, responde en test y live → verificar: `curl -si https://test.fiscalapi.com/api/openapi/v4.json` (en CIERRE). +**Pruebas focalizadas:** sí (exposición). Prueba de integración: la ruta responde sin auth y no expone un documento distinto de `v4`. +**Fuera de alcance:** importar a Postman (U-03). +**Notas:** +- Verificado con el host de pruebas (ambiente Test) y con `curl` contra la app en Development: no se levantó la app local en Test porque ahí se activan los jobs del SAT sobre el restore de prod. "Caché" es output cache del servidor (10 min); la respuesta no lleva `Cache-Control`. +- Docs con la ruta vieja `/openapi/v4.json` "solo en Development" (prosa, nada lo consume en runtime): `fapi-back/docs/auto-invoice/{backend-emision,portal,release-notes}.md` y `fapi-auto-invoice/CLAUDE.md`. + +### BE-04 — Agregar el trinquete de `EndpointSummary` y cubrir facturas +**Objetivo:** que ninguna operación pública nueva quede sin resumen, empezando por la ruta crítica del integrador. +**Depende de:** BE-01. +**Skills:** `dotnet-aspnetcore:dotnet-webapi`, `dotnet-test:run-tests`, `postman:api-documentation` y `postman:ai-readiness` (para redactar resúmenes y descripciones que contextualicen el OpenAPI generado). +**Modelo sugerido (opcional):** haiku para escribir los resúmenes; opus revisa la prueba. +**Contexto:** RF-01. Cada resumen es una línea en español más una `EndpointDescription` con el enlace a la sección de docs. No se ingieren comentarios XML: los `///` son notas internas. Indicación del usuario (2026-10-14): las descripciones deben usar el vocabulario fiscal con que busca un integrador, para que las consultas que fallaron en la prueba de búsqueda (`requisitos.md` → Contexto medido: "complemento de pago", "crear cliente receptor", "estatus ante el SAT", "descarga masiva") encuentren su operación. +**Alcance:** +- Prueba trinquete: lista las operaciones públicas sin `EndpointSummary` y la compara con una lista permitida en el código que solo puede bajar. +- Resúmenes de Invoices, People y TaxFiles. +**Textos visibles (propuesta al usuario):** los resúmenes de esta tarea; el usuario los revisa en el diff antes del commit. +**Criterios de aceptación (todos con `→ verificar`):** +- Agregar un `[PublicApi]` sin resumen hace fallar la prueba → verificar: agregarlo de forma temporal y ver la prueba en rojo. +- Invoices, People y TaxFiles salen de la lista permitida → verificar: `jq '[.paths[][]|select(.summary==null)]|length'` baja en ese número. +**Pruebas focalizadas:** sí (contrato). `--filter "FullyQualifiedName~OpenApi"`. +**Fuera de alcance:** el resto de controllers (BE-05). +**Notas:** +- La lista permitida es `OperationsWithoutSummary` en `WebApi.Tests/OpenApi/PublicApiSummaryTests.cs` (41 `operationId`). BE-05 borra filas y al final borra la lista. +- Las descripciones terminan en una línea "Docs:" que se arma con `PublicApiDocument.DocsUrl` y el ancla H2 real. Antes de usar cada ancla, compruébala contra docs.fiscalapi.com. +- El usuario delegó la aprobación de los textos (2026-10-15, "analízalo tú y decide"); ver D-19. Que `details` de `getPerson` no tenga efecto quedó en `known-issues.md` de fapi-back. + +### BE-05 — Completar `EndpointSummary` en el resto de la superficie +**Objetivo:** que la lista permitida del trinquete quede vacía. +**Depende de:** BE-04. +**Skills:** `dotnet-aspnetcore:dotnet-webapi`, `dotnet-test:run-tests`, `postman:api-documentation` y `postman:ai-readiness`. +**Modelo sugerido (opcional):** haiku. +**Contexto:** igual que BE-04, incluida la indicación del usuario sobre el vocabulario fiscal de las descripciones. +**Alcance:** resúmenes y descripciones con enlace de Catalogs, Products, Stamps, ApiKeys, DownloadCatalogs, DownloadRequests, DownloadRules, Manifests, SatValidations, Tickets, Portal, AutoInvoice, Webhooks y Events (los que falten); la lista permitida se borra. +**Textos visibles (propuesta al usuario):** los resúmenes de esta tarea, revisados en el diff. +**Criterios de aceptación (todos con `→ verificar`):** +- No hay operaciones públicas sin resumen → verificar: `jq '[.paths[][]|select(.summary==null)]|length'` igual a 0. +- La lista permitida ya no existe → verificar: grep del nombre de la lista en WebApi.Tests sin resultados. +**Pruebas focalizadas:** las mismas de BE-04. +**Fuera de alcance:** — +**Notas:** +- Ya no hay operaciones sin resumen (41 → 0) y `PublicApiSummaryTests` lo exige sin lista. Los textos los decidió el agente por delegación del usuario (D-19). Las 31 operaciones que ya tenían resumen (Webhooks, Tickets, Portal, AutoInvoice, Events) siguen sin enlace "Docs:". +- En Test (`FakeResults=true`), `runSatValidations` simula todos los tipos, aunque el comentario de `RunSatValidationsCommand.cs:161` diga que solo `sat.cfdi.status`. +- Dos defectos nuevos quedaron en `known-issues.md` de fapi-back, los dos de descarga masiva: el SAT puede no aceptar una solicitud y aun así se guarda, y crear o borrar solicitudes solo exige `XmlDownloader.Read`. diff --git a/docs/ai-integracion/decisiones.md b/docs/ai-integracion/decisiones.md new file mode 100644 index 0000000..0c50306 --- /dev/null +++ b/docs/ai-integracion/decisiones.md @@ -0,0 +1,140 @@ +# Integración asistida por IA: decisiones vigentes + +Reglas de este archivo: +- Cada decisión describe el estado actual. Si cambia, se reescribe en su lugar; la historia vive en git. +- Estado (uno de tres): + - `confirmada`: el usuario la aprobó o es técnica ya resuelta. + - `propuesta`: requiere el OK del usuario antes de la tarea que la usa. + - `técnica — la fija `: la decide el agente en esa tarea. +- Una decisión nueva se agrega al final, con el siguiente D-NN libre. +- Vocabulario del feature: + - **superficie pública:** las acciones con `[PublicApi]`. + - **catálogo de operaciones:** la lista compacta operationId + método + ruta + resumen. + - **sección:** un bloque H2 de una página de docs con su propia `.md`. + - **variante de eval:** sin nada, con skills, o con skills más MCP. + +--- + +### D-01 — La audiencia son los desarrolladores que integran +- **Decisión:** los recursos van dirigidos a equipos que meten FiscalAPI en su ERP, POS o SaaS desde agentes de código (Claude Code, Codex, Cursor, Copilot). No a contadores ni pymes que facturan desde un chat. +- **Por qué:** es donde se reduce el tiempo de integración, y en test no hay riesgo fiscal. +- **Consecuencias:** nada de acciones en live; las skills enseñan a escribir el código de integración. +- **Estado:** confirmada. + +### D-02 — Cero mantenimiento adicional y nada hosteado +- **Decisión:** no se crea ningún servicio en ejecución (ni MCP remoto, ni BD, ni modelos). Todo lo derivado se genera solo desde el código o las docs. +- **Por qué:** es una restricción dura del usuario: "No quiero agregar superficie de mantenimiento adicional a fiscalapi". +- **Consecuencias:** se descarta el MCP remoto; el MCP stdio lleva el índice dentro del paquete y se regenera en CI (D-07, D-14). +- **Estado:** confirmada. + +### D-03 — Las skills enrutan y enlazan; no copian las docs +- **Decisión:** 8 skills (`fiscalapi-integracion`, `-ingreso-egreso`, `-pagos`, `-nomina`, `-carta-porte`, `-comercio-exterior`, `-ciclo-de-vida`, `-diagnostico`) y 3 comandos (`primer-timbre`, `explicar-error`, `revisar-integracion`). Siguen el estándar agentskills.io. Sus `references/` solo tienen índices de enlaces a las `.md` y árboles de decisión. +- **Por qué:** si copiaran contenido de las docs, cada cambio de API obligaría a editarlas en paralelo (D-02). +- **Consecuencias:** una skill solo cambia cuando cambia "cómo se decide"; un enlace roto lo detecta el CI de SK-01. +- **Estado:** confirmada. + +### D-04 — Todo en español +- **Decisión:** las skills, las herramientas y descripciones del MCP y las docs de este feature van en español. +- **Por qué:** docs.fiscalapi.com y el vocabulario del SAT están en español. +- **Consecuencias:** las consultas de regresión y las evals se escriben en español. Si la localización de fapi-docs se fusiona, se revisa si `buscar_docs` debe indexar también el inglés. +- **Estado:** confirmada. + +### D-05 — OpenAPI desde el código; la superficie es la colección menos 6 +- **Decisión:** `/api/openapi/v4.json` se genera del código. Incluye solo las acciones con `[PublicApi]`: las 106 de la colección "Fiscalapi API Reference" menos `POST account/register`, `POST account/login`, `GET reports/params/{documentType}/{tin}/{invoiceType}`, `GET tickets/load`, `GET webhooks/load` y `GET webhooks/{id}/deliveries/load`. En total, 100. `portal/*` y `events/trigger` sí quedan públicos. +- **Por qué:** lo indicó el usuario. La colección ya es la referencia pública validada. +- **Consecuencias:** una prueba de equivalencia lee la colección, resta esas 6 exclusiones y exige igualdad con el documento. Que `portal/*` y `events/trigger` sigan públicos es interpretación del agente (el usuario no los excluyó); el usuario puede corregirla. +- **Estado:** confirmada. + +### D-06 — Un solo contrato; la colección conserva su propósito +- **Decisión:** el spec "Fiscalapi Specification" del workspace de Postman se reemplaza importando `/api/openapi/v4.json` (U-03) y deja de generarse desde la colección. La colección sigue siendo el artefacto de ejemplos curados por tipo de CFDI. +- **Por qué:** dos contratos en paralelo serían mantenimiento extra. El spec generado desde la colección no tiene esquemas ni `operationId`. +- **Consecuencias:** el `info.description` del spec de Postman pasa al transformer de BE-02. El archivo local `postman/specs/` queda obsoleto. +- **Estado:** confirmada. + +### D-07 — MCP stdio de solo lectura con 3 herramientas +- **Decisión:** `@fiscalapi/mcp` expone `listar_operaciones(tag?)`, `obtener_operacion(operationId)` (con los `$ref` resueltos y recortado) y `buscar_docs(consulta)`. No tiene `invocar`, y no usa red en runtime. +- **Por qué:** la llamada a la API debe vivir en el código del integrador. Invocar desde el MCP no adelanta la integración y agrega superficie de seguridad. +- **Consecuencias:** el OpenAPI y el índice van dentro del paquete. Se publica solo si EV-03 muestra mejora frente a las skills solas (U-04). +- **Estado:** confirmada. + +### D-08 — Se busca en las docs, no en el OpenAPI, y sin embeddings +- **Decisión:** `listar_operaciones` devuelve el catálogo completo, sin búsqueda. `buscar_docs` usa MiniSearch sobre las secciones, con un `processTerm` en español: minúsculas, sin acentos, stopwords de `@orama/stopwords` y stemmer de `@orama/stemmers`. +- **Por qué:** medido (ver `requisitos.md`). Sobre las operaciones, todos los motores fallan en lo fiscal; sobre las secciones de las docs, el léxico acierta 14 de 16. Los embeddings cuestan 130 MB y 14 s de arranque sin ganar nada. +- **Consecuencias:** las 16 consultas de la medición son la prueba de regresión de MCP-02. +- **Estado:** confirmada. + +### D-09 — El sandbox sin "compra" se pospone +- **Decisión:** no se toca billing en este feature. La skill `fiscalapi-integracion` guía por el flujo actual (cuenta, suscripción de prueba con tarjeta de prueba y llaves). +- **Por qué:** sería código nuevo en un área crítica. El usuario pidió medir primero. +- **Consecuencias:** MED-02 mide el abandono en el paso de compra y el resultado se lleva al usuario. +- **Estado:** confirmada. + +### D-10 — Los CSD de prueba se siguen enlazando desde SW +- **Decisión:** no se alojan certificados de prueba propios. +- **Por qué:** alojarlos obliga a renovarlos cuando venzan (D-02). +- **Consecuencias:** las skills enlazan a la página `testing-data` de las docs. +- **Estado:** confirmada. + +### D-11 — Evals manuales antes de cada release, en 3 variantes +- **Decisión:** cada caso CFDI se corre sin nada, con skills, y con skills más MCP. Se califica con un UUID real en test más la consulta de su estado, y se cuentan los tokens. Se corre a mano antes de publicar, no en CI nocturno. +- **Por qué:** se mide la mejora sin un costo fijo de tokens. +- **Consecuencias:** EV-01 decide si reutilizar `skill-validator` de dotnet/skills o escribir un script mínimo. +- **Estado:** técnica — la fija EV-01. + +### D-12 — El feature vive en fiscalapi-ai +- **Decisión:** el tablero y todo el trabajo de skills, MCP y evals van en `FiscalAPI/fiscalapi-ai` (público), en la rama `feat/ai-integracion` desde `main`. fapi-back y fapi-docs solo reciben los cambios mínimos de BE-* y DOC-*, cada uno en su rama. +- **Por qué:** lo indicó el usuario: "solo escribe sobre fiscalapi-ai". +- **Consecuencias:** las filas BE-* y DOC-* se cierran con `@`. +- **Estado:** confirmada. + +### D-13 — `operationId` estable por acción pública +- **Decisión:** cada acción con `[PublicApi]` declara un `operationId` estable, en camelCase y en inglés como el código. Va como argumento obligatorio del mismo atributo (`[PublicApi("createInvoice")]`), que implementa `IEndpointNameMetadata`: el generador de ASP.NET Core 10 toma el operationId del nombre de ruta o, si no hay, de ese metadato (`OpenApiDocumentService.GetOperationId`, release/10.0). Convención: `list` para colecciones, `get` / `create` / `update` / `delete` para el CRUD y el verbo del dominio cuando lo hay (`cancelInvoice`, `revokeApiKey`, `cancelTicket`, `transferStamps`, `runSatValidations`). +- **Por qué:** el MCP y las skills lo usan como id. El spec de Postman no trae ninguno. +- **Consecuencias:** cambiar un `operationId` es un cambio visible para los consumidores del OpenAPI. +- **Estado:** confirmada (técnica, la fijó BE-02). + +### D-14 — Paquete `@fiscalapi/mcp`, publicado por CI cuando cambia el hash +- **Decisión:** + - Nombre `@fiscalapi/mcp`, publicado con `access: public` por el usuario npm `fiscalapi`. + - Node ≥22 y `@modelcontextprotocol/server` v2 con versión fijada, más Dependabot. + - El build descarga `/api/openapi/v4.json` de live y las `.md` de sección, y publica solo si cambia el hash. + - Publica con trusted publishing desde un runner hospedado por GitHub. +- **Por qué:** el usuario confirmó el nombre (2026-10-14), que está libre en npm. Trusted publishing no acepta runners self-hosted. +- **Consecuencias:** MCP-03 verifica en la documentación de npm si la primera publicación puede ser por OIDC o si requiere una publicación manual del usuario. +- **Estado:** confirmada. + +### D-15 — Sin `llms-full.txt` +- **Decisión:** no se genera `llms-full.txt`. Lo que se agrega es la `.md` por sección. +- **Por qué:** serían unos 600k tokens, inservibles para un agente; una sola sección pesa hasta 156 KB. +- **Consecuencias:** DOC-01 genera las secciones y `llms.txt` sigue siendo el índice. +- **Estado:** confirmada. + +### D-16 — El OpenAPI se sirve bajo `/api` para no tocar nginx +- **Decisión:** `MapOpenApi` se mapea a `/api/openapi/{documentName}.json`, anónimo y en todos los ambientes. +- **Por qué:** el nginx de test y el de live ya mandan `location ^~ /api` al back. El CD no toca nginx, que se cambia a mano (`infra/nginx/live/README.md`). Con otra ruta habría un paso manual. +- **Consecuencias:** cero cambios en nginx. `/api` no tiene `limit_req` en nginx y el limiter de la app solo cubre familias marcadas, así que la ruta no tiene rate limit: el costo lo acota una output cache de una sola entrada (no varía por query, host ni `Authorization`). Corregido en BE-03; antes decía que heredaba el rate limiting de `/api`. +- **Estado:** confirmada. + +### D-17 — Un solo documento OpenAPI: el público +- **Decisión:** fapi-back registra un solo documento, `v4`, con las acciones `[PublicApi]`. Scalar en Development muestra ese mismo documento; no se conserva un documento completo con lo interno, y el grupo v5 (`Controllers/V5/ValuesController`) se queda sin documento. +- **Por qué:** lo interno (dashboard, Host, DevExtreme) no es contrato, y un segundo documento sería configuración que nadie consume. Así Scalar muestra lo mismo que reciben los integradores. +- **Consecuencias:** las pruebas de contrato OpenAPI solo cubren operaciones públicas. Las rutas internas se siguen probando por HTTP. +- **Estado:** confirmada (técnica, la fijó BE-01). + +### D-18 — Manifiestos: `.claude-plugin/` para tres hosts y una copia para Cursor +- **Decisión:** el plugin `fiscalapi` (marketplace `fiscalapi`, `source: "./"`) se declara en `.claude-plugin/plugin.json` y `.claude-plugin/marketplace.json`, sin campo `skills` (cada host descubre `skills//SKILL.md`). Cursor lee `.cursor-plugin/plugin.json`, que es una copia exacta. No se crean `.codex-plugin/` ni `.github/plugin/`. +- **Por qué:** Codex lee `.claude-plugin/marketplace.json` y `.claude-plugin/plugin.json` (`MARKETPLACE_MANIFEST_RELATIVE_PATHS` y `DISCOVERABLE_PLUGIN_MANIFEST_PATHS` en `openai/codex`), y Copilot los busca en su orden documentado. Un manifiesto por host sería mantenimiento en paralelo (D-02). El campo `skills` se suma al descubrimiento en Claude, lo reemplaza en Cursor y está prohibido en Agent Plugins 1.0. +- **Consecuencias:** el CI valida con `claude plugin validate . --strict` y exige que la copia de Cursor sea idéntica (`cmp`). El MCP no se declara hasta U-04 (D-07). +- **Estado:** confirmada (técnica, la fijó SK-01). + +### D-19 — Resúmenes OpenAPI con el vocabulario de búsqueda del integrador +- **Decisión:** el `EndpointSummary` y la `EndpointDescription` de cada operación pública usan los términos con que busca un integrador ("complemento de pago", "cliente receptor", "estatus ante el SAT", "patrón"), aunque el vocabulario interno de fapi-core evite "cliente" para una persona. Cada descripción termina con una línea "Docs:" que enlaza la sección H2 de docs.fiscalapi.com que la documenta. +- **Por qué:** se buscan para que las consultas que fallaron en la prueba de búsqueda (`requisitos.md`) encuentren su operación, como pidió el usuario el 2026-10-14. El vocabulario interno gobierna el código y la prosa del equipo, no las palabras con que busca un integrador. +- **Consecuencias:** BE-05 sigue la misma regla. Un texto que afirme un código HTTP o un campo se valida contra el handler. +- **Estado:** asumida (agente, 2026-10-15). El usuario delegó la aprobación de los textos de BE-04 ("analízalo tú y decide"); revisar con el usuario. + +### D-20 — El top de rechazos del PAC queda fuera de alcance +- **Decisión:** no se mide el top de rechazos del PAC en el Seq de producción (MED-03), y no se abre el túnel a Seq (U-01). Las dos filas salen del tablero. +- **Por qué:** lo decidió el usuario el 2026-10-15. Las skills ya están escritas sin ese dato, y ninguna otra tarea depende de MED-03. +- **Consecuencias:** EV-01 usa un código representativo de cada matriz (`CFDI40`, `CRP`, `NOM`, `CP`, `CCE`) para los casos de "explicar error". `fiscalapi-diagnostico` cubre todas las familias, aunque sin ordenarlas por frecuencia real. +- **Estado:** confirmada. diff --git a/docs/ai-integracion/docs.md b/docs/ai-integracion/docs.md new file mode 100644 index 0000000..f41fb8b --- /dev/null +++ b/docs/ai-integracion/docs.md @@ -0,0 +1,77 @@ +# Integración asistida por IA: docs (fapi-docs y fapi-reusables) + +El código va en fapi-docs (y en fapi-reusables solo si cambia el generador), en la rama `feat/ai-integracion` desde `main`. La fila del tablero se cierra con `@`. + +**Hecho al planear:** fapi-docs tenía en curso la rama `feat/localization`. En `main` hay 0 archivos `content.en.mdx` y 52 `content.mdx`. Antes de DOC-01, revisa si la localización ya llegó a `main`; si llegó, la generación por sección cubre también las páginas en inglés y se carga `fapi-i18n`. + +### DOC-01 — Generar `.md` por sección H2 en el build +**Objetivo:** que un agente pueda leer una sola sección (por ejemplo "Crear factura de pago") sin descargar la página completa, que llega a 647 KB. +**Depende de:** — +**Skills:** `fapi-sdk-docs-sync` (docs públicas), `fapi-i18n` (solo si la localización ya está en `main`). +**Modelo sugerido (opcional):** opus. Cambia rutas públicas del sitio. +**Contexto:** D-15, RF-02. Hoy `src/app/md/[[...slug]]/route.js` y los rewrites de `next.config.mjs` sirven `.md`, y `getPageMarkdown` vive en `src/lib/docs.js`. +**Alcance:** +- Ruta de sección `//.md`, con el mismo slug que el ancla H2 que ya genera el sitio. Devuelve el markdown de esa sección, del H2 hasta el siguiente H2. +- Se genera desde los mismos MDX en build (prerender), sin contenido nuevo que mantener. +- `llms.txt` agrega, por página, la lista de sus secciones con su URL `.md`. +**Criterios de aceptación (todos con `→ verificar`):** +- La sección se sirve sola → verificar: `npm run build && npm start` y `curl -s localhost:3000/invoices-by-value/crear-factura-de-pago.md | head`; el contenido empieza en ese H2 y no incluye el siguiente. +- Cada ancla del sitio tiene su `.md` → verificar: un script de build cuenta las secciones de los MDX (427 al planear) y las rutas generadas, y las compara. +- `llms.txt` lista las secciones → verificar: `curl -s localhost:3000/llms.txt | grep -c "\.md"` sube respecto a `main`. +**Pruebas focalizadas:** ninguna (no aplica). La verificación es el conteo del build. +**Fuera de alcance:** `llms-full.txt` (D-15). +**Notas:** +- Hay 378 secciones H2 en 53 páginas y 429 copias `.md`. Las 427 del plan eran una estimación. La guarda `npm run check:markdown` corre como `postbuild` y compara contra las anclas del HTML construido. Los H2 de ``, `` y `` no tienen copia. +- Las secciones de `invoices-by-value` y `invoices-by-reference` (`EXCLUDED_ROUTES`) tienen copia y van en `llms.txt` como `[Página: Sección]`, por decisión del usuario. Como robots no se tocó, su `Disallow` por prefijo también bloquea esas 46 copias para los crawlers que respetan robots. +- Antes de fusionar, `es:diff` de localización (`feat/localization`) va a marcar `/llms.txt` y las `.md` nuevas: declara la diferencia en `scripts/i18n/es-allowed.json`. + +### DOC-02 — Generar la página de errores de Pagos +**Objetivo:** que `fiscalapi-diagnostico` y `explicar-error` puedan enlazar los errores `CRP*`, igual que hoy enlazan los de CFDI 4.0, Nómina y Carta Porte. Comercio Exterior pasó a DOC-04. +**Depende de:** — +**Skills:** `fapi-cfdi-rules`, `fapi-sdk-docs-sync`. +**Modelo sugerido (opcional):** haiku. Corre el generador existente. +**Contexto:** RF-03. Las páginas actuales salen de `fapi-reusables/scripts/sat-docs-md-errors-generator/app.py` sobre las matrices de `fapi-reusables/sat-docs`. Ya existen `sat-docs/pagos` y `sat-docs/comercio-exterior`. +**Alcance:** +- Correr el generador con la matriz de Pagos 2.0 y crear la página `errors/pagos-20` con el mismo formato que `errors/nomina-12`. +- Agregarlas a la navegación y a `llms.txt`. +- Si el generador necesita un cambio, va en fapi-reusables en su propio commit. +**Criterios de aceptación (todos con `→ verificar`):** +- La página responde en `.md` → verificar: `curl -s localhost:3100/errors/pagos-20.md | grep -c "CRP"` mayor que 0. +- El número de códigos coincide con la matriz del SAT → verificar: conteo de filas de la matriz contra el conteo en la página. +**Pruebas focalizadas:** ninguna (no aplica). +**Fuera de alcance:** traducir los mensajes del SAT. +**Notas:** +- Comercio Exterior pasó a DOC-04: `sat-docs/comercio-exterior` no trae su matriz de errores. Las páginas de errores actuales se editaron a mano después de generarse, así que `pagos-20` toma la estructura de `nomina-12` y solo la tabla sale del generador. +- El generador se corrió con la salida en el scratchpad: fapi-reusables está en `feat/auto-invoice` con cambios propios. 103 códigos CRP en la matriz y en la página. El repo fapi-docs tiene `user.name` local "Fiscalapi": se fija el autor mendozagit al hacer commit. + +### DOC-03 — Corregir el enlace del SDK de Node en `llms.txt` +**Objetivo:** que los agentes lleguen al repo real del SDK de Node. +**Depende de:** — +**Skills:** `fapi-sdk-docs-sync`. +**Modelo sugerido (opcional):** haiku. +**Contexto:** `src/app/llms.txt/route.js` enlaza `github.com/FiscalAPI/fiscalapi-js`, y el repo real es `fiscalapi-node`. +**Alcance:** cambiar el enlace y hacer grep de `fiscalapi-js` en todo fapi-docs por si aparece en otro lugar. +**Criterios de aceptación (todos con `→ verificar`):** +- No queda ningún `fiscalapi-js` → verificar: `grep -rn "fiscalapi-js" src` sin resultados. +- El enlace nuevo existe → verificar: `gh repo view FiscalAPI/fiscalapi-node --json name`. +**Pruebas focalizadas:** ninguna (no aplica). +**Fuera de alcance:** — +**Notas:** +- También estaba en `src/components/Libraries.jsx` (la tarjeta del SDK en el sitio). `fiscalapi-js` es el nombre anterior del repo y GitHub redirige; los otros 4 SDK resuelven con su nombre. + +### DOC-04 — Generar la página de errores de Comercio Exterior +**Objetivo:** que `fiscalapi-diagnostico` y `explicar-error` puedan enlazar los errores `CCE*`, como DOC-02 hizo con los `CRP*`. +**Depende de:** — +**Skills:** `fapi-cfdi-rules`, `fapi-sdk-docs-sync`. +**Modelo sugerido (opcional):** haiku. Corre el generador existente. +**Contexto:** RF-03. Se separó de DOC-02: `fapi-reusables/sat-docs/comercio-exterior` no trae la matriz de errores (solo `Catalogos_Comercio_Exterior.xlsx` y las guías, sin ningún código `CCE###`). MED-01 midió 0 facturas de Comercio Exterior en producción. +**Alcance:** agregar la matriz oficial del SAT a `sat-docs/comercio-exterior/catalogs` (commit propio en fapi-reusables) y crear `errors/comercio-exterior-20` como DOC-02 creó `errors/pagos-20`, con su entrada en la navegación y en la tabla de `errors`. +**Criterios de aceptación (todos con `→ verificar`):** +- La página responde en `.md` → verificar: `curl -s localhost:3100/errors/comercio-exterior-20.md | grep -c "CCE"` mayor que 0. +- El número de códigos coincide con la matriz → verificar: conteo de códigos `CCE` únicos de la matriz contra los de la página. +**Pruebas focalizadas:** ninguna (no aplica). +**Fuera de alcance:** traducir los mensajes del SAT. +**Notas:** +- La matriz 2.0 Rev A quedó versionada en `fapi-reusables@3be8d36` (worktree `fapi-reusables-ai`). La copia sin rastrear del working tree del usuario sigue ahí y no se tocó. +- La página toma la estructura de `pagos-20` y del generador solo sale la tabla. La columna Aclaraciones vino vacía en las 73 filas, así que la tabla tiene las mismas 6 columnas que Pagos. +- Matriz y página tienen los mismos 73 códigos CCE, sin diferencias. diff --git a/docs/ai-integracion/evals.md b/docs/ai-integracion/evals.md new file mode 100644 index 0000000..6019ed5 --- /dev/null +++ b/docs/ai-integracion/evals.md @@ -0,0 +1,61 @@ +# Integración asistida por IA: evals + +Todo va en fiscalapi-ai, en `evals/`, en la rama `feat/ai-integracion`. Las evals se corren a mano antes de cada release (D-11), con una cuenta y una llave de test de FiscalAPI que se leen de variables de entorno y nunca se escriben en el repo. + +### EV-01 — Montar el harness de evals con los casos de ingreso y pagos +**Objetivo:** medir, caso por caso, si un agente integra FiscalAPI mejor con las skills y con el MCP que sin nada. +**Depende de:** SK-05, MCP-02. +**Skills:** `anthropic-skills:skill-creator` (sección de evals), `superpowers:writing-skills`. El patrón de `skill-validator` de dotnet/skills se revisa con `gh api`. +**Modelo sugerido (opcional):** opus. Diseña la medición de la que depende U-04. +**Contexto:** D-11, RF-06. +**Alcance:** +- Elegir el runner: reutilizar `skill-validator` (dotnet/skills) o un script mínimo que lance el agente en modo no interactivo sobre un proyecto plantilla. Se decide aquí y se documenta en D-11. +- Cada caso trae: + - el prompt del integrador en español; + - un proyecto plantilla (Node Express y .NET mínimo); + - las 3 variantes de D-11. +- Calificación automática: el proyecto resultante, ejecutado, produce un UUID en test, y `GET /api/v4/invoices/{id}` confirma su estado. +- Métricas: éxito, tokens e intentos. +- Casos iniciales: factura de ingreso, nota de crédito, factura global, y factura PPD con su complemento de pago. +**Criterios de aceptación (todos con `→ verificar`):** +- Un solo comando corre los 4 casos en las 3 variantes y genera un reporte en tabla → verificar: ejecución y reporte generado. +- Un caso con una skill rota a propósito baja su tasa de éxito → verificar: corrida con un enlace o una regla falsa plantada. +**Pruebas focalizadas:** ninguna (no aplica). El harness es la prueba. +**Fuera de alcance:** correr en CI (D-11). +**Notas:** pausada el 2026-10-15 por el usuario antes de empezar: sin código ni decisiones. Al retomar, D-11 sigue sin fijar el runner. MCP-02 quedó en `fiscalapi-ai@3594080`, y las docs locales de verificación se sirven con `npx next start -p 3999` en fapi-docs-ai. + +### EV-02 — Agregar casos de nómina, carta porte y comercio exterior +**Objetivo:** cubrir con evals todos los casos CFDI del feature. +**Depende de:** EV-01, SK-09. +**Skills:** `fapi-cfdi-rules`. +**Modelo sugerido (opcional):** haiku para escribir los casos; opus revisa la calificación. +**Contexto:** D-03; el orden y los casos extra los fija MED-01; los códigos de "explicar error" son representativos de las matrices (D-20). +**Alcance:** +- Casos de nómina ordinaria, Carta Porte de traslado por autotransporte y exportación definitiva con Comercio Exterior. +- Un caso de "explicar error" por familia de código: `CFDI40`, `CRP`, `NOM`, `CP` y `CCE`, uno representativo de su matriz en las docs. +**Criterios de aceptación (todos con `→ verificar`):** +- Los casos nuevos corren en las 3 variantes → verificar: reporte con las filas nuevas. +**Pruebas focalizadas:** ninguna (no aplica). +**Fuera de alcance:** — +**Notas:** + +### EV-03 — Correr las 3 variantes y reportar +**Objetivo:** dar al usuario los datos para U-04: publicar o no el MCP y pedir o no los marketplaces oficiales (Q-01). +**Depende de:** EV-02. +**Skills:** `superpowers:verification-before-completion`. +**Modelo sugerido (opcional):** opus. +**Contexto:** D-07, D-11, Q-01. +**Alcance:** +- Corrida completa con N repeticiones por caso (N lo fija EV-01 según la varianza). +- Tabla de éxito, tokens e intentos por caso y variante. +- Recomendación: + - si las skills mejoran frente a sin nada; + - si el MCP mejora frente a las skills solas; + - qué skills reescribir. +- El resumen va en las Notas de esta tarea y en el cuerpo del commit; no se versiona ningún archivo de evidencia. +**Criterios de aceptación (todos con `→ verificar`):** +- El reporte existe y cubre todos los casos y variantes → verificar: tabla en el cuerpo del commit. +- La recomendación se apoya en las cifras → verificar: cada afirmación cita su fila. +**Pruebas focalizadas:** ninguna (no aplica). +**Fuera de alcance:** publicar (U-04). +**Notas:** diff --git a/docs/ai-integracion/mcp.md b/docs/ai-integracion/mcp.md new file mode 100644 index 0000000..01e396e --- /dev/null +++ b/docs/ai-integracion/mcp.md @@ -0,0 +1,84 @@ +# Integración asistida por IA: MCP stdio (`@fiscalapi/mcp`) + +Todo va en fiscalapi-ai, en `mcp/`, en la rama `feat/ai-integracion`. + +Antes de escribir código contra el SDK de MCP, se consulta su documentación vigente (context7 o el repo `modelcontextprotocol/typescript-sdk`). La spec cambió el 2026-07-28 y el SDK v2 se mueve rápido. + +### MCP-01 — Crear el paquete stdio con `listar_operaciones` y `obtener_operacion` +**Objetivo:** que `npx` levante un servidor stdio que dé al agente el catálogo completo de operaciones y el esquema de una operación, sin usar red. +**Depende de:** SK-01, BE-03. +**Skills:** `context7-mcp` (SDK de MCP), `superpowers:test-driven-development`. +**Modelo sugerido (opcional):** opus. Fija la base del paquete. +**Contexto:** D-07, D-08, D-13, D-14, RF-05, RNF-05, RNF-06. +**Alcance:** +- Paquete `@fiscalapi/mcp`: + - TypeScript y ESM; + - `engines.node >=22`; + - `@modelcontextprotocol/server` v2 con versión exacta; + - `bin` con shebang; + - `publishConfig.access: public`; + - licencia igual a la de los SDK. +- Script de build: + - descarga `/api/openapi/v4.json` (live por omisión; ruta local para desarrollo); + - genera `catalog.json` (operationId, método, ruta, resumen, tag) y `operations/.json` con los `$ref` resueltos y recortados a unos 10k tokens como máximo; + - guarda el hash de la fuente. +- Herramientas, con descripciones en español: + - `listar_operaciones(tag?)` devuelve el catálogo compacto; + - `obtener_operacion(operationId)` devuelve parámetros, cuerpo y respuestas; + - un operationId inexistente devuelve un error claro con sugerencias por prefijo. +- Pruebas con un cliente MCP en proceso: `tools/list`, las dos herramientas, el error de id inexistente y el límite de tamaño. +**Criterios de aceptación (todos con `→ verificar`):** +- Arranca sin red → verificar: con la red desactivada (o variable que bloquea `fetch`), `node dist/index.js` responde `tools/list`. +- `listar_operaciones` devuelve 100 operaciones y cabe en unos 3k tokens como máximo → verificar: prueba que cuenta elementos y caracteres. +- `obtener_operacion("createInvoice")` (el nombre que fije D-13) no pasa del límite → verificar: prueba de tamaño. +- Funciona registrado en Claude Code → verificar: `claude mcp add fiscalapi -- node /dist/index.js` y una consulta en la sesión. +**Pruebas focalizadas:** sí (contrato del paquete). `npm test` en `mcp/`. +**Fuera de alcance:** búsqueda en docs (MCP-02); publicación (MCP-03); herramienta `invocar` (D-07). +**Notas:** +- `@modelcontextprotocol/server` 2.3.1 (v2: `McpServer.registerTool` y `serveStdio`, que atiende las eras del protocolo 2025 y 2026) y zod 4.6.7, con versiones exactas. `data/` no se versiona: lo genera `npm run build:data`, que lee live por omisión o `FISCALAPI_OPENAPI_SOURCE`. Las pruebas usan el fixture del OpenAPI de BE-05. +- Con el fixture: 100 operaciones; el catálogo mide 9,023 caracteres y la operación mayor, createInvoice, 28,130 (se colapsan los media types duplicados de ASP.NET). Arranca en unos 200 ms sin red y pasan 123 pruebas. En Claude Code (`--mcp-config`) respondió una consulta con las dos herramientas. +- Para publicar hay que regenerar `data/` desde live cuando BE-03 esté desplegado (hoy live responde 401). Eso lo hace MCP-03. + +### MCP-02 — Agregar `buscar_docs` con índice en español +**Objetivo:** que el agente encuentre la sección de las docs que responde a su duda fiscal sin leer páginas completas. +**Depende de:** MCP-01, DOC-01. +**Skills:** `context7-mcp` (MiniSearch, `@orama/stemmers`), `superpowers:test-driven-development`. +**Modelo sugerido (opcional):** opus. +**Contexto:** D-04, D-08. Medición en `requisitos.md`: 14 de 16 aciertos con MiniSearch y stemmer en español sobre las secciones. +**Alcance:** +- En el build: leer `llms.txt`, descargar las `.md` de sección (DOC-01), sin bloques de código en el texto indexado, e indexar con MiniSearch. + - `processTerm`: minúsculas, quitar acentos (NFD), stopwords de `@orama/stopwords` en español y stemmer de `@orama/stemmers` en español. + - Boost al título. + - El índice se serializa con `toJSON`. +- `buscar_docs(consulta, limite=5)` devuelve título, URL `.md` de la sección, página y un extracto de unas 300 palabras como máximo. +- Prueba de regresión con las 16 consultas de la medición y la sección esperada de cada una. "lista negra 69b" queda documentada como caso conocido si sigue fallando. +**Criterios de aceptación (todos con `→ verificar`):** +- 15 de 16 consultas de regresión, o más, traen la sección esperada en el top 3 → verificar: `npm test`. +- La salida de `buscar_docs` no pasa de unos 10k tokens → verificar: prueba de tamaño. +- El paquete completo pesa menos de 5 MB → verificar: `npm pack --dry-run`. +**Pruebas focalizadas:** sí. `npm test` en `mcp/`. +**Fuera de alcance:** embeddings (D-08). +**Notas:** regresión 15/16 en el top 3, con las 16 consultas recuperadas de la sesión de planeación; caso conocido "buscar clave de producto sat" (sale primero la página "Catálogos SAT"). "lista negra 69b" ya pasa, y "estatus ante el sat" acepta `sat-validations/validar` (criterio `sat.cfdi.status`); ambas se asumieron y quedan documentadas en `test/search-regression.test.ts`. +Sin prefijo, porque "sat" coincidía con cada `SatXxx` de los catálogos de Carta Porte; fuzzy solo para términos de 5 letras o más. El extracto se ancla en la primera coincidencia exacta, así "CFDI40147" muestra su fila de la tabla. Paquete de 342.5 kB (1.8 MB desempaquetado) y arranque de 0.6 s. +El fixture `docs-corpus.json.gz` sale del build local de fapi-docs@73d326f, porque live todavía da 404 en las `.md` por sección. Después del deploy se regenera con `scripts/snapshot-docs.mjs`, y MCP-03 genera `data/` desde docs.fiscalapi.com. + +### MCP-03 — Agregar el workflow de build y publicación por hash +**Objetivo:** que el paquete se regenere solo cuando cambian el OpenAPI o las docs, sin que nadie lo mantenga. +**Depende de:** MCP-02, U-02. +**Skills:** `fapi-release` (CI/CD sin pasos manuales), `context7-mcp` (documentación de npm trusted publishing). +**Modelo sugerido (opcional):** opus. Publica en un registro público. +**Contexto:** D-02, D-14. Trusted publishing exige un runner hospedado por GitHub, `id-token: write`, npm ≥11.5.1 y Node ≥22.14 (docs.npmjs.com/trusted-publishers, a revalidar en la tarea). +**Alcance:** +- Workflow en push a `main` (ruta `mcp/**`), semanal y manual: + 1. build; + 2. comparar el hash de la fuente con el de la última versión publicada; + 3. si cambió, subir el patch y publicar con provenance. +- El workflow queda deshabilitado para publicar (`if:` con una variable del repo) hasta que el usuario decida U-04. +- Verificar en la documentación de npm si la primera publicación puede hacerse por OIDC o requiere una publicación manual del usuario, y dejar el paso exacto en las Notas. +- Dependabot para `mcp/package.json`. +**Criterios de aceptación (todos con `→ verificar`):** +- El build sin cambios no publica → verificar: corrida manual del workflow con publicación en dry-run; el log dice "sin cambios". +- Con un cambio simulado en la fuente, el dry-run publicaría una versión nueva → verificar: corrida con un hash forzado y `npm publish --dry-run` en el log. +**Pruebas focalizadas:** ninguna (no aplica). La prueba es el dry-run. +**Fuera de alcance:** publicar de verdad (U-04). +**Notas:** diff --git a/docs/ai-integracion/medicion.md b/docs/ai-integracion/medicion.md new file mode 100644 index 0000000..5733ca2 --- /dev/null +++ b/docs/ai-integracion/medicion.md @@ -0,0 +1,66 @@ +# Integración asistida por IA: medición + +Los resultados de estas tareas van a `requisitos.md` → **Contexto medido**, con su fuente y sin datos personales (solo conteos y porcentajes). No se crea un archivo de resultados aparte. + +### MED-01 — Medir mezcla de tipos y complementos en el restore de prod +**Objetivo:** saber qué tipos de CFDI y qué complementos timbran los clientes, para ordenar SK-04 a SK-09 y las evals. +**Depende de:** — +**Skills:** `fapi-investigate`, `fapi-local-env` (restore y sqlcmd local). +**Modelo sugerido (opcional):** haiku. Son consultas de lectura, sin decisiones. +**Contexto:** D-03; la métrica norte en `requisitos.md`. +**Alcance:** +- Confirmar que el restore local es de producción (`backupset.server_name`). +- Contar, en los últimos 12 meses y por mes, las facturas timbradas por `Invoices.TypeCode`. +- Contar las facturas con fila 1:1 en `Payroll`, `CartaPorte`, `ComercioExterior`, `InvoicePayment` y `GlobalInformation`. +- Contar los tenants distintos que usan cada tipo o complemento. +**Criterios de aceptación (todos con `→ verificar`):** +- El origen del restore está confirmado → verificar: `SELECT TOP 1 server_name, backup_finish_date FROM msdb.dbo.backupset WHERE database_name='WebApi' ORDER BY backup_finish_date DESC`. +- `requisitos.md` tiene una tabla de tipos y complementos con volumen, tenants y el orden resultante → verificar: `grep -n "Mezcla de tipos" docs/ai-integracion/requisitos.md`. +- El orden de SK-04 a SK-09 y de los casos de EV-01 y EV-02 queda ajustado en el tablero si la medición lo cambia → verificar: revisar el diff del README. +**Pruebas focalizadas:** ninguna (no aplica). +**Fuera de alcance:** errores del PAC (MED-03); embudo de Test (MED-02). +**Notas:** +- Hechos corregidos: Nómina y Pagos viven en `Payrolls` e `InvoicePayments` (1:1 por índice único en `InvoiceId`); Información global no es tabla sino columnas `Invoices.GlobalInformation_*`. +- Restore aparte `WebApi_ProdSnap_20261014` (READ_ONLY), borrado al terminar. Comercio Exterior: 0 facturas desde siempre; el orden de skills no cambia. + +### MED-02 — Medir embudo y tiempo al primer timbre en el restore de Test +**Objetivo:** fijar la línea base de la métrica norte y medir cuántas cuentas abandonan en el paso de compra de prueba (para revisar D-09). +**Depende de:** — +**Skills:** `fapi-investigate`, `fapi-local-env`. +**Modelo sugerido (opcional):** opus. Hay que interpretar el embudo y separar las cuentas internas de las reales. +**Contexto:** D-09; la métrica norte en `requisitos.md`. +**Alcance:** +- Restaurar en local el respaldo de Test (origen en la memoria local `project_local_credentials`) y confirmar su `server_name`. +- Por cohorte semanal de los últimos 6 meses: + - owners creados; + - con correo confirmado; + - con suscripción de prueba activa; + - con primer `Invoice` timbrado; + - mediana de minutos desde la creación hasta el primer timbre; + - porcentaje que timbra en menos de 24 h. +- Excluir las cuentas internas (fixtures `rl-test-*`, dominios propios) con un criterio explícito. +**Criterios de aceptación (todos con `→ verificar`):** +- `requisitos.md` tiene la tabla del embudo, la línea base de la métrica norte y el criterio de exclusión → verificar: `grep -n "Embudo de Test" docs/ai-integracion/requisitos.md`. +- El abandono en el paso de suscripción queda cuantificado y llevado al usuario como dato para D-09 → verificar: la cifra aparece en las Notas de la tarea. +**Pruebas focalizadas:** ninguna (no aplica). +**Fuera de alcance:** cambiar el onboarding (D-09). +**Notas:** +- Dato para D-09: 215 de 378 owners confirmados (56.9 %) nunca hacen la compra de prueba, y nadie timbra sin ella. Línea base: mediana de 1,424 min al primer timbre, 15.4 % en menos de 24 h. +- "Suscripción" se midió como cualquier fila en `Features` (desde 2026-10-05 casi no hay `Type = 'Base'`). Restore aparte `WebApi_TestSnap` (READ_ONLY), borrado al terminar. + +### MED-03 — Medir el top de rechazos del PAC en Seq +**Estado:** fuera de alcance por decisión del usuario (D-20); se quitó del tablero junto con U-01. +**Objetivo:** saber qué códigos de rechazo del PAC son los más frecuentes, para dar prioridad a `fiscalapi-diagnostico` y a los casos de las evals. +**Depende de:** U-01 (el usuario abre el túnel a Seq de producción). +**Skills:** `fapi-investigate`. +**Modelo sugerido (opcional):** haiku. +**Contexto:** los rechazos no se guardan en la BD; solo quedan en Seq con las propiedades `PacCode` y `TypeCode` (log de `InvoiceIssuanceService`). +**Alcance:** +- En Seq, por el túnel, sacar los 30 `PacCode` más frecuentes de los últimos 90 días, con su `TypeCode` y el número de tenants afectados. +- Cruzarlos con las tablas `/errors/*.md` de las docs para confirmar que cada código tiene página. +**Criterios de aceptación (todos con `→ verificar`):** +- `requisitos.md` tiene el top 30 con su volumen y si tiene página en las docs → verificar: `grep -n "Top de rechazos" docs/ai-integracion/requisitos.md`. +- Los códigos sin página quedan como Notas de DOC-02 o como "Fuera de alcance" → verificar: revisar el diff. +**Pruebas focalizadas:** ninguna (no aplica). +**Fuera de alcance:** guardar los rechazos en la BD. +**Notas:** diff --git a/docs/ai-integracion/requisitos.md b/docs/ai-integracion/requisitos.md new file mode 100644 index 0000000..47d3b61 --- /dev/null +++ b/docs/ai-integracion/requisitos.md @@ -0,0 +1,170 @@ +# Integración asistida por IA: requisitos + +## Contexto medido +Fecha de las mediciones: 2026-10-14. + +**Lo que FiscalAPI ya tiene** +- docs.fiscalapi.com (Next.js y MDX) publica `llms.txt` y una versión `.md` de cada página (agregando `.md` a la URL). +- No hay `llms-full.txt`, ni OpenAPI público, ni MCP, ni skills para integradores. Las skills de `fapi-reusables/ai/skills` son todas internas. + +**OpenAPI** +- fapi-back genera el documento con `Microsoft.AspNetCore.OpenApi` y Scalar, pero `MapOpenApi` solo se registra en Development (`WebConfigurePipeline`). +- El documento se registra con un `BuildServiceProvider` intermedio (`Infrastructure/DependencyInjection`) y expone también el grupo v5 (`Controllers/V5/ValuesController`). +- En nginx test y live, `/openapi` cae en `location /` (el front). +- Pocos controllers V4 tienen `EndpointSummary` o `EndpointDescription`. + +**Superficie pública** +- La colección `fapi-back/postman/collections/Fiscalapi API Reference` tiene 301 requests: 107 pares MÉTODO+ruta y 106 acciones. pdf y xml del portal son una sola acción, `downloads/{downloadToken}/{format}`. +- Todos los pares tienen acción en `WebApi.App/Controllers/V4`. +- En esos mismos controllers hay 52 acciones fuera de la colección: + - `load`, `all` y `lookup` de DevExtreme; + - Account (salvo register y login); + - ProductsForSale, ProductTaxes, `webapp/manifests`; + - WebhookSecrets y WebhookCliListener. + +**Spec generado desde Postman** +- Está en `fapi-back/postman/specs/Fiscalapi Specification/Fiscalapi Specification.yaml`, sin rastrear en git: OpenAPI 3.1, 107 operaciones. +- Le falta para ser contrato: + - el servidor es `{{baseUrl}}`; + - los path params van literales (``); + - hay rutas con valores fijos; + - hay un solo ejemplo por método y ruta; + - no tiene `components/schemas` de los DTO ni `operationId`. +- Su `info.description` sí es útil. + +**Tamaño de las docs** +- 52 páginas, 427 secciones H2 y 2.3 MB de MDX (unos 600k tokens). +- La página `invoices-by-value` pesa 647 KB y su sección "Modelo factura", 156 KB. + +**Prueba de búsqueda** (desechable, mismas 14 a 16 consultas de un integrador) +- Sobre las 107 operaciones, MiniSearch 7.2.0 y Orama 3.1.18 con stemmer en español fallan en las consultas fiscales: "complemento de pago", "crear cliente receptor", "estatus ante el SAT", "descarga masiva". +- Los embeddings locales (`Xenova/multilingual-e5-small` q8, transformers.js 4.3.1) descargan 130 MB de modelo, tardan 14 s en cargar y fallan en lo mismo. +- Sobre las secciones H2 de las docs, MiniSearch con stemmer y stopwords en español (`@orama/stemmers`, `@orama/stopwords`) acierta 14 de 16. +- El catálogo de las 107 operaciones (MÉTODO, ruta y resumen) ocupa unos 6.5k caracteres, alrededor de 1,600 tokens. + +**Mercado** +- Ningún PAC ni API mexicana de CFDI publica un MCP oficial que timbre. +- Sobre Facturapi hay MCP de la comunidad (`@codespar/mcp-facturapi`, con 20 herramientas). +- Gigstack tiene un MCP de docs y un CLI con `context --json`; Alegra anuncia un MCP de solo lectura. +- Los MCP generados desde OpenAPI que corren en local (Stainless con dynamic tools, @ivotoby/openapi-mcp-server) listan el catálogo completo en vez de buscar. Los que usan embeddings (Speakeasy Gram, el buscador de docs de Stainless) son hosteados. + +**npm** +- `@fiscalapi/mcp` no existe (404). +- Los paquetes `fiscalapi` y `fiscalapi-csf` los publica el usuario npm `fiscalapi`. +- El SDK de MCP v2 (`@modelcontextprotocol/server`) se publicó el 2026-07-27 con la spec 2026-07-28. +- Node 20 ya no tiene soporte. +- npm trusted publishing exige un runner hospedado por GitHub. + +**Mezcla de tipos y complementos** (MED-01) +- Fuente: restore local de producción (`backupset.server_name` = `649fafb1cc93`, corte 2026-10-15 01:00 UTC), facturas con `CreatedAt` del 2025-10-15 al corte. Todas tienen `Uuid`: 60,179 timbres de 100 tenants. +- Volumen y tenants: + + | Tipo o complemento | Facturas | Tenants | Tenant mayor | Skill | + |---|---|---|---|---| + | Ingreso (`I`), sin Carta Porte | 38,828 | 97 (con `E`) | 41.5 % de `I` | SK-04 | + | Egreso (`E`) | 252 | 9 | 37.3 % | SK-04 | + | Pagos (`P`, fila en `InvoicePayments`) | 6,441 | 27 | 65.7 % | SK-05 | + | Nómina (`N`, fila en `Payrolls`) | 14,481 | 9 | 82.3 % | SK-07 | + | Carta Porte (`I` con fila en `CartaPortes`) | 177 | 2 | — | SK-08 | + | Comercio Exterior (`ComercioExteriores`) | 0 (0 desde siempre) | 0 | — | SK-09 | + | Información global (`Invoices.GlobalInformation_*`) | 46 | 11 | — | SK-04 | + +- Por mes, `I` crece de 51 (2025-10) a 10,263 (2026-09); `P` de 2 a 2,112; `N` sube a 4,636 en 2026-09. Carta Porte aparece en 2026-07 (14 a 75 al mes). +- Información global es una columna nueva (primer registro 2026-10-10), así que 46 subestima su uso. No hay tabla de Impuestos Locales. +- **Orden resultante:** ingreso-egreso, pagos, nómina, carta porte y comercio exterior. Pagos va antes que nómina porque la usan 3 veces más tenants (27 contra 9) y el 82 % de la nómina es de un solo tenant. Coincide con el orden de SK-04 a SK-09 y con EV-01 (ingreso y pagos) y EV-02 (nómina, carta porte y comercio exterior), así que el tablero no cambia. + +**Embudo de Test** (MED-02) +- Fuente: restore local de Test (`backupset.server_name` = `83f15e2487c3`, corte 2026-10-15 01:00 UTC). Cohortes semanales (lunes) del 2026-04-13 al corte; la última semana está incompleta. +- Owner = persona con `Id = CreatedBy`. Confirmado = `EmailConfirmed`. Compra de prueba = cualquier fila en `Features` del tenant. Desde 2026-10-05 casi no hay filas `Type = 'Base'` aunque sí compras, así que se cuenta cualquier tipo. Primer timbre = primer `Invoices.CreatedAt` del tenant. +- **Criterio de exclusión:** personas de sistema (`-1`, `0`, `1`) y correos `@fiscalapi.com`, `@mailinator.com`, `rl-test-*`, los que contienen `test`, `prueba` o `e2e` y el correo del equipo. Quedan 397 de 404 owners. +- Embudo de las 26 semanas: + + | Paso | Owners | De los owners | Del paso anterior | + |---|---|---|---| + | Creados | 397 | 100 % | — | + | Con correo confirmado | 378 | 95.2 % | 95.2 % | + | Con compra de prueba | 164 | 41.3 % | 43.4 % | + | Con primer timbre | 121 | 30.5 % | 73.8 % | + | Timbran en menos de 24 h | 61 | 15.4 % | 50.4 % | + +- Ningún owner timbró sin compra de prueba ni antes de ella: la compra es la puerta del primer timbre. **215 de 378 confirmados (56.9 %) nunca compran.** La mediana a la compra es de 333 min. +- **Línea base de la métrica norte:** mediana de 1,424 min (23.7 h) de la creación al primer timbre, y 15.4 % de las cuentas nuevas timbra en menos de 24 h. +- Por cohorte, la mediana va de 87 a 28,730 min. Con 5 a 23 owners por semana, la cifra semanal es ruidosa; se sigue la de 4 semanas móviles. + + | Semana | Owners | Confirmados | Compra | Timbran | < 24 h | Mediana (min) | + |---|---|---|---|---|---|---| + | 2026-04-13 | 12 | 11 | 4 | 4 | 3 | 618 | + | 2026-04-20 | 5 | 5 | 1 | 1 | 0 | 5,958 | + | 2026-04-27 | 18 | 18 | 11 | 9 | 2 | 8,706 | + | 2026-05-04 | 5 | 5 | 4 | 4 | 3 | 314 | + | 2026-05-11 | 10 | 10 | 1 | 0 | 0 | — | + | 2026-05-18 | 20 | 19 | 9 | 6 | 4 | 715 | + | 2026-05-25 | 14 | 13 | 6 | 3 | 2 | 135 | + | 2026-06-01 | 12 | 11 | 3 | 2 | 1 | 3,431 | + | 2026-06-08 | 18 | 17 | 9 | 6 | 2 | 9,147 | + | 2026-06-15 | 15 | 15 | 4 | 4 | 1 | 28,731 | + | 2026-06-22 | 11 | 11 | 8 | 8 | 1 | 15,954 | + | 2026-06-29 | 11 | 10 | 4 | 4 | 2 | 3,078 | + | 2026-07-06 | 11 | 10 | 3 | 2 | 0 | 15,139 | + | 2026-07-13 | 17 | 17 | 7 | 5 | 3 | 798 | + | 2026-07-20 | 14 | 14 | 4 | 4 | 2 | 1,281 | + | 2026-07-27 | 14 | 14 | 6 | 6 | 4 | 87 | + | 2026-08-03 | 23 | 22 | 8 | 7 | 2 | 3,853 | + | 2026-08-10 | 15 | 15 | 8 | 6 | 4 | 888 | + | 2026-08-17 | 15 | 14 | 8 | 5 | 2 | 2,794 | + | 2026-08-24 | 15 | 14 | 8 | 4 | 2 | 727 | + | 2026-08-31 | 20 | 16 | 6 | 5 | 3 | 703 | + | 2026-09-07 | 18 | 17 | 8 | 4 | 3 | 207 | + | 2026-09-14 | 18 | 17 | 9 | 5 | 3 | 1,119 | + | 2026-09-21 | 18 | 17 | 7 | 4 | 2 | 3,728 | + | 2026-09-28 | 17 | 17 | 7 | 4 | 2 | 1,623 | + | 2026-10-05 | 17 | 15 | 6 | 5 | 4 | 249 | + | 2026-10-12 (incompleta) | 14 | 14 | 5 | 4 | 4 | 128 | + +**Fuera de alcance:** el top de rechazos del PAC (MED-03), por decisión del usuario (D-20). + +## Requisitos funcionales +- **RF-01 — OpenAPI público.** `GET https://{test,live}.fiscalapi.com/api/openapi/v4.json` responde sin autenticación con exactamente las 100 acciones de D-05. Cada operación tiene `operationId` estable y servidores reales. + Aceptación: la prueba de equivalencia con la colección pasa y `curl` a ambos ambientes responde 200. +- **RF-02 — Docs por sección.** Cada sección H2 de las docs tiene su versión `.md` propia, generada en el build. + Aceptación: `curl` a la `.md` de una sección devuelve solo esa sección. +- **RF-03 — Errores de Pagos y Comercio Exterior.** Las docs publican las tablas de errores de Pagos 2.0 y Comercio Exterior 2.0, igual que las de CFDI 4.0, Nómina y Carta Porte. + Aceptación: las dos rutas `.md` responden 200. +- **RF-04 — Skills.** Las 8 skills y los 3 comandos de D-03 se instalan como plugin en Claude Code, Codex, Cursor y Copilot. + Aceptación: instalación desde el repo en los 4 hosts. +- **RF-05 — MCP stdio.** `npx -y @fiscalapi/mcp` expone `listar_operaciones`, `obtener_operacion` y `buscar_docs` sin usar red en runtime. + Aceptación: las 16 consultas de regresión de `buscar_docs` pasan. +- **RF-06 — Evals.** Cada caso CFDI se corre en 3 variantes (sin nada, con skills, con skills más MCP), calificadas por un UUID real en test. + Aceptación: el reporte de EV-03 incluye la tasa de éxito y los tokens por caso y variante. + +## Requisitos no funcionales +- **RNF-01 — Cero mantenimiento adicional (D-02).** Nada hosteado. Todo lo derivado (OpenAPI, `.md` por sección, índice del MCP) se genera solo desde el código o las docs. Las skills enlazan, no copian. +- **RNF-02 — Compatibilidad v4.** Publicar el OpenAPI no cambia ningún contrato: solo agrega metadatos y una ruta de lectura. +- **RNF-03 — Sin exposición de lo interno.** Ninguna ruta Host, solo JWT, InternalKey, `webapp/`, de facturación del dashboard, register o login aparece en el OpenAPI. Lo garantiza la prueba de equivalencia. +- **RNF-04 — Seguridad de llaves.** Las skills y el MCP nunca piden ni guardan llaves live. El MCP no invoca la API (D-07). +- **RNF-05 — Presupuesto de contexto.** + - `SKILL.md` en menos de 500 líneas. + - La salida de una herramienta del MCP en menos de unos 10k tokens. + - `listar_operaciones` completo en menos de unos 3k tokens. +- **RNF-06 — Arranque del MCP.** Menos de 1 s y funcionando sin conexión: el índice va dentro del paquete y no se descarga ningún modelo. +- **RNF-07 — Español.** Skills, herramientas, descripciones y mensajes en español (D-04). + +## Métrica norte +- Mediana de minutos entre la creación del owner y su primer `Invoice` timbrado en la BD de Test, por cohorte semanal. +- Porcentaje de cuentas nuevas que timbran en menos de 24 h. + +La línea base la fija MED-02. Se mide sin telemetría nueva. + +## Preguntas abiertas +- **Q-01 — Marketplaces oficiales.** ¿Se solicita la inclusión en `claude-plugins-official` y en el catálogo curado de Codex, además del marketplace propio del repo? + - Opciones: (a) solo el marketplace propio; (b) los dos. + - Recomendación: (a) hasta tener el reporte de EV-03, porque la inclusión se pide con evidencia de calidad. + - Bloquea: U-04. + +## Fuera de alcance +- **MCP remoto u hosteado** (D-02): exige operar un servicio. +- **Sandbox sin "compra"** (D-09): requiere código nuevo en billing. Se revisa con los datos de MED-02. +- **CSD de prueba propios** (D-10): habría que renovarlos al vencer. Se sigue enlazando el ZIP de SW. +- **`llms-full.txt`** (D-15): unos 600k tokens, no le sirve a un agente. +- **Búsqueda semántica con embeddings:** medida sin ganancia (ver Contexto medido). +- **Herramienta `invocar` en el MCP** (D-07). diff --git a/docs/ai-integracion/skills.md b/docs/ai-integracion/skills.md new file mode 100644 index 0000000..a12236c --- /dev/null +++ b/docs/ai-integracion/skills.md @@ -0,0 +1,211 @@ +# Integración asistida por IA: skills y plugin + +Todo va en fiscalapi-ai, en la rama `feat/ai-integracion`. Reglas comunes a SK-02 a SK-09: +- **Formato agentskills.io:** + - `name` igual al nombre de la carpeta; + - `description` de 1024 caracteres como máximo, que diga qué hace la skill, cuándo usarla y cuándo no; + - `SKILL.md` en menos de 500 líneas. +- **D-03:** `references/` solo tiene índices de enlaces a las `.md` (de página o de sección) y árboles de decisión. Nada de payloads, tablas de catálogos ni mensajes de error copiados. +- **Fuentes permitidas:** + - las `.md` de docs.fiscalapi.com; + - `/api/openapi/v4.json`; + - las herramientas de `@fiscalapi/mcp` (opcionales: la skill funciona sin MCP); + - la API de test con la llave del integrador (`GET /api/v4/catalogs/{name}/{search}`, `POST /api/v4/invoices/calculate`). +- **Reglas de seguridad en todas:** nunca pedir ni escribir una llave live en el código; las llaves van en variables de entorno. + +### SK-01 — Armar el esqueleto del repo, los manifiestos y el CI de enlaces +**Objetivo:** que el repo se instale como plugin en Claude Code, Codex, Cursor y Copilot, y que un enlace roto a las docs haga fallar el CI. +**Depende de:** — +**Skills:** `superpowers:writing-skills`, `anthropic-skills:skill-creator`, `plugin-authoring`. El formato de cada manifiesto se toma del repo de referencia que ya lo usa (stripe/ai, dotnet/skills, DevExpress/agent-skills), revisado con `gh api`. +**Modelo sugerido (opcional):** opus. Fija la estructura de la que dependen todas las tareas. +**Contexto:** D-03, D-12, RF-04. +**Alcance:** +- Estructura: `skills/`, `.claude-plugin/marketplace.json` y `plugin.json`, `.codex-plugin/`, `.cursor-plugin/`, `.github/plugin/`, `LICENSE` y README de instalación con el comando de cada host. +- CI (GitHub Actions, runner hospedado por GitHub) en cada PR y una vez por semana: + - valida el frontmatter contra el estándar; + - extrae cada URL de docs.fiscalapi.com de `skills/**`; + - exige 200 en cada URL y que cada ancla exista. +- El MCP **no** se declara en los manifiestos todavía; eso depende de U-04 (D-07). +**Criterios de aceptación (todos con `→ verificar`):** +- Se instala en Claude Code desde la ruta local → verificar: `/plugin marketplace add ./` y `/plugin install`; `/plugin` lo muestra. +- Los manifiestos de Codex, Cursor y Copilot son válidos según el formato de referencia → verificar: diff de estructura contra el repo de referencia, más la instalación local en los hosts disponibles. +- El CI falla con un ancla rota → verificar: un PR de prueba con un ancla inventada sale en rojo. +**Pruebas focalizadas:** ninguna (no aplica). La prueba es el CI de enlaces. +**Fuera de alcance:** el contenido de las skills (SK-02 en adelante). +**Notas:** +- Manifiestos según D-18. Verificado: `claude plugin validate . --strict` en verde; instalación local en Claude Code 2.1.300 con una skill temporal (descubierta sin declararla, 0 MCP), desinstalada después. Codex, Cursor y Copilot no están instalados aquí: se validó su formato contra dotnet/skills, DevExpress/agent-skills y el código de `openai/codex`. +- `scripts/check-skills.mjs` (frontmatter, 200 y anclas) sale en 1 con un ancla inventada, una página 404 o un `name` inválido (fixture local). El PR de prueba en rojo necesita push: es del usuario. La instalación en Cursor es local (`~/.cursor/plugins/local`); su marketplace es parte de U-04. + +### SK-02 — Escribir `fiscalapi-integracion` y `primer-timbre` +**Objetivo:** que en un proyecto vacío el agente instale el SDK correcto, configure ambiente y llaves, y logre un UUID en test. +**Depende de:** SK-01, DOC-01. +**Skills:** `superpowers:writing-skills`, `fapi-cfdi-rules` (para no decir nada falso del CFDI). +**Modelo sugerido (opcional):** opus. Es la skill más usada y fija el tono de todas. +**Contexto:** D-01, D-03, D-09, D-10. +**Alcance:** +- `fiscalapi-integracion`, con: + - tabla de ruteo "qué vas a construir → qué skill y qué `.md` de sección"; + - ambientes y headers (`X-API-KEY`, `X-TENANT-KEY`, `X-TIME-ZONE`); + - elección de SDK según el lenguaje del proyecto; + - modo por valores o por referencias; + - flujo actual para obtener llaves de test (D-09) y datos de prueba (D-10); + - regla crítica: un 5xx o timeout puede significar ya timbrado, así que se consulta el estado antes de reintentar. +- Comando `primer-timbre`: detecta el lenguaje, instala el SDK, lee las llaves de variables de entorno y timbra una factura de ingreso en test. +**Criterios de aceptación (todos con `→ verificar`):** +- En un proyecto Node vacío y en uno .NET vacío, `primer-timbre` produce un UUID en test → verificar: correrlo con una llave de test local y consultar el UUID con `GET /api/v4/invoices/{id}`. +- La skill se dispara sola con "integra FiscalAPI" → verificar: el log de la sesión de Claude Code muestra la skill cargada. +- El CI de enlaces pasa → verificar: CI en verde. +**Pruebas focalizadas:** ninguna (no aplica). Lo cubre EV-01. +**Fuera de alcance:** complementos (SK-04 en adelante). +**Notas:** +- `primer-timbre` dio UUID en Node (`6b92a718-…`) y en .NET (`fba6dd24-…`), confirmados con `GET /invoices/{id}`. Corrió contra el back local (PAC de pruebas) con una cuenta creada por el producto (registro, Mailpit y Stripe test). La skill se dispara con "Quiero integrar FiscalAPI…". +- Las URL `.md` por sección (DOC-01) dan 404 en live hasta el deploy de fapi-docs. El check pasa contra el build local (138 enlaces). `references/sdks.md` documenta las trampas reales de los README: Node `FiscalApiClient`, `double` en Java y fechas. +- Con resultado incierto (5xx o timeout), el back no guarda la factura, así que "Listar facturas" no la muestra y el camino real es soporte con el `traceIdentifier`. La regla de la skill sigue a las docs: falta decidir si el back debe registrarla. + +### SK-03 — Escribir `fiscalapi-diagnostico` y `explicar-error` +**Objetivo:** que ante una respuesta de error el agente distinga una validación de FiscalAPI, un rechazo del PAC o un 5xx, y proponga la corrección leyendo la tabla correcta. +**Depende de:** SK-01, DOC-02. +**Skills:** `superpowers:writing-skills`, `fapi-cfdi-rules`. +**Modelo sugerido (opcional):** opus. +**Contexto:** D-03. El top de rechazos del PAC (MED-03) quedó fuera de alcance (D-20). +**Alcance:** +- Árbol de decisión: + - 400 con `propertyName` → validación; + - códigos `CFDI40*`, `CRP*`, `NOM*`, `CCE*`, `CP*` → la `.md` de `/errors/…` que corresponda; + - 5xx o timeout → consultar el estado antes de reintentar. +- Comando `explicar-error `. +**Criterios de aceptación (todos con `→ verificar`):** +- Con tres respuestas reales de ejemplo (una de cada tipo), `explicar-error` llega a la página y la fila correctas → verificar: transcript de la sesión. +- El CI de enlaces pasa → verificar: CI en verde. +**Pruebas focalizadas:** ninguna (no aplica). +**Fuera de alcance:** guardar o consultar rechazos históricos. +**Notas:** +- Contrato verificado en fapi-back: un rechazo del PAC responde el status del PAC con `message` = `"CÓDIGO - texto"`, `details` = detalle del PAC y `data` = null (`InvoiceIssuanceTests`, `PacRejectionCodeTests`). Las respuestas de prueba se armaron con ese contrato y la fila real CFDI40147, sin timbrar contra el PAC. `claude -p /fiscalapi:explicar-error` llegó a la página y la fila correctas en los 3 casos. +- `errors/pagos-20` aún no está publicada: el CI sale en rojo hasta desplegar fapi-docs (DOC-02). Contra el build local pasa (`FISCALAPI_DOCS_ORIGIN=http://localhost:3100`). Sin tabla de `CCE` (DOC-04), la skill usa el texto del rechazo. Solo los H2 tienen ancla en el sitio. + +### SK-04 — Escribir `fiscalapi-ingreso-egreso` +**Objetivo:** que el agente arme bien una factura de ingreso, una nota de crédito y una factura global. +**Depende de:** SK-02, MED-01. +**Skills:** `superpowers:writing-skills`, `fapi-cfdi-rules`. +**Modelo sugerido (opcional):** opus. +**Contexto:** D-03. El orden de las secciones lo fija MED-01. +**Alcance:** +- Decisiones de régimen, uso CFDI, objeto de impuesto, método y forma de pago, factura global y relación de la nota de crédito. +- Cada decisión enlaza a su sección `.md`. +- Las claves del SAT se buscan con la API de catálogos de test. +**Criterios de aceptación (todos con `→ verificar`):** +- Con los prompts de ingreso, nota de crédito y global, el agente timbra en test → verificar: UUID y estado consultado (caso de EV-01). +- El CI de enlaces pasa → verificar: CI en verde. +**Pruebas focalizadas:** ninguna (no aplica). +**Fuera de alcance:** — +**Notas:** +- Timbrado en el back local (PAC de pruebas) con el plugin. Ingreso PUE `e84b01e8-…`. Nota de crédito `fb6129f0-…`: el XML trae `TipoRelacion="01"` al ingreso. Global mensual `c4405f7f-…` (XAXX010101000, 616, S01, CP igual al de expedición). Estatus consultado (simulado fuera de Production). +- Sin las skills del usuario (`--setting-sources project,local`), "factura global" dispara `fiscalapi-ingreso-egreso`. Con ellas, el caso global cargó `fapi-cfdi-rules`, una skill personal: las pruebas de disparo van aisladas. +- Lo que el back no valida antes del PAC: PPD con forma distinta de 99, y periodicidad 05 o meses 13-18 en la global; además, con `taxObjectCode` 06 u 08 descarta `itemTaxes`. `GET invoices/{id}?details=true` no devuelve `relatedInvoices`, aunque el XML sí los trae. La docs (`invoices-by-value:5943`) etiqueta `E` como "Ingreso". + +### SK-05 — Escribir `fiscalapi-pagos` +**Objetivo:** que el agente emita un REP 2.0 correcto para facturas PPD y parcialidades. +**Depende de:** SK-02, MED-01. +**Skills:** `superpowers:writing-skills`, `fapi-cfdi-rules`. +**Modelo sugerido (opcional):** opus. +**Contexto:** D-03; errores `CRP*` (DOC-02). +**Alcance:** PUE frente a PPD, saldo anterior e insoluto, parcialidades, tipo de cambio y equivalencia, impuestos del pago; cada punto con su sección `.md`. +**Criterios de aceptación (todos con `→ verificar`):** +- El agente timbra en test una factura PPD y su complemento de pago → verificar: los dos UUID y su estado (caso de EV-01). +- El CI de enlaces pasa → verificar: CI en verde. +**Pruebas focalizadas:** ninguna (no aplica). +**Fuera de alcance:** — +**Notas:** +- Timbrado con el plugin, sin skills del usuario. PPD `99b472e9-…` (PPD/99, total 2320). Pago `60fc947f-…`: tipo P, XXX, CP01, sin forma ni método en la raíz; DoctoRelacionado a la PPD, parcialidad 1 (2320 → 1160 → 1160) y BaseDR proporcional de 1000. Estatus consultado (simulado). +- Defecto encontrado: `FechaPago` sale en UTC (18:58) mientras `Fecha` sale en hora local (12:58). Quedó en `known-issues.md` de fapi-back (`fapi-back@c9d1baff`), con prioridad alta. +- FiscalAPI no busca la factura PPD ni calcula saldos, y no rechaza forma o método de pago en la raíz de un P (lo hace el PAC). Las docs llaman `amount` al importe pagado del documento, pero el campo real es `paymentAmount`. + +### SK-06 — Escribir `fiscalapi-ciclo-de-vida` y `revisar-integracion` +**Objetivo:** que el agente cubra lo que pasa después de timbrar (cancelar, consultar estado, XML y PDF, correo y webhooks) y revise una integración existente. +**Depende de:** SK-02. +**Skills:** `superpowers:writing-skills`, `fapi-cfdi-rules`. +**Modelo sugerido (opcional):** haiku para el borrador; opus revisa. +**Contexto:** D-03. `fiscalapi-cli` cubre webhooks en local (`listen`, `trigger`); la skill enlaza a su página de docs. +**Alcance:** +- Skill con cancelación (motivos y sustitución), estado ante el SAT, descarga de XML y PDF, envío por correo y verificación de la firma de webhooks. +- Comando `revisar-integracion`: busca llaves `sk_live_` o llaves en código, falta de `X-TIME-ZONE`, reintentos a ciegas tras un 5xx y webhooks sin verificar la firma. +**Criterios de aceptación (todos con `→ verificar`):** +- `revisar-integracion` detecta 4 defectos plantados en un proyecto de prueba → verificar: transcript con los 4 hallazgos. +- El CI de enlaces pasa → verificar: CI en verde. +**Pruebas focalizadas:** ninguna (no aplica). +**Fuera de alcance:** descarga masiva (se evalúa con MED-01 si merece su propia skill). +**Notas:** +- `revisar-integracion`, sin skills del usuario, encontró en un proyecto de prueba los 4 defectos plantados: llave `sk_live_` y tenant en `config.js`, falta de `X-TIME-ZONE`, reintento a ciegas tras un 5xx y webhook sin verificar la firma. Agregó otros 2, también reales: URL `live` en el código y `succeeded` sin revisar. +- El prefijo `sk_live_` sí es de FiscalAPI: `ApiKeyValueGenerator.cs` genera `sk_live_`, `sk_test_` o `sk_development_` más un GUID con guiones bajos. La firma de webhooks es Standard Webhooks (HMAC-SHA256 sobre el cuerpo crudo). +- Sin resolver: las docs (`webhooks/content.mdx:13`) dicen que sin `X-TIME-ZONE` las fechas de webhooks vienen en UTC con `Z`, pero el middleware usaría `America/Mexico_City`. La skill solo recomienda mandar siempre el header. + +### SK-07 — Escribir `fiscalapi-nomina` +**Objetivo:** que el agente emita una nómina 1.2 correcta (empleador, empleado, percepciones y deducciones). +**Depende de:** SK-02, MED-01. +**Skills:** `superpowers:writing-skills`, `fapi-cfdi-rules`. +**Modelo sugerido (opcional):** opus. +**Contexto:** D-03; errores `NOM*` (página existente). +**Alcance:** datos de empleador y empleado en la persona, tipos de nómina, percepciones, deducciones, otros pagos e incapacidades; cada punto con su sección `.md`. +**Criterios de aceptación (todos con `→ verificar`):** +- El agente timbra en test una nómina ordinaria → verificar: UUID y estado (caso de EV-02). +- El CI de enlaces pasa → verificar: CI en verde. +**Pruebas focalizadas:** ninguna (no aplica). +**Fuera de alcance:** — +**Notas:** +- Timbrado con el plugin, sin skills del usuario: nómina ordinaria quincenal `a153d832-…`, de EKU9003173C9 (registro patronal B5510768108) a FUNK671228PH6. El XML no trae `FormaPago`; trae receptor 605/CN01, contrato 01 con régimen 02, `P112W`, ISR 520 en impuestos retenidos y subsidio 002 en 0. Estatus consultado (simulado). +- Riesgo del back: no limpia `paymentFormCode` en N (`ComprobanteBuilder.cs:15`) y no fuerza 605/CN01, así que la skill pide no mandar el primero y sí mandar los dos últimos. Los ejemplos de C# y Python de las docs usan 601/G01 y `PaymentFormCode`, y el cURL "por referencias" usa datos por valores. +- Las guías `md-version` de nómina en sat-docs están vacías: las reglas salen del PDF y de la matriz `errors/nomina-12`. + +### SK-08 — Escribir `fiscalapi-carta-porte` +**Objetivo:** que el agente emita una Carta Porte 3.1 correcta (ubicaciones, mercancías y autotransporte). +**Depende de:** SK-02, MED-01. +**Skills:** `superpowers:writing-skills`, `fapi-cfdi-rules`. +**Modelo sugerido (opcional):** opus. +**Contexto:** D-03; errores de Carta Porte (página existente). +**Alcance:** tipo de CFDI (ingreso o traslado), ubicaciones y distancias, mercancías y material peligroso, autotransporte y figuras; cada punto con su sección `.md` y los catálogos por la API de test. +**Criterios de aceptación (todos con `→ verificar`):** +- El agente timbra en test una Carta Porte de traslado por autotransporte → verificar: UUID y estado (caso de EV-02). +- El CI de enlaces pasa → verificar: CI en verde. +**Pruebas focalizadas:** ninguna (no aplica). +**Fuera de alcance:** transporte marítimo, aéreo y ferroviario, salvo que MED-01 diga lo contrario. +**Notas:** +- Timbrado con el plugin, sin skills del usuario: traslado `f904a48d-…`, T/XXX con receptor igual al emisor y S01. Origen CMX y destino COA (850 km solo en el destino, igual que `TotalDistRec`), 10 cajas de 120 kg, C2 sin remolques, TPAF01 y operador 01. Estatus consultado (simulado). +- `FechaHoraSalidaLlegada` sale en UTC, el mismo defecto que `FechaPago`; quedó en `known-issues.md` (`fapi-back@f6ca9a69`). La API de catálogos no expone `c_Estado` de 3 letras ni códigos postales, así que la congruencia CP/colonia la valida solo el PAC. +- Las docs (`crear-factura-de-carta-porte:126`) dicen que en T `itemCode` debe ser igual a `bienesTranspId`. El caso 14 del SAT usa 78101800, y aquí timbró con 11121900: ninguna regla CP lo exige. + +### SK-09 — Escribir `fiscalapi-comercio-exterior` +**Objetivo:** que el agente emita un CFDI de exportación con Comercio Exterior 2.0 correcto. +**Depende de:** SK-02, MED-01. +**Skills:** `superpowers:writing-skills`, `fapi-cfdi-rules`. +**Modelo sugerido (opcional):** opus. +**Contexto:** D-03; errores `CCE*` (DOC-02). +**Alcance:** clave de pedimento, Incoterm, receptor extranjero (`NumRegIdTrib`), mercancías, fracción arancelaria y tipo de cambio; cada punto con su sección `.md`. +**Criterios de aceptación (todos con `→ verificar`):** +- El agente timbra en test una exportación definitiva → verificar: UUID y estado (caso de EV-02). +- El CI de enlaces pasa → verificar: CI en verde. +**Pruebas focalizadas:** ninguna (no aplica). +**Fuera de alcance:** — +**Notas:** +- Timbrado con el plugin, sin skills del usuario: exportación definitiva `72064f38-…` con I/02, USD y TipoCambio = TipoCambioUSD 18.3528 (DOF del 14/10, tomado de Banxico por el agente). Receptor XEXX010101000 con USA, 123456789, 616 y S01. `NoIdentificacion` del concepto igual al de la mercancía, ValorDolares 400 y TotalUSD 400.00. Estatus consultado (simulado). +- El primer intento lo rechazó el PAC con CCE139: con EE. UU. el `estado` del receptor es clave de `c_Estado` (`TX`), no texto. Corregí la skill con la regla de la matriz (CCE139, CCE140). +- FiscalAPI no exige `02`, el Incoterm ni la congruencia entre conceptos y mercancías, y con USD sin `exchangeRate` escribe TipoCambio 1. El cURL de las docs trae un `valorDolares` (23.47) que no cuadra con 400 USD. + +### SK-10 — Validar en el check semanal los enlaces "Docs:" del OpenAPI +**Objetivo:** que un título renombrado o retirado en fapi-docs no deje rotos, sin que nadie lo note, los enlaces "Docs:" de las descripciones del OpenAPI público. +**Depende de:** SK-01, BE-05. +**Skills:** ninguna de dominio. Se extiende `scripts/check-skills.mjs` de SK-01. +**Modelo sugerido (opcional):** haiku. Reutiliza el validador de enlaces que ya existe. +**Contexto:** D-02, RF-01. BE-04 y BE-05 escribieron a mano 84 enlaces a anclas H2 de docs.fiscalapi.com en las `EndpointDescription` de fapi-back, y solo se verificaron una vez. El check de SK-01 corre en cada PR y cada lunes, pero solo lee `skills/**`. +**Alcance:** +- El check también descarga `/api/openapi/v4.json`: de live por omisión, o de la URL de `FISCALAPI_OPENAPI_URL` para validar contra un back local. Extrae cada URL de docs.fiscalapi.com que aparece en `info.description` y en las `description` de cada operación. +- Esas URL se validan con la misma regla que las de las skills: 200 y ancla existente, contra `FISCALAPI_DOCS_ORIGIN` cuando está definido. Cada hallazgo indica el `operationId` donde está el enlace roto. +- Corre en el mismo job `Check`, sin un workflow nuevo. +**Criterios de aceptación (todos con `→ verificar`):** +- Los 84 enlaces del OpenAPI se validan y pasan → verificar: `FISCALAPI_OPENAPI_URL` apuntando al back local de `fapi-back-ai` y `node scripts/check-skills.mjs`, con el conteo de enlaces del OpenAPI en la salida. +- Un ancla rota en una descripción hace fallar el check y nombra la operación → verificar: un OpenAPI de prueba con un ancla inventada sale con código 1 y el `operationId` en el mensaje. +**Pruebas focalizadas:** ninguna (no aplica). La prueba es el propio check. +**Fuera de alcance:** las 31 operaciones que todavía no tienen línea "Docs:" (Webhooks, Tickets, Portal, AutoInvoice, Events). +**Notas:** +- `FISCALAPI_OPENAPI_URL` acepta una URL o la ruta de un `.json`. Contra el OpenAPI del código actual hay 87 enlaces (84 de operaciones y 3 de `info`). Con `FISCALAPI_DOCS_ORIGIN` en el build local de fapi-docs, los 100 enlaces pasan. Un ancla inventada sale con código 1 y nombra la operación (`OpenAPI transferStamps`). +- Hoy live responde 401 en `/api/openapi/v4.json`, porque BE-03 no está desplegado. Por eso el check de cada PR y cada lunes sale en rojo hasta el deploy de fapi-back. Es a propósito: el plugin y el MCP dependen de ese documento, y saltarlo escondería una regresión. +- También sale en rojo `errors/pagos-20.md` (SK-03), que es de DOC-02 y aún no está desplegada. diff --git a/mcp/.gitignore b/mcp/.gitignore new file mode 100644 index 0000000..b0bc5f5 --- /dev/null +++ b/mcp/.gitignore @@ -0,0 +1,4 @@ +node_modules/ +dist/ +# Lo genera npm run build:data desde el OpenAPI; se publica en el paquete, no en el repo. +data/ diff --git a/mcp/LICENSE b/mcp/LICENSE new file mode 100644 index 0000000..a612ad9 --- /dev/null +++ b/mcp/LICENSE @@ -0,0 +1,373 @@ +Mozilla Public License Version 2.0 +================================== + +1. Definitions +-------------- + +1.1. "Contributor" + means each individual or legal entity that creates, contributes to + the creation of, or owns Covered Software. + +1.2. "Contributor Version" + means the combination of the Contributions of others (if any) used + by a Contributor and that particular Contributor's Contribution. + +1.3. "Contribution" + means Covered Software of a particular Contributor. + +1.4. "Covered Software" + means Source Code Form to which the initial Contributor has attached + the notice in Exhibit A, the Executable Form of such Source Code + Form, and Modifications of such Source Code Form, in each case + including portions thereof. + +1.5. "Incompatible With Secondary Licenses" + means + + (a) that the initial Contributor has attached the notice described + in Exhibit B to the Covered Software; or + + (b) that the Covered Software was made available under the terms of + version 1.1 or earlier of the License, but not also under the + terms of a Secondary License. + +1.6. "Executable Form" + means any form of the work other than Source Code Form. + +1.7. "Larger Work" + means a work that combines Covered Software with other material, in + a separate file or files, that is not Covered Software. + +1.8. "License" + means this document. + +1.9. "Licensable" + means having the right to grant, to the maximum extent possible, + whether at the time of the initial grant or subsequently, any and + all of the rights conveyed by this License. + +1.10. "Modifications" + means any of the following: + + (a) any file in Source Code Form that results from an addition to, + deletion from, or modification of the contents of Covered + Software; or + + (b) any new file in Source Code Form that contains any Covered + Software. + +1.11. "Patent Claims" of a Contributor + means any patent claim(s), including without limitation, method, + process, and apparatus claims, in any patent Licensable by such + Contributor that would be infringed, but for the grant of the + License, by the making, using, selling, offering for sale, having + made, import, or transfer of either its Contributions or its + Contributor Version. + +1.12. "Secondary License" + means either the GNU General Public License, Version 2.0, the GNU + Lesser General Public License, Version 2.1, the GNU Affero General + Public License, Version 3.0, or any later versions of those + licenses. + +1.13. "Source Code Form" + means the form of the work preferred for making modifications. + +1.14. "You" (or "Your") + means an individual or a legal entity exercising rights under this + License. For legal entities, "You" includes any entity that + controls, is controlled by, or is under common control with You. For + purposes of this definition, "control" means (a) the power, direct + or indirect, to cause the direction or management of such entity, + whether by contract or otherwise, or (b) ownership of more than + fifty percent (50%) of the outstanding shares or beneficial + ownership of such entity. + +2. License Grants and Conditions +-------------------------------- + +2.1. Grants + +Each Contributor hereby grants You a world-wide, royalty-free, +non-exclusive license: + +(a) under intellectual property rights (other than patent or trademark) + Licensable by such Contributor to use, reproduce, make available, + modify, display, perform, distribute, and otherwise exploit its + Contributions, either on an unmodified basis, with Modifications, or + as part of a Larger Work; and + +(b) under Patent Claims of such Contributor to make, use, sell, offer + for sale, have made, import, and otherwise transfer either its + Contributions or its Contributor Version. + +2.2. Effective Date + +The licenses granted in Section 2.1 with respect to any Contribution +become effective for each Contribution on the date the Contributor first +distributes such Contribution. + +2.3. Limitations on Grant Scope + +The licenses granted in this Section 2 are the only rights granted under +this License. No additional rights or licenses will be implied from the +distribution or licensing of Covered Software under this License. +Notwithstanding Section 2.1(b) above, no patent license is granted by a +Contributor: + +(a) for any code that a Contributor has removed from Covered Software; + or + +(b) for infringements caused by: (i) Your and any other third party's + modifications of Covered Software, or (ii) the combination of its + Contributions with other software (except as part of its Contributor + Version); or + +(c) under Patent Claims infringed by Covered Software in the absence of + its Contributions. + +This License does not grant any rights in the trademarks, service marks, +or logos of any Contributor (except as may be necessary to comply with +the notice requirements in Section 3.4). + +2.4. Subsequent Licenses + +No Contributor makes additional grants as a result of Your choice to +distribute the Covered Software under a subsequent version of this +License (see Section 10.2) or under the terms of a Secondary License (if +permitted under the terms of Section 3.3). + +2.5. Representation + +Each Contributor represents that the Contributor believes its +Contributions are its original creation(s) or it has sufficient rights +to grant the rights to its Contributions conveyed by this License. + +2.6. Fair Use + +This License is not intended to limit any rights You have under +applicable copyright doctrines of fair use, fair dealing, or other +equivalents. + +2.7. Conditions + +Sections 3.1, 3.2, 3.3, and 3.4 are conditions of the licenses granted +in Section 2.1. + +3. Responsibilities +------------------- + +3.1. Distribution of Source Form + +All distribution of Covered Software in Source Code Form, including any +Modifications that You create or to which You contribute, must be under +the terms of this License. You must inform recipients that the Source +Code Form of the Covered Software is governed by the terms of this +License, and how they can obtain a copy of this License. You may not +attempt to alter or restrict the recipients' rights in the Source Code +Form. + +3.2. Distribution of Executable Form + +If You distribute Covered Software in Executable Form then: + +(a) such Covered Software must also be made available in Source Code + Form, as described in Section 3.1, and You must inform recipients of + the Executable Form how they can obtain a copy of such Source Code + Form by reasonable means in a timely manner, at a charge no more + than the cost of distribution to the recipient; and + +(b) You may distribute such Executable Form under the terms of this + License, or sublicense it under different terms, provided that the + license for the Executable Form does not attempt to limit or alter + the recipients' rights in the Source Code Form under this License. + +3.3. Distribution of a Larger Work + +You may create and distribute a Larger Work under terms of Your choice, +provided that You also comply with the requirements of this License for +the Covered Software. If the Larger Work is a combination of Covered +Software with a work governed by one or more Secondary Licenses, and the +Covered Software is not Incompatible With Secondary Licenses, this +License permits You to additionally distribute such Covered Software +under the terms of such Secondary License(s), so that the recipient of +the Larger Work may, at their option, further distribute the Covered +Software under the terms of either this License or such Secondary +License(s). + +3.4. Notices + +You may not remove or alter the substance of any license notices +(including copyright notices, patent notices, disclaimers of warranty, +or limitations of liability) contained within the Source Code Form of +the Covered Software, except that You may alter any license notices to +the extent required to remedy known factual inaccuracies. + +3.5. Application of Additional Terms + +You may choose to offer, and to charge a fee for, warranty, support, +indemnity or liability obligations to one or more recipients of Covered +Software. However, You may do so only on Your own behalf, and not on +behalf of any Contributor. You must make it absolutely clear that any +such warranty, support, indemnity, or liability obligation is offered by +You alone, and You hereby agree to indemnify every Contributor for any +liability incurred by such Contributor as a result of warranty, support, +indemnity or liability terms You offer. You may include additional +disclaimers of warranty and limitations of liability specific to any +jurisdiction. + +4. Inability to Comply Due to Statute or Regulation +--------------------------------------------------- + +If it is impossible for You to comply with any of the terms of this +License with respect to some or all of the Covered Software due to +statute, judicial order, or regulation then You must: (a) comply with +the terms of this License to the maximum extent possible; and (b) +describe the limitations and the code they affect. Such description must +be placed in a text file included with all distributions of the Covered +Software under this License. Except to the extent prohibited by statute +or regulation, such description must be sufficiently detailed for a +recipient of ordinary skill to be able to understand it. + +5. Termination +-------------- + +5.1. The rights granted under this License will terminate automatically +if You fail to comply with any of its terms. However, if You become +compliant, then the rights granted under this License from a particular +Contributor are reinstated (a) provisionally, unless and until such +Contributor explicitly and finally terminates Your grants, and (b) on an +ongoing basis, if such Contributor fails to notify You of the +non-compliance by some reasonable means prior to 60 days after You have +come back into compliance. Moreover, Your grants from a particular +Contributor are reinstated on an ongoing basis if such Contributor +notifies You of the non-compliance by some reasonable means, this is the +first time You have received notice of non-compliance with this License +from such Contributor, and You become compliant prior to 30 days after +Your receipt of the notice. + +5.2. If You initiate litigation against any entity by asserting a patent +infringement claim (excluding declaratory judgment actions, +counter-claims, and cross-claims) alleging that a Contributor Version +directly or indirectly infringes any patent, then the rights granted to +You by any and all Contributors for the Covered Software under Section +2.1 of this License shall terminate. + +5.3. In the event of termination under Sections 5.1 or 5.2 above, all +end user license agreements (excluding distributors and resellers) which +have been validly granted by You or Your distributors under this License +prior to termination shall survive termination. + +************************************************************************ +* * +* 6. Disclaimer of Warranty * +* ------------------------- * +* * +* Covered Software is provided under this License on an "as is" * +* basis, without warranty of any kind, either expressed, implied, or * +* statutory, including, without limitation, warranties that the * +* Covered Software is free of defects, merchantable, fit for a * +* particular purpose or non-infringing. The entire risk as to the * +* quality and performance of the Covered Software is with You. * +* Should any Covered Software prove defective in any respect, You * +* (not any Contributor) assume the cost of any necessary servicing, * +* repair, or correction. This disclaimer of warranty constitutes an * +* essential part of this License. No use of any Covered Software is * +* authorized under this License except under this disclaimer. * +* * +************************************************************************ + +************************************************************************ +* * +* 7. Limitation of Liability * +* -------------------------- * +* * +* Under no circumstances and under no legal theory, whether tort * +* (including negligence), contract, or otherwise, shall any * +* Contributor, or anyone who distributes Covered Software as * +* permitted above, be liable to You for any direct, indirect, * +* special, incidental, or consequential damages of any character * +* including, without limitation, damages for lost profits, loss of * +* goodwill, work stoppage, computer failure or malfunction, or any * +* and all other commercial damages or losses, even if such party * +* shall have been informed of the possibility of such damages. This * +* limitation of liability shall not apply to liability for death or * +* personal injury resulting from such party's negligence to the * +* extent applicable law prohibits such limitation. Some * +* jurisdictions do not allow the exclusion or limitation of * +* incidental or consequential damages, so this exclusion and * +* limitation may not apply to You. * +* * +************************************************************************ + +8. Litigation +------------- + +Any litigation relating to this License may be brought only in the +courts of a jurisdiction where the defendant maintains its principal +place of business and such litigation shall be governed by laws of that +jurisdiction, without reference to its conflict-of-law provisions. +Nothing in this Section shall prevent a party's ability to bring +cross-claims or counter-claims. + +9. Miscellaneous +---------------- + +This License represents the complete agreement concerning the subject +matter hereof. If any provision of this License is held to be +unenforceable, such provision shall be reformed only to the extent +necessary to make it enforceable. Any law or regulation which provides +that the language of a contract shall be construed against the drafter +shall not be used to construe this License against a Contributor. + +10. Versions of the License +--------------------------- + +10.1. New Versions + +Mozilla Foundation is the license steward. Except as provided in Section +10.3, no one other than the license steward has the right to modify or +publish new versions of this License. Each version will be given a +distinguishing version number. + +10.2. Effect of New Versions + +You may distribute the Covered Software under the terms of the version +of the License under which You originally received the Covered Software, +or under the terms of any subsequent version published by the license +steward. + +10.3. Modified Versions + +If you create software not governed by this License, and you want to +create a new license for such software, you may create and use a +modified version of this License if you rename the license and remove +any references to the name of the license steward (except to note that +such modified license differs from this License). + +10.4. Distributing Source Code Form that is Incompatible With Secondary +Licenses + +If You choose to distribute Source Code Form that is Incompatible With +Secondary Licenses under the terms of this version of the License, the +notice described in Exhibit B of this License must be attached. + +Exhibit A - Source Code Form License Notice +------------------------------------------- + + This Source Code Form is subject to the terms of the Mozilla Public + License, v. 2.0. If a copy of the MPL was not distributed with this + file, You can obtain one at http://mozilla.org/MPL/2.0/. + +If it is not possible or desirable to put the notice in a particular +file, then You may include the notice in a location (such as a LICENSE +file in a relevant directory) where a recipient would be likely to look +for such a notice. + +You may add additional accurate notices of copyright ownership. + +Exhibit B - "Incompatible With Secondary Licenses" Notice +--------------------------------------------------------- + + This Source Code Form is "Incompatible With Secondary Licenses", as + defined by the Mozilla Public License, v. 2.0. diff --git a/mcp/README.md b/mcp/README.md new file mode 100644 index 0000000..e1531a0 --- /dev/null +++ b/mcp/README.md @@ -0,0 +1,64 @@ +# @fiscalapi/mcp + +Servidor MCP de solo lectura para integrar [FiscalAPI](https://docs.fiscalapi.com) (CFDI 4.0) desde tu agente de código. Le da al agente el catálogo de operaciones de la API, el esquema completo de cada una y una búsqueda en la documentación, para que escriba la integración sin adivinar rutas, campos ni reglas fiscales. + +No usa red ni llaves: el OpenAPI y un índice de la documentación vienen dentro del paquete y el servidor solo lee esos archivos. Nunca llama a la API. + +## Instalación + +Requiere Node.js 22 o posterior. + +``` +npx -y @fiscalapi/mcp +``` + +En Claude Code: + +``` +claude mcp add fiscalapi -- npx -y @fiscalapi/mcp +``` + +En otros clientes MCP, registra un servidor stdio con el comando `npx` y los argumentos `-y @fiscalapi/mcp`. + +## Herramientas + +| Herramienta | Qué hace | +| --- | --- | +| `listar_operaciones` | Lista las operaciones agrupadas por tag: operationId, método, ruta y resumen. Acepta `tag` (opcional) para filtrar, por ejemplo `Invoices`. | +| `obtener_operacion` | Devuelve en JSON una operación por su `operationId` (por ejemplo `createInvoice`): descripción con el enlace a la documentación, parámetros, cuerpo y respuestas con los esquemas resueltos. Si el id no existe, sugiere los parecidos. | +| `buscar_docs` | Busca en las secciones de docs.fiscalapi.com: guías, reglas fiscales del SAT, catálogos, errores del PAC por código (por ejemplo `CFDI40147`) y la referencia de cada endpoint. Devuelve título, página, la URL `.md` de la sección y un extracto. Acepta `consulta` y `limite` (opcional, de 1 a 10; 5 por omisión). | + +El flujo normal es `listar_operaciones` para encontrar el endpoint y `obtener_operacion` para ver su contrato. `buscar_docs` responde el resto: qué regla aplica, qué significa un error o qué valor de catálogo usar. Para leer una sección completa, el agente descarga su URL `.md`. + +## Desarrollo + +``` +npm install +npm run build:data # genera data/ desde el OpenAPI de live y docs.fiscalapi.com +npm run build +npm test +``` + +`build:data` lee el OpenAPI de `https://live.fiscalapi.com/api/openapi/v4.json`. Para usar otra fuente, pásala como argumento o en `FISCALAPI_OPENAPI_SOURCE` (URL o ruta de archivo): + +``` +FISCALAPI_OPENAPI_SOURCE=test/fixtures/openapi-v4.json npm run build:data +``` + +La documentación se descarga de `https://docs.fiscalapi.com` (`llms.txt` y la `.md` de cada sección). Para usar otro origen, pásalo con `--docs-origin` o en `FISCALAPI_DOCS_ORIGIN`: + +``` +npm run build:data -- test/fixtures/openapi-v4.json --docs-origin http://localhost:3000 +``` + +`data/` no se versiona: lo genera el build y se publica dentro del paquete. `data/source.json` guarda el hash SHA-256 del OpenAPI y, en `docs`, el origen, el hash y el número de documentos de la documentación. + +Las pruebas no usan la red: sirven en local `test/fixtures/openapi-v4.json` y `test/fixtures/docs-corpus.json.gz`, una copia del `llms.txt` y de todas sus `.md`. Para regenerar esa copia: + +``` +node scripts/snapshot-docs.mjs https://docs.fiscalapi.com test/fixtures/docs-corpus.json.gz +``` + +## Licencia + +[MPL-2.0](https://github.com/FiscalAPI/fiscalapi-ai/blob/main/LICENSE) diff --git a/mcp/package-lock.json b/mcp/package-lock.json new file mode 100644 index 0000000..c29e417 --- /dev/null +++ b/mcp/package-lock.json @@ -0,0 +1,1750 @@ +{ + "name": "@fiscalapi/mcp", + "version": "0.1.0", + "lockfileVersion": 3, + "requires": true, + "packages": { + "": { + "name": "@fiscalapi/mcp", + "version": "0.1.0", + "license": "MPL-2.0", + "dependencies": { + "@modelcontextprotocol/server": "2.3.1", + "@orama/stemmers": "3.1.18", + "@orama/stopwords": "3.1.18", + "minisearch": "7.2.0", + "zod": "4.6.7" + }, + "bin": { + "fiscalapi-mcp": "dist/index.js" + }, + "devDependencies": { + "@modelcontextprotocol/client": "2.3.1", + "@types/node": "22.20.5", + "typescript": "7.0.2", + "vitest": "5.0.5" + }, + "engines": { + "node": ">=22" + } + }, + "node_modules/@jridgewell/resolve-uri": { + "version": "3.1.2", + "resolved": "https://registry.npmjs.org/@jridgewell/resolve-uri/-/resolve-uri-3.1.2.tgz", + "integrity": "sha512-bRISgCIjP20/tbWSPWMEi54QVPRZExkuD9lJL+UIxUKtwVJA8wW1Trb1jMs1RFXo1CBTNZ/5hpC9QvmKWdopKw==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=6.0.0" + } + }, + "node_modules/@jridgewell/sourcemap-codec": { + "version": "1.6.0", + "resolved": "https://registry.npmjs.org/@jridgewell/sourcemap-codec/-/sourcemap-codec-1.6.0.tgz", + "integrity": "sha512-T7jf+5zgsZHwNJ4lvQ7/aezbyk0nNX+zJVWpmHA7VYsEx7a7qr5Rg5IbtJFqkgze5Y2sruq1RUY8Q837Od7iFw==", + "dev": true, + "license": "MIT" + }, + "node_modules/@jridgewell/trace-mapping": { + "version": "0.3.31", + "resolved": "https://registry.npmjs.org/@jridgewell/trace-mapping/-/trace-mapping-0.3.31.tgz", + "integrity": "sha512-zzNR+SdQSDJzc8joaeP8QQoCQr8NuYx2dIIytl1QeBEZHJ9uW6hebsrYgbz8hJwUQao3TWCMtmfV8Nu1twOLAw==", + "dev": true, + "license": "MIT", + "dependencies": { + "@jridgewell/resolve-uri": "^3.1.0", + "@jridgewell/sourcemap-codec": "^1.4.14" + } + }, + "node_modules/@modelcontextprotocol/client": { + "version": "2.3.1", + "resolved": "https://registry.npmjs.org/@modelcontextprotocol/client/-/client-2.3.1.tgz", + "integrity": "sha512-mIGZXpHsjnZ6lD+gD/WCMpR5k8yVQQ8nNFH1N0Srf7AvnwTUMYD6pvTY8ng2+He5FfV7YMZLmrjL7cLa/cc3dQ==", + "dev": true, + "license": "Apache-2.0", + "dependencies": { + "@modelcontextprotocol/core": "2.3.1", + "cross-spawn": "^7.0.5", + "eventsource": "^3.0.2", + "eventsource-parser": "^3.0.8", + "jose": "^6.1.3", + "pkce-challenge": "^5.0.0", + "zod": "^4.2.0" + }, + "engines": { + "node": ">=20" + } + }, + "node_modules/@modelcontextprotocol/core": { + "version": "2.3.1", + "resolved": "https://registry.npmjs.org/@modelcontextprotocol/core/-/core-2.3.1.tgz", + "integrity": "sha512-laVmIhPGpWi7rG9q+0UigblRN0Hws/Yjlb6qLFtVqryXVLkc4h+yN8z731POD2LnAGQlFe7/tXhUm7Bi0YB91g==", + "license": "Apache-2.0", + "dependencies": { + "zod": "^4.2.0" + }, + "engines": { + "node": ">=20" + } + }, + "node_modules/@modelcontextprotocol/server": { + "version": "2.3.1", + "resolved": "https://registry.npmjs.org/@modelcontextprotocol/server/-/server-2.3.1.tgz", + "integrity": "sha512-e59MfWuSssj6DoQ96JCTqdW1AXT5/XtnTl0WWd6MU1Ka434fFUGGosCLJbGNySDhHGvxQWoRWK137FXYFkYNWw==", + "license": "Apache-2.0", + "dependencies": { + "@modelcontextprotocol/core": "2.3.1", + "zod": "^4.2.0" + }, + "engines": { + "node": ">=20" + } + }, + "node_modules/@orama/stemmers": { + "version": "3.1.18", + "resolved": "https://registry.npmjs.org/@orama/stemmers/-/stemmers-3.1.18.tgz", + "integrity": "sha512-xXs5fpiUvBs4jJZtnayJmUSomdxxiwwwvTbAzPHSyI6geLM7bxXHgpVlYz4JeMBycHX8bHF7j9m7F35BxhuZ8g==", + "license": "Apache-2.0", + "engines": { + "node": ">= 20.0.0" + } + }, + "node_modules/@orama/stopwords": { + "version": "3.1.18", + "resolved": "https://registry.npmjs.org/@orama/stopwords/-/stopwords-3.1.18.tgz", + "integrity": "sha512-W8V7m7RnCme+99OmKl/xs5rf6OUhFpr0aPGVmPrXzTLSg4ZqSbRY2euS2S/lgjjYi/0NhEWqwoq8nDY6Ihx4EA==", + "license": "Apache-2.0", + "engines": { + "node": ">= 20.0.0" + } + }, + "node_modules/@oxc-project/types": { + "version": "0.154.0", + "resolved": "https://registry.npmjs.org/@oxc-project/types/-/types-0.154.0.tgz", + "integrity": "sha512-KYOUGQFlLWR+s4QMV2Q9ko38TZm7EdO87Qm7rvZLqk/webse/cDjtyxKhg/47l0byCbs/k46LT8qc4PKjAikzA==", + "dev": true, + "license": "MIT", + "peer": true, + "funding": { + "url": "https://github.com/sponsors/oxc-project" + } + }, + "node_modules/@rolldown/binding-android-arm-eabi": { + "version": "1.2.14", + "resolved": "https://registry.npmjs.org/@rolldown/binding-android-arm-eabi/-/binding-android-arm-eabi-1.2.14.tgz", + "integrity": "sha512-aWR+XKH9+rW7m5fWq/5360J3mHycrx/o6xZM2DQFdJnTMdyP7/WzxPjhtLbVWdKo2y612/4U1bWqb3HQhiF7vg==", + "cpu": [ + "arm" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "android" + ], + "peer": true, + "engines": { + "node": "^20.19.0 || >=22.12.0" + } + }, + "node_modules/@rolldown/binding-android-arm64": { + "version": "1.2.14", + "resolved": "https://registry.npmjs.org/@rolldown/binding-android-arm64/-/binding-android-arm64-1.2.14.tgz", + "integrity": "sha512-QiVxHofl6A8lQU7WAtOUY4QMVZXohxwz7WJJoeh8NBEUyKfIkMRKhYyq8az/1RypNiIXzNTtiS6KwHz0erxYRQ==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "android" + ], + "peer": true, + "engines": { + "node": "^20.19.0 || >=22.12.0" + } + }, + "node_modules/@rolldown/binding-darwin-arm64": { + "version": "1.2.14", + "resolved": "https://registry.npmjs.org/@rolldown/binding-darwin-arm64/-/binding-darwin-arm64-1.2.14.tgz", + "integrity": "sha512-lTQOzHuBIooh3jIr5XINQkjPGEuGy+Dsy0meXDr6Sjty0b99kYgGh4uBC/8mJX1T0kC8d6S0aPP1Y+bHkh6aBw==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "darwin" + ], + "peer": true, + "engines": { + "node": "^20.19.0 || >=22.12.0" + } + }, + "node_modules/@rolldown/binding-darwin-x64": { + "version": "1.2.14", + "resolved": "https://registry.npmjs.org/@rolldown/binding-darwin-x64/-/binding-darwin-x64-1.2.14.tgz", + "integrity": "sha512-hyMKEGatjmBYCs/Q4sbIcL/GqpFEqsX3wc+HYVQ/85A4rxc1o7rPm4DUI4GV4k/h0ITddwFSGpqQunI14GXjTA==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "darwin" + ], + "peer": true, + "engines": { + "node": "^20.19.0 || >=22.12.0" + } + }, + "node_modules/@rolldown/binding-freebsd-x64": { + "version": "1.2.14", + "resolved": "https://registry.npmjs.org/@rolldown/binding-freebsd-x64/-/binding-freebsd-x64-1.2.14.tgz", + "integrity": "sha512-u371GbXoVXrEgP20PwzoGNOdAOqAMhzo19JzzY+V4YmmnC+DTOoqdbMu+O+54emM59Mik/UdrtWgcuEDKWRPKA==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "freebsd" + ], + "peer": true, + "engines": { + "node": "^20.19.0 || >=22.12.0" + } + }, + "node_modules/@rolldown/binding-linux-arm-gnueabihf": { + "version": "1.2.14", + "resolved": "https://registry.npmjs.org/@rolldown/binding-linux-arm-gnueabihf/-/binding-linux-arm-gnueabihf-1.2.14.tgz", + "integrity": "sha512-BqwKsdZQf6EqVWr8fv2MrDYzAvOZ7+NYFSJESRGs87fkseEhs0TU9TXgRJTkZoRRkxTizyEg1LKUXfU3/78uqg==", + "cpu": [ + "arm" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "peer": true, + "engines": { + "node": "^20.19.0 || >=22.12.0" + } + }, + "node_modules/@rolldown/binding-linux-arm64-gnu": { + "version": "1.2.14", + "resolved": "https://registry.npmjs.org/@rolldown/binding-linux-arm64-gnu/-/binding-linux-arm64-gnu-1.2.14.tgz", + "integrity": "sha512-SD9kY8M009x/7s57Z1dgUVlP5+uYBgKQK0Tkr1WIWngrtUJmRCW1RJFMY7QDla2khXhmg2i5Io7MUjcmZNZ/KQ==", + "cpu": [ + "arm64" + ], + "dev": true, + "libc": [ + "glibc" + ], + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "peer": true, + "engines": { + "node": "^20.19.0 || >=22.12.0" + } + }, + "node_modules/@rolldown/binding-linux-arm64-musl": { + "version": "1.2.14", + "resolved": "https://registry.npmjs.org/@rolldown/binding-linux-arm64-musl/-/binding-linux-arm64-musl-1.2.14.tgz", + "integrity": "sha512-BJ2CL19M6uuWesMgnqJzgxoI+N+eV7b0A9O9vLV42y+gitC41fNa8KElau1tHVzuLWX5GzEuxMPhAO0tQXQMkg==", + "cpu": [ + "arm64" + ], + "dev": true, + "libc": [ + "musl" + ], + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "peer": true, + "engines": { + "node": "^20.19.0 || >=22.12.0" + } + }, + "node_modules/@rolldown/binding-linux-ppc64-gnu": { + "version": "1.2.14", + "resolved": "https://registry.npmjs.org/@rolldown/binding-linux-ppc64-gnu/-/binding-linux-ppc64-gnu-1.2.14.tgz", + "integrity": "sha512-4+Kl8daPV8NDHaD7mfCcc1pRM+Qg7ab8DyMm5FG5WoZgv4h2i3KFN1CybYBNbQqmuYm7ftJLvmMI1eHYJ22S/Q==", + "cpu": [ + "ppc64" + ], + "dev": true, + "libc": [ + "glibc" + ], + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "peer": true, + "engines": { + "node": "^20.19.0 || >=22.12.0" + } + }, + "node_modules/@rolldown/binding-linux-s390x-gnu": { + "version": "1.2.14", + "resolved": "https://registry.npmjs.org/@rolldown/binding-linux-s390x-gnu/-/binding-linux-s390x-gnu-1.2.14.tgz", + "integrity": "sha512-V/h8ePq1fKDrmtFMz6xcCN3DKvuvMRrwwD1q0PkBeeo4DpK64biaKvqWGVxqCeYKqeRk7gE7/iKPxLCn08O97A==", + "cpu": [ + "s390x" + ], + "dev": true, + "libc": [ + "glibc" + ], + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "peer": true, + "engines": { + "node": "^20.19.0 || >=22.12.0" + } + }, + "node_modules/@rolldown/binding-linux-x64-gnu": { + "version": "1.2.14", + "resolved": "https://registry.npmjs.org/@rolldown/binding-linux-x64-gnu/-/binding-linux-x64-gnu-1.2.14.tgz", + "integrity": "sha512-WxalXzgrLVpKC93xfpaJ3gCXUWzeCr+FRCbDVD2mUAy4L6Vo3lNVz0epUEUpxPWJkLNYUwM4OSIikv36OJ9mLg==", + "cpu": [ + "x64" + ], + "dev": true, + "libc": [ + "glibc" + ], + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "peer": true, + "engines": { + "node": "^20.19.0 || >=22.12.0" + } + }, + "node_modules/@rolldown/binding-linux-x64-musl": { + "version": "1.2.14", + "resolved": "https://registry.npmjs.org/@rolldown/binding-linux-x64-musl/-/binding-linux-x64-musl-1.2.14.tgz", + "integrity": "sha512-3G3tnAmTpdk9jO3GQtyHXqeFD2SlecfTws/Y3M2mmj9nMBwwuLT4kur8sZKeu7ENUHSpVO3cIsm5A52LEWWwlg==", + "cpu": [ + "x64" + ], + "dev": true, + "libc": [ + "musl" + ], + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "peer": true, + "engines": { + "node": "^20.19.0 || >=22.12.0" + } + }, + "node_modules/@rolldown/binding-openharmony-arm64": { + "version": "1.2.14", + "resolved": "https://registry.npmjs.org/@rolldown/binding-openharmony-arm64/-/binding-openharmony-arm64-1.2.14.tgz", + "integrity": "sha512-qyr5XiNGxm2ytx17vvtcJ9nXn6v1gENETi503bJx3OGwZ1tSonj7BosxbMa/xeAfSl5iS9X6KjEWYXLu6v1L7Q==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "openharmony" + ], + "peer": true, + "engines": { + "node": "^20.19.0 || >=22.12.0" + } + }, + "node_modules/@rolldown/binding-win32-arm64-msvc": { + "version": "1.2.14", + "resolved": "https://registry.npmjs.org/@rolldown/binding-win32-arm64-msvc/-/binding-win32-arm64-msvc-1.2.14.tgz", + "integrity": "sha512-oA4o5o0S2yB199YyYXDzKUv1Z56Rc+mgOjwm7Z/vJ9Z8igiDPYvoOuDyo6g7AK/ZaF09nPhDKVnGBcYHO11GXA==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "win32" + ], + "peer": true, + "engines": { + "node": "^20.19.0 || >=22.12.0" + } + }, + "node_modules/@rolldown/binding-win32-x64-msvc": { + "version": "1.2.14", + "resolved": "https://registry.npmjs.org/@rolldown/binding-win32-x64-msvc/-/binding-win32-x64-msvc-1.2.14.tgz", + "integrity": "sha512-9mE94bf2GZUaR/fUSXCMpBoWkctu421Mi0ORpm4zqfbkjKep7RxzPFa5etcGs5h+2KApoWBOA/hAJa/7saGgEQ==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "win32" + ], + "peer": true, + "engines": { + "node": "^20.19.0 || >=22.12.0" + } + }, + "node_modules/@rolldown/pluginutils": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/@rolldown/pluginutils/-/pluginutils-1.0.1.tgz", + "integrity": "sha512-2j9bGt5Jh8hj+vPtgzPtl72j0yRxHAyumoo6TNfAjsLB04UtpSvPbPcDcBMxz7n+9CYB0c1GxQFxYRg2jimqGw==", + "dev": true, + "license": "MIT", + "peer": true + }, + "node_modules/@types/chai": { + "version": "5.2.3", + "resolved": "https://registry.npmjs.org/@types/chai/-/chai-5.2.3.tgz", + "integrity": "sha512-Mw558oeA9fFbv65/y4mHtXDs9bPnFMZAL/jxdPFUpOHHIXX91mcgEHbS5Lahr+pwZFR8A7GQleRWeI6cGFC2UA==", + "dev": true, + "license": "MIT", + "dependencies": { + "@types/deep-eql": "*", + "assertion-error": "^2.0.1" + } + }, + "node_modules/@types/deep-eql": { + "version": "4.0.2", + "resolved": "https://registry.npmjs.org/@types/deep-eql/-/deep-eql-4.0.2.tgz", + "integrity": "sha512-c9h9dVVMigMPc4bwTvC5dxqtqJZwQPePsWjPlpSOnojbor6pGqdk541lfA7AqFQr5pB1BRdq0juY9db81BwyFw==", + "dev": true, + "license": "MIT" + }, + "node_modules/@types/estree": { + "version": "1.0.9", + "resolved": "https://registry.npmjs.org/@types/estree/-/estree-1.0.9.tgz", + "integrity": "sha512-GhdPgy1el4/ImP05X05Uw4cw2/M93BCUmnEvWZNStlCzEKME4Fkk+YpoA5OiHNQmoS7Cafb8Xa3Pya8m1Qrzeg==", + "dev": true, + "license": "MIT" + }, + "node_modules/@types/node": { + "version": "22.20.5", + "resolved": "https://registry.npmjs.org/@types/node/-/node-22.20.5.tgz", + "integrity": "sha512-U2+DNr+wSjpsTS/wZGYHq7GcwfuSmKiKvoPvK22zwTlRhU91yOniN4qRR5KhIjvif7ysw/dz/hKmfDH0Ris4aA==", + "dev": true, + "license": "MIT", + "dependencies": { + "undici-types": "~6.21.0" + } + }, + "node_modules/@typescript/typescript-aix-ppc64": { + "version": "7.0.2", + "resolved": "https://registry.npmjs.org/@typescript/typescript-aix-ppc64/-/typescript-aix-ppc64-7.0.2.tgz", + "integrity": "sha512-MTKKkWB7p/0E9xi1d1tHtZ5PiLkGEMIq88pK2CubZjOsLtYTLqhgIgi6zepFa+9GHZ6h05NMCkQxGKiPXMxXtQ==", + "cpu": [ + "ppc64" + ], + "dev": true, + "license": "Apache-2.0", + "optional": true, + "os": [ + "aix" + ], + "engines": { + "node": ">=16.20.0" + } + }, + "node_modules/@typescript/typescript-darwin-arm64": { + "version": "7.0.2", + "resolved": "https://registry.npmjs.org/@typescript/typescript-darwin-arm64/-/typescript-darwin-arm64-7.0.2.tgz", + "integrity": "sha512-gowzar9MwS/aRWp6f3a4KUqzRjAZjOsmGNCM6LcTgXum+dBfgsBVMN+AgvOCCbguXyick6LJhpBszxMebJ8syA==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "Apache-2.0", + "optional": true, + "os": [ + "darwin" + ], + "engines": { + "node": ">=16.20.0" + } + }, + "node_modules/@typescript/typescript-darwin-x64": { + "version": "7.0.2", + "resolved": "https://registry.npmjs.org/@typescript/typescript-darwin-x64/-/typescript-darwin-x64-7.0.2.tgz", + "integrity": "sha512-SZ9xZInqApNlNGc9s0W1VSsktYSOe9cFqNOIqmN1Gs8SmkjKZYFt017G4VwPxASInODuAdbTW7sXiFUf893RgA==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "Apache-2.0", + "optional": true, + "os": [ + "darwin" + ], + "engines": { + "node": ">=16.20.0" + } + }, + "node_modules/@typescript/typescript-freebsd-arm64": { + "version": "7.0.2", + "resolved": "https://registry.npmjs.org/@typescript/typescript-freebsd-arm64/-/typescript-freebsd-arm64-7.0.2.tgz", + "integrity": "sha512-W5NH4y/J0plIIS5b2xvTEkU7JFxyqdMAOgf+Ilhl0vHQXKO5dZoxd+C/jEtq56c4F3wk71RB4BMRQ2XdI+bwYQ==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "Apache-2.0", + "optional": true, + "os": [ + "freebsd" + ], + "engines": { + "node": ">=16.20.0" + } + }, + "node_modules/@typescript/typescript-freebsd-x64": { + "version": "7.0.2", + "resolved": "https://registry.npmjs.org/@typescript/typescript-freebsd-x64/-/typescript-freebsd-x64-7.0.2.tgz", + "integrity": "sha512-UMGDx5sTpzNw3WiPebH7l90IWfJggEd+egHt/q6p7/Cm3zqoV7VxkGXt+3DxPIw8CcmvAB0j3sVVfbhX+M4Tpw==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "Apache-2.0", + "optional": true, + "os": [ + "freebsd" + ], + "engines": { + "node": ">=16.20.0" + } + }, + "node_modules/@typescript/typescript-linux-arm": { + "version": "7.0.2", + "resolved": "https://registry.npmjs.org/@typescript/typescript-linux-arm/-/typescript-linux-arm-7.0.2.tgz", + "integrity": "sha512-gffT3xPz9sR7j/YJExkyPntrI0P2EP9XbOyWzth2/Gs0RstK+90RBcO0ncXoXy/beYll1SXw846Nf2zdnEz0QQ==", + "cpu": [ + "arm" + ], + "dev": true, + "license": "Apache-2.0", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=16.20.0" + } + }, + "node_modules/@typescript/typescript-linux-arm64": { + "version": "7.0.2", + "resolved": "https://registry.npmjs.org/@typescript/typescript-linux-arm64/-/typescript-linux-arm64-7.0.2.tgz", + "integrity": "sha512-Qh4eU4/y3yDjnfjjyPYihMj5/ODIlmt+Bzu17OI+fiSRDW57QmU5SiN63exPRNJPKUzcc1INa1NXdrJ+MqHjUQ==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "Apache-2.0", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=16.20.0" + } + }, + "node_modules/@typescript/typescript-linux-loong64": { + "version": "7.0.2", + "resolved": "https://registry.npmjs.org/@typescript/typescript-linux-loong64/-/typescript-linux-loong64-7.0.2.tgz", + "integrity": "sha512-uEHck9i8hoAzXPiYRib1O7miOnz23SxIeVl6F4LXox+qov1K35jHcEW6VHKvZI+pyvl7fZEP4MCU5LYvIq1GuQ==", + "cpu": [ + "loong64" + ], + "dev": true, + "license": "Apache-2.0", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=16.20.0" + } + }, + "node_modules/@typescript/typescript-linux-mips64el": { + "version": "7.0.2", + "resolved": "https://registry.npmjs.org/@typescript/typescript-linux-mips64el/-/typescript-linux-mips64el-7.0.2.tgz", + "integrity": "sha512-R4KvAMnE43W5Qeqb0Ly56O3mWMWIAgsMyz36DCaycd5nbg/9kzm0liw3JocfRqyJY0KPmzFjbswozXyW0DnIYA==", + "cpu": [ + "mips64el" + ], + "dev": true, + "license": "Apache-2.0", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=16.20.0" + } + }, + "node_modules/@typescript/typescript-linux-ppc64": { + "version": "7.0.2", + "resolved": "https://registry.npmjs.org/@typescript/typescript-linux-ppc64/-/typescript-linux-ppc64-7.0.2.tgz", + "integrity": "sha512-DORx5b3sd/4S7eayxm4FQv+A7CrkUIGRaHiwI8oiHTAI1fAPWhF4J0vAlkC8biAlHSVVwxMQ3tjZ2/DVbnQiiA==", + "cpu": [ + "ppc64" + ], + "dev": true, + "license": "Apache-2.0", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=16.20.0" + } + }, + "node_modules/@typescript/typescript-linux-riscv64": { + "version": "7.0.2", + "resolved": "https://registry.npmjs.org/@typescript/typescript-linux-riscv64/-/typescript-linux-riscv64-7.0.2.tgz", + "integrity": "sha512-wf0jqEDOjrPRnKwYRyyJDRo11KMbvMFrU+q4zqKyChODBzvlkbhNQfKvLxQCcwTpdDaXSHZTVuh0JoCrKCUMHQ==", + "cpu": [ + "riscv64" + ], + "dev": true, + "license": "Apache-2.0", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=16.20.0" + } + }, + "node_modules/@typescript/typescript-linux-s390x": { + "version": "7.0.2", + "resolved": "https://registry.npmjs.org/@typescript/typescript-linux-s390x/-/typescript-linux-s390x-7.0.2.tgz", + "integrity": "sha512-IkwJc3L7yhytWd/ewjyxNDfOmswCm9GWMJT/ue/dU4aZNbwZeYAetq42VyLmsmSjvoX7z74X6ZaYCtzAr0EuGw==", + "cpu": [ + "s390x" + ], + "dev": true, + "license": "Apache-2.0", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=16.20.0" + } + }, + "node_modules/@typescript/typescript-linux-x64": { + "version": "7.0.2", + "resolved": "https://registry.npmjs.org/@typescript/typescript-linux-x64/-/typescript-linux-x64-7.0.2.tgz", + "integrity": "sha512-EYdf2cNg7rgCWJnxCdJ+F3V39O8ihb37eHAu1LK8oAFizgTQbPOK7zHHXbPt8rX24COqODXeI3sIf0fCXG7H/A==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "Apache-2.0", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=16.20.0" + } + }, + "node_modules/@typescript/typescript-netbsd-arm64": { + "version": "7.0.2", + "resolved": "https://registry.npmjs.org/@typescript/typescript-netbsd-arm64/-/typescript-netbsd-arm64-7.0.2.tgz", + "integrity": "sha512-+polYF4MF04aPpO5FTkHran9yUQDSXqy5GiSDKpsll5jy3l3+g9QLhpf39T+ePtefhXLOGrLl0QIjkQP6VnelA==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "Apache-2.0", + "optional": true, + "os": [ + "netbsd" + ], + "engines": { + "node": ">=16.20.0" + } + }, + "node_modules/@typescript/typescript-netbsd-x64": { + "version": "7.0.2", + "resolved": "https://registry.npmjs.org/@typescript/typescript-netbsd-x64/-/typescript-netbsd-x64-7.0.2.tgz", + "integrity": "sha512-8YIT0EHM/3dq10ZOVF/A7pc/YSMtbcecct4rWtexrnSCHOPcpC2KTLXfTCR6vDpnSiY12heNb1GiN/wu+T/FyA==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "Apache-2.0", + "optional": true, + "os": [ + "netbsd" + ], + "engines": { + "node": ">=16.20.0" + } + }, + "node_modules/@typescript/typescript-openbsd-arm64": { + "version": "7.0.2", + "resolved": "https://registry.npmjs.org/@typescript/typescript-openbsd-arm64/-/typescript-openbsd-arm64-7.0.2.tgz", + "integrity": "sha512-APT8+ClYnuYm1u9+kgGXoMj2VzWzcymwh2gNSQVySHfkRDGOTVkoWLjCmOQSaO+PoqQ57B0flRp9SA+7GnnkzQ==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "Apache-2.0", + "optional": true, + "os": [ + "openbsd" + ], + "engines": { + "node": ">=16.20.0" + } + }, + "node_modules/@typescript/typescript-openbsd-x64": { + "version": "7.0.2", + "resolved": "https://registry.npmjs.org/@typescript/typescript-openbsd-x64/-/typescript-openbsd-x64-7.0.2.tgz", + "integrity": "sha512-yX7s+Q0Dln0Dt9tEzZsAjXXR/+ytBM7AlglaqyeMPxQszJ1JhlJdZ6jLA+IzldHtflX81em7lDao1xXu+aRRkg==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "Apache-2.0", + "optional": true, + "os": [ + "openbsd" + ], + "engines": { + "node": ">=16.20.0" + } + }, + "node_modules/@typescript/typescript-sunos-x64": { + "version": "7.0.2", + "resolved": "https://registry.npmjs.org/@typescript/typescript-sunos-x64/-/typescript-sunos-x64-7.0.2.tgz", + "integrity": "sha512-dLJDGaLZ1D4HPQn62u1n8mBDkJREwMsAkCdkwd4Ieqw+x3TUyTsqY0YiBCtE6H6OzzgGk3iuZ3vFWRS+E8/d1g==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "Apache-2.0", + "optional": true, + "os": [ + "sunos" + ], + "engines": { + "node": ">=16.20.0" + } + }, + "node_modules/@typescript/typescript-win32-arm64": { + "version": "7.0.2", + "resolved": "https://registry.npmjs.org/@typescript/typescript-win32-arm64/-/typescript-win32-arm64-7.0.2.tgz", + "integrity": "sha512-Gyl1Vy6OsWesLzmq+EP0Fb7b4Nid5232AvcA2SFcdYreldpNtYFFofPjnt62y9hQy7VTaZp65ICJjuAQRaVcIQ==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "Apache-2.0", + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": ">=16.20.0" + } + }, + "node_modules/@typescript/typescript-win32-x64": { + "version": "7.0.2", + "resolved": "https://registry.npmjs.org/@typescript/typescript-win32-x64/-/typescript-win32-x64-7.0.2.tgz", + "integrity": "sha512-0BQ3HkAHHlKLSp1qRvf3SUhGpGsDuhB/jgFw75guyqbxJqEaS0Cw/VFO8i2nHglJUzQCRtMMR/IBAKE3ETMC4g==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "Apache-2.0", + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": ">=16.20.0" + } + }, + "node_modules/@vitest/mocker": { + "version": "5.0.5", + "resolved": "https://registry.npmjs.org/@vitest/mocker/-/mocker-5.0.5.tgz", + "integrity": "sha512-xLL3lkDegJZw+MGoGaCCsFayDZthGgpAtr3Vziyfc4p7xH89K7KRPcRBBh/kEYm0Aqkt5DtTmkYpBtRV/FrHZg==", + "dev": true, + "license": "MIT", + "dependencies": { + "@jridgewell/trace-mapping": "0.3.31", + "@vitest/spy": "5.0.5", + "estree-walker": "^3.0.3", + "magic-string": "^1.4.3" + }, + "funding": { + "url": "https://opencollective.com/vitest" + }, + "peerDependencies": { + "msw": "^2.4.9 || ^3.0.0", + "vite": "^6.0.0 || ^7.0.0 || ^8.0.0" + }, + "peerDependenciesMeta": { + "msw": { + "optional": true + }, + "vite": { + "optional": true + } + } + }, + "node_modules/@vitest/spy": { + "version": "5.0.5", + "resolved": "https://registry.npmjs.org/@vitest/spy/-/spy-5.0.5.tgz", + "integrity": "sha512-lwJRExuH417257Q5wv9QYugN9IMJkoxcrKrYxGPQkBzl7wlNpTUxD/5yNX4ESKVUIShKPgkGchYPFMWOOk0wzw==", + "dev": true, + "license": "MIT", + "funding": { + "url": "https://opencollective.com/vitest" + } + }, + "node_modules/assertion-error": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/assertion-error/-/assertion-error-2.0.1.tgz", + "integrity": "sha512-Izi8RQcffqCeNVgFigKli1ssklIbpHnCYc6AknXGYoB6grJqyeby7jv12JUQgmTAnIDnbck1uxksT4dzN3PWBA==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=12" + } + }, + "node_modules/chai": { + "version": "6.3.0", + "resolved": "https://registry.npmjs.org/chai/-/chai-6.3.0.tgz", + "integrity": "sha512-XWAtwJ6OHO+tj0EKCs0Y2UamnyOxseZWltU4x2U2wh8g4AigdjwvtUjvLP2tqkA/avxHEtzxNaqGq/YGNwckKg==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=18" + } + }, + "node_modules/cross-spawn": { + "version": "7.0.6", + "resolved": "https://registry.npmjs.org/cross-spawn/-/cross-spawn-7.0.6.tgz", + "integrity": "sha512-uV2QOWP2nWzsy2aMp8aRibhi9dlzF5Hgh5SHaB9OiTGEyDTiJJyx0uy51QXdyWbtAHNua4XJzUKca3OzKUd3vA==", + "dev": true, + "license": "MIT", + "dependencies": { + "path-key": "^3.1.0", + "shebang-command": "^2.0.0", + "which": "^2.0.1" + }, + "engines": { + "node": ">= 8" + } + }, + "node_modules/detect-libc": { + "version": "2.1.2", + "resolved": "https://registry.npmjs.org/detect-libc/-/detect-libc-2.1.2.tgz", + "integrity": "sha512-Btj2BOOO83o3WyH59e8MgXsxEQVcarkUOpEYrubB0urwnN10yQ364rsiByU11nZlqWYZm05i/of7io4mzihBtQ==", + "dev": true, + "license": "Apache-2.0", + "peer": true, + "engines": { + "node": ">=8" + } + }, + "node_modules/es-module-lexer": { + "version": "2.3.2", + "resolved": "https://registry.npmjs.org/es-module-lexer/-/es-module-lexer-2.3.2.tgz", + "integrity": "sha512-poHGpORABojJJucnV9KbOavETW8lBVnphkW77ER5/BQ5Fz7oXSoCNek7IH3vR5nRjdsEz926ibFYX8KtLQmdyw==", + "dev": true, + "license": "MIT" + }, + "node_modules/estree-walker": { + "version": "3.0.3", + "resolved": "https://registry.npmjs.org/estree-walker/-/estree-walker-3.0.3.tgz", + "integrity": "sha512-7RUKfXgSMMkzt6ZuXmqapOurLGPPfgj6l9uRZ7lRGolvk0y2yocc35LdcxKC5PQZdn2DMqioAQ2NoWcrTKmm6g==", + "dev": true, + "license": "MIT", + "dependencies": { + "@types/estree": "^1.0.0" + } + }, + "node_modules/eventsource": { + "version": "3.0.7", + "resolved": "https://registry.npmjs.org/eventsource/-/eventsource-3.0.7.tgz", + "integrity": "sha512-CRT1WTyuQoD771GW56XEZFQ/ZoSfWid1alKGDYMmkt2yl8UXrVR4pspqWNEcqKvVIzg6PAltWjxcSSPrboA4iA==", + "dev": true, + "license": "MIT", + "dependencies": { + "eventsource-parser": "^3.0.1" + }, + "engines": { + "node": ">=18.0.0" + } + }, + "node_modules/eventsource-parser": { + "version": "3.1.1", + "resolved": "https://registry.npmjs.org/eventsource-parser/-/eventsource-parser-3.1.1.tgz", + "integrity": "sha512-EKN1vKAMcZ8MlYMpaNuxN6R9yakzH6uajHcHVTqWJzvu5pWw9DyhbP35HH8MVBQ+dZjAfDxk+A8NiR9KWaXiyQ==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=18.0.0" + } + }, + "node_modules/expect-type": { + "version": "1.4.0", + "resolved": "https://registry.npmjs.org/expect-type/-/expect-type-1.4.0.tgz", + "integrity": "sha512-KfYbmpRm0VbLjEvVa9yGwCi9GI34xvi7A/HXYWQO65CSD2u3MczUJSuwXKFIxlGsgBQizV9q5J9NHj4VG0n+pA==", + "dev": true, + "license": "Apache-2.0", + "engines": { + "node": ">=12.0.0" + } + }, + "node_modules/fdir": { + "version": "6.5.0", + "resolved": "https://registry.npmjs.org/fdir/-/fdir-6.5.0.tgz", + "integrity": "sha512-tIbYtZbucOs0BRGqPJkshJUYdL+SDH7dVM8gjy+ERp3WAUjLEFJE+02kanyHtwjWOnwrKYBiwAmM0p4kLJAnXg==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=12.0.0" + }, + "peerDependencies": { + "picomatch": "^3 || ^4" + }, + "peerDependenciesMeta": { + "picomatch": { + "optional": true + } + } + }, + "node_modules/fsevents": { + "version": "2.3.3", + "resolved": "https://registry.npmjs.org/fsevents/-/fsevents-2.3.3.tgz", + "integrity": "sha512-5xoDfX+fL7faATnagmWPpbFtwh/R77WmMMqqHGS65C3vvB0YHrgF+B1YmZ3441tMj5n63k0212XNoJwzlhffQw==", + "dev": true, + "hasInstallScript": true, + "license": "MIT", + "optional": true, + "os": [ + "darwin" + ], + "peer": true, + "engines": { + "node": "^8.16.0 || ^10.6.0 || >=11.0.0" + } + }, + "node_modules/isexe": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/isexe/-/isexe-2.0.0.tgz", + "integrity": "sha512-RHxMLp9lnKHGHRng9QFhRCMbYAcVpn69smSGcq3f36xjgVVWThj4qqLbTLlq7Ssj8B+fIQ1EuCEGI2lKsyQeIw==", + "dev": true, + "license": "ISC" + }, + "node_modules/jose": { + "version": "6.2.12", + "resolved": "https://registry.npmjs.org/jose/-/jose-6.2.12.tgz", + "integrity": "sha512-9NiFmJEex0sy2Dk58j2UGBSHgUs2ypF9eZSu4L6vjOX3Dp96Sw1F3uL+H+D1sx02jZZdzUT0HgvCy59CuvXcWw==", + "dev": true, + "license": "MIT", + "funding": { + "url": "https://github.com/sponsors/panva" + } + }, + "node_modules/lightningcss": { + "version": "1.33.0", + "resolved": "https://registry.npmjs.org/lightningcss/-/lightningcss-1.33.0.tgz", + "integrity": "sha512-WkUDrojuJs0xkgGf2udWxa3yGBRxPtxUkB79i6aCZLRgc7PM8fZe9TosfPDcvEpQZbuFASnHYmRLBLUbmLOIIA==", + "dev": true, + "license": "MPL-2.0", + "peer": true, + "dependencies": { + "detect-libc": "^2.0.3" + }, + "engines": { + "node": ">= 12.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/parcel" + }, + "optionalDependencies": { + "lightningcss-android-arm64": "1.33.0", + "lightningcss-darwin-arm64": "1.33.0", + "lightningcss-darwin-x64": "1.33.0", + "lightningcss-freebsd-x64": "1.33.0", + "lightningcss-linux-arm-gnueabihf": "1.33.0", + "lightningcss-linux-arm64-gnu": "1.33.0", + "lightningcss-linux-arm64-musl": "1.33.0", + "lightningcss-linux-x64-gnu": "1.33.0", + "lightningcss-linux-x64-musl": "1.33.0", + "lightningcss-win32-arm64-msvc": "1.33.0", + "lightningcss-win32-x64-msvc": "1.33.0" + } + }, + "node_modules/lightningcss-android-arm64": { + "version": "1.33.0", + "resolved": "https://registry.npmjs.org/lightningcss-android-arm64/-/lightningcss-android-arm64-1.33.0.tgz", + "integrity": "sha512-gEpRTalKdosp4Bb8qWtc2iOgE5SeIHlpS1up9bFq2wAyYhl1UdTObYiHe98zEM9SQvSoqQZ1IQD0JNpg3Ml5pg==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MPL-2.0", + "optional": true, + "os": [ + "android" + ], + "peer": true, + "engines": { + "node": ">= 12.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/parcel" + } + }, + "node_modules/lightningcss-darwin-arm64": { + "version": "1.33.0", + "resolved": "https://registry.npmjs.org/lightningcss-darwin-arm64/-/lightningcss-darwin-arm64-1.33.0.tgz", + "integrity": "sha512-Sciaz8eenNTKn9b3t7+xr0ipTp9YxKQY4npwQ3mrRuL0BAVHBLyZxofhaKBAVtzmtRZ/zTyo0/to4B1uWG/Djg==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MPL-2.0", + "optional": true, + "os": [ + "darwin" + ], + "peer": true, + "engines": { + "node": ">= 12.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/parcel" + } + }, + "node_modules/lightningcss-darwin-x64": { + "version": "1.33.0", + "resolved": "https://registry.npmjs.org/lightningcss-darwin-x64/-/lightningcss-darwin-x64-1.33.0.tgz", + "integrity": "sha512-Z5UPAxzrjlWNNyGy6i65cJzzvgJ5D3T6wMvs+gWpY9d7qRhANrxqAp6LhxIgZhWEw18RfJTGcRxjuLIBr+m8XQ==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MPL-2.0", + "optional": true, + "os": [ + "darwin" + ], + "peer": true, + "engines": { + "node": ">= 12.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/parcel" + } + }, + "node_modules/lightningcss-freebsd-x64": { + "version": "1.33.0", + "resolved": "https://registry.npmjs.org/lightningcss-freebsd-x64/-/lightningcss-freebsd-x64-1.33.0.tgz", + "integrity": "sha512-QQM/Ti/hQajJwCY+RiWuCZ9sdtI/XQk7nDK5vC8kkdwixezOlDgvDx7+RT+QjK6FcFT4MpsuoBnHIo/O3StRRg==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MPL-2.0", + "optional": true, + "os": [ + "freebsd" + ], + "peer": true, + "engines": { + "node": ">= 12.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/parcel" + } + }, + "node_modules/lightningcss-linux-arm-gnueabihf": { + "version": "1.33.0", + "resolved": "https://registry.npmjs.org/lightningcss-linux-arm-gnueabihf/-/lightningcss-linux-arm-gnueabihf-1.33.0.tgz", + "integrity": "sha512-N7FVBe6iS24MlM6R/4RBTxGhQheZGs7tiQ9U32UtF75NzP5Q7xWPRqLBCKxlRQRk3rY1jCIPLzx7WzOhuUIRLQ==", + "cpu": [ + "arm" + ], + "dev": true, + "license": "MPL-2.0", + "optional": true, + "os": [ + "linux" + ], + "peer": true, + "engines": { + "node": ">= 12.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/parcel" + } + }, + "node_modules/lightningcss-linux-arm64-gnu": { + "version": "1.33.0", + "resolved": "https://registry.npmjs.org/lightningcss-linux-arm64-gnu/-/lightningcss-linux-arm64-gnu-1.33.0.tgz", + "integrity": "sha512-j2v/itmy4HlNxlc6voKXYgBqNi0Ng2LShg4z7GufpEgs05P+2suBVyi9I6YHq5uoVFx9ETin3eCEhLVyXGQnKg==", + "cpu": [ + "arm64" + ], + "dev": true, + "libc": [ + "glibc" + ], + "license": "MPL-2.0", + "optional": true, + "os": [ + "linux" + ], + "peer": true, + "engines": { + "node": ">= 12.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/parcel" + } + }, + "node_modules/lightningcss-linux-arm64-musl": { + "version": "1.33.0", + "resolved": "https://registry.npmjs.org/lightningcss-linux-arm64-musl/-/lightningcss-linux-arm64-musl-1.33.0.tgz", + "integrity": "sha512-yiO5ROMuYQgXbC60yjZU5CYSFZGKXL0HFATXt9mHJn1+zW55oCtMI9NfcVhYLMFDL7gV7oBPon/EmMMGg2OvtQ==", + "cpu": [ + "arm64" + ], + "dev": true, + "libc": [ + "musl" + ], + "license": "MPL-2.0", + "optional": true, + "os": [ + "linux" + ], + "peer": true, + "engines": { + "node": ">= 12.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/parcel" + } + }, + "node_modules/lightningcss-linux-x64-gnu": { + "version": "1.33.0", + "resolved": "https://registry.npmjs.org/lightningcss-linux-x64-gnu/-/lightningcss-linux-x64-gnu-1.33.0.tgz", + "integrity": "sha512-ar+Ju7LmcN0Jo4FpL4hpFybwNG9/3A/Br5KW2n2jyODg3MEZXaDYADdemoNS+BDNfMgKvylJLj4S5tyRActuAg==", + "cpu": [ + "x64" + ], + "dev": true, + "libc": [ + "glibc" + ], + "license": "MPL-2.0", + "optional": true, + "os": [ + "linux" + ], + "peer": true, + "engines": { + "node": ">= 12.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/parcel" + } + }, + "node_modules/lightningcss-linux-x64-musl": { + "version": "1.33.0", + "resolved": "https://registry.npmjs.org/lightningcss-linux-x64-musl/-/lightningcss-linux-x64-musl-1.33.0.tgz", + "integrity": "sha512-RYiYbkokw0trfKqqzfF55lginwEPrD3OJDfTuJzFs1MK6iFnDenaz1fqLLtX4ITG3OktJQXOeTaw1awrBAlZPw==", + "cpu": [ + "x64" + ], + "dev": true, + "libc": [ + "musl" + ], + "license": "MPL-2.0", + "optional": true, + "os": [ + "linux" + ], + "peer": true, + "engines": { + "node": ">= 12.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/parcel" + } + }, + "node_modules/lightningcss-win32-arm64-msvc": { + "version": "1.33.0", + "resolved": "https://registry.npmjs.org/lightningcss-win32-arm64-msvc/-/lightningcss-win32-arm64-msvc-1.33.0.tgz", + "integrity": "sha512-1K+MPfLSFVpphzpdbfkhlWk6wBrTObBzS2T6db10PNOZgR9GoVsAWzwNyuhUYYbTp23j+4RrncfujZ4uAzXvwA==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MPL-2.0", + "optional": true, + "os": [ + "win32" + ], + "peer": true, + "engines": { + "node": ">= 12.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/parcel" + } + }, + "node_modules/lightningcss-win32-x64-msvc": { + "version": "1.33.0", + "resolved": "https://registry.npmjs.org/lightningcss-win32-x64-msvc/-/lightningcss-win32-x64-msvc-1.33.0.tgz", + "integrity": "sha512-OlEICDx/Xl0FqSp4bry8zFnCvGpig3Gl4gCquvYwHuqJKEC1+n9NgDniFvqHGmMv1ZkqDJrDqKKSykTDX+ehuA==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MPL-2.0", + "optional": true, + "os": [ + "win32" + ], + "peer": true, + "engines": { + "node": ">= 12.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/parcel" + } + }, + "node_modules/magic-string": { + "version": "1.4.3", + "resolved": "https://registry.npmjs.org/magic-string/-/magic-string-1.4.3.tgz", + "integrity": "sha512-z12OxmaPGE0F4xlpGdjuAUckipLpQlTAuAmyGhNoD7ODpYUzvDfvtxUbjM/jwznyP178oju/KDdyi220wNJ0RQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "@jridgewell/sourcemap-codec": "^1.6.0" + } + }, + "node_modules/minisearch": { + "version": "7.2.0", + "resolved": "https://registry.npmjs.org/minisearch/-/minisearch-7.2.0.tgz", + "integrity": "sha512-dqT2XBYUOZOiC5t2HRnwADjhNS2cecp9u+TJRiJ1Qp/f5qjkeT5APcGPjHw+bz89Ms8Jp+cG4AlE+QZ/QnDglg==", + "license": "MIT" + }, + "node_modules/nanoid": { + "version": "3.3.20", + "resolved": "https://registry.npmjs.org/nanoid/-/nanoid-3.3.20.tgz", + "integrity": "sha512-uKdg2G3GNCKQn9byYOpxbGqrT2fGO5KRt5J/8b3pok8rT6qxGWF6hxMyJiEYtAf+FVyYuD9hRaDqX5uPFYJ4ZQ==", + "dev": true, + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/ai" + } + ], + "license": "MIT", + "peer": true, + "bin": { + "nanoid": "bin/nanoid.cjs" + }, + "engines": { + "node": "^10 || ^12 || ^13.7 || ^14 || >=15.0.1" + } + }, + "node_modules/obug": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/obug/-/obug-3.0.0.tgz", + "integrity": "sha512-5vvB5+W7ePv+p3uqxi+RcW1XAzLW0/hxt3/4X4Lc4qHudzOhmBiBwOY6DRob4WnanAEGvNcLjF+KNOufrUoEQw==", + "dev": true, + "funding": [ + "https://github.com/sponsors/sxzz", + "https://opencollective.com/debug" + ], + "license": "MIT", + "engines": { + "node": ">=12.20.0" + } + }, + "node_modules/path-key": { + "version": "3.1.1", + "resolved": "https://registry.npmjs.org/path-key/-/path-key-3.1.1.tgz", + "integrity": "sha512-ojmeN0qd+y0jszEtoY48r0Peq5dwMEkIlCOu6Q5f41lfkswXuKtYrhgoTpLnyIcHm24Uhqx+5Tqm2InSwLhE6Q==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=8" + } + }, + "node_modules/picocolors": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/picocolors/-/picocolors-1.1.1.tgz", + "integrity": "sha512-xceH2snhtb5M9liqDsmEw56le376mTZkEX/jEb/RxNFyegNul7eNslCXP9FDj/Lcu0X8KEyMceP2ntpaHrDEVA==", + "dev": true, + "license": "ISC", + "peer": true + }, + "node_modules/picomatch": { + "version": "4.0.7", + "resolved": "https://registry.npmjs.org/picomatch/-/picomatch-4.0.7.tgz", + "integrity": "sha512-qcJu88Q2IWqJsDD529JKMdwGm/dvInW4HvQnRwiH9JtihJvzGOscDtHE3x1pBKeUOTysQ8kVmLnJ2kJu7yhcGA==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=12" + }, + "funding": { + "url": "https://github.com/sponsors/jonschlinkert" + } + }, + "node_modules/pkce-challenge": { + "version": "5.0.1", + "resolved": "https://registry.npmjs.org/pkce-challenge/-/pkce-challenge-5.0.1.tgz", + "integrity": "sha512-wQ0b/W4Fr01qtpHlqSqspcj3EhBvimsdh0KlHhH8HRZnMsEa0ea2fTULOXOS9ccQr3om+GcGRk4e+isrZWV8qQ==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=16.20.0" + } + }, + "node_modules/postcss": { + "version": "8.5.30", + "resolved": "https://registry.npmjs.org/postcss/-/postcss-8.5.30.tgz", + "integrity": "sha512-zkd+e9S1AQHhmaBKye9zlFHWjOwVgqXl1q3ECYy8561K7qxR2cREf/STLskulBFd78k6AOFeA+ff1TtEzk9bAA==", + "dev": true, + "funding": [ + { + "type": "opencollective", + "url": "https://opencollective.com/postcss/" + }, + { + "type": "tidelift", + "url": "https://tidelift.com/funding/github/npm/postcss" + }, + { + "type": "github", + "url": "https://github.com/sponsors/ai" + } + ], + "license": "MIT", + "peer": true, + "dependencies": { + "nanoid": "^3.3.20", + "picocolors": "^1.1.1", + "source-map-js": "^1.2.2" + }, + "engines": { + "node": "^10 || ^12 || >=14" + } + }, + "node_modules/rolldown": { + "version": "1.2.14", + "resolved": "https://registry.npmjs.org/rolldown/-/rolldown-1.2.14.tgz", + "integrity": "sha512-eTBTTqMR6uHKLgcUqpJP8ntzDI6V4LYA8negAMBEI7gkgVvOdrx6VsvS2Ym+83KhOPdLCHWJu0w7UQFJZhHKNw==", + "dev": true, + "license": "MIT", + "peer": true, + "dependencies": { + "@oxc-project/types": "=0.154.0", + "@rolldown/pluginutils": "^1.0.0" + }, + "bin": { + "rolldown": "bin/cli.mjs" + }, + "engines": { + "node": "^20.19.0 || >=22.12.0" + }, + "optionalDependencies": { + "@rolldown/binding-android-arm-eabi": "1.2.14", + "@rolldown/binding-android-arm64": "1.2.14", + "@rolldown/binding-darwin-arm64": "1.2.14", + "@rolldown/binding-darwin-x64": "1.2.14", + "@rolldown/binding-freebsd-x64": "1.2.14", + "@rolldown/binding-linux-arm-gnueabihf": "1.2.14", + "@rolldown/binding-linux-arm64-gnu": "1.2.14", + "@rolldown/binding-linux-arm64-musl": "1.2.14", + "@rolldown/binding-linux-ppc64-gnu": "1.2.14", + "@rolldown/binding-linux-s390x-gnu": "1.2.14", + "@rolldown/binding-linux-x64-gnu": "1.2.14", + "@rolldown/binding-linux-x64-musl": "1.2.14", + "@rolldown/binding-openharmony-arm64": "1.2.14", + "@rolldown/binding-win32-arm64-msvc": "1.2.14", + "@rolldown/binding-win32-x64-msvc": "1.2.14" + } + }, + "node_modules/shebang-command": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/shebang-command/-/shebang-command-2.0.0.tgz", + "integrity": "sha512-kHxr2zZpYtdmrN1qDjrrX/Z1rR1kG8Dx+gkpK1G4eXmvXswmcE1hTWBWYUzlraYw1/yZp6YuDY77YtvbN0dmDA==", + "dev": true, + "license": "MIT", + "dependencies": { + "shebang-regex": "^3.0.0" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/shebang-regex": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/shebang-regex/-/shebang-regex-3.0.0.tgz", + "integrity": "sha512-7++dFhtcx3353uBaq8DDR4NuxBetBzC7ZQOhmTQInHEd6bSrXdiEyzCvG07Z44UYdLShWUyXt5M/yhz8ekcb1A==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=8" + } + }, + "node_modules/source-map-js": { + "version": "1.2.2", + "resolved": "https://registry.npmjs.org/source-map-js/-/source-map-js-1.2.2.tgz", + "integrity": "sha512-KGj/8Y43x35aZVDtt+J4mK1hoLGHULMYfSkODJNQjNDC3oW1PqPoxMwo0pLUsWM/UEGzON/NxeHywEfNXNP3Vw==", + "dev": true, + "license": "BSD-3-Clause", + "peer": true, + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/std-env": { + "version": "4.3.0", + "resolved": "https://registry.npmjs.org/std-env/-/std-env-4.3.0.tgz", + "integrity": "sha512-OtU/EgQ1kIm5KwqQpBC6ZEMXrZRui11w8zgfTWp8cdO9B8OaPsbA8bTHO2P+HNo1VlUTGMVBwPhydu6poeXiag==", + "dev": true, + "license": "MIT" + }, + "node_modules/tinybench": { + "version": "6.2.2", + "resolved": "https://registry.npmjs.org/tinybench/-/tinybench-6.2.2.tgz", + "integrity": "sha512-nIZ2N2dFpjBsg2hgitKu7mA+FvsIJuGJOwysRngmVRRmWgR/UyPjwJj8c/fT/dhm8EkHnF5EOogRf+0Z2qrimA==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=20.0.0" + } + }, + "node_modules/tinyexec": { + "version": "1.3.1", + "resolved": "https://registry.npmjs.org/tinyexec/-/tinyexec-1.3.1.tgz", + "integrity": "sha512-GCvB3aoys96IuDFBMcTB46JOR6mdMtAToqwiW8JlWhsoh1mhHi/xn9ss/Dg7N555GiJyEt2qzoG/NHCwM6h1EA==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=18" + } + }, + "node_modules/tinyglobby": { + "version": "0.2.17", + "resolved": "https://registry.npmjs.org/tinyglobby/-/tinyglobby-0.2.17.tgz", + "integrity": "sha512-wXR/dYpcqKmfWpEdZjiKJOwCNFndD0DMnrW/cYjVGttEkBfVgcLFHoNrlj47mjOVic9yyNu65alsgF4NQyTa2g==", + "dev": true, + "license": "MIT", + "dependencies": { + "fdir": "^6.5.0", + "picomatch": "^4.0.4" + }, + "engines": { + "node": ">=12.0.0" + }, + "funding": { + "url": "https://github.com/sponsors/SuperchupuDev" + } + }, + "node_modules/typescript": { + "version": "7.0.2", + "resolved": "https://registry.npmjs.org/typescript/-/typescript-7.0.2.tgz", + "integrity": "sha512-8FYau96o3NKOhbjKi/qNvG/W5jhzxkbdm5sj9AbZ/5T5sWqn3hJgLfGx27sRKZWTvyzCP8dLRBTf5tBTSRVUNA==", + "dev": true, + "license": "Apache-2.0", + "bin": { + "tsc": "bin/tsc" + }, + "engines": { + "node": ">=16.20.0" + }, + "optionalDependencies": { + "@typescript/typescript-aix-ppc64": "7.0.2", + "@typescript/typescript-darwin-arm64": "7.0.2", + "@typescript/typescript-darwin-x64": "7.0.2", + "@typescript/typescript-freebsd-arm64": "7.0.2", + "@typescript/typescript-freebsd-x64": "7.0.2", + "@typescript/typescript-linux-arm": "7.0.2", + "@typescript/typescript-linux-arm64": "7.0.2", + "@typescript/typescript-linux-loong64": "7.0.2", + "@typescript/typescript-linux-mips64el": "7.0.2", + "@typescript/typescript-linux-ppc64": "7.0.2", + "@typescript/typescript-linux-riscv64": "7.0.2", + "@typescript/typescript-linux-s390x": "7.0.2", + "@typescript/typescript-linux-x64": "7.0.2", + "@typescript/typescript-netbsd-arm64": "7.0.2", + "@typescript/typescript-netbsd-x64": "7.0.2", + "@typescript/typescript-openbsd-arm64": "7.0.2", + "@typescript/typescript-openbsd-x64": "7.0.2", + "@typescript/typescript-sunos-x64": "7.0.2", + "@typescript/typescript-win32-arm64": "7.0.2", + "@typescript/typescript-win32-x64": "7.0.2" + } + }, + "node_modules/undici-types": { + "version": "6.21.0", + "resolved": "https://registry.npmjs.org/undici-types/-/undici-types-6.21.0.tgz", + "integrity": "sha512-iwDZqg0QAGrg9Rav5H4n0M64c3mkR59cJ6wQp+7C4nI0gsmExaedaYLNO44eT4AtBBwjbTiGPMlt2Md0T9H9JQ==", + "dev": true, + "license": "MIT" + }, + "node_modules/vite": { + "version": "8.3.4", + "resolved": "https://registry.npmjs.org/vite/-/vite-8.3.4.tgz", + "integrity": "sha512-IftLE8phJhdNRIGNEV4JCCvfNScoZQVfcXaN8TQropWGJtOHcu+iAB9pHcuGvmEb/kddw0LBe/jS8ztdC6xdiQ==", + "dev": true, + "license": "MIT", + "peer": true, + "dependencies": { + "lightningcss": "^1.33.0", + "picomatch": "^4.0.7", + "postcss": "^8.5.29", + "rolldown": "~1.2.12", + "tinyglobby": "^0.2.17" + }, + "bin": { + "vite": "bin/vite.js" + }, + "engines": { + "node": "^20.19.0 || >=22.12.0" + }, + "funding": { + "url": "https://github.com/vitejs/vite?sponsor=1" + }, + "optionalDependencies": { + "fsevents": "~2.3.3" + }, + "peerDependencies": { + "@types/node": "^20.19.0 || >=22.12.0", + "@vitejs/devtools": "^0.7.1", + "esbuild": "^0.27.0 || ^0.28.0", + "jiti": ">=1.21.0", + "less": "^4.0.0", + "sass": "^1.70.0", + "sass-embedded": "^1.70.0", + "stylus": ">=0.54.8", + "sugarss": "^5.0.0", + "terser": "^5.16.0", + "tsx": "^4.8.1", + "yaml": "^2.4.2" + }, + "peerDependenciesMeta": { + "@types/node": { + "optional": true + }, + "@vitejs/devtools": { + "optional": true + }, + "esbuild": { + "optional": true + }, + "jiti": { + "optional": true + }, + "less": { + "optional": true + }, + "sass": { + "optional": true + }, + "sass-embedded": { + "optional": true + }, + "stylus": { + "optional": true + }, + "sugarss": { + "optional": true + }, + "terser": { + "optional": true + }, + "tsx": { + "optional": true + }, + "yaml": { + "optional": true + } + } + }, + "node_modules/vitest": { + "version": "5.0.5", + "resolved": "https://registry.npmjs.org/vitest/-/vitest-5.0.5.tgz", + "integrity": "sha512-cPODOMF8YnkiiYtDdHo6n4wWmBrOBBbOIYH2d33M4l3CdNn72h+qMmO6uXffUN6A5AZFXNND/d+krS9mgkXRfg==", + "dev": true, + "license": "MIT", + "dependencies": { + "@types/chai": "^5.2.2", + "@vitest/mocker": "5.0.5", + "chai": "^6.3.0", + "es-module-lexer": "^2.3.2", + "expect-type": "^1.4.0", + "magic-string": "^1.4.3", + "obug": "^3.0.0", + "picomatch": "^4.0.7", + "std-env": "^4.3.0", + "tinybench": "^6.2.2", + "tinyexec": "^1.3.1", + "tinyglobby": "^0.2.17", + "why-is-node-running": "3.2.1" + }, + "bin": { + "vitest": "vitest.mjs" + }, + "engines": { + "node": "^22.12.0 || ^24.0.0 || >=26.0.0" + }, + "funding": { + "url": "https://opencollective.com/vitest" + }, + "peerDependencies": { + "@edge-runtime/vm": "*", + "@opentelemetry/api": "^1.9.0", + "@types/node": "^22.0.0 || >=24.0.0", + "@vitest/browser-playwright": "5.0.5", + "@vitest/browser-preview": "5.0.5", + "@vitest/browser-webdriverio": "^5.0.0-beta.5 || >=5.0.0", + "@vitest/coverage-istanbul": "5.0.5", + "@vitest/coverage-v8": "5.0.5", + "@vitest/ui": "5.0.5", + "happy-dom": "*", + "jsdom": "*", + "vite": "^6.4.0 || ^7.0.0 || ^8.0.0" + }, + "peerDependenciesMeta": { + "@edge-runtime/vm": { + "optional": true + }, + "@opentelemetry/api": { + "optional": true + }, + "@types/node": { + "optional": true + }, + "@vitest/browser-playwright": { + "optional": true + }, + "@vitest/browser-preview": { + "optional": true + }, + "@vitest/browser-webdriverio": { + "optional": true + }, + "@vitest/coverage-istanbul": { + "optional": true + }, + "@vitest/coverage-v8": { + "optional": true + }, + "@vitest/ui": { + "optional": true + }, + "happy-dom": { + "optional": true + }, + "jsdom": { + "optional": true + }, + "vite": { + "optional": false + } + } + }, + "node_modules/which": { + "version": "2.0.2", + "resolved": "https://registry.npmjs.org/which/-/which-2.0.2.tgz", + "integrity": "sha512-BLI3Tl1TW3Pvl70l3yq3Y64i+awpwXqsGBYWkkqMtnbXgrMD+yj7rhW0kuEDxzJaYXGjEW5ogapKNMEKNMjibA==", + "dev": true, + "license": "ISC", + "dependencies": { + "isexe": "^2.0.0" + }, + "bin": { + "node-which": "bin/node-which" + }, + "engines": { + "node": ">= 8" + } + }, + "node_modules/why-is-node-running": { + "version": "3.2.1", + "resolved": "https://registry.npmjs.org/why-is-node-running/-/why-is-node-running-3.2.1.tgz", + "integrity": "sha512-Tb2FUhB4vUsGQlfSquQLYkApkuPAFQXGFzxWKHHumVz2dK+X1RUm/HnID4+TfIGYJ1kTcwOaCk/buYCEJr6YjQ==", + "dev": true, + "license": "MIT", + "bin": { + "why-is-node-running": "cli.js" + }, + "engines": { + "node": ">=20.11" + } + }, + "node_modules/zod": { + "version": "4.6.7", + "resolved": "https://registry.npmjs.org/zod/-/zod-4.6.7.tgz", + "integrity": "sha512-g8W2D2kU6a8untc053fX1uVBKzM7yqAxW/3zwk83+6RyF2+SKOuV+c0wBDhUwzV1PaGapmZH+rmcUzPf+3CGwg==", + "license": "MIT", + "funding": { + "url": "https://github.com/sponsors/colinhacks" + } + } + } +} diff --git a/mcp/package.json b/mcp/package.json new file mode 100644 index 0000000..9c9d9ab --- /dev/null +++ b/mcp/package.json @@ -0,0 +1,57 @@ +{ + "name": "@fiscalapi/mcp", + "version": "0.1.0", + "description": "Servidor MCP de solo lectura con el catálogo de operaciones de la API de FiscalAPI (CFDI 4.0) y una búsqueda en su documentación", + "type": "module", + "bin": { + "fiscalapi-mcp": "dist/index.js" + }, + "files": [ + "dist", + "data", + "README.md" + ], + "scripts": { + "build:data": "node scripts/build-data.mjs", + "build": "tsc -p tsconfig.json", + "test": "vitest run" + }, + "keywords": [ + "fiscalapi", + "mcp", + "cfdi", + "factura", + "sat", + "mexico" + ], + "author": "FISCAL API S DE R.L DE C.V", + "license": "MPL-2.0", + "homepage": "https://docs.fiscalapi.com", + "repository": { + "type": "git", + "url": "git+https://github.com/FiscalAPI/fiscalapi-ai.git", + "directory": "mcp" + }, + "bugs": { + "url": "https://github.com/FiscalAPI/fiscalapi-ai/issues" + }, + "engines": { + "node": ">=22" + }, + "publishConfig": { + "access": "public" + }, + "dependencies": { + "@modelcontextprotocol/server": "2.3.1", + "@orama/stemmers": "3.1.18", + "@orama/stopwords": "3.1.18", + "minisearch": "7.2.0", + "zod": "4.6.7" + }, + "devDependencies": { + "@modelcontextprotocol/client": "2.3.1", + "@types/node": "22.20.5", + "typescript": "7.0.2", + "vitest": "5.0.5" + } +} diff --git a/mcp/scripts/build-data.mjs b/mcp/scripts/build-data.mjs new file mode 100644 index 0000000..559e6fa --- /dev/null +++ b/mcp/scripts/build-data.mjs @@ -0,0 +1,114 @@ +// Genera mcp/data desde el OpenAPI público de FiscalAPI y la documentación de docs.fiscalapi.com. +// +// Uso: node scripts/build-data.mjs [fuente] [--docs-origin ] [--out ] +// fuente: URL http(s) o ruta de archivo. Por omisión FISCALAPI_OPENAPI_SOURCE o el OpenAPI de live. +// --docs-origin: origen del que se descargan llms.txt y las secciones .md. Por omisión +// FISCALAPI_DOCS_ORIGIN o https://docs.fiscalapi.com. +// --out: directorio de salida. Por omisión mcp/data. +import { createHash } from 'node:crypto'; +import { mkdir, readFile, rename, rm, writeFile } from 'node:fs/promises'; +import path from 'node:path'; +import { fileURLToPath } from 'node:url'; +import { parseArgs } from 'node:util'; + +import MiniSearch from 'minisearch'; + +import { DOCS_INDEX_OPTIONS } from '../src/docs-search.ts'; +import { DOCS_SITE, DocsError, downloadMarkdown, fetchLlms, normalizeOrigin, parseLlms, toDocument } from './docs.mjs'; +import { buildCatalog, buildOperation, extractOperations, OpenApiError, parseOpenApi, serializeOperation } from './openapi.mjs'; + +const DEFAULT_SOURCE = 'https://live.fiscalapi.com/api/openapi/v4.json'; +const DEFAULT_OUT_DIR = fileURLToPath(new URL('../data', import.meta.url)); +const FETCH_TIMEOUT_MS = 60_000; + +async function main() { + const { positionals, values } = parseArgs({ + allowPositionals: true, + options: { out: { type: 'string' }, 'docs-origin': { type: 'string' } }, + }); + const source = positionals[0] ?? process.env.FISCALAPI_OPENAPI_SOURCE ?? DEFAULT_SOURCE; + const docsOrigin = normalizeOrigin(values['docs-origin'] ?? process.env.FISCALAPI_DOCS_ORIGIN ?? DOCS_SITE); + const outDir = path.resolve(values.out ?? DEFAULT_OUT_DIR); + + const text = await readSource(source); + const doc = parseOpenApi(text); + const operations = extractOperations(doc); + const catalog = buildCatalog(operations); + const docs = await downloadDocs(docsOrigin); + + const staging = `${outDir}.tmp-${process.pid}`; + await rm(staging, { recursive: true, force: true }); + try { + await mkdir(path.join(staging, 'operations'), { recursive: true }); + let largest = { id: '', chars: 0 }; + for (const entry of operations) { + const json = serializeOperation(buildOperation(doc, entry)); + if (json.length > largest.chars) largest = { id: entry.id, chars: json.length }; + await writeFile(path.join(staging, 'operations', `${entry.id}.json`), json); + } + await writeFile(path.join(staging, 'catalog.json'), JSON.stringify(catalog)); + await writeFile(path.join(staging, 'docs-index.json'), docs.indexJson); + const sha256 = createHash('sha256').update(text).digest('hex'); + const sourceInfo = { + source, + sha256, + operations: catalog.length, + docs: { origin: docsOrigin, sha256: docs.sha256, documents: docs.documents }, + }; + await writeFile(path.join(staging, 'source.json'), JSON.stringify(sourceInfo, null, 2)); + + // Se reemplaza de golpe para que un build fallido nunca deje data/ a medias. + await rm(outDir, { recursive: true, force: true }); + await rename(staging, outDir); + + console.error(`build-data: ${catalog.length} operaciones en ${outDir}`); + console.error(`build-data: la más grande es ${largest.id} con ${largest.chars} caracteres`); + console.error(`build-data: sha256 ${sha256}`); + console.error(`build-data: ${docs.documents} documentos de ${docsOrigin}; el índice mide ${Buffer.byteLength(docs.indexJson)} bytes`); + console.error(`build-data: sha256 de la documentación ${docs.sha256}`); + } finally { + await rm(staging, { recursive: true, force: true }); + } +} + +// El hash cubre el llms.txt y cada Markdown en el orden del llms.txt: cambia solo si cambia la documentación. +async function downloadDocs(origin) { + const llms = await fetchLlms(origin); + const entries = parseLlms(llms); + const markdown = await downloadMarkdown(origin, entries.map((entry) => entry.url)); + + const hash = createHash('sha256').update(llms); + for (const body of markdown) hash.update(body); + const index = new MiniSearch(DOCS_INDEX_OPTIONS); + index.addAll(entries.map((entry, i) => toDocument(entry, markdown[i]))); + return { indexJson: JSON.stringify(index), sha256: hash.digest('hex'), documents: entries.length }; +} + +async function readSource(source) { + if (!/^https?:\/\//i.test(source)) { + try { + return await readFile(source, 'utf8'); + } catch (error) { + throw new OpenApiError(`no se pudo leer ${source}: ${error.message}`); + } + } + let response; + try { + response = await fetch(source, { signal: AbortSignal.timeout(FETCH_TIMEOUT_MS) }); + } catch (error) { + throw new OpenApiError(`no se pudo descargar ${source}: ${error.message}`); + } + if (response.status !== 200) { + throw new OpenApiError(`${source} respondió HTTP ${response.status}; se esperaba 200.`); + } + return response.text(); +} + +try { + await main(); +} catch (error) { + const expected = error instanceof OpenApiError || error instanceof DocsError; + const message = expected ? error.message : (error?.stack ?? String(error)); + console.error(`build-data: ${message}`); + process.exitCode = 1; +} diff --git a/mcp/scripts/docs.mjs b/mcp/scripts/docs.mjs new file mode 100644 index 0000000..74c21e5 --- /dev/null +++ b/mcp/scripts/docs.mjs @@ -0,0 +1,169 @@ +// Documentación de docs.fiscalapi.com para el índice de buscar_docs. parseLlms y cleanSection son +// transformaciones puras; fetchLlms y downloadMarkdown son la única parte que usa la red. + +// El llms.txt siempre enlaza a este sitio; para descargar de otro origen se cambia solo el prefijo. +export const DOCS_SITE = 'https://docs.fiscalapi.com'; +export const DOWNLOAD_CONCURRENCY = 8; +export const FETCH_TIMEOUT_MS = 60_000; + +const SITE_URL = `${escapeRegExp(DOCS_SITE)}/\\S+?\\.md`; +// Una página: "- [Título](https://docs.fiscalapi.com/ruta.md): descripción". +const PAGE_LINE = new RegExp(`^- \\[([^\\]]+)\\]\\((${SITE_URL})\\)`); +// Una sección de la página anterior: " - [Título](https://docs.fiscalapi.com/ruta/slug.md)". +const SECTION_LINE = new RegExp(`^ {2}- \\[([^\\]]+)\\]\\((${SITE_URL})\\)`); +const ANY_MD_LINK = new RegExp(`\\((${SITE_URL})\\)`, 'g'); + +const CODE_BLOCK = /```[\s\S]*?```/g; +const HEADING = /^#{1,2} (.+)$/m; +const ANNOTATION = /\{\{([\s\S]*?)\}\}/g; +const ANNOTATION_TAG = /\btag:\s*'([^']*)'/; +const ANNOTATION_LABEL = /\blabel:\s*'([^']*)'/; +const TAG_WITH_NAME = /<[A-Za-z][^>]*?\sname="([^"]*)"[^>]*>/g; +// o : el valor, sin comillas. +const ITEM_VALUE = /]*?\bvalue=(?:'"?([^'"]*)"?'|"([^"]*)")[^>]*>/g; +const ANY_TAG = /<\/?[A-Za-z][^>]*>/g; +const LINK = /!?\[([^\]]*)\]\([^)]*\)/g; +const INLINE_CODE_MARK = /`/g; +const LINE_INDENT = /^[ \t]+|[ \t]+$/gm; +const REPEATED_BLANK_LINES = /\n{3,}/g; + +export class DocsError extends Error {} + +/** + * Los documentos a indexar, sin repetir URL y en el orden del llms.txt: cada sección con el título + * de su página, y cada página sin secciones entera. Una página con secciones no se indexa entera + * porque es la suma de ellas y competiría con sus propias secciones. + */ +export function parseLlms(text) { + const pages = new Map(); + let current; + for (const line of text.split(/\r?\n/)) { + const page = PAGE_LINE.exec(line); + if (page) { + const [, title, url] = page; + current = pages.get(url) ?? { title, url, sections: [] }; + pages.set(url, current); + continue; + } + const section = SECTION_LINE.exec(line); + if (section && current) current.sections.push(section[2]); + } + + const documents = new Map(); + for (const page of pages.values()) { + const urls = page.sections.length > 0 ? page.sections : [page.url]; + for (const url of urls) if (!documents.has(url)) documents.set(url, { url, page: page.title }); + } + if (documents.size === 0) throw new DocsError(`el llms.txt no enlaza ninguna página .md de ${DOCS_SITE}.`); + return [...documents.values()]; +} + +/** Todas las URLs .md del llms.txt, páginas y secciones, sin repetir. */ +export function listMarkdownUrls(text) { + return [...new Set([...text.matchAll(ANY_MD_LINK)].map((match) => match[1]))]; +} + +/** La ruta de una URL del sitio, por ejemplo /invoices/cancelar-factura.md. */ +export function sitePath(url) { + return url.slice(DOCS_SITE.length); +} + +/** + * Convierte el MDX crudo de una sección en texto indexable y legible: título del primer encabezado, + * endpoint de su anotación {{ tag, label }} y el cuerpo sin código, etiquetas ni URLs. Conserva los + * saltos de línea y las tablas para que el extracto se lea. + */ +export function cleanSection(markdown) { + const withoutCode = markdown.replace(CODE_BLOCK, ''); + const heading = HEADING.exec(withoutCode)?.[1] ?? ''; + const text = withoutCode + .replace(HEADING, '') + .replace(ANNOTATION, '') + // El name de es el nombre del campo: se busca por él. + .replace(TAG_WITH_NAME, '$1') + // El value de un es la clave del catálogo (por ejemplo el motivo de cancelación "01"). + .replace(ITEM_VALUE, (_, quoted, plain) => `${quoted ?? plain} `) + .replace(ANY_TAG, '') + .replace(LINK, '$1') + // El tokenizador no corta en el acento grave: `sat.blacklist.69b` indexaría "69b`" y no "69b". + .replace(INLINE_CODE_MARK, '') + .replace(LINE_INDENT, '') + .replace(REPEATED_BLANK_LINES, '\n\n') + .trim(); + return { title: heading.replace(ANNOTATION, '').trim(), endpoint: endpointOf(heading), text }; +} + +/** El documento del índice de una sección; si la sección no tiene encabezado, se titula como su página. */ +export function toDocument({ url, page }, markdown) { + const { title, endpoint, text } = cleanSection(markdown); + return { id: url, url, page, title: title || page, endpoint, text }; +} + +export async function fetchLlms(origin) { + return fetchMarkdown(`${origin}/llms.txt`); +} + +/** Descarga las URLs del sitio desde origin, como mucho 8 a la vez; devuelve el Markdown en el mismo orden. */ +export async function downloadMarkdown(origin, urls) { + const markdown = new Array(urls.length); + let next = 0; + let failed = false; + async function worker() { + while (!failed && next < urls.length) { + const index = next++; + try { + markdown[index] = await fetchMarkdown(`${origin}${sitePath(urls[index])}`); + } catch (error) { + failed = true; + throw error; + } + } + } + await Promise.all(Array.from({ length: Math.min(DOWNLOAD_CONCURRENCY, urls.length) }, worker)); + return markdown; +} + +/** El origen sin "/" final; solo se aceptan URLs http(s). */ +export function normalizeOrigin(origin) { + let url; + try { + url = new URL(origin); + } catch { + throw new DocsError(`el origen de la documentación no es una URL válida: ${origin}`); + } + if (url.protocol !== 'http:' && url.protocol !== 'https:') { + throw new DocsError(`el origen de la documentación debe ser http(s): ${origin}`); + } + return origin.replace(/\/+$/, ''); +} + +// Un 200 con HTML es la página del sitio (por ejemplo un 404 servido como página) y no su Markdown. +async function fetchMarkdown(url) { + let response; + let body; + try { + response = await fetch(url, { signal: AbortSignal.timeout(FETCH_TIMEOUT_MS) }); + if (response.status !== 200) { + await response.body?.cancel(); + throw new DocsError(`${url} respondió HTTP ${response.status}; se esperaba 200.`); + } + body = await response.text(); + } catch (error) { + if (error instanceof DocsError) throw error; + throw new DocsError(`no se pudo descargar ${url}: ${error.message}`); + } + if (body.trimStart().startsWith('<')) throw new DocsError(`${url} devolvió HTML en lugar de Markdown.`); + return body; +} + +function endpointOf(heading) { + const annotation = [...heading.matchAll(ANNOTATION)][0]?.[1]; + if (annotation === undefined) return ''; + const tag = ANNOTATION_TAG.exec(annotation)?.[1]; + const label = ANNOTATION_LABEL.exec(annotation)?.[1]; + return tag !== undefined && label !== undefined ? `${tag} ${label}` : ''; +} + +function escapeRegExp(value) { + return value.replace(/[.*+?^${}()|[\]\\]/g, '\\$&'); +} diff --git a/mcp/scripts/openapi.mjs b/mcp/scripts/openapi.mjs new file mode 100644 index 0000000..127caab --- /dev/null +++ b/mcp/scripts/openapi.mjs @@ -0,0 +1,258 @@ +// Transformación pura del OpenAPI de FiscalAPI a los datos que sirve el MCP: sin red ni disco. + +export const MAX_OPERATION_CHARS = 40_000; + +const HTTP_METHODS = ['get', 'put', 'post', 'delete', 'options', 'head', 'patch', 'trace']; +// El operationId se vuelve nombre de archivo: solo se aceptan ids que no puedan salir de data/operations. +const SAFE_OPERATION_ID = /^[A-Za-z][A-Za-z0-9_]*$/; +const PREFERRED_MEDIA_TYPE = 'application/json'; +const DEFAULT_TAG = 'General'; + +export class OpenApiError extends Error {} + +export function parseOpenApi(text) { + let doc; + try { + doc = JSON.parse(text); + } catch { + throw new OpenApiError('la fuente no es JSON válido.'); + } + if (!isObject(doc) || typeof doc.openapi !== 'string' || !doc.openapi.startsWith('3.') || !isObject(doc.paths)) { + throw new OpenApiError('la fuente no es un documento OpenAPI 3 (faltan "openapi" o "paths").'); + } + return doc; +} + +export function extractOperations(doc) { + const operations = []; + const seen = new Set(); + for (const [path, pathItem] of Object.entries(doc.paths)) { + for (const method of HTTP_METHODS) { + const operation = pathItem[method]; + if (!isObject(operation)) continue; + const id = operation.operationId; + if (typeof id !== 'string' || !SAFE_OPERATION_ID.test(id)) { + throw new OpenApiError(`${method.toUpperCase()} ${path} no tiene un operationId válido (${JSON.stringify(id)}).`); + } + if (seen.has(id)) throw new OpenApiError(`el operationId "${id}" está repetido.`); + seen.add(id); + operations.push({ id, method: method.toUpperCase(), path, pathItem, operation }); + } + } + if (operations.length === 0) throw new OpenApiError('el documento no tiene operaciones.'); + return operations; +} + +export function buildCatalog(operations) { + return operations + .map(({ id, method, path, operation }) => ({ + id, + method, + path, + summary: operation.summary ?? '', + tag: operation.tags?.[0] ?? DEFAULT_TAG, + })) + .sort((a, b) => compare(a.tag, b.tag) || compare(a.id, b.id)); +} + +/** + * Resuelve los $ref de la operación y la recorta, en etapas deterministas, hasta que su JSON + * mida como máximo maxChars. Lanza OpenApiError si ni el recorte más agresivo cabe. + */ +export function buildOperation(doc, entry, maxChars = MAX_OPERATION_CHARS) { + const full = new OperationResolver(doc, {}).resolve(entry); + if (fits(full.value, maxChars)) return full.value; + + const withoutExamples = { dropExamples: true }; + let candidate = new OperationResolver(doc, withoutExamples).resolve(entry).value; + if (fits(candidate, maxChars)) return candidate; + + for (let depth = full.maxDepth; depth >= 1; depth--) { + candidate = new OperationResolver(doc, { ...withoutExamples, descriptionDepth: depth }).resolve(entry).value; + if (fits(candidate, maxChars)) return candidate; + } + + for (let depth = full.maxDepth; depth >= 0; depth--) { + const options = { ...withoutExamples, descriptionDepth: 1, refDepth: depth }; + candidate = new OperationResolver(doc, options).resolve(entry).value; + if (fits(candidate, maxChars)) return candidate; + } + throw new OpenApiError(`la operación "${entry.id}" mide más de ${maxChars} caracteres aun recortada.`); +} + +export function serializeOperation(operation) { + return JSON.stringify(operation); +} + +/** + * Resuelve una operación con unas opciones de recorte fijas. Profundidad 0 es la raíz del esquema + * de un parámetro, cuerpo o respuesta; cada nivel de properties, items o additionalProperties suma uno. + */ +class OperationResolver { + #doc; + #dropExamples; + #descriptionDepth; + #refDepth; + #maxDepth = 0; + + constructor(doc, { dropExamples = false, descriptionDepth = Infinity, refDepth = Infinity }) { + this.#doc = doc; + this.#dropExamples = dropExamples; + this.#descriptionDepth = descriptionDepth; + this.#refDepth = refDepth; + } + + resolve({ id, method, path, pathItem, operation }) { + const parameters = [...(pathItem.parameters ?? []), ...(operation.parameters ?? [])]; + const value = { + operationId: id, + method, + path, + tag: operation.tags?.[0] ?? DEFAULT_TAG, + summary: operation.summary ?? '', + description: operation.description ?? '', + }; + if (operation.deprecated === true) value.deprecated = true; + if (parameters.length > 0) value.parameters = parameters.map((p) => this.#parameter(p)); + if (operation.requestBody) value.requestBody = this.#withContent(this.#deref(operation.requestBody)); + if (operation.responses) value.responses = this.#responses(operation.responses); + const security = operation.security ?? this.#doc.security; + if (security) { + value.security = security; + value.securitySchemes = this.#securitySchemes(security); + } + return { value, maxDepth: this.#maxDepth }; + } + + #parameter(parameter) { + const { schema, ...rest } = this.#withoutExamples(this.#deref(parameter)); + return schema === undefined ? rest : { ...rest, schema: this.#schema(schema, 0, []) }; + } + + #responses(responses) { + const result = {}; + for (const [code, response] of Object.entries(responses)) { + const resolved = this.#withContent(this.#deref(response)); + if (isObject(resolved.headers)) { + resolved.headers = mapValues(resolved.headers, (header) => this.#parameter(header)); + } + result[code] = resolved; + } + return result; + } + + #withContent(holder) { + if (!isObject(holder.content)) return { ...holder }; + const content = {}; + for (const [mediaType, media] of Object.entries(collapseMediaTypes(holder.content))) { + const { schema, ...rest } = this.#withoutExamples(media); + content[mediaType] = schema === undefined ? rest : { ...rest, schema: this.#schema(schema, 0, []) }; + } + return { ...holder, content }; + } + + #schema(schema, depth, stack) { + if (!isObject(schema)) return schema; + this.#maxDepth = Math.max(this.#maxDepth, depth); + + if (typeof schema.$ref === 'string') { + const { $ref, ...siblings } = schema; + const name = refName($ref); + if (stack.includes($ref)) return { type: 'object', 'x-recursivo': name }; + if (depth > this.#refDepth) return { type: 'object', 'x-recortado': name }; + return this.#schema({ ...this.#pointer($ref), ...siblings }, depth, [...stack, $ref]); + } + + const result = {}; + for (const [key, value] of Object.entries(schema)) { + if (this.#dropExamples && (key === 'example' || key === 'examples')) continue; + if (key === 'description' && depth >= this.#descriptionDepth) continue; + result[key] = this.#schemaKeyword(key, value, depth, stack); + } + return result; + } + + #schemaKeyword(key, value, depth, stack) { + switch (key) { + case 'properties': + case 'patternProperties': + return isObject(value) ? mapValues(value, (child) => this.#schema(child, depth + 1, stack)) : value; + case 'items': + case 'additionalProperties': + case 'not': + return this.#schema(value, depth + 1, stack); + case 'allOf': + case 'oneOf': + case 'anyOf': + case 'prefixItems': + return Array.isArray(value) ? value.map((child) => this.#schema(child, depth, stack)) : value; + default: + return value; + } + } + + #securitySchemes(security) { + const declared = this.#doc.components?.securitySchemes ?? {}; + const names = [...new Set(security.flatMap((requirement) => Object.keys(requirement)))]; + return Object.fromEntries(names.filter((name) => declared[name]).map((name) => [name, declared[name]])); + } + + #withoutExamples(object) { + if (!this.#dropExamples) return { ...object }; + const { example: _example, examples: _examples, ...rest } = object; + return rest; + } + + // Parámetros, cuerpos y respuestas también pueden venir por $ref a components. + #deref(object, seen = new Set()) { + if (!isObject(object) || typeof object.$ref !== 'string') return object; + if (seen.has(object.$ref)) throw new OpenApiError(`$ref circular fuera de un esquema: ${object.$ref}.`); + seen.add(object.$ref); + return this.#deref(this.#pointer(object.$ref), seen); + } + + #pointer(ref) { + if (!ref.startsWith('#/')) throw new OpenApiError(`solo se admiten $ref locales: ${ref}.`); + let target = this.#doc; + for (const segment of ref.slice(2).split('/')) { + const key = decodeURIComponent(segment).replaceAll('~1', '/').replaceAll('~0', '~'); + target = isObject(target) ? target[key] : undefined; + } + if (target === undefined) throw new OpenApiError(`$ref sin destino: ${ref}.`); + return target; + } +} + +// ASP.NET publica el mismo esquema en application/json, text/json, text/plain y application/*+json. +// Se conserva application/json y solo los tipos cuyo contenido es distinto. +function collapseMediaTypes(content) { + const preferred = content[PREFERRED_MEDIA_TYPE]; + if (preferred === undefined) return content; + const preferredJson = JSON.stringify(preferred); + return Object.fromEntries( + Object.entries(content).filter( + ([mediaType, media]) => mediaType === PREFERRED_MEDIA_TYPE || JSON.stringify(media) !== preferredJson, + ), + ); +} + +function fits(operation, maxChars) { + return serializeOperation(operation).length <= maxChars; +} + +function refName(ref) { + return ref.slice(ref.lastIndexOf('/') + 1); +} + +function mapValues(object, fn) { + return Object.fromEntries(Object.entries(object).map(([key, value]) => [key, fn(value)])); +} + +function isObject(value) { + return typeof value === 'object' && value !== null && !Array.isArray(value); +} + +// Orden por unidades de código: no depende del locale ni de la versión de ICU. +function compare(a, b) { + return a < b ? -1 : a > b ? 1 : 0; +} diff --git a/mcp/scripts/snapshot-docs.mjs b/mcp/scripts/snapshot-docs.mjs new file mode 100644 index 0000000..123bfc9 --- /dev/null +++ b/mcp/scripts/snapshot-docs.mjs @@ -0,0 +1,30 @@ +// Guarda una copia de la documentación para las pruebas: llms.txt y todas las .md que enlaza. +// +// Uso: node scripts/snapshot-docs.mjs +// origen: de dónde descargar, por ejemplo http://localhost:3999 o https://docs.fiscalapi.com. +// salida: { "llms": "", "files": { "/.md": "" } } comprimido con gzip. +import { writeFile } from 'node:fs/promises'; +import { gzipSync } from 'node:zlib'; + +import { DocsError, downloadMarkdown, fetchLlms, listMarkdownUrls, normalizeOrigin, sitePath } from './docs.mjs'; + +async function main() { + const [originArg, outFile] = process.argv.slice(2); + if (originArg === undefined || outFile === undefined) { + throw new DocsError('uso: node scripts/snapshot-docs.mjs '); + } + const origin = normalizeOrigin(originArg); + const llms = await fetchLlms(origin); + const urls = listMarkdownUrls(llms); + const markdown = await downloadMarkdown(origin, urls); + const files = Object.fromEntries(urls.map((url, i) => [sitePath(url), markdown[i]])); + await writeFile(outFile, gzipSync(JSON.stringify({ llms, files }))); + console.error(`snapshot-docs: ${urls.length} archivos .md de ${origin} en ${outFile}`); +} + +try { + await main(); +} catch (error) { + console.error(`snapshot-docs: ${error instanceof DocsError ? error.message : (error?.stack ?? String(error))}`); + process.exitCode = 1; +} diff --git a/mcp/src/catalog.ts b/mcp/src/catalog.ts new file mode 100644 index 0000000..95d0b1d --- /dev/null +++ b/mcp/src/catalog.ts @@ -0,0 +1,100 @@ +import { readFileSync } from 'node:fs'; +import { readFile } from 'node:fs/promises'; +import { fileURLToPath } from 'node:url'; + +export interface CatalogEntry { + readonly id: string; + readonly method: string; + readonly path: string; + readonly summary: string; + readonly tag: string; +} + +const MAX_SUGGESTIONS = 5; + +/** + * Catálogo de operaciones generado por scripts/build-data.mjs. Solo lee archivos de data/ cuyo + * nombre sale del propio catálogo, nunca de la entrada del usuario. + */ +export class OperationCatalog { + readonly #entries: readonly CatalogEntry[]; + readonly #byId: ReadonlyMap; + readonly #operationsDir: URL; + + private constructor(entries: readonly CatalogEntry[], dataDir: URL) { + this.#entries = entries; + this.#byId = new Map(entries.map((entry) => [entry.id, entry])); + this.#operationsDir = new URL('operations/', dataDir); + } + + /** dataDir debe terminar en "/" para que las rutas relativas se resuelvan dentro de él. */ + static load(dataDir: URL): OperationCatalog { + const catalogFile = new URL('catalog.json', dataDir); + let entries: CatalogEntry[]; + try { + entries = JSON.parse(readFileSync(catalogFile, 'utf8')) as CatalogEntry[]; + } catch (error) { + throw new Error(`No se pudo leer el catálogo de operaciones (${fileURLToPath(catalogFile)}). Corre npm run build:data.`, { + cause: error, + }); + } + return new OperationCatalog(entries, dataDir); + } + + get tags(): string[] { + return [...new Set(this.#entries.map((entry) => entry.tag))]; + } + + /** Las operaciones en el orden del catálogo (tag, operationId); con tag, solo las de ese tag sin distinguir mayúsculas. */ + list(tag?: string): CatalogEntry[] { + if (tag === undefined) return [...this.#entries]; + const wanted = tag.toLowerCase(); + return this.#entries.filter((entry) => entry.tag.toLowerCase() === wanted); + } + + /** El JSON de la operación, o undefined si el id no está en el catálogo. */ + async readOperation(operationId: string): Promise { + const entry = this.#byId.get(operationId); + if (entry === undefined) return undefined; + return readFile(new URL(`${entry.id}.json`, this.#operationsDir), 'utf8'); + } + + /** + * Hasta 5 ids, sin distinguir mayúsculas: primero los que empiezan con la entrada y luego los que + * la contienen. Si ninguno coincide (un error de dedo como "createInvoce"), los que comparten con + * ella el prefijo más largo, de al menos 3 letras. + */ + suggest(input: string): string[] { + const needle = input.trim().toLowerCase(); + if (needle === '') return []; + const ids = this.#entries.map((entry) => entry.id); + const prefixed = ids.filter((id) => id.toLowerCase().startsWith(needle)); + const containing = ids.filter((id) => !id.toLowerCase().startsWith(needle) && id.toLowerCase().includes(needle)); + const matches = [...prefixed, ...containing]; + if (matches.length > 0) return matches.slice(0, MAX_SUGGESTIONS); + return closestByCommonPrefix(ids, needle).slice(0, MAX_SUGGESTIONS); + } +} + +const MIN_COMMON_PREFIX = 3; + +// Solo los ids con el prefijo común más largo; entre ellos, primero el de longitud más parecida a la entrada. +function closestByCommonPrefix(ids: readonly string[], needle: string): string[] { + const candidates = ids.map((id) => ({ + id, + common: commonPrefixLength(id.toLowerCase(), needle), + lengthGap: Math.abs(id.length - needle.length), + })); + const longest = Math.max(0, ...candidates.map((candidate) => candidate.common)); + if (longest < MIN_COMMON_PREFIX) return []; + return candidates + .filter((candidate) => candidate.common === longest) + .sort((a, b) => a.lengthGap - b.lengthGap) + .map((candidate) => candidate.id); +} + +function commonPrefixLength(a: string, b: string): number { + let length = 0; + while (length < a.length && length < b.length && a[length] === b[length]) length++; + return length; +} diff --git a/mcp/src/docs-search.ts b/mcp/src/docs-search.ts new file mode 100644 index 0000000..1c6e8cd --- /dev/null +++ b/mcp/src/docs-search.ts @@ -0,0 +1,48 @@ +// Configuración única del índice de la documentación. La importan scripts/build-data.mjs, que arma +// el índice, y src/docs.ts, que lo carga: si divergieran, las búsquedas no coincidirían con lo indexado. +import { stemmer } from '@orama/stemmers/spanish'; +import { stopwords } from '@orama/stopwords/spanish'; +import type { Options, SearchOptions } from 'minisearch'; + +/** Un documento del índice: una sección de docs.fiscalapi.com, o una página entera si no tiene secciones. */ +export interface DocsDocument { + readonly id: string; + readonly url: string; + readonly page: string; + readonly title: string; + readonly endpoint: string; + readonly text: string; +} + +const MIN_FUZZY_TERM_LENGTH = 5; +const FUZZINESS = 0.2; +// Las marcas diacríticas que NFD separa de su letra: "á" queda como "a" más el acento. +const COMBINING_MARKS = /\p{Mn}/gu; + +const STOPWORDS: ReadonlySet = new Set(stopwords.map(withoutAccents)); + +/** Minúsculas y sin acentos; descarta las palabras vacías del español y reduce el resto a su raíz. */ +export function processTerm(term: string): string | null { + const folded = withoutAccents(term.toLowerCase()); + return STOPWORDS.has(folded) ? null : stemmer(folded); +} + +// Sin prefijo: con él, "sat" coincidía con cada SatXxx de las tablas de catálogos y la sección de +// catálogos de Carta Porte encabezaba consultas que no eran de catálogos. El stemmer ya cubre las +// flexiones. El fuzzy solo aplica a términos de 5 o más letras porque "sat" coincidía con "gat". +const searchOptions: SearchOptions = { + boost: { title: 3, page: 1.5, endpoint: 2 }, + prefix: false, + fuzzy: (term) => (term.length >= MIN_FUZZY_TERM_LENGTH ? FUZZINESS : false), +}; + +export const DOCS_INDEX_OPTIONS: Options = { + fields: ['title', 'page', 'endpoint', 'text'], + storeFields: ['title', 'page', 'url', 'text'], + processTerm, + searchOptions, +}; + +function withoutAccents(value: string): string { + return value.normalize('NFD').replace(COMBINING_MARKS, ''); +} diff --git a/mcp/src/docs.ts b/mcp/src/docs.ts new file mode 100644 index 0000000..a7f00a9 --- /dev/null +++ b/mcp/src/docs.ts @@ -0,0 +1,109 @@ +import { readFileSync } from 'node:fs'; +import { fileURLToPath } from 'node:url'; + +import MiniSearch from 'minisearch'; + +import { DOCS_INDEX_OPTIONS, processTerm } from './docs-search.js'; +import type { DocsDocument } from './docs-search.js'; + +export interface DocsResult { + readonly title: string; + readonly page: string; + readonly url: string; + readonly excerpt: string; +} + +// Unos 750 tokens por resultado: 10 resultados caben en el límite de unos 10k tokens por respuesta. +export const MAX_EXCERPT_WORDS = 300; +export const MAX_EXCERPT_CHARS = 3_000; +// Contexto antes de la primera coincidencia, para que se lea de qué habla. +const WORDS_BEFORE_MATCH = 40; +const ELLIPSIS = '…'; +const WORD = /\S+/g; + +type StoredFields = Pick; + +/** Índice léxico de docs.fiscalapi.com generado por scripts/build-data.mjs; se consulta sin red. */ +export class DocsIndex { + readonly #index: MiniSearch; + + private constructor(index: MiniSearch) { + this.#index = index; + } + + /** dataDir debe terminar en "/" para que las rutas relativas se resuelvan dentro de él. */ + static load(dataDir: URL): DocsIndex { + const indexFile = new URL('docs-index.json', dataDir); + try { + return new DocsIndex(MiniSearch.loadJSON(readFileSync(indexFile, 'utf8'), DOCS_INDEX_OPTIONS)); + } catch (error) { + throw new Error(`No se pudo leer el índice de la documentación (${fileURLToPath(indexFile)}). Corre npm run build:data.`, { + cause: error, + }); + } + } + + /** Los limit resultados más relevantes, cada uno con un extracto alrededor de su primera coincidencia. */ + search(query: string, limit: number): DocsResult[] { + return this.#index + .search(query) + .slice(0, limit) + .map((result) => { + const { title, page, url, text } = result as unknown as StoredFields; + return { title, page, url, excerpt: excerpt(text, result.terms, result.queryTerms) }; + }); + } +} + +const tokenize: (text: string) => string[] = MiniSearch.getDefault('tokenize'); + +/** + * Hasta 300 palabras y 3,000 caracteres del texto, desde unas 40 palabras antes de la primera que + * coincidió con la consulta. Así "CFDI40147" muestra la fila de esa tabla y no el inicio de la tabla. + */ +function excerpt(text: string, matchedTerms: readonly string[], queryTerms: readonly string[]): string { + const words = [...text.matchAll(WORD)]; + if (words.length === 0) return ''; + const first = firstWord(words, firstMatch(words, matchedTerms, queryTerms)); + const last = Math.min(words.length, first + MAX_EXCERPT_WORDS) - 1; + const budget = MAX_EXCERPT_CHARS - 2 * ELLIPSIS.length; + let body = text.slice(words[first]!.index, endOf(words[last]!)); + let cutAtEnd = last < words.length - 1; + if (body.length > budget) { + body = cutAtWhitespace(body, budget); + cutAtEnd = true; + } + return `${first > 0 ? ELLIPSIS : ''}${body}${cutAtEnd ? ELLIPSIS : ''}`; +} + +// Primero una coincidencia exacta con la consulta: el fuzzy hace que "CFDI40147" también coincida con +// CFDI40101, que está antes en la misma tabla. Si no hay exacta, la primera coincidencia cualquiera, y +// si la coincidencia fue solo en el título, el inicio del texto. +function firstMatch(words: readonly RegExpExecArray[], matchedTerms: readonly string[], queryTerms: readonly string[]): number { + const exact = new Set(matchedTerms.filter((term) => queryTerms.includes(term))); + const any = new Set(matchedTerms); + for (const terms of [exact, any]) { + const index = words.findIndex((word) => tokenize(word[0]).some((token) => terms.has(processTerm(token) ?? ''))); + if (index >= 0) return index; + } + return 0; +} + +// Retrocede hasta 40 palabras, pero sin que la coincidencia quede fuera del límite de caracteres +// (una fila de tabla sin espacios podría medir más que todo el extracto). +function firstWord(words: readonly RegExpExecArray[], match: number): number { + const matchEnd = endOf(words[match]!); + let first = Math.max(0, match - WORDS_BEFORE_MATCH); + while (first < match && matchEnd - words[first]!.index > MAX_EXCERPT_CHARS / 2) first++; + return first; +} + +function endOf(word: RegExpExecArray): number { + return word.index + word[0].length; +} + +function cutAtWhitespace(value: string, maxChars: number): string { + const cut = value.slice(0, maxChars); + const lastSpace = cut.search(/\s\S*$/); + return (lastSpace > 0 ? cut.slice(0, lastSpace) : cut).trimEnd(); +} diff --git a/mcp/src/index.ts b/mcp/src/index.ts new file mode 100644 index 0000000..e8ba817 --- /dev/null +++ b/mcp/src/index.ts @@ -0,0 +1,34 @@ +#!/usr/bin/env node +import { readFileSync } from 'node:fs'; + +import { serveStdio } from '@modelcontextprotocol/server/stdio'; + +import { OperationCatalog } from './catalog.js'; +import { DocsIndex } from './docs.js'; +import { createServer } from './server.js'; + +// stdout es el canal del protocolo: todo diagnóstico va a stderr. +const packageRoot = new URL('../', import.meta.url); + +let catalog: OperationCatalog; +let docs: DocsIndex; +let version: string; +try { + version = (JSON.parse(readFileSync(new URL('package.json', packageRoot), 'utf8')) as { version: string }).version; + const dataDir = new URL('data/', packageRoot); + catalog = OperationCatalog.load(dataDir); + docs = DocsIndex.load(dataDir); +} catch (error) { + console.error(`fiscalapi-mcp: ${error instanceof Error ? error.message : String(error)}`); + process.exit(1); +} + +const handle = serveStdio(() => createServer(catalog, docs, version), { + onerror: (error) => console.error(`fiscalapi-mcp: ${error.message}`), +}); + +for (const signal of ['SIGINT', 'SIGTERM'] as const) { + process.on(signal, () => { + void handle.close().finally(() => process.exit(0)); + }); +} diff --git a/mcp/src/server.ts b/mcp/src/server.ts new file mode 100644 index 0000000..1c744c0 --- /dev/null +++ b/mcp/src/server.ts @@ -0,0 +1,147 @@ +import { McpServer } from '@modelcontextprotocol/server'; +import type { CallToolResult } from '@modelcontextprotocol/server'; +import * as z from 'zod/v4'; + +import type { CatalogEntry, OperationCatalog } from './catalog.js'; +import type { DocsIndex, DocsResult } from './docs.js'; + +export const SERVER_NAME = 'fiscalapi'; + +// Las herramientas solo leen archivos del paquete: no llaman a la API ni a ningún otro servicio. +const READ_ONLY = { readOnlyHint: true, destructiveHint: false, idempotentHint: true, openWorldHint: false } as const; +const MAX_ECHOED_INPUT = 100; +const MAX_QUERY_CHARS = 200; +const MAX_DOCS_RESULTS = 10; +const DEFAULT_DOCS_RESULTS = 5; + +export function createServer(catalog: OperationCatalog, docs: DocsIndex, version: string): McpServer { + const server = new McpServer({ name: SERVER_NAME, version }); + + server.registerTool( + 'listar_operaciones', + { + title: 'Listar operaciones de FiscalAPI', + description: + 'Lista las operaciones de la API REST de FiscalAPI (facturas CFDI 4.0 y complementos, cancelación, descarga masiva del SAT, personas, productos, certificados, webhooks y más), agrupadas por tag: operationId, método, ruta y resumen. Úsala para encontrar el endpoint que necesitas; el siguiente paso es llamar a obtener_operacion con su operationId para ver parámetros, cuerpo y respuestas. No llama a la API ni pide llaves.', + inputSchema: z.object({ + tag: z + .string() + .max(MAX_ECHOED_INPUT) + .optional() + .describe('Tag por el que filtrar, sin distinguir mayúsculas (por ejemplo Invoices, People o Products). Omítelo para ver todas.'), + }), + annotations: READ_ONLY, + }, + async ({ tag }) => listOperations(catalog, tag), + ); + + server.registerTool( + 'obtener_operacion', + { + title: 'Obtener una operación de FiscalAPI', + description: + 'Devuelve en JSON el detalle de una operación de la API de FiscalAPI: método, ruta, descripción con el enlace a su documentación, parámetros, cuerpo y respuestas con los esquemas resueltos. Usa el operationId exacto que da listar_operaciones. No llama a la API ni pide llaves.', + inputSchema: z.object({ + operationId: z + .string() + .min(1) + .max(MAX_ECHOED_INPUT) + .describe('operationId exacto de la operación, tal como aparece en listar_operaciones (por ejemplo createInvoice).'), + }), + annotations: READ_ONLY, + }, + async ({ operationId }) => getOperation(catalog, operationId), + ); + + server.registerTool( + 'buscar_docs', + { + title: 'Buscar en la documentación de FiscalAPI', + description: + 'Busca en las secciones de docs.fiscalapi.com: guías, reglas fiscales del SAT, catálogos, errores del PAC por código (como CFDI40147) y la referencia de cada endpoint. Devuelve de cada resultado el título, la página, la URL .md de la sección y un extracto; para leer la sección completa, descarga esa URL. No llama a la API ni pide llaves.', + inputSchema: z.object({ + consulta: z + .string() + .min(1) + .max(MAX_QUERY_CHARS) + .describe('Qué buscar, en español o por código (por ejemplo "cancelar factura", "complemento de pago" o "CFDI40147").'), + limite: z + .number() + .int() + .min(1) + .max(MAX_DOCS_RESULTS) + .optional() + .describe(`Número máximo de resultados, de 1 a ${MAX_DOCS_RESULTS}. Por omisión ${DEFAULT_DOCS_RESULTS}.`), + }), + annotations: READ_ONLY, + }, + async ({ consulta, limite }) => searchDocs(docs, consulta, limite ?? DEFAULT_DOCS_RESULTS), + ); + + return server; +} + +function listOperations(catalog: OperationCatalog, tag: string | undefined): CallToolResult { + const entries = catalog.list(tag); + if (entries.length === 0) { + return error(`No hay operaciones con el tag "${tag}". Tags disponibles: ${catalog.tags.join(', ')}.`); + } + const header = + `FiscalAPI: ${entries.length} operaciones. Para ver parámetros, cuerpo y respuestas de una, ` + + 'llama a obtener_operacion con su operationId.'; + return text([header, ...groupByTag(entries)].join('\n')); +} + +async function getOperation(catalog: OperationCatalog, operationId: string): Promise { + const operation = await catalog.readOperation(operationId); + if (operation !== undefined) return text(operation); + + const suggestions = catalog.suggest(operationId); + const hint = suggestions.length > 0 ? `¿Quisiste decir ${suggestions.join(', ')}? ` : ''; + return error(`No existe la operación "${operationId}". ${hint}Usa listar_operaciones para ver los operationId disponibles.`); +} + +function searchDocs(docs: DocsIndex, query: string, limit: number): CallToolResult { + const results = docs.search(query, limit); + const echoed = clip(query); + // Sin resultados no es un error de la llamada: el agente debe reformular, no reintentar igual. + if (results.length === 0) { + return text( + `FiscalAPI docs: no hay resultados para "${echoed}". Prueba con otras palabras, o usa listar_operaciones para ver los endpoints de la API.`, + ); + } + const count = results.length === 1 ? '1 resultado' : `${results.length} resultados`; + const blocks = results.map((result) => `## ${heading(result)}\n${result.url}\n\n${result.excerpt}`); + return text([`FiscalAPI docs: ${count} para "${echoed}".`, ...blocks].join('\n\n')); +} + +// Una página sin secciones se indexa entera y su título es el de la página: no se repite. +function heading({ title, page }: DocsResult): string { + return title === page ? title : `${title} (${page})`; +} + +// La consulta admite más caracteres de los que vale la pena repetir en la respuesta. +function clip(value: string): string { + return value.length > MAX_ECHOED_INPUT ? `${value.slice(0, MAX_ECHOED_INPUT)}…` : value; +} + +function groupByTag(entries: readonly CatalogEntry[]): string[] { + const lines: string[] = []; + let currentTag: string | undefined; + for (const entry of entries) { + if (entry.tag !== currentTag) { + currentTag = entry.tag; + lines.push('', `## ${entry.tag}`); + } + lines.push(`${entry.id} ${entry.method} ${entry.path} ${entry.summary}`); + } + return lines; +} + +function text(value: string): CallToolResult { + return { content: [{ type: 'text', text: value }] }; +} + +function error(message: string): CallToolResult { + return { content: [{ type: 'text', text: message }], isError: true }; +} diff --git a/mcp/test/build-data.test.ts b/mcp/test/build-data.test.ts new file mode 100644 index 0000000..36c4616 --- /dev/null +++ b/mcp/test/build-data.test.ts @@ -0,0 +1,369 @@ +import { existsSync, mkdirSync, mkdtempSync, readdirSync, readFileSync, rmSync, writeFileSync } from 'node:fs'; +import { createServer } from 'node:http'; +import type { AddressInfo } from 'node:net'; +import { tmpdir } from 'node:os'; +import path from 'node:path'; +import { fileURLToPath } from 'node:url'; + +import { afterAll, describe, expect, it } from 'vitest'; + +import { cleanSection, DocsError, parseLlms } from '../scripts/docs.mjs'; +import { + buildCatalog, + buildOperation, + extractOperations, + OpenApiError, + parseOpenApi, + serializeOperation, +} from '../scripts/openapi.mjs'; +import { serveDocs } from './docs-server.js'; +import { dataDirUrl } from './helpers.js'; +import { runBuildData } from './run-build-data.js'; + +const packageDir = fileURLToPath(new URL('../', import.meta.url)); +const openApiFixture = path.join(packageDir, 'test', 'fixtures', 'openapi-v4.json'); +// Límite de RNF-05 escrito aquí a propósito: si alguien sube el del build, la prueba lo detecta. +const SPEC_MAX_OPERATION_CHARS = 40_000; +const LONG_TEXT ='Texto largo de prueba para inflar el esquema. '.repeat(8); + +// Un cuerpo que resuelto mide cientos de miles de caracteres: obliga a recortar. +function hugeDoc(): Record { + const leafProperties = Object.fromEntries( + Array.from({ length: 30 }, (_, i) => [`campo${i}`, { type: 'string', description: LONG_TEXT, example: LONG_TEXT }]), + ); + const rootProperties = Object.fromEntries( + Array.from({ length: 40 }, (_, i) => [`hijo${i}`, { $ref: '#/components/schemas/Hijo' }]), + ); + return { + openapi: '3.1.1', + paths: { + '/api/v4/grande': { + post: { + operationId: 'createGrande', + tags: ['Pruebas'], + summary: 'Crear algo grande', + description: 'Operación enorme. Docs: [grande](https://docs.fiscalapi.com/grande).', + requestBody: { + content: { + 'application/json': { schema: { $ref: '#/components/schemas/Grande' } }, + 'text/json': { schema: { $ref: '#/components/schemas/Grande' } }, + }, + }, + responses: { '200': { description: 'OK' } }, + }, + }, + }, + components: { + schemas: { + Grande: { + type: 'object', + description: 'Raíz del cuerpo', + required: ['hijo0'], + properties: { ...rootProperties, estado: { type: 'string', enum: ['A', 'B'], format: 'x-codigo' } }, + }, + Hijo: { type: ['null', 'object'], required: ['campo0'], properties: leafProperties }, + }, + }, + }; +} + +function entryOf(doc: Record, id: string) { + const entry = extractOperations(doc).find((candidate: { id: string }) => candidate.id === id); + if (!entry) throw new Error(`Falta ${id} en el documento de prueba.`); + return entry; +} + +describe('buildOperation', () => { + it('recorta una operación enorme hasta el límite y conserva la estructura y la documentación', () => { + const doc = hugeDoc(); + + const operation = buildOperation(doc, entryOf(doc, 'createGrande')); + const json = serializeOperation(operation); + + expect(json.length).toBeLessThanOrEqual(SPEC_MAX_OPERATION_CHARS); + expect(operation.description).toContain('https://docs.fiscalapi.com/grande'); + expect(operation.summary).toBe('Crear algo grande'); + expect(json).not.toContain('"example"'); + expect(json).not.toContain('"$ref"'); + const body = operation.requestBody.content['application/json'].schema; + expect(Object.keys(operation.requestBody.content)).toEqual(['application/json']); + expect(body.required).toEqual(['hijo0']); + expect(body.properties.estado).toEqual({ type: 'string', enum: ['A', 'B'], format: 'x-codigo' }); + expect(Object.keys(body.properties)).toHaveLength(41); + }); + + it('quita primero ejemplos y descripciones anidadas antes de recortar subárboles', () => { + const doc = hugeDoc(); + const full = serializeOperation(buildOperation(doc, entryOf(doc, 'createGrande'), Infinity)); + + // Cabe sin ejemplos ni descripciones, pero no sin ejemplos solamente. + const operation = buildOperation(doc, entryOf(doc, 'createGrande'), full.length / 3); + const json = serializeOperation(operation); + + expect(json).not.toContain('x-recortado'); + expect(json).not.toContain(LONG_TEXT); + const body = operation.requestBody.content['application/json'].schema; + expect(body.description).toBe('Raíz del cuerpo'); + expect(body.properties.hijo0).toMatchObject({ type: ['null', 'object'], required: ['campo0'] }); + expect(body.properties.hijo0.properties.campo29).toEqual({ type: 'string' }); + }); + + it('como último recurso reemplaza los subárboles por su nombre de esquema', () => { + const doc = hugeDoc(); + + const operation = buildOperation(doc, entryOf(doc, 'createGrande'), 4_000); + + const body = operation.requestBody.content['application/json'].schema; + expect(serializeOperation(operation).length).toBeLessThanOrEqual(4_000); + expect(body.properties.hijo0).toEqual({ type: 'object', 'x-recortado': 'Hijo' }); + expect(body.properties.estado).toEqual({ type: 'string', enum: ['A', 'B'], format: 'x-codigo' }); + }); + + it('falla si la operación no cabe ni recortada', () => { + const doc = hugeDoc(); + + expect(() => buildOperation(doc, entryOf(doc, 'createGrande'), 100)).toThrow(OpenApiError); + }); + + it('corta los esquemas recursivos sin ciclar', () => { + const doc = { + openapi: '3.1.1', + paths: { + '/api/v4/nodos': { + get: { + operationId: 'listNodos', + responses: { + '200': { description: 'OK', content: { 'application/json': { schema: { $ref: '#/components/schemas/Nodo' } } } }, + }, + }, + }, + }, + components: { + schemas: { Nodo: { type: 'object', properties: { hijos: { type: 'array', items: { $ref: '#/components/schemas/Nodo' } } } } }, + }, + }; + + const operation = buildOperation(doc, entryOf(doc, 'listNodos')); + + const schema = operation.responses['200'].content['application/json'].schema; + expect(schema.properties.hijos.items).toEqual({ type: 'object', 'x-recursivo': 'Nodo' }); + }); +}); + +describe('extractOperations y buildCatalog', () => { + it('rechaza un operationId que podría salir de data/operations', () => { + const doc = { openapi: '3.1.1', paths: { '/x': { get: { operationId: '../x', responses: {} } } } }; + + expect(() => extractOperations(doc)).toThrow(/operationId válido/); + }); + + it('ordena el catálogo por tag y luego por operationId', () => { + const doc = { + openapi: '3.1.1', + paths: { + '/b': { get: { operationId: 'zeta', tags: ['B'], summary: 'z' } }, + '/a': { get: { operationId: 'beta', tags: ['A'], summary: 'b' }, post: { operationId: 'alfa', tags: ['A'], summary: 'a' } }, + }, + }; + + expect(buildCatalog(extractOperations(doc)).map((entry: { id: string }) => entry.id)).toEqual(['alfa', 'beta', 'zeta']); + }); + + it('rechaza JSON que no es OpenAPI 3', () => { + expect(() => parseOpenApi('{"swagger":"2.0","paths":{}}')).toThrow(OpenApiError); + expect(() => parseOpenApi('')).toThrow(/no es JSON/); + }); +}); + +// Cada prueba arranca un proceso de Node: en Windows, con los otros archivos de prueba corriendo en +// paralelo, el primero llegó a tardar más que los 5 s por omisión. +describe('scripts/build-data.mjs', { timeout: 30_000 }, () => { + const workDir = mkdtempSync(path.join(tmpdir(), 'fiscalapi-mcp-build-')); + afterAll(() => rmSync(workDir, { recursive: true, force: true })); + + const outDir = path.join(workDir, 'data'); + + function runBuild(source: string, ...args: string[]): Promise<{ code: number | null; stderr: string }> { + return runBuildData([source, ...args, '--out', outDir]); + } + + it('sale con código 1 si la URL no responde 200', async () => { + const server = createServer((_, response) => response.writeHead(401).end()); + await new Promise((resolve) => server.listen(0, '127.0.0.1', resolve)); + const { port } = server.address() as AddressInfo; + try { + const { code, stderr } = await runBuild(`http://127.0.0.1:${port}/api/openapi/v4.json`); + + expect(code).toBe(1); + expect(stderr).toContain('respondió HTTP 401; se esperaba 200'); + } finally { + server.close(); + } + }); + + it('sale con código 1 si el archivo no es un OpenAPI', async () => { + const notOpenApi = path.join(workDir, 'no-openapi.json'); + writeFileSync(notOpenApi, '{"hola":"mundo"}'); + + const { code, stderr } = await runBuild(notOpenApi); + + expect(code).toBe(1); + expect(stderr).toContain('no es un documento OpenAPI 3'); + }); + + // Un data/ previo con una marca: si el build fallido lo tocara, la marca desaparecería. + function seedPreviousData(): string { + rmSync(outDir, { recursive: true, force: true }); + mkdirSync(outDir, { recursive: true }); + const marker = path.join(outDir, 'previo.txt'); + writeFileSync(marker, 'data/ anterior'); + return marker; + } + + function leftovers(): string[] { + return readdirSync(workDir).filter((name) => name.startsWith('data.tmp-')); + } + + it.each([ + ['responde 404', { '/pagina/seccion-b.md': undefined }, 'pagina/seccion-b.md respondió HTTP 404; se esperaba 200'], + ['devuelve HTML', { '/pagina/seccion-b.md': '' }, 'pagina/seccion-b.md devolvió HTML en lugar de Markdown'], + ])('sale con código 1 y no toca data/ si una sección .md %s', async (_, override, message) => { + const files: Record = { + '/pagina/seccion-a.md': '## Sección A\n\nTexto.', + '/pagina/seccion-b.md': '## Sección B\n\nTexto.', + ...override, + }; + const docs = await serveDocs({ + llms: SMALL_LLMS, + files: Object.fromEntries(Object.entries(files).filter((entry): entry is [string, string] => entry[1] !== undefined)), + }); + const marker = seedPreviousData(); + try { + const { code, stderr } = await runBuild(openApiFixture, '--docs-origin', docs.origin); + + expect(code).toBe(1); + expect(stderr).toContain(`build-data: ${docs.origin}/${message}`); + expect(readFileSync(marker, 'utf8')).toBe('data/ anterior'); + expect(readdirSync(outDir)).toEqual(['previo.txt']); + expect(leftovers()).toEqual([]); + } finally { + await docs.close(); + } + }); + + it('sale con código 1 si llms.txt no existe', async () => { + const docs = await serveDocs({ llms: SMALL_LLMS, files: {} }); + try { + const { code, stderr } = await runBuild(openApiFixture, '--docs-origin', `${docs.origin}/no-existe`); + + expect(code).toBe(1); + expect(stderr).toContain(`${docs.origin}/no-existe/llms.txt respondió HTTP 404`); + } finally { + await docs.close(); + } + }); + + it('guarda en source.json el origen, el hash y el número de documentos de la documentación', () => { + const source = JSON.parse(readFileSync(new URL('source.json', dataDirUrl()), 'utf8')); + + expect(source.operations).toBe(100); + expect(source.docs).toMatchObject({ origin: expect.stringMatching(/^http:\/\/127\.0\.0\.1:\d+$/), documents: 383 }); + expect(source.docs.sha256).toMatch(/^[0-9a-f]{64}$/); + expect(existsSync(new URL('docs-index.json', dataDirUrl()))).toBe(true); + }); +}); + +const SMALL_LLMS = [ + '# FiscalAPI', + '', + '- Los endpoints están bajo `/api/v4/`.', + '- [Página](https://docs.fiscalapi.com/pagina.md): descripción.', + ' - [Sección A](https://docs.fiscalapi.com/pagina/seccion-a.md)', + ' - [Sección B](https://docs.fiscalapi.com/pagina/seccion-b.md)', +].join('\n'); + +describe('parseLlms', () => { + it('indexa cada sección con su página y las páginas sin secciones enteras, sin repetir URL', () => { + const llms = [ + SMALL_LLMS, + '', + '## Referencia', + '', + '- [Sola](https://docs.fiscalapi.com/sola.md): página sin secciones.', + '- [Otra](https://docs.fiscalapi.com/otra.md): repite una sección.', + ' - [Sección A](https://docs.fiscalapi.com/pagina/seccion-a.md)', + ' - [Propia](https://docs.fiscalapi.com/otra/propia.md)', + '- [Sola](https://docs.fiscalapi.com/sola.md): la misma página otra vez.', + '- [Externa](https://example.com/externa.md): de otro sitio.', + ].join('\n'); + + expect(parseLlms(llms)).toEqual([ + { url: 'https://docs.fiscalapi.com/pagina/seccion-a.md', page: 'Página' }, + { url: 'https://docs.fiscalapi.com/pagina/seccion-b.md', page: 'Página' }, + { url: 'https://docs.fiscalapi.com/sola.md', page: 'Sola' }, + { url: 'https://docs.fiscalapi.com/otra/propia.md', page: 'Otra' }, + ]); + }); + + it('falla si el llms.txt no enlaza ninguna .md del sitio', () => { + expect(() => parseLlms('')).toThrow(DocsError); + }); +}); + +describe('cleanSection', () => { + const section = [ + "## Cancelar factura {{ tag: 'DELETE', label: '/api/v4/invoices' }}", + '', + ' Este endpoint cancela una factura. Revisa los [valores](/modes-of-operation) y `sat.blacklist.69b`.', + ' ', + ' ', + ' Folio fiscal (UUID) de la factura.', + ' ', + '', + '', + '', + ` Comprobante emitido con errores **con** relación`, + ' Certificado CSD', + ' ', + '', + '```bash', + '# Esto no es un encabezado', + 'curl -X DELETE https://test.fiscalapi.com/api/v4/invoices', + '```', + '', + '| Código | Descripción |', + '| --- | --- |', + '| CFDI40147 | El campo DomicilioFiscalReceptor |', + ].join('\n'); + + it('separa título y endpoint de la anotación', () => { + const { title, endpoint } = cleanSection(section); + + expect(title).toBe('Cancelar factura'); + expect(endpoint).toBe('DELETE /api/v4/invoices'); + }); + + it('quita código, etiquetas, URLs y sangría; conserva el name y el value de las etiquetas, los saltos de línea y las tablas', () => { + const { text } = cleanSection(section); + + expect(text).toBe( + [ + 'Este endpoint cancela una factura. Revisa los valores y sat.blacklist.69b.', + '', + 'invoiceUuid', + 'Folio fiscal (UUID) de la factura.', + '', + '01 Comprobante emitido con errores **con** relación', + '0 Certificado CSD', + '', + '| Código | Descripción |', + '| --- | --- |', + '| CFDI40147 | El campo DomicilioFiscalReceptor |', + ].join('\n'), + ); + }); + + it('una sección sin anotación no tiene endpoint', () => { + expect(cleanSection('# Webhooks\n\nTexto.')).toEqual({ title: 'Webhooks', endpoint: '', text: 'Texto.' }); + }); +}); diff --git a/mcp/test/docs-server.ts b/mcp/test/docs-server.ts new file mode 100644 index 0000000..db21940 --- /dev/null +++ b/mcp/test/docs-server.ts @@ -0,0 +1,40 @@ +import { readFileSync } from 'node:fs'; +import { createServer } from 'node:http'; +import type { AddressInfo } from 'node:net'; +import { gunzipSync } from 'node:zlib'; + +/** El formato que escribe scripts/snapshot-docs.mjs: llms.txt y cada .md por su ruta. */ +export interface DocsCorpus { + readonly llms: string; + readonly files: Readonly>; +} + +export interface DocsServer { + readonly origin: string; + close(): Promise; +} + +const corpusFile = new URL('fixtures/docs-corpus.json.gz', import.meta.url); + +export function readDocsCorpus(): DocsCorpus { + return JSON.parse(gunzipSync(readFileSync(corpusFile)).toString('utf8')) as DocsCorpus; +} + +/** Sirve el corpus en 127.0.0.1 como lo haría docs.fiscalapi.com: /llms.txt, cada ruta .md y 404 para lo demás. */ +export async function serveDocs(corpus: DocsCorpus): Promise { + const routes = new Map(Object.entries({ ...corpus.files, '/llms.txt': corpus.llms })); + const server = createServer((request, response) => { + const body = routes.get(new URL(request.url ?? '/', 'http://localhost').pathname); + if (body === undefined) { + response.writeHead(404).end(); + return; + } + response.writeHead(200, { 'content-type': 'text/markdown; charset=utf-8' }).end(body); + }); + await new Promise((resolve) => server.listen(0, '127.0.0.1', resolve)); + const { port } = server.address() as AddressInfo; + return { + origin: `http://127.0.0.1:${port}`, + close: () => new Promise((resolve, reject) => server.close((error) => (error ? reject(error) : resolve()))), + }; +} diff --git a/mcp/test/docs-tool.test.ts b/mcp/test/docs-tool.test.ts new file mode 100644 index 0000000..08f0df6 --- /dev/null +++ b/mcp/test/docs-tool.test.ts @@ -0,0 +1,106 @@ +import type { Client } from '@modelcontextprotocol/client'; +import { afterAll, beforeAll, describe, expect, it } from 'vitest'; + +import { callTool, connectClient, textOf } from './helpers.js'; + +// Límite duro de RNF-05: unos 10k tokens por respuesta, aun con 10 resultados largos. +const MAX_RESPONSE_CHARS = 40_000; +// Cada resultado lleva en su propia línea la URL .md de su sección. +const RESULT_URL = /^https:\/\/docs\.fiscalapi\.com\/\S+\.md$/gm; + +let client: Client; +let close: () => Promise; + +beforeAll(async () => { + ({ client, close } = await connectClient()); +}); + +afterAll(async () => { + await close(); +}); + +describe('buscar_docs', () => { + it('aparece en tools/list como herramienta de solo lectura', async () => { + const { tools } = await client.listTools(); + + const tool = tools.find((candidate) => candidate.name === 'buscar_docs'); + expect(tool?.title).toBe('Buscar en la documentación de FiscalAPI'); + expect(tool?.annotations).toMatchObject({ readOnlyHint: true, destructiveHint: false, openWorldHint: false }); + expect(tool?.inputSchema.required).toEqual(['consulta']); + }); + + it('con un código de error del PAC trae el extracto de la fila de ese código', async () => { + const result = await callTool(client, 'buscar_docs', { consulta: 'CFDI40147' }); + const text = textOf(result); + + expect(result.isError).toBeFalsy(); + expect(text).toMatch(/^FiscalAPI docs: \d+ resultados para "CFDI40147"\./); + expect(text).toContain('https://docs.fiscalapi.com/errors/cfdi-40/errores-cfdi-4-0.md'); + expect(text).toContain('| CFDI40147 |'); + }); + + it('respeta limite', async () => { + const text = textOf(await callTool(client, 'buscar_docs', { consulta: 'factura', limite: 2 })); + + expect(text.match(RESULT_URL)).toHaveLength(2); + }); + + it('sin limite devuelve 5 resultados', async () => { + const text = textOf(await callTool(client, 'buscar_docs', { consulta: 'factura' })); + + expect(text.match(RESULT_URL)).toHaveLength(5); + }); + + it('titula una sección con su página entre paréntesis', async () => { + const text = textOf(await callTool(client, 'buscar_docs', { consulta: 'cancelar una factura', limite: 1 })); + + expect(text).toMatch(/^FiscalAPI docs: 1 resultado para "cancelar una factura"\./); + expect(text).toMatch(/^## Cancelar factura \(.+\)$/m); + }); + + it('no repite la página cuando el resultado es la página entera', async () => { + const text = textOf(await callTool(client, 'buscar_docs', { consulta: 'catálogos sat', limite: 1 })); + + expect(text).toContain('## Catálogos SAT\nhttps://docs.fiscalapi.com/catalogs-info.md\n'); + }); + + it.each(['factura', 'CFDI40147', 'modelo factura', 'catálogo'])( + 'con limite 10, "%s" cabe en el límite de tamaño', + async (consulta) => { + const text = textOf(await callTool(client, 'buscar_docs', { consulta, limite: 10 })); + + expect(text.length).toBeLessThanOrEqual(MAX_RESPONSE_CHARS); + }, + ); + + it('sin coincidencias responde sin error y sugiere reformular', async () => { + const result = await callTool(client, 'buscar_docs', { consulta: 'zzzqqq' }); + + expect(result.isError).toBeFalsy(); + expect(textOf(result)).toContain('no hay resultados para "zzzqqq"'); + expect(textOf(result)).toContain('listar_operaciones'); + }); + + it('recorta la consulta que repite en la respuesta', async () => { + const consulta = `${'a'.repeat(150)} zzzqqq`; + + const text = textOf(await callTool(client, 'buscar_docs', { consulta })); + + expect(text).toContain(`"${'a'.repeat(100)}…"`); + expect(text).not.toContain('a'.repeat(101)); + }); + + it.each([ + ['limite 0', { consulta: 'factura', limite: 0 }], + ['limite 11', { consulta: 'factura', limite: 11 }], + ['limite no entero', { consulta: 'factura', limite: 2.5 }], + ['consulta vacía', { consulta: '' }], + ['consulta de más de 200 caracteres', { consulta: 'a'.repeat(201) }], + ['sin consulta', {}], + ])('rechaza %s', async (_, args) => { + const result = await callTool(client, 'buscar_docs', args); + + expect(result.isError).toBe(true); + expect(textOf(result)).not.toContain('FiscalAPI docs:'); + }); +}); diff --git a/mcp/test/fixtures/docs-corpus.json.gz b/mcp/test/fixtures/docs-corpus.json.gz new file mode 100644 index 0000000..42d271b Binary files /dev/null and b/mcp/test/fixtures/docs-corpus.json.gz differ diff --git a/mcp/test/fixtures/openapi-v4.json b/mcp/test/fixtures/openapi-v4.json new file mode 100644 index 0000000..e4eb2af --- /dev/null +++ b/mcp/test/fixtures/openapi-v4.json @@ -0,0 +1,15077 @@ +{ + "openapi": "3.1.1", + "info": { + "title": "Fiscalapi API", + "description": "API REST para emitir facturas CFDI 4.0 y sus complementos, cancelarlas, validarlas, descargar CFDI del SAT y administrar personas, productos, certificados y créditos. Cada petición es JSON y se autentica con los headers `X-API-KEY` y `X-TENANT-KEY`.\n\nAmbientes: `https://test.fiscalapi.com` (pruebas) y `https://live.fiscalapi.com` (producción).\n\n## Modos de operación\n\n- **[Por valores](https://docs.fiscalapi.com/modes-of-operation):** el cuerpo lleva todos los datos de la factura, incluidos los certificados (CSD) del emisor en base64.\n- **[Por referencias](https://docs.fiscalapi.com/modes-of-operation):** el cuerpo lleva los ids de las personas y los productos ya registrados en tu cuenta, desde el dashboard o la API.\n\nConsulta [cómo obtener tus credenciales](https://docs.fiscalapi.com/credentials-info) y los [datos de prueba](https://docs.fiscalapi.com/testing-data).", + "version": "4.0" + }, + "servers": [ + { + "url": "https://test.fiscalapi.com", + "description": "Pruebas" + }, + { + "url": "https://live.fiscalapi.com", + "description": "Producción" + } + ], + "paths": { + "/api/v4/apikeys": { + "get": { + "tags": [ + "ApiKeys" + ], + "summary": "Listar API keys", + "description": "Las API keys de la persona autenticada, de la más antigua a la más reciente, paginadas con pageNumber y pageSize (10 por omisión y máximo); el owner del tenant recibe las de todas las personas. Cada una trae su valor en apiKeyValue, igual que al crearla. Docs: [listar API keys](https://docs.fiscalapi.com/api-keys#listar-api-keys).", + "operationId": "listApiKeys", + "parameters": [ + { + "name": "PageNumber", + "in": "query", + "schema": { + "pattern": "^-?(?:0|[1-9]\\d*)$", + "type": [ + "integer", + "string" + ], + "format": "int32" + } + }, + { + "name": "PageSize", + "in": "query", + "schema": { + "pattern": "^-?(?:0|[1-9]\\d*)$", + "type": [ + "integer", + "string" + ], + "format": "int32" + } + }, + { + "name": "X-TIME-ZONE", + "in": "header", + "description": "Zona horaria IANA de las fechas sin zona del cuerpo JSON: el request las lee y la respuesta las escribe en ella. Si falta o no es válida, America/Mexico_City. No aplica a los campos que la operación documenta en UTC o en hora civil.", + "schema": { + "type": "string" + }, + "example": "America/Mexico_City" + } + ], + "responses": { + "200": { + "description": "OK" + } + }, + "security": [ + { + "ApiKey": [ ], + "TenantKey": [ ] + } + ] + }, + "post": { + "tags": [ + "ApiKeys" + ], + "summary": "Crear una API key", + "description": "Genera una API key activa para personId (obligatorio), con description opcional, y devuelve su valor en apiKeyValue; obtener y listar también lo devuelven. Cada persona crea API keys para sí misma y solo el owner del tenant las crea para otra persona (403). Un personId que no es del tenant responde 400. Docs: [crear API key](https://docs.fiscalapi.com/api-keys#crear-api-key) y [API keys de otras personas](https://docs.fiscalapi.com/api-keys#api-keys-de-otras-personas).", + "operationId": "createApiKey", + "parameters": [ + { + "name": "X-TIME-ZONE", + "in": "header", + "description": "Zona horaria IANA de las fechas sin zona del cuerpo JSON: el request las lee y la respuesta las escribe en ella. Si falta o no es válida, America/Mexico_City. No aplica a los campos que la operación documenta en UTC o en hora civil.", + "schema": { + "type": "string" + }, + "example": "America/Mexico_City" + } + ], + "requestBody": { + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/CreateApiKeyCommand" + } + }, + "text/json": { + "schema": { + "$ref": "#/components/schemas/CreateApiKeyCommand" + } + }, + "application/*+json": { + "schema": { + "$ref": "#/components/schemas/CreateApiKeyCommand" + } + } + }, + "required": true + }, + "responses": { + "200": { + "description": "OK" + } + }, + "security": [ + { + "ApiKey": [ ], + "TenantKey": [ ] + } + ] + } + }, + "/api/v4/apikeys/{id}": { + "get": { + "tags": [ + "ApiKeys" + ], + "summary": "Obtener una API key", + "description": "Una API key por su id, con su valor (apiKeyValue), la persona a la que pertenece (personId) y su estado (apiKeyStatus: 1 activa, 0 deshabilitada). Responde 404 si no existe o si es de otra persona y la persona autenticada no es el owner del tenant. Docs: [obtener API key](https://docs.fiscalapi.com/api-keys#obtener-api-key-por-id).", + "operationId": "getApiKey", + "parameters": [ + { + "name": "id", + "in": "path", + "required": true, + "schema": { + "type": "string" + } + }, + { + "name": "details", + "in": "query", + "schema": { + "type": "boolean", + "default": false + } + }, + { + "name": "X-TIME-ZONE", + "in": "header", + "description": "Zona horaria IANA de las fechas sin zona del cuerpo JSON: el request las lee y la respuesta las escribe en ella. Si falta o no es válida, America/Mexico_City. No aplica a los campos que la operación documenta en UTC o en hora civil.", + "schema": { + "type": "string" + }, + "example": "America/Mexico_City" + } + ], + "responses": { + "200": { + "description": "OK" + } + }, + "security": [ + { + "ApiKey": [ ], + "TenantKey": [ ] + } + ] + }, + "put": { + "tags": [ + "ApiKeys" + ], + "summary": "Actualizar una API key", + "description": "Cambia description y apiKeyStatus: con 0 la API key queda deshabilitada y deja de autenticar hasta que vuelva a 1. Un apiKeyStatus omitido se conserva, pero una description omitida se borra. El id de la ruta y el del cuerpo deben coincidir; responde 404 si no existe o si es de otra persona y la persona autenticada no es el owner del tenant. Docs: [actualizar API key](https://docs.fiscalapi.com/api-keys#actualizar-api-key).", + "operationId": "updateApiKey", + "parameters": [ + { + "name": "id", + "in": "path", + "required": true, + "schema": { + "type": "string" + } + }, + { + "name": "X-TIME-ZONE", + "in": "header", + "description": "Zona horaria IANA de las fechas sin zona del cuerpo JSON: el request las lee y la respuesta las escribe en ella. Si falta o no es válida, America/Mexico_City. No aplica a los campos que la operación documenta en UTC o en hora civil.", + "schema": { + "type": "string" + }, + "example": "America/Mexico_City" + } + ], + "requestBody": { + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/UpdateApiKeyCommand" + } + }, + "text/json": { + "schema": { + "$ref": "#/components/schemas/UpdateApiKeyCommand" + } + }, + "application/*+json": { + "schema": { + "$ref": "#/components/schemas/UpdateApiKeyCommand" + } + } + }, + "required": true + }, + "responses": { + "200": { + "description": "OK" + } + }, + "security": [ + { + "ApiKey": [ ], + "TenantKey": [ ] + } + ] + }, + "delete": { + "tags": [ + "ApiKeys" + ], + "summary": "Revocar una API key", + "description": "Deshabilita la API key y la da de baja: deja de autenticar, ya no aparece al listar y obtenerla o actualizarla responde 404, así que no se puede reactivar. Responde 404 si no existe o si es de otra persona y la persona autenticada no es el owner del tenant. Docs: [eliminar API key](https://docs.fiscalapi.com/api-keys#eliminar-api-key).", + "operationId": "revokeApiKey", + "parameters": [ + { + "name": "id", + "in": "path", + "required": true, + "schema": { + "type": "string" + } + }, + { + "name": "X-TIME-ZONE", + "in": "header", + "description": "Zona horaria IANA de las fechas sin zona del cuerpo JSON: el request las lee y la respuesta las escribe en ella. Si falta o no es válida, America/Mexico_City. No aplica a los campos que la operación documenta en UTC o en hora civil.", + "schema": { + "type": "string" + }, + "example": "America/Mexico_City" + } + ], + "responses": { + "200": { + "description": "OK" + } + }, + "security": [ + { + "ApiKey": [ ], + "TenantKey": [ ] + } + ] + } + }, + "/api/v4/auto-invoice/settings": { + "get": { + "tags": [ + "AutoInvoice" + ], + "summary": "Obtener la configuración de Auto CFDI", + "description": "La ventana de autofactura (si un ticket pagado en otro mes o en otro año todavía se puede facturar) y la URL del aviso de privacidad del portal. Un tenant que nunca la guardó recibe la ventana cerrada y sin aviso. No exige el feature Auto CFDI. 401: sin API key o JWT válidos. 403: la persona no tiene el permiso AutoInvoice.Read.", + "operationId": "getAutoInvoiceSettings", + "parameters": [ + { + "name": "X-TIME-ZONE", + "in": "header", + "description": "Zona horaria IANA de las fechas sin zona del cuerpo JSON: el request las lee y la respuesta las escribe en ella. Si falta o no es válida, America/Mexico_City. No aplica a los campos que la operación documenta en UTC o en hora civil.", + "schema": { + "type": "string" + }, + "example": "America/Mexico_City" + } + ], + "responses": { + "200": { + "description": "OK", + "content": { + "text/plain": { + "schema": { + "$ref": "#/components/schemas/ApiResponseOfAutoInvoiceSettingDto" + } + }, + "application/json": { + "schema": { + "$ref": "#/components/schemas/ApiResponseOfAutoInvoiceSettingDto" + } + }, + "text/json": { + "schema": { + "$ref": "#/components/schemas/ApiResponseOfAutoInvoiceSettingDto" + } + } + } + }, + "401": { + "description": "Unauthorized", + "content": { + "text/plain": { + "schema": { + "$ref": "#/components/schemas/ApiResponseOfObject" + } + }, + "application/json": { + "schema": { + "$ref": "#/components/schemas/ApiResponseOfObject" + } + }, + "text/json": { + "schema": { + "$ref": "#/components/schemas/ApiResponseOfObject" + } + } + } + }, + "403": { + "description": "Forbidden", + "content": { + "text/plain": { + "schema": { + "$ref": "#/components/schemas/ApiResponseOfObject" + } + }, + "application/json": { + "schema": { + "$ref": "#/components/schemas/ApiResponseOfObject" + } + }, + "text/json": { + "schema": { + "$ref": "#/components/schemas/ApiResponseOfObject" + } + } + } + } + }, + "security": [ + { + "ApiKey": [ ], + "TenantKey": [ ] + } + ] + }, + "put": { + "tags": [ + "AutoInvoice" + ], + "summary": "Actualizar la configuración de Auto CFDI", + "description": "Guarda la ventana de autofactura y la URL https del aviso de privacidad del portal. Permitir otro año exige permitir otro mes. Exige el feature Auto CFDI. 400: una URL de aviso de privacidad inválida o permitir otro año sin permitir otro mes. 401: sin API key o JWT válidos. 403: la persona no tiene el permiso AutoInvoice.Manage (owner) o el tenant no tiene Auto CFDI.", + "operationId": "updateAutoInvoiceSettings", + "parameters": [ + { + "name": "X-TIME-ZONE", + "in": "header", + "description": "Zona horaria IANA de las fechas sin zona del cuerpo JSON: el request las lee y la respuesta las escribe en ella. Si falta o no es válida, America/Mexico_City. No aplica a los campos que la operación documenta en UTC o en hora civil.", + "schema": { + "type": "string" + }, + "example": "America/Mexico_City" + } + ], + "requestBody": { + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/UpdateAutoInvoiceSettingCommand" + } + }, + "text/json": { + "schema": { + "$ref": "#/components/schemas/UpdateAutoInvoiceSettingCommand" + } + }, + "application/*+json": { + "schema": { + "$ref": "#/components/schemas/UpdateAutoInvoiceSettingCommand" + } + } + }, + "required": true + }, + "responses": { + "200": { + "description": "OK", + "content": { + "text/plain": { + "schema": { + "$ref": "#/components/schemas/ApiResponseOfAutoInvoiceSettingDto" + } + }, + "application/json": { + "schema": { + "$ref": "#/components/schemas/ApiResponseOfAutoInvoiceSettingDto" + } + }, + "text/json": { + "schema": { + "$ref": "#/components/schemas/ApiResponseOfAutoInvoiceSettingDto" + } + } + } + }, + "400": { + "description": "Bad Request", + "content": { + "text/plain": { + "schema": { + "$ref": "#/components/schemas/ApiResponseOfObject" + } + }, + "application/json": { + "schema": { + "$ref": "#/components/schemas/ApiResponseOfObject" + } + }, + "text/json": { + "schema": { + "$ref": "#/components/schemas/ApiResponseOfObject" + } + } + } + }, + "401": { + "description": "Unauthorized", + "content": { + "text/plain": { + "schema": { + "$ref": "#/components/schemas/ApiResponseOfObject" + } + }, + "application/json": { + "schema": { + "$ref": "#/components/schemas/ApiResponseOfObject" + } + }, + "text/json": { + "schema": { + "$ref": "#/components/schemas/ApiResponseOfObject" + } + } + } + }, + "403": { + "description": "Forbidden", + "content": { + "text/plain": { + "schema": { + "$ref": "#/components/schemas/ApiResponseOfObject" + } + }, + "application/json": { + "schema": { + "$ref": "#/components/schemas/ApiResponseOfObject" + } + }, + "text/json": { + "schema": { + "$ref": "#/components/schemas/ApiResponseOfObject" + } + } + } + } + }, + "security": [ + { + "ApiKey": [ ], + "TenantKey": [ ] + } + ] + } + }, + "/api/v4/auto-invoice/portal-name": { + "get": { + "tags": [ + "AutoInvoice" + ], + "summary": "Obtener el nombre del portal", + "description": "El nombre vigente del portal de autofactura del tenant, cuándo se asignó y desde cuándo puede cambiarse otra vez. Sin nombre, sus campos van en null. No exige el feature Auto CFDI. 401: sin API key o JWT válidos. 403: la persona no tiene el permiso AutoInvoice.Read.", + "operationId": "getAutoInvoicePortalName", + "parameters": [ + { + "name": "X-TIME-ZONE", + "in": "header", + "description": "Zona horaria IANA de las fechas sin zona del cuerpo JSON: el request las lee y la respuesta las escribe en ella. Si falta o no es válida, America/Mexico_City. No aplica a los campos que la operación documenta en UTC o en hora civil.", + "schema": { + "type": "string" + }, + "example": "America/Mexico_City" + } + ], + "responses": { + "200": { + "description": "OK", + "content": { + "text/plain": { + "schema": { + "$ref": "#/components/schemas/ApiResponseOfPortalNameDto" + } + }, + "application/json": { + "schema": { + "$ref": "#/components/schemas/ApiResponseOfPortalNameDto" + } + }, + "text/json": { + "schema": { + "$ref": "#/components/schemas/ApiResponseOfPortalNameDto" + } + } + } + }, + "401": { + "description": "Unauthorized", + "content": { + "text/plain": { + "schema": { + "$ref": "#/components/schemas/ApiResponseOfObject" + } + }, + "application/json": { + "schema": { + "$ref": "#/components/schemas/ApiResponseOfObject" + } + }, + "text/json": { + "schema": { + "$ref": "#/components/schemas/ApiResponseOfObject" + } + } + } + }, + "403": { + "description": "Forbidden", + "content": { + "text/plain": { + "schema": { + "$ref": "#/components/schemas/ApiResponseOfObject" + } + }, + "application/json": { + "schema": { + "$ref": "#/components/schemas/ApiResponseOfObject" + } + }, + "text/json": { + "schema": { + "$ref": "#/components/schemas/ApiResponseOfObject" + } + } + } + } + }, + "security": [ + { + "ApiKey": [ ], + "TenantKey": [ ] + } + ] + }, + "put": { + "tags": [ + "AutoInvoice" + ], + "summary": "Cambiar el nombre del portal", + "description": "Asigna el nombre del portal de autofactura del tenant. El nombre anterior queda como alias del tenant y el nombre solo puede cambiarse una vez por intervalo. Exige el feature Auto CFDI. 400: un nombre con formato inválido o reservado, o un cambio antes de que termine el intervalo. 401: sin API key o JWT válidos. 403: la persona no tiene el permiso AutoInvoice.Manage (owner) o el tenant no tiene Auto CFDI. 409: otro tenant tiene el nombre, o hay otro cambio del nombre en curso.", + "operationId": "updateAutoInvoicePortalName", + "parameters": [ + { + "name": "X-TIME-ZONE", + "in": "header", + "description": "Zona horaria IANA de las fechas sin zona del cuerpo JSON: el request las lee y la respuesta las escribe en ella. Si falta o no es válida, America/Mexico_City. No aplica a los campos que la operación documenta en UTC o en hora civil.", + "schema": { + "type": "string" + }, + "example": "America/Mexico_City" + } + ], + "requestBody": { + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/UpdatePortalNameCommand" + } + }, + "text/json": { + "schema": { + "$ref": "#/components/schemas/UpdatePortalNameCommand" + } + }, + "application/*+json": { + "schema": { + "$ref": "#/components/schemas/UpdatePortalNameCommand" + } + } + }, + "required": true + }, + "responses": { + "200": { + "description": "OK", + "content": { + "text/plain": { + "schema": { + "$ref": "#/components/schemas/ApiResponseOfPortalNameDto" + } + }, + "application/json": { + "schema": { + "$ref": "#/components/schemas/ApiResponseOfPortalNameDto" + } + }, + "text/json": { + "schema": { + "$ref": "#/components/schemas/ApiResponseOfPortalNameDto" + } + } + } + }, + "400": { + "description": "Bad Request", + "content": { + "text/plain": { + "schema": { + "$ref": "#/components/schemas/ApiResponseOfObject" + } + }, + "application/json": { + "schema": { + "$ref": "#/components/schemas/ApiResponseOfObject" + } + }, + "text/json": { + "schema": { + "$ref": "#/components/schemas/ApiResponseOfObject" + } + } + } + }, + "401": { + "description": "Unauthorized", + "content": { + "text/plain": { + "schema": { + "$ref": "#/components/schemas/ApiResponseOfObject" + } + }, + "application/json": { + "schema": { + "$ref": "#/components/schemas/ApiResponseOfObject" + } + }, + "text/json": { + "schema": { + "$ref": "#/components/schemas/ApiResponseOfObject" + } + } + } + }, + "403": { + "description": "Forbidden", + "content": { + "text/plain": { + "schema": { + "$ref": "#/components/schemas/ApiResponseOfObject" + } + }, + "application/json": { + "schema": { + "$ref": "#/components/schemas/ApiResponseOfObject" + } + }, + "text/json": { + "schema": { + "$ref": "#/components/schemas/ApiResponseOfObject" + } + } + } + }, + "409": { + "description": "Conflict", + "content": { + "text/plain": { + "schema": { + "$ref": "#/components/schemas/ApiResponseOfObject" + } + }, + "application/json": { + "schema": { + "$ref": "#/components/schemas/ApiResponseOfObject" + } + }, + "text/json": { + "schema": { + "$ref": "#/components/schemas/ApiResponseOfObject" + } + } + } + } + }, + "security": [ + { + "ApiKey": [ ], + "TenantKey": [ ] + } + ] + } + }, + "/api/v4/auto-invoice/portal-names/availability": { + "get": { + "tags": [ + "AutoInvoice" + ], + "summary": "Consultar si un nombre de portal está disponible", + "description": "Si el tenant puede usar un nombre de portal. Un nombre inválido, reservado o de otro tenant responde available: false con su razón (no es un error); uno que ya es del tenant está disponible. Exige el feature Auto CFDI. 401: sin API key o JWT válidos. 403: la persona no tiene el permiso AutoInvoice.Manage (owner) o el tenant no tiene Auto CFDI.", + "operationId": "getAutoInvoicePortalNameAvailability", + "parameters": [ + { + "name": "Name", + "in": "query", + "description": "El nombre de portal a consultar; uno inválido responde available: false con su razón, no un 400.", + "schema": { + "type": "string" + } + }, + { + "name": "X-TIME-ZONE", + "in": "header", + "description": "Zona horaria IANA de las fechas sin zona del cuerpo JSON: el request las lee y la respuesta las escribe en ella. Si falta o no es válida, America/Mexico_City. No aplica a los campos que la operación documenta en UTC o en hora civil.", + "schema": { + "type": "string" + }, + "example": "America/Mexico_City" + } + ], + "responses": { + "200": { + "description": "OK", + "content": { + "text/plain": { + "schema": { + "$ref": "#/components/schemas/ApiResponseOfPortalNameAvailabilityDto" + } + }, + "application/json": { + "schema": { + "$ref": "#/components/schemas/ApiResponseOfPortalNameAvailabilityDto" + } + }, + "text/json": { + "schema": { + "$ref": "#/components/schemas/ApiResponseOfPortalNameAvailabilityDto" + } + } + } + }, + "401": { + "description": "Unauthorized", + "content": { + "text/plain": { + "schema": { + "$ref": "#/components/schemas/ApiResponseOfObject" + } + }, + "application/json": { + "schema": { + "$ref": "#/components/schemas/ApiResponseOfObject" + } + }, + "text/json": { + "schema": { + "$ref": "#/components/schemas/ApiResponseOfObject" + } + } + } + }, + "403": { + "description": "Forbidden", + "content": { + "text/plain": { + "schema": { + "$ref": "#/components/schemas/ApiResponseOfObject" + } + }, + "application/json": { + "schema": { + "$ref": "#/components/schemas/ApiResponseOfObject" + } + }, + "text/json": { + "schema": { + "$ref": "#/components/schemas/ApiResponseOfObject" + } + } + } + } + }, + "security": [ + { + "ApiKey": [ ], + "TenantKey": [ ] + } + ] + } + }, + "/api/v4/auto-invoice/reports/consumption": { + "get": { + "tags": [ + "AutoInvoice" + ], + "summary": "Obtener el reporte de consumo de Auto CFDI", + "description": "Los tickets pagados en el periodo por emisor y estado, y los créditos de ticket que consumió cada persona que los pagó. El periodo es obligatorio, en fechas civiles de México, de un año como máximo. No exige el feature Auto CFDI. 400: falta el periodo, una fecha es inválida, el periodo está invertido o pasa de un año. 401: sin API key o JWT válidos. 403: la persona no tiene el permiso Tickets.Read. 404: personId no es una persona del tenant.", + "operationId": "getAutoInvoiceConsumptionReport", + "parameters": [ + { + "name": "From", + "in": "query", + "description": "Primer día del periodo (incluido), fecha civil de México YYYY-MM-DD; el periodo es de un año como máximo.", + "schema": { + "type": "string" + } + }, + { + "name": "To", + "in": "query", + "description": "Último día del periodo (incluido), fecha civil de México YYYY-MM-DD.", + "schema": { + "type": "string" + } + }, + { + "name": "PersonId", + "in": "query", + "description": "Id de una persona del tenant: solo sus tickets como emisora y sus créditos como pagadora.", + "schema": { + "type": "string" + } + }, + { + "name": "X-TIME-ZONE", + "in": "header", + "description": "Zona horaria IANA de las fechas sin zona del cuerpo JSON: el request las lee y la respuesta las escribe en ella. Si falta o no es válida, America/Mexico_City. No aplica a los campos que la operación documenta en UTC o en hora civil.", + "schema": { + "type": "string" + }, + "example": "America/Mexico_City" + } + ], + "responses": { + "200": { + "description": "OK", + "content": { + "text/plain": { + "schema": { + "$ref": "#/components/schemas/ApiResponseOfConsumptionReportDto" + } + }, + "application/json": { + "schema": { + "$ref": "#/components/schemas/ApiResponseOfConsumptionReportDto" + } + }, + "text/json": { + "schema": { + "$ref": "#/components/schemas/ApiResponseOfConsumptionReportDto" + } + } + } + }, + "400": { + "description": "Bad Request", + "content": { + "text/plain": { + "schema": { + "$ref": "#/components/schemas/ApiResponseOfObject" + } + }, + "application/json": { + "schema": { + "$ref": "#/components/schemas/ApiResponseOfObject" + } + }, + "text/json": { + "schema": { + "$ref": "#/components/schemas/ApiResponseOfObject" + } + } + } + }, + "401": { + "description": "Unauthorized", + "content": { + "text/plain": { + "schema": { + "$ref": "#/components/schemas/ApiResponseOfObject" + } + }, + "application/json": { + "schema": { + "$ref": "#/components/schemas/ApiResponseOfObject" + } + }, + "text/json": { + "schema": { + "$ref": "#/components/schemas/ApiResponseOfObject" + } + } + } + }, + "403": { + "description": "Forbidden", + "content": { + "text/plain": { + "schema": { + "$ref": "#/components/schemas/ApiResponseOfObject" + } + }, + "application/json": { + "schema": { + "$ref": "#/components/schemas/ApiResponseOfObject" + } + }, + "text/json": { + "schema": { + "$ref": "#/components/schemas/ApiResponseOfObject" + } + } + } + }, + "404": { + "description": "Not Found", + "content": { + "text/plain": { + "schema": { + "$ref": "#/components/schemas/ApiResponseOfObject" + } + }, + "application/json": { + "schema": { + "$ref": "#/components/schemas/ApiResponseOfObject" + } + }, + "text/json": { + "schema": { + "$ref": "#/components/schemas/ApiResponseOfObject" + } + } + } + } + }, + "security": [ + { + "ApiKey": [ ], + "TenantKey": [ ] + } + ] + } + }, + "/api/v4/catalogs": { + "get": { + "tags": [ + "Catalogs" + ], + "summary": "Listar catálogos", + "description": "Los nombres de los catálogos del SAT y de los internos de FiscalAPI, sin paginar. Cada nombre es el que va en la ruta de los demás endpoints de catálogos y distingue mayúsculas y minúsculas: c_RegimenFiscal es SatTaxRegimes, c_UsoCFDI es SatCfdiUses, c_ClaveProdServ es SatProductCodes y c_ClaveUnidad es SatUnitMeasurements. Un nombre que no está en la lista responde 404. Docs: [listar catálogos](https://docs.fiscalapi.com/catalogs#listar-catalogos).", + "operationId": "listCatalogs", + "parameters": [ + { + "name": "X-TIME-ZONE", + "in": "header", + "description": "Zona horaria IANA de las fechas sin zona del cuerpo JSON: el request las lee y la respuesta las escribe en ella. Si falta o no es válida, America/Mexico_City. No aplica a los campos que la operación documenta en UTC o en hora civil.", + "schema": { + "type": "string" + }, + "example": "America/Mexico_City" + } + ], + "responses": { + "200": { + "description": "OK" + } + }, + "security": [ + { + "ApiKey": [ ], + "TenantKey": [ ] + } + ] + } + }, + "/api/v4/catalogs/{catalogName}/key/{id}": { + "get": { + "tags": [ + "Catalogs" + ], + "summary": "Obtener un registro de un catálogo", + "description": "Un registro de un catálogo por su clave: catalogName es el nombre del catálogo (p. ej. SatTaxRegimes) e id es la clave del SAT (p. ej. 601 en c_RegimenFiscal). Responde 404 si el catálogo no existe o si la clave no está en él. Docs: [obtener registro por ID](https://docs.fiscalapi.com/catalogs#obtener-registro-por-id).", + "operationId": "getCatalogRecord", + "parameters": [ + { + "name": "catalogName", + "in": "path", + "required": true, + "schema": { + "type": "string" + } + }, + { + "name": "id", + "in": "path", + "required": true, + "schema": { + "type": "string" + } + }, + { + "name": "X-TIME-ZONE", + "in": "header", + "description": "Zona horaria IANA de las fechas sin zona del cuerpo JSON: el request las lee y la respuesta las escribe en ella. Si falta o no es válida, America/Mexico_City. No aplica a los campos que la operación documenta en UTC o en hora civil.", + "schema": { + "type": "string" + }, + "example": "America/Mexico_City" + } + ], + "responses": { + "200": { + "description": "OK" + } + }, + "security": [ + { + "ApiKey": [ ], + "TenantKey": [ ] + } + ] + } + }, + "/api/v4/catalogs/{catalogName}/{searchText}": { + "get": { + "tags": [ + "Catalogs" + ], + "summary": "Buscar en un catálogo", + "description": "Los registros de un catálogo cuya descripción contiene searchText, de al menos 4 caracteres; no busca por clave (para eso, obtén el registro por su id). Paginado con pageNumber y pageSize (50 por omisión, de 1 a 100); un parámetro fuera de rango responde 400 y un catálogo inexistente, 404. Docs: [buscar en un catálogo](https://docs.fiscalapi.com/catalogs#buscar-en-un-catalogo).", + "operationId": "searchCatalogRecords", + "parameters": [ + { + "name": "catalogName", + "in": "path", + "required": true, + "schema": { + "type": "string" + } + }, + { + "name": "searchText", + "in": "path", + "required": true, + "schema": { + "type": "string" + } + }, + { + "name": "pageNumber", + "in": "query", + "schema": { + "pattern": "^-?(?:0|[1-9]\\d*)$", + "type": [ + "integer", + "string" + ], + "format": "int32", + "default": 1 + } + }, + { + "name": "pageSize", + "in": "query", + "schema": { + "pattern": "^-?(?:0|[1-9]\\d*)$", + "type": [ + "integer", + "string" + ], + "format": "int32", + "default": 50 + } + }, + { + "name": "X-TIME-ZONE", + "in": "header", + "description": "Zona horaria IANA de las fechas sin zona del cuerpo JSON: el request las lee y la respuesta las escribe en ella. Si falta o no es válida, America/Mexico_City. No aplica a los campos que la operación documenta en UTC o en hora civil.", + "schema": { + "type": "string" + }, + "example": "America/Mexico_City" + } + ], + "responses": { + "200": { + "description": "OK" + } + }, + "security": [ + { + "ApiKey": [ ], + "TenantKey": [ ] + } + ] + } + }, + "/api/v4/download-catalogs": { + "get": { + "tags": [ + "DownloadCatalogs" + ], + "summary": "Listar los catálogos de descarga masiva", + "description": "Los nombres de los catálogos de la descarga masiva, sin paginar: SatQueryTypes, DownloadTypes, SatInvoiceStatuses, DownloadRequestStatuses y los demás que usan las reglas y las solicitudes. Usa cada nombre con listDownloadCatalogRecords. Docs: [listar catálogos](https://docs.fiscalapi.com/download-catalogs#listar-catalogos).", + "operationId": "listDownloadCatalogs", + "parameters": [ + { + "name": "X-TIME-ZONE", + "in": "header", + "description": "Zona horaria IANA de las fechas sin zona del cuerpo JSON: el request las lee y la respuesta las escribe en ella. Si falta o no es válida, America/Mexico_City. No aplica a los campos que la operación documenta en UTC o en hora civil.", + "schema": { + "type": "string" + }, + "example": "America/Mexico_City" + } + ], + "responses": { + "200": { + "description": "OK" + } + }, + "security": [ + { + "ApiKey": [ ], + "TenantKey": [ ] + } + ] + } + }, + "/api/v4/download-catalogs/{catalogName}": { + "get": { + "tags": [ + "DownloadCatalogs" + ], + "summary": "Listar los registros de un catálogo de descarga masiva", + "description": "Los registros (id y description) del catálogo catalogName, escrito como lo devuelve listDownloadCatalogs (distingue mayúsculas); un nombre que no existe responde 404. Los id son los códigos que usan las reglas y las solicitudes, como satQueryTypeId o downloadTypeId. Docs: [listar catálogo](https://docs.fiscalapi.com/download-catalogs#listar-catalogo).", + "operationId": "listDownloadCatalogRecords", + "parameters": [ + { + "name": "catalogName", + "in": "path", + "required": true, + "schema": { + "type": "string" + } + }, + { + "name": "details", + "in": "query", + "schema": { + "type": "boolean", + "default": false + } + }, + { + "name": "X-TIME-ZONE", + "in": "header", + "description": "Zona horaria IANA de las fechas sin zona del cuerpo JSON: el request las lee y la respuesta las escribe en ella. Si falta o no es válida, America/Mexico_City. No aplica a los campos que la operación documenta en UTC o en hora civil.", + "schema": { + "type": "string" + }, + "example": "America/Mexico_City" + } + ], + "responses": { + "200": { + "description": "OK" + } + }, + "security": [ + { + "ApiKey": [ ], + "TenantKey": [ ] + } + ] + } + }, + "/api/v4/download-requests": { + "get": { + "tags": [ + "DownloadRequests" + ], + "summary": "Listar solicitudes de descarga masiva", + "description": "Las solicitudes de descarga masiva de CFDI, metadata y Retenciones del SAT del tenant, manuales y automáticas, paginadas con pageNumber y pageSize (10 por omisión, máximo 50). Cada una trae su estado en FiscalAPI (downloadRequestStatus), su estatus ante el SAT (satRequestStatus) y, ya terminada en el SAT, invoiceCount y packageIds. Docs: [listar solicitudes](https://docs.fiscalapi.com/download-requests#listar-solicitudes).", + "operationId": "listDownloadRequests", + "parameters": [ + { + "name": "PageNumber", + "in": "query", + "schema": { + "pattern": "^-?(?:0|[1-9]\\d*)$", + "type": [ + "integer", + "string" + ], + "format": "int32" + } + }, + { + "name": "PageSize", + "in": "query", + "schema": { + "pattern": "^-?(?:0|[1-9]\\d*)$", + "type": [ + "integer", + "string" + ], + "format": "int32" + } + }, + { + "name": "X-TIME-ZONE", + "in": "header", + "description": "Zona horaria IANA de las fechas sin zona del cuerpo JSON: el request las lee y la respuesta las escribe en ella. Si falta o no es válida, America/Mexico_City. No aplica a los campos que la operación documenta en UTC o en hora civil.", + "schema": { + "type": "string" + }, + "example": "America/Mexico_City" + } + ], + "responses": { + "200": { + "description": "OK" + } + }, + "security": [ + { + "ApiKey": [ ], + "TenantKey": [ ] + } + ] + }, + "post": { + "tags": [ + "DownloadRequests" + ], + "summary": "Crear una solicitud de descarga masiva", + "description": "Pide al SAT en la misma petición, con la FIEL vigente de la persona de la regla, la descarga masiva de la regla downloadRuleId entre startDate y endDate; downloadRequestTypeId debe ser \"Manual\". El periodo admite hasta 31 días (hasta 400 días en reglas de Retenciones); un periodo más largo, una startDate que no sea anterior a endDate, una endDate futura o una persona sin RFC o FIEL vigente responden 400. Responde 404 si la regla no existe; 403 si no está aprobada, si su persona no tiene un asiento activo de Descarga CFDI o en el ambiente de pruebas, y 502 si el SAT rechaza la autenticación o no crea la solicitud. Aceptada, queda en Esperando SAT y FiscalAPI la procesa en segundo plano. Docs: [crear solicitud](https://docs.fiscalapi.com/download-requests#crear-solicitud).", + "operationId": "createDownloadRequest", + "parameters": [ + { + "name": "X-TIME-ZONE", + "in": "header", + "description": "Zona horaria IANA de las fechas sin zona del cuerpo JSON: el request las lee y la respuesta las escribe en ella. Si falta o no es válida, America/Mexico_City. No aplica a los campos que la operación documenta en UTC o en hora civil.", + "schema": { + "type": "string" + }, + "example": "America/Mexico_City" + } + ], + "requestBody": { + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/CreateDownloadRequestCommand" + } + }, + "text/json": { + "schema": { + "$ref": "#/components/schemas/CreateDownloadRequestCommand" + } + }, + "application/*+json": { + "schema": { + "$ref": "#/components/schemas/CreateDownloadRequestCommand" + } + } + }, + "required": true + }, + "responses": { + "200": { + "description": "OK" + } + }, + "security": [ + { + "ApiKey": [ ], + "TenantKey": [ ] + } + ] + } + }, + "/api/v4/download-requests/search": { + "get": { + "tags": [ + "DownloadRequests" + ], + "summary": "Buscar solicitudes de descarga masiva por fecha de creación", + "description": "Las solicitudes de descarga masiva creadas el día createdAt en UTC (obligatorio, AAAA-MM-DD; sin él responde 400), sin paginar y de la más reciente a la más antigua. Docs: [listar solicitudes](https://docs.fiscalapi.com/download-requests#listar-solicitudes).", + "operationId": "searchDownloadRequests", + "parameters": [ + { + "name": "CreatedAt", + "in": "query", + "schema": { + "type": "string", + "format": "date-time" + } + }, + { + "name": "X-TIME-ZONE", + "in": "header", + "description": "Zona horaria IANA de las fechas sin zona del cuerpo JSON: el request las lee y la respuesta las escribe en ella. Si falta o no es válida, America/Mexico_City. No aplica a los campos que la operación documenta en UTC o en hora civil.", + "schema": { + "type": "string" + }, + "example": "America/Mexico_City" + } + ], + "responses": { + "200": { + "description": "OK" + } + }, + "security": [ + { + "ApiKey": [ ], + "TenantKey": [ ] + } + ] + } + }, + "/api/v4/download-requests/{id}": { + "get": { + "tags": [ + "DownloadRequests" + ], + "summary": "Obtener una solicitud de descarga masiva", + "description": "Una solicitud por su id, con su estado en FiscalAPI (downloadRequestStatus: Esperando SAT, Esperando API, Completada o Abandonada) y en el SAT (satRequestStatus). Consúltala hasta que sea Completada para leer sus XML, su metadata o sus paquetes; 404 si no existe en el tenant. Docs: [obtener solicitud](https://docs.fiscalapi.com/download-requests#obtener-solicitud-por-id).", + "operationId": "getDownloadRequest", + "parameters": [ + { + "name": "id", + "in": "path", + "required": true, + "schema": { + "type": "string" + } + }, + { + "name": "X-TIME-ZONE", + "in": "header", + "description": "Zona horaria IANA de las fechas sin zona del cuerpo JSON: el request las lee y la respuesta las escribe en ella. Si falta o no es válida, America/Mexico_City. No aplica a los campos que la operación documenta en UTC o en hora civil.", + "schema": { + "type": "string" + }, + "example": "America/Mexico_City" + } + ], + "responses": { + "200": { + "description": "OK" + } + }, + "security": [ + { + "ApiKey": [ ], + "TenantKey": [ ] + } + ] + }, + "delete": { + "tags": [ + "DownloadRequests" + ], + "summary": "Eliminar una solicitud de descarga masiva", + "description": "Borra de forma definitiva la solicitud con sus paquetes, XML y metadata; 404 si no existe en el tenant. Docs: [eliminar solicitud](https://docs.fiscalapi.com/download-requests#eliminar-solicitud-por-id).", + "operationId": "deleteDownloadRequest", + "parameters": [ + { + "name": "id", + "in": "path", + "required": true, + "schema": { + "type": "string" + } + }, + { + "name": "X-TIME-ZONE", + "in": "header", + "description": "Zona horaria IANA de las fechas sin zona del cuerpo JSON: el request las lee y la respuesta las escribe en ella. Si falta o no es válida, America/Mexico_City. No aplica a los campos que la operación documenta en UTC o en hora civil.", + "schema": { + "type": "string" + }, + "example": "America/Mexico_City" + } + ], + "responses": { + "200": { + "description": "OK" + } + }, + "security": [ + { + "ApiKey": [ ], + "TenantKey": [ ] + } + ] + } + }, + "/api/v4/download-requests/{id}/xmls": { + "get": { + "tags": [ + "DownloadRequests" + ], + "summary": "Listar los CFDI (XML) de una solicitud de descarga masiva", + "description": "Los CFDI que FiscalAPI extrajo de los paquetes de una solicitud de tipo CFDI, paginados con pageNumber y pageSize (10 por omisión, máximo 50): cada uno con emisor (xmlIssuer), receptor (xmlRecipient), conceptos (xmlItems), impuestos (taxes), complementos (xmlComplements) y el XML en base64Content. El id debe medir 36 caracteres (400). Docs: [listar XML](https://docs.fiscalapi.com/download-requests#listar-xmls-por-id).", + "operationId": "listDownloadRequestXmls", + "parameters": [ + { + "name": "id", + "in": "path", + "required": true, + "schema": { + "type": "string" + } + }, + { + "name": "RequestId", + "in": "query", + "schema": { + "type": "string" + } + }, + { + "name": "PageNumber", + "in": "query", + "schema": { + "pattern": "^-?(?:0|[1-9]\\d*)$", + "type": [ + "integer", + "string" + ], + "format": "int32" + } + }, + { + "name": "PageSize", + "in": "query", + "schema": { + "pattern": "^-?(?:0|[1-9]\\d*)$", + "type": [ + "integer", + "string" + ], + "format": "int32" + } + }, + { + "name": "X-TIME-ZONE", + "in": "header", + "description": "Zona horaria IANA de las fechas sin zona del cuerpo JSON: el request las lee y la respuesta las escribe en ella. Si falta o no es válida, America/Mexico_City. No aplica a los campos que la operación documenta en UTC o en hora civil.", + "schema": { + "type": "string" + }, + "example": "America/Mexico_City" + } + ], + "responses": { + "200": { + "description": "OK" + } + }, + "security": [ + { + "ApiKey": [ ], + "TenantKey": [ ] + } + ] + } + }, + "/api/v4/download-requests/{id}/meta-items": { + "get": { + "tags": [ + "DownloadRequests" + ], + "summary": "Listar la metadata de una solicitud de descarga masiva", + "description": "Los registros de metadata que FiscalAPI extrajo de los paquetes de una solicitud de tipo Metadata, paginados con pageNumber y pageSize (10 por omisión, máximo 50): invoiceUuid, issuerTin, recipientTin, amount, status y cancellationDate, entre otros. Es la vía para los CFDI cancelados. El id debe medir 36 caracteres (400). Docs: [listar metadata](https://docs.fiscalapi.com/download-requests#listar-meta-items-por-id).", + "operationId": "listDownloadRequestMetaItems", + "parameters": [ + { + "name": "id", + "in": "path", + "required": true, + "schema": { + "type": "string" + } + }, + { + "name": "RequestId", + "in": "query", + "schema": { + "type": "string" + } + }, + { + "name": "PageNumber", + "in": "query", + "schema": { + "pattern": "^-?(?:0|[1-9]\\d*)$", + "type": [ + "integer", + "string" + ], + "format": "int32" + } + }, + { + "name": "PageSize", + "in": "query", + "schema": { + "pattern": "^-?(?:0|[1-9]\\d*)$", + "type": [ + "integer", + "string" + ], + "format": "int32" + } + }, + { + "name": "X-TIME-ZONE", + "in": "header", + "description": "Zona horaria IANA de las fechas sin zona del cuerpo JSON: el request las lee y la respuesta las escribe en ella. Si falta o no es válida, America/Mexico_City. No aplica a los campos que la operación documenta en UTC o en hora civil.", + "schema": { + "type": "string" + }, + "example": "America/Mexico_City" + } + ], + "responses": { + "200": { + "description": "OK" + } + }, + "security": [ + { + "ApiKey": [ ], + "TenantKey": [ ] + } + ] + } + }, + "/api/v4/download-requests/{id}/withholding-packages": { + "get": { + "tags": [ + "DownloadRequests" + ], + "summary": "Listar los paquetes de una solicitud de Retenciones", + "description": "Los paquetes que entregó el SAT para la solicitud, cada uno con satPackageId y el zip en base64Package, paginados con pageNumber y pageSize (10 por omisión y máximo). Sirve para leer por páginas una solicitud de Retenciones e información de pagos, cuyos paquetes FiscalAPI no extrae como XML ni metadata. El id debe medir 36 caracteres (400). Docs: [descargar paquete](https://docs.fiscalapi.com/download-requests#descargar-paquete-por-id).", + "operationId": "listDownloadRequestWithholdingPackages", + "parameters": [ + { + "name": "id", + "in": "path", + "required": true, + "schema": { + "type": "string" + } + }, + { + "name": "RequestId", + "in": "query", + "schema": { + "type": "string" + } + }, + { + "name": "PageNumber", + "in": "query", + "schema": { + "pattern": "^-?(?:0|[1-9]\\d*)$", + "type": [ + "integer", + "string" + ], + "format": "int32" + } + }, + { + "name": "PageSize", + "in": "query", + "schema": { + "pattern": "^-?(?:0|[1-9]\\d*)$", + "type": [ + "integer", + "string" + ], + "format": "int32" + } + }, + { + "name": "X-TIME-ZONE", + "in": "header", + "description": "Zona horaria IANA de las fechas sin zona del cuerpo JSON: el request las lee y la respuesta las escribe en ella. Si falta o no es válida, America/Mexico_City. No aplica a los campos que la operación documenta en UTC o en hora civil.", + "schema": { + "type": "string" + }, + "example": "America/Mexico_City" + } + ], + "responses": { + "200": { + "description": "OK" + } + }, + "security": [ + { + "ApiKey": [ ], + "TenantKey": [ ] + } + ] + } + }, + "/api/v4/download-requests/{id}/package": { + "get": { + "tags": [ + "DownloadRequests" + ], + "summary": "Descargar los paquetes de una solicitud de descarga masiva", + "description": "Los paquetes zip que entregó el SAT para la solicitud, cada uno en base64File con fileName .zip. Solo con la solicitud en Completada: en otro estado responde 425 (Too Early) con el encabezado Retry-After: 1800, y 404 si no existe. Docs: [descargar paquete](https://docs.fiscalapi.com/download-requests#descargar-paquete-por-id).", + "operationId": "getDownloadRequestPackage", + "parameters": [ + { + "name": "id", + "in": "path", + "required": true, + "schema": { + "type": "string" + } + }, + { + "name": "X-TIME-ZONE", + "in": "header", + "description": "Zona horaria IANA de las fechas sin zona del cuerpo JSON: el request las lee y la respuesta las escribe en ella. Si falta o no es válida, America/Mexico_City. No aplica a los campos que la operación documenta en UTC o en hora civil.", + "schema": { + "type": "string" + }, + "example": "America/Mexico_City" + } + ], + "responses": { + "200": { + "description": "OK" + } + }, + "security": [ + { + "ApiKey": [ ], + "TenantKey": [ ] + } + ] + } + }, + "/api/v4/download-requests/{id}/raw-request": { + "get": { + "tags": [ + "DownloadRequests" + ], + "summary": "Descargar la última petición enviada al SAT por una solicitud", + "description": "El XML de la última petición que FiscalAPI envió al SAT por la solicitud (al crearla o al verificarla), en base64File con fileName RawRequest_.xml; 404 si no existe. Docs: [descargar SAT request](https://docs.fiscalapi.com/download-requests#descargar-sat-request-por-id).", + "operationId": "getDownloadRequestRawRequest", + "parameters": [ + { + "name": "id", + "in": "path", + "required": true, + "schema": { + "type": "string" + } + }, + { + "name": "X-TIME-ZONE", + "in": "header", + "description": "Zona horaria IANA de las fechas sin zona del cuerpo JSON: el request las lee y la respuesta las escribe en ella. Si falta o no es válida, America/Mexico_City. No aplica a los campos que la operación documenta en UTC o en hora civil.", + "schema": { + "type": "string" + }, + "example": "America/Mexico_City" + } + ], + "responses": { + "200": { + "description": "OK" + } + }, + "security": [ + { + "ApiKey": [ ], + "TenantKey": [ ] + } + ] + } + }, + "/api/v4/download-requests/{id}/raw-response": { + "get": { + "tags": [ + "DownloadRequests" + ], + "summary": "Descargar la última respuesta del SAT a una solicitud", + "description": "El XML de la última respuesta del SAT a la solicitud (al crearla o al verificarla), en base64File con fileName RawResponse_.xml; 404 si no existe. Docs: [descargar SAT response](https://docs.fiscalapi.com/download-requests#descargar-sat-response-por-id).", + "operationId": "getDownloadRequestRawResponse", + "parameters": [ + { + "name": "id", + "in": "path", + "required": true, + "schema": { + "type": "string" + } + }, + { + "name": "X-TIME-ZONE", + "in": "header", + "description": "Zona horaria IANA de las fechas sin zona del cuerpo JSON: el request las lee y la respuesta las escribe en ella. Si falta o no es válida, America/Mexico_City. No aplica a los campos que la operación documenta en UTC o en hora civil.", + "schema": { + "type": "string" + }, + "example": "America/Mexico_City" + } + ], + "responses": { + "200": { + "description": "OK" + } + }, + "security": [ + { + "ApiKey": [ ], + "TenantKey": [ ] + } + ] + } + }, + "/api/v4/download-rules": { + "get": { + "tags": [ + "DownloadRules" + ], + "summary": "Listar reglas de descarga masiva", + "description": "Las reglas de descarga masiva del tenant, paginadas con pageNumber y pageSize (10 por omisión, máximo 50), cada una con su persona, RFC (tin), tipo de consulta (satQueryType), dirección (downloadType), estatus de los documentos (satInvoiceStatus) y estado de la regla (downloadRuleStatus). Docs: [listar reglas](https://docs.fiscalapi.com/download-rules#listar-reglas).", + "operationId": "listDownloadRules", + "parameters": [ + { + "name": "PageNumber", + "in": "query", + "schema": { + "pattern": "^-?(?:0|[1-9]\\d*)$", + "type": [ + "integer", + "string" + ], + "format": "int32" + } + }, + { + "name": "PageSize", + "in": "query", + "schema": { + "pattern": "^-?(?:0|[1-9]\\d*)$", + "type": [ + "integer", + "string" + ], + "format": "int32" + } + }, + { + "name": "X-TIME-ZONE", + "in": "header", + "description": "Zona horaria IANA de las fechas sin zona del cuerpo JSON: el request las lee y la respuesta las escribe en ella. Si falta o no es válida, America/Mexico_City. No aplica a los campos que la operación documenta en UTC o en hora civil.", + "schema": { + "type": "string" + }, + "example": "America/Mexico_City" + } + ], + "responses": { + "200": { + "description": "OK" + } + }, + "security": [ + { + "ApiKey": [ ], + "TenantKey": [ ] + } + ] + }, + "post": { + "tags": [ + "DownloadRules" + ], + "summary": "Crear una regla de descarga masiva", + "description": "Programa la descarga masiva diaria de un RFC: con la regla aprobada y la persona con asiento activo, FiscalAPI pide al SAT cada día, con la FIEL de la persona, los documentos del día anterior. Campos obligatorios: personId, description (máximo 100), satQueryTypeId (CFDI, Metadata o Retenciones), downloadTypeId (Emitidos o Recibidos) y satInvoiceStatusId (Vigente o Cancelado; Cancelado solo con Metadata). Responde 400 si personId no es del tenant o la persona no tiene RFC o FIEL vigente, y 403 si la persona es el owner del tenant y la persona autenticada no lo es, si ya hay una regla igual para el RFC, si no quedan asientos de Descarga CFDI o en el ambiente de pruebas. Docs: [crear regla](https://docs.fiscalapi.com/download-rules#crear-regla).", + "operationId": "createDownloadRule", + "parameters": [ + { + "name": "X-TIME-ZONE", + "in": "header", + "description": "Zona horaria IANA de las fechas sin zona del cuerpo JSON: el request las lee y la respuesta las escribe en ella. Si falta o no es válida, America/Mexico_City. No aplica a los campos que la operación documenta en UTC o en hora civil.", + "schema": { + "type": "string" + }, + "example": "America/Mexico_City" + } + ], + "requestBody": { + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/CreateDownloadRuleCommand" + } + }, + "text/json": { + "schema": { + "$ref": "#/components/schemas/CreateDownloadRuleCommand" + } + }, + "application/*+json": { + "schema": { + "$ref": "#/components/schemas/CreateDownloadRuleCommand" + } + } + }, + "required": true + }, + "responses": { + "200": { + "description": "OK" + } + }, + "security": [ + { + "ApiKey": [ ], + "TenantKey": [ ] + } + ] + } + }, + "/api/v4/download-rules/{id}": { + "get": { + "tags": [ + "DownloadRules" + ], + "summary": "Obtener una regla de descarga masiva", + "description": "Una regla por su id, con su persona, RFC (tin), tipo de consulta (satQueryType), dirección (downloadType), estatus de los documentos (satInvoiceStatus) y estado de la regla (downloadRuleStatus); 404 si no existe en el tenant. Docs: [obtener regla](https://docs.fiscalapi.com/download-rules#obtener-regla-por-id).", + "operationId": "getDownloadRule", + "parameters": [ + { + "name": "id", + "in": "path", + "required": true, + "schema": { + "type": "string" + } + }, + { + "name": "X-TIME-ZONE", + "in": "header", + "description": "Zona horaria IANA de las fechas sin zona del cuerpo JSON: el request las lee y la respuesta las escribe en ella. Si falta o no es válida, America/Mexico_City. No aplica a los campos que la operación documenta en UTC o en hora civil.", + "schema": { + "type": "string" + }, + "example": "America/Mexico_City" + } + ], + "responses": { + "200": { + "description": "OK" + } + }, + "security": [ + { + "ApiKey": [ ], + "TenantKey": [ ] + } + ] + }, + "put": { + "tags": [ + "DownloadRules" + ], + "summary": "Actualizar la descripción de una regla de descarga masiva", + "description": "Solo cambia description (máximo 100 caracteres); la persona, el tipo de consulta, la dirección y el estatus de los documentos no se editan. El cuerpo lleva el mismo id de la ruta (400 si no coincide) y una description omitida deja la regla sin descripción. Responde 404 si no existe en el tenant y 403 si la regla es del owner del tenant y la persona autenticada no lo es. Docs: [actualizar regla](https://docs.fiscalapi.com/download-rules#actualizar-regla).", + "operationId": "updateDownloadRule", + "parameters": [ + { + "name": "id", + "in": "path", + "required": true, + "schema": { + "type": "string" + } + }, + { + "name": "X-TIME-ZONE", + "in": "header", + "description": "Zona horaria IANA de las fechas sin zona del cuerpo JSON: el request las lee y la respuesta las escribe en ella. Si falta o no es válida, America/Mexico_City. No aplica a los campos que la operación documenta en UTC o en hora civil.", + "schema": { + "type": "string" + }, + "example": "America/Mexico_City" + } + ], + "requestBody": { + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/UpdateDownloadRuleCommand" + } + }, + "text/json": { + "schema": { + "$ref": "#/components/schemas/UpdateDownloadRuleCommand" + } + }, + "application/*+json": { + "schema": { + "$ref": "#/components/schemas/UpdateDownloadRuleCommand" + } + } + }, + "required": true + }, + "responses": { + "200": { + "description": "OK" + } + }, + "security": [ + { + "ApiKey": [ ], + "TenantKey": [ ] + } + ] + }, + "delete": { + "tags": [ + "DownloadRules" + ], + "summary": "Eliminar una regla de descarga masiva", + "description": "Borra de forma definitiva la regla con todas sus solicitudes, paquetes, XML y metadata, y recalcula los asientos de Descarga CFDI ocupados. Responde 404 si no existe en el tenant y 403 si la regla es del owner del tenant y la persona autenticada no lo es. Docs: [eliminar regla](https://docs.fiscalapi.com/download-rules#eliminar-regla).", + "operationId": "deleteDownloadRule", + "parameters": [ + { + "name": "id", + "in": "path", + "required": true, + "schema": { + "type": "string" + } + }, + { + "name": "X-TIME-ZONE", + "in": "header", + "description": "Zona horaria IANA de las fechas sin zona del cuerpo JSON: el request las lee y la respuesta las escribe en ella. Si falta o no es válida, America/Mexico_City. No aplica a los campos que la operación documenta en UTC o en hora civil.", + "schema": { + "type": "string" + }, + "example": "America/Mexico_City" + } + ], + "responses": { + "200": { + "description": "OK" + } + }, + "security": [ + { + "ApiKey": [ ], + "TenantKey": [ ] + } + ] + } + }, + "/api/v4/download-rules/test": { + "post": { + "tags": [ + "DownloadRules" + ], + "summary": "Simular una descarga masiva en el ambiente de pruebas", + "description": "Sin contactar al SAT, crea una regla de prueba (CFDI, Recibidos, Vigente) y una solicitud ya Completada con los últimos CFDI timbrados del tenant (hasta 3) como XML, metadata y paquete, y emite el evento download_request.completed. Responde la solicitud creada; 400 en producción o si el tenant no tiene facturas timbradas. Docs: [ambiente de pruebas](https://docs.fiscalapi.com/download-info#ambiente-de-pruebas).", + "operationId": "createDownloadRuleTestRequest", + "parameters": [ + { + "name": "X-TIME-ZONE", + "in": "header", + "description": "Zona horaria IANA de las fechas sin zona del cuerpo JSON: el request las lee y la respuesta las escribe en ella. Si falta o no es válida, America/Mexico_City. No aplica a los campos que la operación documenta en UTC o en hora civil.", + "schema": { + "type": "string" + }, + "example": "America/Mexico_City" + } + ], + "responses": { + "200": { + "description": "OK" + } + }, + "security": [ + { + "ApiKey": [ ], + "TenantKey": [ ] + } + ] + } + }, + "/api/v4/events": { + "get": { + "tags": [ + "Events" + ], + "summary": "Listar eventos de webhook", + "description": "Los eventos del tenant de los últimos 30 días, del más nuevo al más viejo; cada uno es el sobre tal como se entregó (id, type, createdAt en UTC, personId y data). Filtra por type, relatedObjectId y el rango from..to (instantes UTC, ambos incluidos). 400: un tipo de evento que no existe, from posterior a to, un rango de más de 30 días o una página fuera de rango. 401: sin API key o JWT válidos. 403: la persona no tiene el permiso Webhooks.Manage.", + "operationId": "listEvents", + "parameters": [ + { + "name": "Type", + "in": "query", + "description": "Solo los eventos de este tipo (p. ej. invoice.canceled o webhook.test).", + "schema": { + "type": "string" + } + }, + { + "name": "RelatedObjectId", + "in": "query", + "description": "Solo los eventos de este recurso (data.relatedObject.id).", + "schema": { + "type": "string" + } + }, + { + "name": "From", + "in": "query", + "description": "Desde este instante, incluido, en ISO-8601 UTC (p. ej. 2026-10-01T00:00:00Z). Junto con to (o hasta ahora si se omite) no puede abarcar más de 30 días.", + "schema": { + "type": "string", + "format": "date-time" + } + }, + { + "name": "To", + "in": "query", + "description": "Hasta este instante, incluido, en ISO-8601 UTC. Por omisión, ahora.", + "schema": { + "type": "string", + "format": "date-time" + } + }, + { + "name": "PageNumber", + "in": "query", + "description": "Número de página, desde 1 (por omisión 1).", + "schema": { + "pattern": "^-?(?:0|[1-9]\\d*)$", + "type": [ + "integer", + "string" + ], + "format": "int32" + } + }, + { + "name": "PageSize", + "in": "query", + "description": "Eventos por página, de 1 a 50 (por omisión 10).", + "schema": { + "pattern": "^-?(?:0|[1-9]\\d*)$", + "type": [ + "integer", + "string" + ], + "format": "int32" + } + }, + { + "name": "X-TIME-ZONE", + "in": "header", + "description": "Zona horaria IANA de las fechas sin zona del cuerpo JSON: el request las lee y la respuesta las escribe en ella. Si falta o no es válida, America/Mexico_City. No aplica a los campos que la operación documenta en UTC o en hora civil.", + "schema": { + "type": "string" + }, + "example": "America/Mexico_City" + } + ], + "responses": { + "200": { + "description": "OK", + "content": { + "text/plain": { + "schema": { + "$ref": "#/components/schemas/ApiResponseOfPagedListOfJsonElement" + } + }, + "application/json": { + "schema": { + "$ref": "#/components/schemas/ApiResponseOfPagedListOfJsonElement" + } + }, + "text/json": { + "schema": { + "$ref": "#/components/schemas/ApiResponseOfPagedListOfJsonElement" + } + } + } + }, + "400": { + "description": "Bad Request", + "content": { + "text/plain": { + "schema": { + "$ref": "#/components/schemas/ApiResponseOfObject" + } + }, + "application/json": { + "schema": { + "$ref": "#/components/schemas/ApiResponseOfObject" + } + }, + "text/json": { + "schema": { + "$ref": "#/components/schemas/ApiResponseOfObject" + } + } + } + }, + "401": { + "description": "Unauthorized", + "content": { + "text/plain": { + "schema": { + "$ref": "#/components/schemas/ApiResponseOfObject" + } + }, + "application/json": { + "schema": { + "$ref": "#/components/schemas/ApiResponseOfObject" + } + }, + "text/json": { + "schema": { + "$ref": "#/components/schemas/ApiResponseOfObject" + } + } + } + }, + "403": { + "description": "Forbidden", + "content": { + "text/plain": { + "schema": { + "$ref": "#/components/schemas/ApiResponseOfObject" + } + }, + "application/json": { + "schema": { + "$ref": "#/components/schemas/ApiResponseOfObject" + } + }, + "text/json": { + "schema": { + "$ref": "#/components/schemas/ApiResponseOfObject" + } + } + } + } + }, + "security": [ + { + "ApiKey": [ ], + "TenantKey": [ ] + } + ] + } + }, + "/api/v4/events/{id}": { + "get": { + "tags": [ + "Events" + ], + "summary": "Obtener un evento de webhook", + "description": "El sobre del evento tal como lo recibió el receptor, con createdAt en UTC sin importar X-TIME-ZONE. 401: sin API key o JWT válidos. 403: la persona no tiene el permiso Webhooks.Manage. 404: no existe un evento con ese id en el tenant, o tiene más de 30 días (la retención ya lo purgó o está por purgarlo).", + "operationId": "getEvent", + "parameters": [ + { + "name": "id", + "in": "path", + "description": "Id del evento: el webhook-id con el que se entregó.", + "required": true, + "schema": { + "type": "string" + } + }, + { + "name": "X-TIME-ZONE", + "in": "header", + "description": "Zona horaria IANA de las fechas sin zona del cuerpo JSON: el request las lee y la respuesta las escribe en ella. Si falta o no es válida, America/Mexico_City. No aplica a los campos que la operación documenta en UTC o en hora civil.", + "schema": { + "type": "string" + }, + "example": "America/Mexico_City" + } + ], + "responses": { + "200": { + "description": "OK", + "content": { + "text/plain": { + "schema": { + "$ref": "#/components/schemas/ApiResponseOfJsonElement" + } + }, + "application/json": { + "schema": { + "$ref": "#/components/schemas/ApiResponseOfJsonElement" + } + }, + "text/json": { + "schema": { + "$ref": "#/components/schemas/ApiResponseOfJsonElement" + } + } + } + }, + "401": { + "description": "Unauthorized", + "content": { + "text/plain": { + "schema": { + "$ref": "#/components/schemas/ApiResponseOfObject" + } + }, + "application/json": { + "schema": { + "$ref": "#/components/schemas/ApiResponseOfObject" + } + }, + "text/json": { + "schema": { + "$ref": "#/components/schemas/ApiResponseOfObject" + } + } + } + }, + "403": { + "description": "Forbidden", + "content": { + "text/plain": { + "schema": { + "$ref": "#/components/schemas/ApiResponseOfObject" + } + }, + "application/json": { + "schema": { + "$ref": "#/components/schemas/ApiResponseOfObject" + } + }, + "text/json": { + "schema": { + "$ref": "#/components/schemas/ApiResponseOfObject" + } + } + } + }, + "404": { + "description": "Not Found", + "content": { + "text/plain": { + "schema": { + "$ref": "#/components/schemas/ApiResponseOfObject" + } + }, + "application/json": { + "schema": { + "$ref": "#/components/schemas/ApiResponseOfObject" + } + }, + "text/json": { + "schema": { + "$ref": "#/components/schemas/ApiResponseOfObject" + } + } + } + } + }, + "security": [ + { + "ApiKey": [ ], + "TenantKey": [ ] + } + ] + } + }, + "/api/v4/events/{id}/resend": { + "post": { + "tags": [ + "Events" + ], + "summary": "Reenviar un evento de webhook", + "description": "Crea una entrega nueva del evento, firmada y de un solo intento (sin reintentos), al webhook webhookId o, si se omite (o sin cuerpo), a la escucha de CLI de la persona autenticada. La de una URL sale en segundo plano; la de la escucha la toma fiscalapi listen. 202: deliveryId y webhookId, para seguirla en el log de entregas. 400: un webhookId vacío (\"'Webhook Id' no debería estar vacío.\"); un campo que el cuerpo no admite (p. ej. webhookEndpointId), con el texto \"El campo '{field}' no existe en este recurso.\"; un webhook de URL deshabilitado ('El webhook está deshabilitado: habilítalo antes de reenviarle eventos.'). 401: sin API key o JWT válidos. 403: la persona no tiene el permiso Webhooks.Manage. 404: no existe un evento con ese id en el tenant, o tiene más de 30 días (la retención ya lo purgó o está por purgarlo). También 404 si el webhook no es del tenant o es la escucha de CLI de otra persona (salvo para el owner). 409: la escucha de CLI no tiene una sesión vigente de fiscalapi listen. 429: demasiados reenvíos con la misma API key.", + "operationId": "resendEvent", + "parameters": [ + { + "name": "id", + "in": "path", + "description": "Id del evento: el webhook-id con el que se entregó.", + "required": true, + "schema": { + "type": "string" + } + }, + { + "name": "X-TIME-ZONE", + "in": "header", + "description": "Zona horaria IANA de las fechas sin zona del cuerpo JSON: el request las lee y la respuesta las escribe en ella. Si falta o no es válida, America/Mexico_City. No aplica a los campos que la operación documenta en UTC o en hora civil.", + "schema": { + "type": "string" + }, + "example": "America/Mexico_City" + } + ], + "requestBody": { + "content": { + "application/json": { + "schema": { + "oneOf": [ + { + "type": "null" + }, + { + "$ref": "#/components/schemas/ResendWebhookEventCommand" + } + ] + } + }, + "text/json": { + "schema": { + "oneOf": [ + { + "type": "null" + }, + { + "$ref": "#/components/schemas/ResendWebhookEventCommand" + } + ] + } + }, + "application/*+json": { + "schema": { + "oneOf": [ + { + "type": "null" + }, + { + "$ref": "#/components/schemas/ResendWebhookEventCommand" + } + ] + } + } + } + }, + "responses": { + "202": { + "description": "Accepted", + "content": { + "text/plain": { + "schema": { + "$ref": "#/components/schemas/ApiResponseOfWebhookEventResendDto" + } + }, + "application/json": { + "schema": { + "$ref": "#/components/schemas/ApiResponseOfWebhookEventResendDto" + } + }, + "text/json": { + "schema": { + "$ref": "#/components/schemas/ApiResponseOfWebhookEventResendDto" + } + } + } + }, + "400": { + "description": "Bad Request", + "content": { + "text/plain": { + "schema": { + "$ref": "#/components/schemas/ApiResponseOfObject" + } + }, + "application/json": { + "schema": { + "$ref": "#/components/schemas/ApiResponseOfObject" + } + }, + "text/json": { + "schema": { + "$ref": "#/components/schemas/ApiResponseOfObject" + } + } + } + }, + "401": { + "description": "Unauthorized", + "content": { + "text/plain": { + "schema": { + "$ref": "#/components/schemas/ApiResponseOfObject" + } + }, + "application/json": { + "schema": { + "$ref": "#/components/schemas/ApiResponseOfObject" + } + }, + "text/json": { + "schema": { + "$ref": "#/components/schemas/ApiResponseOfObject" + } + } + } + }, + "403": { + "description": "Forbidden", + "content": { + "text/plain": { + "schema": { + "$ref": "#/components/schemas/ApiResponseOfObject" + } + }, + "application/json": { + "schema": { + "$ref": "#/components/schemas/ApiResponseOfObject" + } + }, + "text/json": { + "schema": { + "$ref": "#/components/schemas/ApiResponseOfObject" + } + } + } + }, + "404": { + "description": "Not Found", + "content": { + "text/plain": { + "schema": { + "$ref": "#/components/schemas/ApiResponseOfObject" + } + }, + "application/json": { + "schema": { + "$ref": "#/components/schemas/ApiResponseOfObject" + } + }, + "text/json": { + "schema": { + "$ref": "#/components/schemas/ApiResponseOfObject" + } + } + } + }, + "409": { + "description": "Conflict", + "content": { + "text/plain": { + "schema": { + "$ref": "#/components/schemas/ApiResponseOfObject" + } + }, + "application/json": { + "schema": { + "$ref": "#/components/schemas/ApiResponseOfObject" + } + }, + "text/json": { + "schema": { + "$ref": "#/components/schemas/ApiResponseOfObject" + } + } + } + }, + "429": { + "description": "Too Many Requests", + "content": { + "text/plain": { + "schema": { + "$ref": "#/components/schemas/ApiResponseOfObject" + } + }, + "application/json": { + "schema": { + "$ref": "#/components/schemas/ApiResponseOfObject" + } + }, + "text/json": { + "schema": { + "$ref": "#/components/schemas/ApiResponseOfObject" + } + } + } + } + }, + "security": [ + { + "ApiKey": [ ], + "TenantKey": [ ] + } + ] + } + }, + "/api/v4/events/trigger": { + "post": { + "tags": [ + "Events" + ], + "summary": "Disparar un evento de webhook de prueba", + "description": "Publica un evento del tipo pedido con la forma de uno real (el recurso y los estados de su tipo) y lo entrega solo al webhook webhookId o, si se omite, a la escucha de CLI de la persona autenticada (fiscalapi trigger); los demás webhooks no lo reciben. Como el reenvío, no filtra por las suscripciones (enabledEvents) ni las personas (personIds) del destino. La entrega es una prueba de un solo intento, sin reintentos. Sin relatedObjectId, el recurso es un id sintético cuyo GET responde 404; sin personId, el evento va sin persona. 202: eventId y webhookId, para seguirlo en GET /events/{id} y en el log de entregas. 400: el cuerpo vacío ('El cuerpo de la petición es requerido y no debe estar vacío.'); un type omitido o que no existe (\"El tipo de evento '{type}' no existe.\"); un relatedObjectId vacío (\"'Related Object Id' no debería estar vacío.\") o de más de 36 caracteres (\"'Related Object Id' debe ser menor o igual que 36 caracteres. Ingresó {n} caracteres.\"); un webhookId vacío (\"'Webhook Id' no debería estar vacío.\"); un campo que el cuerpo no admite (p. ej. webhookEndpointId), con el texto \"El campo '{field}' no existe en este recurso.\"; una persona que no es del tenant ('La persona especificada no es válida o no pertenece a tu organización.'); un webhook de URL deshabilitado ('El webhook está deshabilitado: habilítalo antes de probarlo.'). 401: sin API key o JWT válidos. 403: la persona no tiene el permiso Webhooks.Manage. También 403 fuera de los ambientes de prueba (solo Development y Test): 'fiscalapi trigger no está disponible en producción; usa el ambiente de pruebas.'. 404 ('No such resource for the given Id: {id}'): el webhook no existe, está dado de baja, es de otro tenant o es la escucha de CLI de otra persona (salvo para el owner). 409 ('La escucha de CLI no está conectada; ejecuta fiscalapi listen.'): la escucha de CLI no tiene una sesión vigente de fiscalapi listen, o quien llama no tiene escucha. 429: demasiadas peticiones con la misma API key.", + "operationId": "triggerEvent", + "parameters": [ + { + "name": "X-TIME-ZONE", + "in": "header", + "description": "Zona horaria IANA de las fechas sin zona del cuerpo JSON: el request las lee y la respuesta las escribe en ella. Si falta o no es válida, America/Mexico_City. No aplica a los campos que la operación documenta en UTC o en hora civil.", + "schema": { + "type": "string" + }, + "example": "America/Mexico_City" + } + ], + "requestBody": { + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/TriggerWebhookEventCommand" + } + }, + "text/json": { + "schema": { + "$ref": "#/components/schemas/TriggerWebhookEventCommand" + } + }, + "application/*+json": { + "schema": { + "$ref": "#/components/schemas/TriggerWebhookEventCommand" + } + } + }, + "required": true + }, + "responses": { + "202": { + "description": "Accepted", + "content": { + "text/plain": { + "schema": { + "$ref": "#/components/schemas/ApiResponseOfWebhookEventTriggerDto" + } + }, + "application/json": { + "schema": { + "$ref": "#/components/schemas/ApiResponseOfWebhookEventTriggerDto" + } + }, + "text/json": { + "schema": { + "$ref": "#/components/schemas/ApiResponseOfWebhookEventTriggerDto" + } + } + } + }, + "400": { + "description": "Bad Request", + "content": { + "text/plain": { + "schema": { + "$ref": "#/components/schemas/ApiResponseOfObject" + } + }, + "application/json": { + "schema": { + "$ref": "#/components/schemas/ApiResponseOfObject" + } + }, + "text/json": { + "schema": { + "$ref": "#/components/schemas/ApiResponseOfObject" + } + } + } + }, + "401": { + "description": "Unauthorized", + "content": { + "text/plain": { + "schema": { + "$ref": "#/components/schemas/ApiResponseOfObject" + } + }, + "application/json": { + "schema": { + "$ref": "#/components/schemas/ApiResponseOfObject" + } + }, + "text/json": { + "schema": { + "$ref": "#/components/schemas/ApiResponseOfObject" + } + } + } + }, + "403": { + "description": "Forbidden", + "content": { + "text/plain": { + "schema": { + "$ref": "#/components/schemas/ApiResponseOfObject" + } + }, + "application/json": { + "schema": { + "$ref": "#/components/schemas/ApiResponseOfObject" + } + }, + "text/json": { + "schema": { + "$ref": "#/components/schemas/ApiResponseOfObject" + } + } + } + }, + "404": { + "description": "Not Found", + "content": { + "text/plain": { + "schema": { + "$ref": "#/components/schemas/ApiResponseOfObject" + } + }, + "application/json": { + "schema": { + "$ref": "#/components/schemas/ApiResponseOfObject" + } + }, + "text/json": { + "schema": { + "$ref": "#/components/schemas/ApiResponseOfObject" + } + } + } + }, + "409": { + "description": "Conflict", + "content": { + "text/plain": { + "schema": { + "$ref": "#/components/schemas/ApiResponseOfObject" + } + }, + "application/json": { + "schema": { + "$ref": "#/components/schemas/ApiResponseOfObject" + } + }, + "text/json": { + "schema": { + "$ref": "#/components/schemas/ApiResponseOfObject" + } + } + } + }, + "429": { + "description": "Too Many Requests", + "content": { + "text/plain": { + "schema": { + "$ref": "#/components/schemas/ApiResponseOfObject" + } + }, + "application/json": { + "schema": { + "$ref": "#/components/schemas/ApiResponseOfObject" + } + }, + "text/json": { + "schema": { + "$ref": "#/components/schemas/ApiResponseOfObject" + } + } + } + } + }, + "security": [ + { + "ApiKey": [ ], + "TenantKey": [ ] + } + ] + } + }, + "/api/v4/invoices": { + "get": { + "tags": [ + "Invoices" + ], + "summary": "Listar facturas", + "description": "Las facturas (CFDI) del tenant, paginadas con pageNumber y pageSize (10 por omisión, máximo 50) y ordenadas de la más antigua a la más reciente. Docs: [por valores](https://docs.fiscalapi.com/invoices-by-value#listar-facturas) y [por referencias](https://docs.fiscalapi.com/invoices-by-reference#listar-facturas).", + "operationId": "listInvoices", + "parameters": [ + { + "name": "PageNumber", + "in": "query", + "schema": { + "pattern": "^-?(?:0|[1-9]\\d*)$", + "type": [ + "integer", + "string" + ], + "format": "int32" + } + }, + { + "name": "PageSize", + "in": "query", + "schema": { + "pattern": "^-?(?:0|[1-9]\\d*)$", + "type": [ + "integer", + "string" + ], + "format": "int32" + } + }, + { + "name": "X-TIME-ZONE", + "in": "header", + "description": "Zona horaria IANA de las fechas sin zona del cuerpo JSON: el request las lee y la respuesta las escribe en ella. Si falta o no es válida, America/Mexico_City. No aplica a los campos que la operación documenta en UTC o en hora civil.", + "schema": { + "type": "string" + }, + "example": "America/Mexico_City" + } + ], + "responses": { + "200": { + "description": "OK" + } + }, + "security": [ + { + "ApiKey": [ ], + "TenantKey": [ ] + } + ] + }, + "post": { + "tags": [ + "Invoices" + ], + "summary": "Emitir (timbrar) una factura CFDI 4.0", + "description": "Timbra el CFDI ante el PAC y cobra 1 timbre del saldo; si el PAC lo rechaza, el timbre se reembolsa. Sin saldo, responde 403. typeCode y complement deciden el comprobante: factura de ingreso (I), nota de crédito o egreso (E), complemento de pago (P), recibo de nómina (N) o traslado (T), con carta porte o comercio exterior; globalInformation la vuelve factura global. Por valores, el cuerpo lleva los datos completos del emisor y del receptor; por referencias, issuer.id y recipient.id de personas ya registradas. Docs: [por valores](https://docs.fiscalapi.com/invoices-by-value#crear-factura-ingreso) y [por referencias](https://docs.fiscalapi.com/invoices-by-reference#crear-factura-ingreso).", + "operationId": "createInvoice", + "parameters": [ + { + "name": "X-TIME-ZONE", + "in": "header", + "description": "Zona horaria IANA de las fechas sin zona del cuerpo JSON: el request las lee y la respuesta las escribe en ella. Si falta o no es válida, America/Mexico_City. No aplica a los campos que la operación documenta en UTC o en hora civil.", + "schema": { + "type": "string" + }, + "example": "America/Mexico_City" + } + ], + "requestBody": { + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/InvoiceRequest" + } + }, + "text/json": { + "schema": { + "$ref": "#/components/schemas/InvoiceRequest" + } + }, + "application/*+json": { + "schema": { + "$ref": "#/components/schemas/InvoiceRequest" + } + } + }, + "required": true + }, + "responses": { + "200": { + "description": "OK" + } + }, + "security": [ + { + "ApiKey": [ ], + "TenantKey": [ ] + } + ] + }, + "delete": { + "tags": [ + "Invoices" + ], + "summary": "Cancelar una factura ante el SAT", + "description": "Solicita al SAT, por medio del PAC, la cancelación del CFDI con el motivo en cancellationReasonCode (01, 02, 03 o 04); el motivo 01 exige replacementUuid, el UUID de la factura que la sustituye. Por referencias basta el id; por valores van invoiceUuid, tin y taxCredentials (el CSD del emisor). La respuesta no confirma la cancelación: el SAT puede esperar la aceptación del receptor, así que consulta después su estatus. Docs: [por valores](https://docs.fiscalapi.com/invoices-by-value#cancelar-factura) y [por referencias](https://docs.fiscalapi.com/invoices-by-reference#cancelar-factura).", + "operationId": "cancelInvoice", + "parameters": [ + { + "name": "X-TIME-ZONE", + "in": "header", + "description": "Zona horaria IANA de las fechas sin zona del cuerpo JSON: el request las lee y la respuesta las escribe en ella. Si falta o no es válida, America/Mexico_City. No aplica a los campos que la operación documenta en UTC o en hora civil.", + "schema": { + "type": "string" + }, + "example": "America/Mexico_City" + } + ], + "requestBody": { + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/CancelInvoiceCommand" + } + }, + "text/json": { + "schema": { + "$ref": "#/components/schemas/CancelInvoiceCommand" + } + }, + "application/*+json": { + "schema": { + "$ref": "#/components/schemas/CancelInvoiceCommand" + } + } + }, + "required": true + }, + "responses": { + "200": { + "description": "OK" + } + }, + "security": [ + { + "ApiKey": [ ], + "TenantKey": [ ] + } + ] + } + }, + "/api/v4/invoices/{id}": { + "get": { + "tags": [ + "Invoices" + ], + "summary": "Obtener una factura", + "description": "Una factura por su id. Con details=true incluye el estado, el emisor, el receptor y los conceptos con sus impuestos. Docs: [por valores](https://docs.fiscalapi.com/invoices-by-value#obtener-factura-por-id) y [por referencias](https://docs.fiscalapi.com/invoices-by-reference#obtener-factura-por-id).", + "operationId": "getInvoice", + "parameters": [ + { + "name": "id", + "in": "path", + "required": true, + "schema": { + "type": "string" + } + }, + { + "name": "details", + "in": "query", + "schema": { + "type": "boolean", + "default": false + } + }, + { + "name": "X-TIME-ZONE", + "in": "header", + "description": "Zona horaria IANA de las fechas sin zona del cuerpo JSON: el request las lee y la respuesta las escribe en ella. Si falta o no es válida, America/Mexico_City. No aplica a los campos que la operación documenta en UTC o en hora civil.", + "schema": { + "type": "string" + }, + "example": "America/Mexico_City" + } + ], + "responses": { + "200": { + "description": "OK" + } + }, + "security": [ + { + "ApiKey": [ ], + "TenantKey": [ ] + } + ] + } + }, + "/api/v4/invoices/calculate": { + "post": { + "tags": [ + "Invoices" + ], + "summary": "Calcular los totales de una factura sin timbrar", + "description": "Con el mismo cuerpo que la emisión, responde subTotal, discount y total sin timbrar, sin cobrar timbres y sin consumir folio: sirve para previsualizar los importes antes de emitir. Docs: [por valores](https://docs.fiscalapi.com/invoices-by-value#modelo-factura) y [por referencias](https://docs.fiscalapi.com/invoices-by-reference#modelo-factura).", + "operationId": "calculateInvoice", + "parameters": [ + { + "name": "X-TIME-ZONE", + "in": "header", + "description": "Zona horaria IANA de las fechas sin zona del cuerpo JSON: el request las lee y la respuesta las escribe en ella. Si falta o no es válida, America/Mexico_City. No aplica a los campos que la operación documenta en UTC o en hora civil.", + "schema": { + "type": "string" + }, + "example": "America/Mexico_City" + } + ], + "requestBody": { + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/InvoiceRequest" + } + }, + "text/json": { + "schema": { + "$ref": "#/components/schemas/InvoiceRequest" + } + }, + "application/*+json": { + "schema": { + "$ref": "#/components/schemas/InvoiceRequest" + } + } + }, + "required": true + }, + "responses": { + "200": { + "description": "OK" + } + }, + "security": [ + { + "ApiKey": [ ], + "TenantKey": [ ] + } + ] + } + }, + "/api/v4/invoices/pdf": { + "post": { + "tags": [ + "Invoices" + ], + "summary": "Generar el PDF de una factura", + "description": "La representación impresa de una factura ya emitida, en base64 (base64File). Admite base64Logo, bandColor y fontColor para personalizarla. No timbra ni cobra timbres. Docs: [por valores](https://docs.fiscalapi.com/invoices-by-value#generar-pdf-por-id) y [por referencias](https://docs.fiscalapi.com/invoices-by-reference#generar-pdf-por-id).", + "operationId": "getInvoicePdf", + "parameters": [ + { + "name": "X-TIME-ZONE", + "in": "header", + "description": "Zona horaria IANA de las fechas sin zona del cuerpo JSON: el request las lee y la respuesta las escribe en ella. Si falta o no es válida, America/Mexico_City. No aplica a los campos que la operación documenta en UTC o en hora civil.", + "schema": { + "type": "string" + }, + "example": "America/Mexico_City" + } + ], + "requestBody": { + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/CreatePdfCommand" + } + }, + "text/json": { + "schema": { + "$ref": "#/components/schemas/CreatePdfCommand" + } + }, + "application/*+json": { + "schema": { + "$ref": "#/components/schemas/CreatePdfCommand" + } + } + }, + "required": true + }, + "responses": { + "200": { + "description": "OK" + } + }, + "security": [ + { + "ApiKey": [ ], + "TenantKey": [ ] + } + ] + } + }, + "/api/v4/invoices/{id}/xml": { + "get": { + "tags": [ + "Invoices" + ], + "summary": "Descargar el XML timbrado de una factura", + "description": "El XML del CFDI timbrado de una factura, en base64 (base64File). Docs: [por valores](https://docs.fiscalapi.com/invoices-by-value#obtener-xml-por-id) y [por referencias](https://docs.fiscalapi.com/invoices-by-reference#obtener-xml-por-id).", + "operationId": "getInvoiceXml", + "parameters": [ + { + "name": "id", + "in": "path", + "required": true, + "schema": { + "type": "string" + } + }, + { + "name": "X-TIME-ZONE", + "in": "header", + "description": "Zona horaria IANA de las fechas sin zona del cuerpo JSON: el request las lee y la respuesta las escribe en ella. Si falta o no es válida, America/Mexico_City. No aplica a los campos que la operación documenta en UTC o en hora civil.", + "schema": { + "type": "string" + }, + "example": "America/Mexico_City" + } + ], + "responses": { + "200": { + "description": "OK" + } + }, + "security": [ + { + "ApiKey": [ ], + "TenantKey": [ ] + } + ] + } + }, + "/api/v4/invoices/send": { + "post": { + "tags": [ + "Invoices" + ], + "summary": "Enviar una factura por correo", + "description": "Envía el PDF y el XML de la factura a toEmail o, si falta, al correo del receptor. Admite el mismo logo y colores que el PDF. El envío se programa y puede tardar unos minutos. Docs: [por valores](https://docs.fiscalapi.com/invoices-by-value#enviar-factura-por-correo) y [por referencias](https://docs.fiscalapi.com/invoices-by-reference#enviar-factura-por-correo).", + "operationId": "sendInvoice", + "parameters": [ + { + "name": "X-TIME-ZONE", + "in": "header", + "description": "Zona horaria IANA de las fechas sin zona del cuerpo JSON: el request las lee y la respuesta las escribe en ella. Si falta o no es válida, America/Mexico_City. No aplica a los campos que la operación documenta en UTC o en hora civil.", + "schema": { + "type": "string" + }, + "example": "America/Mexico_City" + } + ], + "requestBody": { + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/SendInvoiceByEmailCommand" + } + }, + "text/json": { + "schema": { + "$ref": "#/components/schemas/SendInvoiceByEmailCommand" + } + }, + "application/*+json": { + "schema": { + "$ref": "#/components/schemas/SendInvoiceByEmailCommand" + } + } + }, + "required": true + }, + "responses": { + "200": { + "description": "OK" + } + }, + "security": [ + { + "ApiKey": [ ], + "TenantKey": [ ] + } + ] + } + }, + "/api/v4/invoices/status": { + "post": { + "tags": [ + "Invoices" + ], + "summary": "Consultar el estatus de una factura ante el SAT", + "description": "Consulta en el SAT si el CFDI está vigente, cancelado o no encontrado, si es cancelable y el avance de su cancelación. Identifica la factura con su id o con issuerTin, recipientTin, invoiceTotal, invoiceUuid y last8DigitsIssuerSignature, no con ambos. En el ambiente de pruebas el SAT se simula y details lo avisa. Docs: [por valores](https://docs.fiscalapi.com/invoices-by-value#consultar-estado-de-factura) y [por referencias](https://docs.fiscalapi.com/invoices-by-reference#consultar-estado-de-factura).", + "operationId": "getInvoiceStatus", + "parameters": [ + { + "name": "X-TIME-ZONE", + "in": "header", + "description": "Zona horaria IANA de las fechas sin zona del cuerpo JSON: el request las lee y la respuesta las escribe en ella. Si falta o no es válida, America/Mexico_City. No aplica a los campos que la operación documenta en UTC o en hora civil.", + "schema": { + "type": "string" + }, + "example": "America/Mexico_City" + } + ], + "requestBody": { + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/StatusCheckCommand" + } + }, + "text/json": { + "schema": { + "$ref": "#/components/schemas/StatusCheckCommand" + } + }, + "application/*+json": { + "schema": { + "$ref": "#/components/schemas/StatusCheckCommand" + } + } + }, + "required": true + }, + "responses": { + "200": { + "description": "OK" + } + }, + "security": [ + { + "ApiKey": [ ], + "TenantKey": [ ] + } + ] + } + }, + "/api/v4/manifests": { + "post": { + "tags": [ + "Manifests" + ], + "summary": "Firmar la carta manifiesto con la FIEL", + "description": "Firma con el PAC la carta manifiesto del RFC de la FIEL que envías en base64Cer, base64Key y password (obligatorios; cada archivo hasta 24,576 caracteres en base64 y la contraseña hasta 256) y devuelve el PDF firmado en base64File; fileName es el RFC con extensión .pdf. Todas las personas del tenant con ese RFC quedan con manifestStatusId Signed. Responde 400 si la contraseña no abre la llave o la credencial no es una FIEL vigente, y 404 si ninguna persona del tenant tiene ese RFC. Docs: [firmar carta manifiesto](https://docs.fiscalapi.com/manifests#firmar-carta-manifiesto) y [límites](https://docs.fiscalapi.com/manifests#limites).", + "operationId": "signManifest", + "parameters": [ + { + "name": "X-TIME-ZONE", + "in": "header", + "description": "Zona horaria IANA de las fechas sin zona del cuerpo JSON: el request las lee y la respuesta las escribe en ella. Si falta o no es válida, America/Mexico_City. No aplica a los campos que la operación documenta en UTC o en hora civil.", + "schema": { + "type": "string" + }, + "example": "America/Mexico_City" + } + ], + "requestBody": { + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/SignManifestCommand" + } + }, + "text/json": { + "schema": { + "$ref": "#/components/schemas/SignManifestCommand" + } + }, + "application/*+json": { + "schema": { + "$ref": "#/components/schemas/SignManifestCommand" + } + } + }, + "required": true + }, + "responses": { + "200": { + "description": "OK" + } + }, + "security": [ + { + "ApiKey": [ ], + "TenantKey": [ ] + } + ] + } + }, + "/api/v4/people": { + "get": { + "tags": [ + "People" + ], + "summary": "Listar personas: emisores, receptores, empleados y empleadores", + "description": "Todas las personas del tenant en una sola lista, paginadas con pageNumber y pageSize (50 por omisión y máximo). La misma persona puede ser emisor, cliente receptor, empleado o empleador según dónde se use su id. Docs: [emisores](https://docs.fiscalapi.com/issuers#listar-emisores) y [receptores](https://docs.fiscalapi.com/recipients#listar-receptores).", + "operationId": "listPeople", + "parameters": [ + { + "name": "PageNumber", + "in": "query", + "schema": { + "pattern": "^-?(?:0|[1-9]\\d*)$", + "type": [ + "integer", + "string" + ], + "format": "int32" + } + }, + { + "name": "PageSize", + "in": "query", + "schema": { + "pattern": "^-?(?:0|[1-9]\\d*)$", + "type": [ + "integer", + "string" + ], + "format": "int32" + } + }, + { + "name": "X-TIME-ZONE", + "in": "header", + "description": "Zona horaria IANA de las fechas sin zona del cuerpo JSON: el request las lee y la respuesta las escribe en ella. Si falta o no es válida, America/Mexico_City. No aplica a los campos que la operación documenta en UTC o en hora civil.", + "schema": { + "type": "string" + }, + "example": "America/Mexico_City" + } + ], + "responses": { + "200": { + "description": "OK" + } + }, + "security": [ + { + "ApiKey": [ ], + "TenantKey": [ ] + } + ] + }, + "post": { + "tags": [ + "People" + ], + "summary": "Crear una persona: emisor o cliente receptor", + "description": "Da de alta una persona para usar su id como emisor (issuer.id) o cliente receptor (recipient.id) al facturar por referencias, o como empleado o empleador en nómina. legalName, email y password son obligatorios y el correo no debe estar registrado; los datos fiscales van en tin, satTaxRegimeId, satCfdiUseId y zipCode. Docs: [emisores](https://docs.fiscalapi.com/issuers#crear-emisor) y [receptores](https://docs.fiscalapi.com/recipients#crear-receptor).", + "operationId": "createPerson", + "parameters": [ + { + "name": "X-TIME-ZONE", + "in": "header", + "description": "Zona horaria IANA de las fechas sin zona del cuerpo JSON: el request las lee y la respuesta las escribe en ella. Si falta o no es válida, America/Mexico_City. No aplica a los campos que la operación documenta en UTC o en hora civil.", + "schema": { + "type": "string" + }, + "example": "America/Mexico_City" + } + ], + "requestBody": { + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/CreateUserCommand" + } + }, + "text/json": { + "schema": { + "$ref": "#/components/schemas/CreateUserCommand" + } + }, + "application/*+json": { + "schema": { + "$ref": "#/components/schemas/CreateUserCommand" + } + } + }, + "required": true + }, + "responses": { + "200": { + "description": "OK" + } + }, + "security": [ + { + "ApiKey": [ ], + "TenantKey": [ ] + } + ] + } + }, + "/api/v4/people/{id}": { + "get": { + "tags": [ + "People" + ], + "summary": "Obtener una persona", + "description": "Una persona (emisor, cliente receptor, empleado o empleador) por su id, con sus datos fiscales: RFC (tin), régimen fiscal, uso de CFDI y código postal. Docs: [emisores](https://docs.fiscalapi.com/issuers#obtener-emisor-por-id) y [receptores](https://docs.fiscalapi.com/recipients#obtener-receptor-por-id).", + "operationId": "getPerson", + "parameters": [ + { + "name": "id", + "in": "path", + "required": true, + "schema": { + "type": "string" + } + }, + { + "name": "details", + "in": "query", + "schema": { + "type": "boolean", + "default": false + } + }, + { + "name": "X-TIME-ZONE", + "in": "header", + "description": "Zona horaria IANA de las fechas sin zona del cuerpo JSON: el request las lee y la respuesta las escribe en ella. Si falta o no es válida, America/Mexico_City. No aplica a los campos que la operación documenta en UTC o en hora civil.", + "schema": { + "type": "string" + }, + "example": "America/Mexico_City" + } + ], + "responses": { + "200": { + "description": "OK" + } + }, + "security": [ + { + "ApiKey": [ ], + "TenantKey": [ ] + } + ] + }, + "patch": { + "tags": [ + "People" + ], + "summary": "Actualizar parcialmente una persona", + "description": "Cambia solo los campos que vienen: un campo omitido o null no se toca y \"\" lo borra (legalName, email y password no se pueden borrar). El id de la ruta y el del cuerpo deben coincidir; un cambio concurrente responde 409. Docs: [emisores](https://docs.fiscalapi.com/issuers#actualizar-emisor) y [receptores](https://docs.fiscalapi.com/recipients#actualizar-receptor).", + "operationId": "patchPerson", + "parameters": [ + { + "name": "id", + "in": "path", + "required": true, + "schema": { + "type": "string" + } + }, + { + "name": "X-TIME-ZONE", + "in": "header", + "description": "Zona horaria IANA de las fechas sin zona del cuerpo JSON: el request las lee y la respuesta las escribe en ella. Si falta o no es válida, America/Mexico_City. No aplica a los campos que la operación documenta en UTC o en hora civil.", + "schema": { + "type": "string" + }, + "example": "America/Mexico_City" + } + ], + "requestBody": { + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/PatchUserCommand" + } + }, + "text/json": { + "schema": { + "$ref": "#/components/schemas/PatchUserCommand" + } + }, + "application/*+json": { + "schema": { + "$ref": "#/components/schemas/PatchUserCommand" + } + } + }, + "required": true + }, + "responses": { + "200": { + "description": "OK" + } + }, + "security": [ + { + "ApiKey": [ ], + "TenantKey": [ ] + } + ] + }, + "put": { + "tags": [ + "People" + ], + "summary": "Actualizar una persona", + "description": "Reemplaza los datos de la persona con el cuerpo completo: un campo omitido se borra (solo userTypeId y taxPassword se conservan). legalName y email son obligatorios. Para cambiar solo algunos campos usa la actualización parcial (PATCH). El id de la ruta y el del cuerpo deben coincidir; un cambio concurrente responde 409. Docs: [emisores](https://docs.fiscalapi.com/issuers#actualizar-emisor) y [receptores](https://docs.fiscalapi.com/recipients#actualizar-receptor).", + "operationId": "updatePerson", + "parameters": [ + { + "name": "id", + "in": "path", + "required": true, + "schema": { + "type": "string" + } + }, + { + "name": "X-TIME-ZONE", + "in": "header", + "description": "Zona horaria IANA de las fechas sin zona del cuerpo JSON: el request las lee y la respuesta las escribe en ella. Si falta o no es válida, America/Mexico_City. No aplica a los campos que la operación documenta en UTC o en hora civil.", + "schema": { + "type": "string" + }, + "example": "America/Mexico_City" + } + ], + "requestBody": { + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/UpdateUserCommand" + } + }, + "text/json": { + "schema": { + "$ref": "#/components/schemas/UpdateUserCommand" + } + }, + "application/*+json": { + "schema": { + "$ref": "#/components/schemas/UpdateUserCommand" + } + } + }, + "required": true + }, + "responses": { + "200": { + "description": "OK" + } + }, + "security": [ + { + "ApiKey": [ ], + "TenantKey": [ ] + } + ] + }, + "delete": { + "tags": [ + "People" + ], + "summary": "Eliminar una persona", + "description": "Da de baja la persona, libera su correo y revoca sus API keys. Valida en este orden: 400 si tiene reglas de descarga aprobadas o certificados (elimínalos primero) y 403 si es el owner, que nunca se puede eliminar. Docs: [emisores](https://docs.fiscalapi.com/issuers#eliminar-emisor) y [receptores](https://docs.fiscalapi.com/recipients#eliminar-receptor).", + "operationId": "deletePerson", + "parameters": [ + { + "name": "id", + "in": "path", + "required": true, + "schema": { + "type": "string" + } + }, + { + "name": "X-TIME-ZONE", + "in": "header", + "description": "Zona horaria IANA de las fechas sin zona del cuerpo JSON: el request las lee y la respuesta las escribe en ella. Si falta o no es válida, America/Mexico_City. No aplica a los campos que la operación documenta en UTC o en hora civil.", + "schema": { + "type": "string" + }, + "example": "America/Mexico_City" + } + ], + "responses": { + "200": { + "description": "OK" + } + }, + "security": [ + { + "ApiKey": [ ], + "TenantKey": [ ] + } + ] + } + }, + "/api/v4/people/{personId}/employee": { + "get": { + "tags": [ + "People" + ], + "summary": "Obtener los datos de empleado de una persona", + "description": "Los datos del empleado que usa el recibo de nómina: número de empleado, tipo de contrato, régimen, periodicidad de pago, seguridad social y salarios, entre otros. Responde 404 si la persona no los tiene. Docs: [datos de empleado](https://docs.fiscalapi.com/employee-data#obtener-datos-de-empleado).", + "operationId": "getEmployeeData", + "parameters": [ + { + "name": "personId", + "in": "path", + "required": true, + "schema": { + "type": "string" + } + }, + { + "name": "X-TIME-ZONE", + "in": "header", + "description": "Zona horaria IANA de las fechas sin zona del cuerpo JSON: el request las lee y la respuesta las escribe en ella. Si falta o no es válida, America/Mexico_City. No aplica a los campos que la operación documenta en UTC o en hora civil.", + "schema": { + "type": "string" + }, + "example": "America/Mexico_City" + } + ], + "responses": { + "200": { + "description": "OK" + } + }, + "security": [ + { + "ApiKey": [ ], + "TenantKey": [ ] + } + ] + }, + "post": { + "tags": [ + "People" + ], + "summary": "Crear los datos de empleado de una persona", + "description": "Registra los datos del empleado para el recibo de nómina. Son obligatorios employerPersonId (la persona empleadora), employeeNumber, satContractTypeId, satTaxRegimeTypeId, satPaymentPeriodicityId y satPayrollStateId; employeePersonId es el personId de la ruta. Cada persona tiene un solo registro: si ya existe, responde 400 y se modifica con PUT. Docs: [datos de empleado](https://docs.fiscalapi.com/employee-data#crear-datos-de-empleado).", + "operationId": "createEmployeeData", + "parameters": [ + { + "name": "personId", + "in": "path", + "required": true, + "schema": { + "type": "string" + } + }, + { + "name": "X-TIME-ZONE", + "in": "header", + "description": "Zona horaria IANA de las fechas sin zona del cuerpo JSON: el request las lee y la respuesta las escribe en ella. Si falta o no es válida, America/Mexico_City. No aplica a los campos que la operación documenta en UTC o en hora civil.", + "schema": { + "type": "string" + }, + "example": "America/Mexico_City" + } + ], + "requestBody": { + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/CreateEmployeeDataCommand" + } + }, + "text/json": { + "schema": { + "$ref": "#/components/schemas/CreateEmployeeDataCommand" + } + }, + "application/*+json": { + "schema": { + "$ref": "#/components/schemas/CreateEmployeeDataCommand" + } + } + }, + "required": true + }, + "responses": { + "200": { + "description": "OK" + } + }, + "security": [ + { + "ApiKey": [ ], + "TenantKey": [ ] + } + ] + }, + "put": { + "tags": [ + "People" + ], + "summary": "Actualizar los datos de empleado de una persona", + "description": "Actualiza los datos del empleado para el recibo de nómina; employeePersonId es el personId de la ruta. Responde 404 si la persona no tiene datos de empleado. Docs: [datos de empleado](https://docs.fiscalapi.com/employee-data#actualizar-datos-de-empleado).", + "operationId": "updateEmployeeData", + "parameters": [ + { + "name": "personId", + "in": "path", + "required": true, + "schema": { + "type": "string" + } + }, + { + "name": "X-TIME-ZONE", + "in": "header", + "description": "Zona horaria IANA de las fechas sin zona del cuerpo JSON: el request las lee y la respuesta las escribe en ella. Si falta o no es válida, America/Mexico_City. No aplica a los campos que la operación documenta en UTC o en hora civil.", + "schema": { + "type": "string" + }, + "example": "America/Mexico_City" + } + ], + "requestBody": { + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/UpdateEmployeeDataCommand" + } + }, + "text/json": { + "schema": { + "$ref": "#/components/schemas/UpdateEmployeeDataCommand" + } + }, + "application/*+json": { + "schema": { + "$ref": "#/components/schemas/UpdateEmployeeDataCommand" + } + } + }, + "required": true + }, + "responses": { + "200": { + "description": "OK" + } + }, + "security": [ + { + "ApiKey": [ ], + "TenantKey": [ ] + } + ] + }, + "delete": { + "tags": [ + "People" + ], + "summary": "Eliminar los datos de empleado de una persona", + "description": "Borra los datos de empleado; la persona se conserva. Responde 400 (data false) si la persona no los tenía. Docs: [datos de empleado](https://docs.fiscalapi.com/employee-data#eliminar-datos-de-empleado).", + "operationId": "deleteEmployeeData", + "parameters": [ + { + "name": "personId", + "in": "path", + "required": true, + "schema": { + "type": "string" + } + }, + { + "name": "X-TIME-ZONE", + "in": "header", + "description": "Zona horaria IANA de las fechas sin zona del cuerpo JSON: el request las lee y la respuesta las escribe en ella. Si falta o no es válida, America/Mexico_City. No aplica a los campos que la operación documenta en UTC o en hora civil.", + "schema": { + "type": "string" + }, + "example": "America/Mexico_City" + } + ], + "responses": { + "200": { + "description": "OK" + } + }, + "security": [ + { + "ApiKey": [ ], + "TenantKey": [ ] + } + ] + } + }, + "/api/v4/people/{personId}/employer": { + "get": { + "tags": [ + "People" + ], + "summary": "Obtener los datos de empleador de una persona", + "description": "Los datos del empleador (patrón) que usa el recibo de nómina: registro patronal (employerRegistration), RFC del patrón de origen (originEmployerTin), origen de los recursos (satFundSource) y monto de recursos propios (ownResourceAmount). Responde 404 si la persona no los tiene. Docs: [datos de empleador](https://docs.fiscalapi.com/employer-data#obtener-datos-de-empleador).", + "operationId": "getEmployerData", + "parameters": [ + { + "name": "personId", + "in": "path", + "required": true, + "schema": { + "type": "string" + } + }, + { + "name": "X-TIME-ZONE", + "in": "header", + "description": "Zona horaria IANA de las fechas sin zona del cuerpo JSON: el request las lee y la respuesta las escribe en ella. Si falta o no es válida, America/Mexico_City. No aplica a los campos que la operación documenta en UTC o en hora civil.", + "schema": { + "type": "string" + }, + "example": "America/Mexico_City" + } + ], + "responses": { + "200": { + "description": "OK" + } + }, + "security": [ + { + "ApiKey": [ ], + "TenantKey": [ ] + } + ] + }, + "post": { + "tags": [ + "People" + ], + "summary": "Crear los datos de empleador de una persona", + "description": "Registra los datos del empleador (patrón) para el recibo de nómina; personId del cuerpo es el de la ruta. Cada persona tiene un solo registro: si ya existe, responde 400 y se modifica con PUT. Docs: [datos de empleador](https://docs.fiscalapi.com/employer-data#crear-datos-de-empleador).", + "operationId": "createEmployerData", + "parameters": [ + { + "name": "personId", + "in": "path", + "required": true, + "schema": { + "type": "string" + } + }, + { + "name": "X-TIME-ZONE", + "in": "header", + "description": "Zona horaria IANA de las fechas sin zona del cuerpo JSON: el request las lee y la respuesta las escribe en ella. Si falta o no es válida, America/Mexico_City. No aplica a los campos que la operación documenta en UTC o en hora civil.", + "schema": { + "type": "string" + }, + "example": "America/Mexico_City" + } + ], + "requestBody": { + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/CreateEmployerDataCommand" + } + }, + "text/json": { + "schema": { + "$ref": "#/components/schemas/CreateEmployerDataCommand" + } + }, + "application/*+json": { + "schema": { + "$ref": "#/components/schemas/CreateEmployerDataCommand" + } + } + }, + "required": true + }, + "responses": { + "200": { + "description": "OK" + } + }, + "security": [ + { + "ApiKey": [ ], + "TenantKey": [ ] + } + ] + }, + "put": { + "tags": [ + "People" + ], + "summary": "Actualizar los datos de empleador de una persona", + "description": "Actualiza los datos del empleador (patrón) para el recibo de nómina; personId del cuerpo es el de la ruta. Responde 404 si la persona no tiene datos de empleador. Docs: [datos de empleador](https://docs.fiscalapi.com/employer-data#actualizar-datos-de-empleador).", + "operationId": "updateEmployerData", + "parameters": [ + { + "name": "personId", + "in": "path", + "required": true, + "schema": { + "type": "string" + } + }, + { + "name": "X-TIME-ZONE", + "in": "header", + "description": "Zona horaria IANA de las fechas sin zona del cuerpo JSON: el request las lee y la respuesta las escribe en ella. Si falta o no es válida, America/Mexico_City. No aplica a los campos que la operación documenta en UTC o en hora civil.", + "schema": { + "type": "string" + }, + "example": "America/Mexico_City" + } + ], + "requestBody": { + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/UpdateEmployerDataCommand" + } + }, + "text/json": { + "schema": { + "$ref": "#/components/schemas/UpdateEmployerDataCommand" + } + }, + "application/*+json": { + "schema": { + "$ref": "#/components/schemas/UpdateEmployerDataCommand" + } + } + }, + "required": true + }, + "responses": { + "200": { + "description": "OK" + } + }, + "security": [ + { + "ApiKey": [ ], + "TenantKey": [ ] + } + ] + }, + "delete": { + "tags": [ + "People" + ], + "summary": "Eliminar los datos de empleador de una persona", + "description": "Borra los datos de empleador; la persona se conserva. Responde 400 (data false) si la persona no los tenía. Docs: [datos de empleador](https://docs.fiscalapi.com/employer-data#eliminar-datos-de-empleador).", + "operationId": "deleteEmployerData", + "parameters": [ + { + "name": "personId", + "in": "path", + "required": true, + "schema": { + "type": "string" + } + }, + { + "name": "X-TIME-ZONE", + "in": "header", + "description": "Zona horaria IANA de las fechas sin zona del cuerpo JSON: el request las lee y la respuesta las escribe en ella. Si falta o no es válida, America/Mexico_City. No aplica a los campos que la operación documenta en UTC o en hora civil.", + "schema": { + "type": "string" + }, + "example": "America/Mexico_City" + } + ], + "responses": { + "200": { + "description": "OK" + } + }, + "security": [ + { + "ApiKey": [ ], + "TenantKey": [ ] + } + ] + } + }, + "/api/v4/portal/{tenantName}/branding": { + "get": { + "tags": [ + "Portal" + ], + "summary": "Obtener la marca del portal", + "description": "La marca del portal de un tenant por su nombre de portal: nombre canónico (distinto del recibido si llegó un alias), logo, colores, si el portal atiende y la URL del aviso de privacidad. Anónimo. 404: el 404 genérico del portal, el mismo cuerpo para todo lo que no existe o no está disponible (no revela tenants ni tickets).", + "operationId": "getPortalBranding", + "parameters": [ + { + "name": "tenantName", + "in": "path", + "description": "El nombre del portal del tenant o uno de sus alias.", + "required": true, + "schema": { + "type": "string" + } + }, + { + "name": "X-TIME-ZONE", + "in": "header", + "description": "Zona horaria IANA de las fechas sin zona del cuerpo JSON: el request las lee y la respuesta las escribe en ella. Si falta o no es válida, America/Mexico_City. No aplica a los campos que la operación documenta en UTC o en hora civil.", + "schema": { + "type": "string" + }, + "example": "America/Mexico_City" + } + ], + "responses": { + "200": { + "description": "OK", + "content": { + "text/plain": { + "schema": { + "$ref": "#/components/schemas/ApiResponseOfPortalBrandingDto" + } + }, + "application/json": { + "schema": { + "$ref": "#/components/schemas/ApiResponseOfPortalBrandingDto" + } + }, + "text/json": { + "schema": { + "$ref": "#/components/schemas/ApiResponseOfPortalBrandingDto" + } + } + } + }, + "404": { + "description": "Not Found", + "content": { + "text/plain": { + "schema": { + "$ref": "#/components/schemas/ApiResponseOfObject" + } + }, + "application/json": { + "schema": { + "$ref": "#/components/schemas/ApiResponseOfObject" + } + }, + "text/json": { + "schema": { + "$ref": "#/components/schemas/ApiResponseOfObject" + } + } + } + } + }, + "security": [ ] + } + }, + "/api/v4/portal/tickets/{ticketCode}": { + "get": { + "tags": [ + "Portal" + ], + "summary": "Consultar un ticket en el portal", + "description": "El ticket por su código: el emisor, la marca, la venta con sus conceptos e impuestos, su estado y si el receptor puede autofacturarlo (o por qué no, en ineligibleReason), con el último momento facturable en eligibleUntil (como en la API de tickets; null si no vence). Si ya tiene factura, trae el correo enmascarado para recuperarla. Anónimo, con reCAPTCHA v3 (acción autoinvoice_lookup). 403: falló la verificación de reCAPTCHA (token inválido, de otra acción o con puntuación baja). 404: el 404 genérico del portal, el mismo cuerpo para todo lo que no existe o no está disponible (no revela tenants ni tickets). 429: demasiadas solicitudes desde la misma IP; el header Retry-After dice en cuántos segundos reintentar.", + "operationId": "getPortalTicket", + "parameters": [ + { + "name": "ticketCode", + "in": "path", + "description": "El código del ticket, con o sin guiones, como lo lleva el QR.", + "required": true, + "schema": { + "type": "string" + } + }, + { + "name": "X-Recaptcha-Token", + "in": "header", + "description": "Token de reCAPTCHA v3 de la acción autoinvoice_lookup.", + "schema": { + "type": "string" + } + }, + { + "name": "X-TIME-ZONE", + "in": "header", + "description": "Zona horaria IANA de las fechas sin zona del cuerpo JSON: el request las lee y la respuesta las escribe en ella. Si falta o no es válida, America/Mexico_City. No aplica a los campos que la operación documenta en UTC o en hora civil.", + "schema": { + "type": "string" + }, + "example": "America/Mexico_City" + } + ], + "responses": { + "200": { + "description": "OK", + "content": { + "text/plain": { + "schema": { + "$ref": "#/components/schemas/ApiResponseOfPortalTicketDto" + } + }, + "application/json": { + "schema": { + "$ref": "#/components/schemas/ApiResponseOfPortalTicketDto" + } + }, + "text/json": { + "schema": { + "$ref": "#/components/schemas/ApiResponseOfPortalTicketDto" + } + } + } + }, + "403": { + "description": "Forbidden", + "content": { + "text/plain": { + "schema": { + "$ref": "#/components/schemas/ApiResponseOfObject" + } + }, + "application/json": { + "schema": { + "$ref": "#/components/schemas/ApiResponseOfObject" + } + }, + "text/json": { + "schema": { + "$ref": "#/components/schemas/ApiResponseOfObject" + } + } + } + }, + "404": { + "description": "Not Found", + "content": { + "text/plain": { + "schema": { + "$ref": "#/components/schemas/ApiResponseOfObject" + } + }, + "application/json": { + "schema": { + "$ref": "#/components/schemas/ApiResponseOfObject" + } + }, + "text/json": { + "schema": { + "$ref": "#/components/schemas/ApiResponseOfObject" + } + } + } + }, + "429": { + "description": "Too Many Requests", + "content": { + "text/plain": { + "schema": { + "$ref": "#/components/schemas/ApiResponseOfObject" + } + }, + "application/json": { + "schema": { + "$ref": "#/components/schemas/ApiResponseOfObject" + } + }, + "text/json": { + "schema": { + "$ref": "#/components/schemas/ApiResponseOfObject" + } + } + } + } + }, + "security": [ ] + } + }, + "/api/v4/portal/tickets/{ticketCode}/invoice": { + "post": { + "tags": [ + "Portal" + ], + "summary": "Autofacturar un ticket", + "description": "Timbra la factura individual del ticket con los datos fiscales del receptor y la envía a su correo. Anónimo, con reCAPTCHA v3 (acción autoinvoice_invoice). 201: quedó timbrada (data.uuid, data.downloadToken, data.emailSent y el correo enmascarado en data.maskedEmail). 202: otra solicitud la está facturando o su resultado aún no se conoce (data.status: invoicing); consulta el ticket después. Los rechazos sobre el ticket o el receptor llevan data.errorCode, y data.field si hay que corregir un dato del receptor. 400: el cuerpo no es un JSON con la estructura esperada. 403: falló la verificación de reCAPTCHA (token inválido, de otra acción o con puntuación baja). 404: el 404 genérico del portal, el mismo cuerpo para todo lo que no existe o no está disponible (no revela tenants ni tickets). 409: el ticket ya tiene su factura (TicketAlreadyInvoiced) o está en una factura global (TicketInGlobalInvoice). 422: el ticket venció (TicketExpired), el comercio no factura en línea (SelfInvoicingDisabled) o no puede facturar (MerchantCannotInvoice), un dato del receptor no es válido para el SAT (InvalidRecipientData y Recipient*) o el SAT rechazó la factura (InvoiceRejected). 429: demasiadas solicitudes desde la misma IP; el header Retry-After dice en cuántos segundos reintentar. 503: el servicio de timbrado no respondió (ServiceUnavailable); no se cobró nada y se puede reintentar.", + "operationId": "invoicePortalTicket", + "parameters": [ + { + "name": "ticketCode", + "in": "path", + "description": "El código del ticket, con o sin guiones, como lo lleva el QR.", + "required": true, + "schema": { + "type": "string" + } + }, + { + "name": "X-TIME-ZONE", + "in": "header", + "description": "Zona horaria IANA de las fechas sin zona del cuerpo JSON: el request las lee y la respuesta las escribe en ella. Si falta o no es válida, America/Mexico_City. No aplica a los campos que la operación documenta en UTC o en hora civil.", + "schema": { + "type": "string" + }, + "example": "America/Mexico_City" + } + ], + "requestBody": { + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/PortalInvoiceTicketBody" + } + }, + "text/json": { + "schema": { + "$ref": "#/components/schemas/PortalInvoiceTicketBody" + } + }, + "application/*+json": { + "schema": { + "$ref": "#/components/schemas/PortalInvoiceTicketBody" + } + } + }, + "required": true + }, + "responses": { + "201": { + "description": "Created", + "content": { + "text/plain": { + "schema": { + "$ref": "#/components/schemas/ApiResponseOfPortalInvoiceDto" + } + }, + "application/json": { + "schema": { + "$ref": "#/components/schemas/ApiResponseOfPortalInvoiceDto" + } + }, + "text/json": { + "schema": { + "$ref": "#/components/schemas/ApiResponseOfPortalInvoiceDto" + } + } + } + }, + "202": { + "description": "Accepted", + "content": { + "text/plain": { + "schema": { + "$ref": "#/components/schemas/ApiResponseOfPortalInvoiceDto" + } + }, + "application/json": { + "schema": { + "$ref": "#/components/schemas/ApiResponseOfPortalInvoiceDto" + } + }, + "text/json": { + "schema": { + "$ref": "#/components/schemas/ApiResponseOfPortalInvoiceDto" + } + } + } + }, + "400": { + "description": "Bad Request", + "content": { + "text/plain": { + "schema": { + "$ref": "#/components/schemas/ApiResponseOfObject" + } + }, + "application/json": { + "schema": { + "$ref": "#/components/schemas/ApiResponseOfObject" + } + }, + "text/json": { + "schema": { + "$ref": "#/components/schemas/ApiResponseOfObject" + } + } + } + }, + "403": { + "description": "Forbidden", + "content": { + "text/plain": { + "schema": { + "$ref": "#/components/schemas/ApiResponseOfPortalInvoiceDto" + } + }, + "application/json": { + "schema": { + "$ref": "#/components/schemas/ApiResponseOfPortalInvoiceDto" + } + }, + "text/json": { + "schema": { + "$ref": "#/components/schemas/ApiResponseOfPortalInvoiceDto" + } + } + } + }, + "404": { + "description": "Not Found", + "content": { + "text/plain": { + "schema": { + "$ref": "#/components/schemas/ApiResponseOfPortalInvoiceDto" + } + }, + "application/json": { + "schema": { + "$ref": "#/components/schemas/ApiResponseOfPortalInvoiceDto" + } + }, + "text/json": { + "schema": { + "$ref": "#/components/schemas/ApiResponseOfPortalInvoiceDto" + } + } + } + }, + "409": { + "description": "Conflict", + "content": { + "text/plain": { + "schema": { + "$ref": "#/components/schemas/ApiResponseOfPortalInvoiceDto" + } + }, + "application/json": { + "schema": { + "$ref": "#/components/schemas/ApiResponseOfPortalInvoiceDto" + } + }, + "text/json": { + "schema": { + "$ref": "#/components/schemas/ApiResponseOfPortalInvoiceDto" + } + } + } + }, + "422": { + "description": "Unprocessable Entity", + "content": { + "text/plain": { + "schema": { + "$ref": "#/components/schemas/ApiResponseOfPortalInvoiceDto" + } + }, + "application/json": { + "schema": { + "$ref": "#/components/schemas/ApiResponseOfPortalInvoiceDto" + } + }, + "text/json": { + "schema": { + "$ref": "#/components/schemas/ApiResponseOfPortalInvoiceDto" + } + } + } + }, + "429": { + "description": "Too Many Requests", + "content": { + "text/plain": { + "schema": { + "$ref": "#/components/schemas/ApiResponseOfObject" + } + }, + "application/json": { + "schema": { + "$ref": "#/components/schemas/ApiResponseOfObject" + } + }, + "text/json": { + "schema": { + "$ref": "#/components/schemas/ApiResponseOfObject" + } + } + } + }, + "503": { + "description": "Service Unavailable", + "content": { + "text/plain": { + "schema": { + "$ref": "#/components/schemas/ApiResponseOfPortalInvoiceDto" + } + }, + "application/json": { + "schema": { + "$ref": "#/components/schemas/ApiResponseOfPortalInvoiceDto" + } + }, + "text/json": { + "schema": { + "$ref": "#/components/schemas/ApiResponseOfPortalInvoiceDto" + } + } + } + } + }, + "security": [ ] + } + }, + "/api/v4/portal/tickets/{ticketCode}/recover": { + "post": { + "tags": [ + "Portal" + ], + "summary": "Recuperar la factura de un ticket", + "description": "Entrega el token de descarga de la factura del ticket (data.downloadToken) si el correo es el del receptor. Cinco intentos fallidos en 24 horas bloquean la recuperación del ticket por 24 horas. Anónimo, con reCAPTCHA v3 (acción autoinvoice_recover). 400: el cuerpo no es un JSON con la estructura esperada. 403: falló la verificación de reCAPTCHA (data en null) o no se pudo recuperar la factura (RecoveryRejected: correo distinto, ticket sin factura o recuperación bloqueada). 404: el 404 genérico del portal, el mismo cuerpo para todo lo que no existe o no está disponible (no revela tenants ni tickets). 422: falta el correo (InvalidRecipientData, data.field: email); no cuenta como intento. 429: demasiadas solicitudes desde la misma IP; el header Retry-After dice en cuántos segundos reintentar.", + "operationId": "recoverPortalTicketInvoice", + "parameters": [ + { + "name": "ticketCode", + "in": "path", + "description": "El código del ticket, con o sin guiones, como lo lleva el QR.", + "required": true, + "schema": { + "type": "string" + } + }, + { + "name": "X-TIME-ZONE", + "in": "header", + "description": "Zona horaria IANA de las fechas sin zona del cuerpo JSON: el request las lee y la respuesta las escribe en ella. Si falta o no es válida, America/Mexico_City. No aplica a los campos que la operación documenta en UTC o en hora civil.", + "schema": { + "type": "string" + }, + "example": "America/Mexico_City" + } + ], + "requestBody": { + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/PortalRecoverTicketBody" + } + }, + "text/json": { + "schema": { + "$ref": "#/components/schemas/PortalRecoverTicketBody" + } + }, + "application/*+json": { + "schema": { + "$ref": "#/components/schemas/PortalRecoverTicketBody" + } + } + }, + "required": true + }, + "responses": { + "200": { + "description": "OK", + "content": { + "text/plain": { + "schema": { + "$ref": "#/components/schemas/ApiResponseOfPortalRecoveryDto" + } + }, + "application/json": { + "schema": { + "$ref": "#/components/schemas/ApiResponseOfPortalRecoveryDto" + } + }, + "text/json": { + "schema": { + "$ref": "#/components/schemas/ApiResponseOfPortalRecoveryDto" + } + } + } + }, + "400": { + "description": "Bad Request", + "content": { + "text/plain": { + "schema": { + "$ref": "#/components/schemas/ApiResponseOfObject" + } + }, + "application/json": { + "schema": { + "$ref": "#/components/schemas/ApiResponseOfObject" + } + }, + "text/json": { + "schema": { + "$ref": "#/components/schemas/ApiResponseOfObject" + } + } + } + }, + "403": { + "description": "Forbidden", + "content": { + "text/plain": { + "schema": { + "$ref": "#/components/schemas/ApiResponseOfPortalRecoveryDto" + } + }, + "application/json": { + "schema": { + "$ref": "#/components/schemas/ApiResponseOfPortalRecoveryDto" + } + }, + "text/json": { + "schema": { + "$ref": "#/components/schemas/ApiResponseOfPortalRecoveryDto" + } + } + } + }, + "404": { + "description": "Not Found", + "content": { + "text/plain": { + "schema": { + "$ref": "#/components/schemas/ApiResponseOfPortalRecoveryDto" + } + }, + "application/json": { + "schema": { + "$ref": "#/components/schemas/ApiResponseOfPortalRecoveryDto" + } + }, + "text/json": { + "schema": { + "$ref": "#/components/schemas/ApiResponseOfPortalRecoveryDto" + } + } + } + }, + "422": { + "description": "Unprocessable Entity", + "content": { + "text/plain": { + "schema": { + "$ref": "#/components/schemas/ApiResponseOfPortalRecoveryDto" + } + }, + "application/json": { + "schema": { + "$ref": "#/components/schemas/ApiResponseOfPortalRecoveryDto" + } + }, + "text/json": { + "schema": { + "$ref": "#/components/schemas/ApiResponseOfPortalRecoveryDto" + } + } + } + }, + "429": { + "description": "Too Many Requests", + "content": { + "text/plain": { + "schema": { + "$ref": "#/components/schemas/ApiResponseOfObject" + } + }, + "application/json": { + "schema": { + "$ref": "#/components/schemas/ApiResponseOfObject" + } + }, + "text/json": { + "schema": { + "$ref": "#/components/schemas/ApiResponseOfObject" + } + } + } + } + }, + "security": [ ] + } + }, + "/api/v4/portal/tickets/{ticketCode}/resend": { + "post": { + "tags": [ + "Portal" + ], + "summary": "Reenviar la factura de un ticket", + "description": "Reenvía el XML y el PDF de la factura al correo del receptor guardado en la factura, con el token de descarga. Hasta 3 reenvíos por factura en 24 horas. Anónimo, con reCAPTCHA v3 (acción autoinvoice_resend). 202: el reenvío quedó encolado (sin data). 400: el cuerpo no es un JSON con la estructura esperada. 403: falló la verificación de reCAPTCHA (token inválido, de otra acción o con puntuación baja). 404: el 404 genérico del portal, el mismo cuerpo para todo lo que no existe o no está disponible (no revela tenants ni tickets). También un token de descarga que no es de la factura del ticket. 429: se alcanzó el límite de reenvíos (ResendLimitReached, data.retryAfterSeconds) o el de solicitudes por IP; el header Retry-After dice en cuántos segundos reintentar.", + "operationId": "resendPortalTicketInvoice", + "parameters": [ + { + "name": "ticketCode", + "in": "path", + "description": "El código del ticket, con o sin guiones, como lo lleva el QR.", + "required": true, + "schema": { + "type": "string" + } + }, + { + "name": "X-TIME-ZONE", + "in": "header", + "description": "Zona horaria IANA de las fechas sin zona del cuerpo JSON: el request las lee y la respuesta las escribe en ella. Si falta o no es válida, America/Mexico_City. No aplica a los campos que la operación documenta en UTC o en hora civil.", + "schema": { + "type": "string" + }, + "example": "America/Mexico_City" + } + ], + "requestBody": { + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/PortalResendTicketBody" + } + }, + "text/json": { + "schema": { + "$ref": "#/components/schemas/PortalResendTicketBody" + } + }, + "application/*+json": { + "schema": { + "$ref": "#/components/schemas/PortalResendTicketBody" + } + } + }, + "required": true + }, + "responses": { + "202": { + "description": "Accepted", + "content": { + "text/plain": { + "schema": { + "$ref": "#/components/schemas/ApiResponseOfPortalResendDto" + } + }, + "application/json": { + "schema": { + "$ref": "#/components/schemas/ApiResponseOfPortalResendDto" + } + }, + "text/json": { + "schema": { + "$ref": "#/components/schemas/ApiResponseOfPortalResendDto" + } + } + } + }, + "400": { + "description": "Bad Request", + "content": { + "text/plain": { + "schema": { + "$ref": "#/components/schemas/ApiResponseOfObject" + } + }, + "application/json": { + "schema": { + "$ref": "#/components/schemas/ApiResponseOfObject" + } + }, + "text/json": { + "schema": { + "$ref": "#/components/schemas/ApiResponseOfObject" + } + } + } + }, + "403": { + "description": "Forbidden", + "content": { + "text/plain": { + "schema": { + "$ref": "#/components/schemas/ApiResponseOfPortalResendDto" + } + }, + "application/json": { + "schema": { + "$ref": "#/components/schemas/ApiResponseOfPortalResendDto" + } + }, + "text/json": { + "schema": { + "$ref": "#/components/schemas/ApiResponseOfPortalResendDto" + } + } + } + }, + "404": { + "description": "Not Found", + "content": { + "text/plain": { + "schema": { + "$ref": "#/components/schemas/ApiResponseOfPortalResendDto" + } + }, + "application/json": { + "schema": { + "$ref": "#/components/schemas/ApiResponseOfPortalResendDto" + } + }, + "text/json": { + "schema": { + "$ref": "#/components/schemas/ApiResponseOfPortalResendDto" + } + } + } + }, + "429": { + "description": "Too Many Requests", + "content": { + "text/plain": { + "schema": { + "$ref": "#/components/schemas/ApiResponseOfPortalResendDto" + } + }, + "application/json": { + "schema": { + "$ref": "#/components/schemas/ApiResponseOfPortalResendDto" + } + }, + "text/json": { + "schema": { + "$ref": "#/components/schemas/ApiResponseOfPortalResendDto" + } + } + } + } + }, + "security": [ ] + } + }, + "/api/v4/portal/downloads/{downloadToken}/{format}": { + "get": { + "tags": [ + "Portal" + ], + "summary": "Descargar la factura de un ticket", + "description": "El XML o el PDF de la autofactura como adjunto (.xml o .pdf), sin caché. Anónimo: el token de descarga es la credencial. 404: el 404 genérico del portal, el mismo cuerpo para todo lo que no existe o no está disponible (no revela tenants ni tickets). También un token inválido o vencido, o un formato desconocido. 429: demasiadas solicitudes desde la misma IP; el header Retry-After dice en cuántos segundos reintentar.", + "operationId": "downloadPortalTicketInvoice", + "parameters": [ + { + "name": "downloadToken", + "in": "path", + "description": "El token de descarga que entregan la autofactura y la recuperación.", + "required": true, + "schema": { + "type": "string" + } + }, + { + "name": "format", + "in": "path", + "description": "El formato del archivo: xml o pdf.", + "required": true, + "schema": { + "type": "string" + } + }, + { + "name": "X-TIME-ZONE", + "in": "header", + "description": "Zona horaria IANA de las fechas sin zona del cuerpo JSON: el request las lee y la respuesta las escribe en ella. Si falta o no es válida, America/Mexico_City. No aplica a los campos que la operación documenta en UTC o en hora civil.", + "schema": { + "type": "string" + }, + "example": "America/Mexico_City" + } + ], + "responses": { + "200": { + "description": "OK", + "content": { + "application/xml": { + "schema": { + "$ref": "#/components/schemas/FileContentResult" + } + }, + "application/pdf": { + "schema": { + "$ref": "#/components/schemas/FileContentResult" + } + } + } + }, + "404": { + "description": "Not Found", + "content": { + "text/plain": { + "schema": { + "$ref": "#/components/schemas/ApiResponseOfObject" + } + }, + "application/json": { + "schema": { + "$ref": "#/components/schemas/ApiResponseOfObject" + } + }, + "text/json": { + "schema": { + "$ref": "#/components/schemas/ApiResponseOfObject" + } + } + } + }, + "429": { + "description": "Too Many Requests", + "content": { + "text/plain": { + "schema": { + "$ref": "#/components/schemas/ApiResponseOfObject" + } + }, + "application/json": { + "schema": { + "$ref": "#/components/schemas/ApiResponseOfObject" + } + }, + "text/json": { + "schema": { + "$ref": "#/components/schemas/ApiResponseOfObject" + } + } + } + } + }, + "security": [ ] + } + }, + "/api/v4/portal/catalogs/tax-regimes": { + "get": { + "tags": [ + "Portal" + ], + "summary": "Listar los regímenes fiscales del receptor", + "description": "El catálogo c_RegimenFiscal del SAT con su descripción y los tipos de persona a los que aplica, para el formulario del receptor. Anónimo; no es de un tenant.", + "operationId": "listPortalTaxRegimes", + "parameters": [ + { + "name": "X-TIME-ZONE", + "in": "header", + "description": "Zona horaria IANA de las fechas sin zona del cuerpo JSON: el request las lee y la respuesta las escribe en ella. Si falta o no es válida, America/Mexico_City. No aplica a los campos que la operación documenta en UTC o en hora civil.", + "schema": { + "type": "string" + }, + "example": "America/Mexico_City" + } + ], + "responses": { + "200": { + "description": "OK", + "content": { + "text/plain": { + "schema": { + "$ref": "#/components/schemas/ApiResponseOfListOfPortalTaxRegimeDto" + } + }, + "application/json": { + "schema": { + "$ref": "#/components/schemas/ApiResponseOfListOfPortalTaxRegimeDto" + } + }, + "text/json": { + "schema": { + "$ref": "#/components/schemas/ApiResponseOfListOfPortalTaxRegimeDto" + } + } + } + } + }, + "security": [ ] + } + }, + "/api/v4/portal/catalogs/cfdi-uses": { + "get": { + "tags": [ + "Portal" + ], + "summary": "Listar los usos de CFDI del receptor", + "description": "El catálogo c_UsoCFDI del SAT con los tipos de persona y los regímenes fiscales del receptor con los que se puede usar cada uso, para el formulario del receptor. Anónimo; no es de un tenant.", + "operationId": "listPortalCfdiUses", + "parameters": [ + { + "name": "X-TIME-ZONE", + "in": "header", + "description": "Zona horaria IANA de las fechas sin zona del cuerpo JSON: el request las lee y la respuesta las escribe en ella. Si falta o no es válida, America/Mexico_City. No aplica a los campos que la operación documenta en UTC o en hora civil.", + "schema": { + "type": "string" + }, + "example": "America/Mexico_City" + } + ], + "responses": { + "200": { + "description": "OK", + "content": { + "text/plain": { + "schema": { + "$ref": "#/components/schemas/ApiResponseOfListOfPortalCfdiUseDto" + } + }, + "application/json": { + "schema": { + "$ref": "#/components/schemas/ApiResponseOfListOfPortalCfdiUseDto" + } + }, + "text/json": { + "schema": { + "$ref": "#/components/schemas/ApiResponseOfListOfPortalCfdiUseDto" + } + } + } + } + }, + "security": [ ] + } + }, + "/api/v4/products": { + "get": { + "tags": [ + "Products" + ], + "summary": "Listar productos", + "description": "Los productos y servicios del tenant ordenados por description, paginados con pageNumber y pageSize (10 por omisión, máximo 50). Cada uno trae su clave de producto o servicio del SAT (satProductCodeId), su clave de unidad (satUnitMeasurementId), su objeto de impuesto (satTaxObjectId) y sus impuestos (productTaxes). Docs: [listar productos](https://docs.fiscalapi.com/products#listar-productos).", + "operationId": "listProducts", + "parameters": [ + { + "name": "PageNumber", + "in": "query", + "schema": { + "pattern": "^-?(?:0|[1-9]\\d*)$", + "type": [ + "integer", + "string" + ], + "format": "int32" + } + }, + { + "name": "PageSize", + "in": "query", + "schema": { + "pattern": "^-?(?:0|[1-9]\\d*)$", + "type": [ + "integer", + "string" + ], + "format": "int32" + } + }, + { + "name": "X-TIME-ZONE", + "in": "header", + "description": "Zona horaria IANA de las fechas sin zona del cuerpo JSON: el request las lee y la respuesta las escribe en ella. Si falta o no es válida, America/Mexico_City. No aplica a los campos que la operación documenta en UTC o en hora civil.", + "schema": { + "type": "string" + }, + "example": "America/Mexico_City" + } + ], + "responses": { + "200": { + "description": "OK" + } + }, + "security": [ + { + "ApiKey": [ ], + "TenantKey": [ ] + } + ] + }, + "post": { + "tags": [ + "Products" + ], + "summary": "Crear un producto", + "description": "Da de alta un producto o servicio: description (hasta 255 caracteres) y unitPrice (mayor que 0) son obligatorios. Si se omiten, la clave de unidad (satUnitMeasurementId) es H87, el objeto de impuesto (satTaxObjectId) 02, la clave de producto o servicio (satProductCodeId) 01010101 y productTaxes trae IVA trasladado al 16 %. Cada clave debe existir en su catálogo del SAT y cada impuesto (taxId, taxTypeId, taxFlagId, rate) va una sola vez; si no, responde 400. Docs: [crear producto](https://docs.fiscalapi.com/products#crear-producto).", + "operationId": "createProduct", + "parameters": [ + { + "name": "X-TIME-ZONE", + "in": "header", + "description": "Zona horaria IANA de las fechas sin zona del cuerpo JSON: el request las lee y la respuesta las escribe en ella. Si falta o no es válida, America/Mexico_City. No aplica a los campos que la operación documenta en UTC o en hora civil.", + "schema": { + "type": "string" + }, + "example": "America/Mexico_City" + } + ], + "requestBody": { + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/CreateCommand" + } + }, + "text/json": { + "schema": { + "$ref": "#/components/schemas/CreateCommand" + } + }, + "application/*+json": { + "schema": { + "$ref": "#/components/schemas/CreateCommand" + } + } + }, + "required": true + }, + "responses": { + "200": { + "description": "OK" + } + }, + "security": [ + { + "ApiKey": [ ], + "TenantKey": [ ] + } + ] + } + }, + "/api/v4/products/{id}": { + "get": { + "tags": [ + "Products" + ], + "summary": "Obtener un producto", + "description": "Un producto por su id, con description, unitPrice, su clave de producto o servicio del SAT (satProductCodeId), su clave de unidad (satUnitMeasurementId), su objeto de impuesto (satTaxObjectId) y sus impuestos (productTaxes). Responde 404 si no existe. Docs: [obtener producto](https://docs.fiscalapi.com/products#obtener-producto-por-id).", + "operationId": "getProduct", + "parameters": [ + { + "name": "id", + "in": "path", + "required": true, + "schema": { + "type": "string" + } + }, + { + "name": "X-TIME-ZONE", + "in": "header", + "description": "Zona horaria IANA de las fechas sin zona del cuerpo JSON: el request las lee y la respuesta las escribe en ella. Si falta o no es válida, America/Mexico_City. No aplica a los campos que la operación documenta en UTC o en hora civil.", + "schema": { + "type": "string" + }, + "example": "America/Mexico_City" + } + ], + "responses": { + "200": { + "description": "OK" + } + }, + "security": [ + { + "ApiKey": [ ], + "TenantKey": [ ] + } + ] + }, + "put": { + "tags": [ + "Products" + ], + "summary": "Actualizar un producto", + "description": "Reemplaza el producto con el cuerpo completo: description, unitPrice, satUnitMeasurementId y satTaxObjectId son obligatorios y un satProductCodeId omitido se borra. Los productTaxes que envías reemplazan a los guardados; si omites la lista o la envías vacía, se conservan. El id de la ruta y el del cuerpo deben coincidir; responde 404 si el producto no existe. Docs: [actualizar producto](https://docs.fiscalapi.com/products#actualizar-producto).", + "operationId": "updateProduct", + "parameters": [ + { + "name": "id", + "in": "path", + "required": true, + "schema": { + "type": "string" + } + }, + { + "name": "X-TIME-ZONE", + "in": "header", + "description": "Zona horaria IANA de las fechas sin zona del cuerpo JSON: el request las lee y la respuesta las escribe en ella. Si falta o no es válida, America/Mexico_City. No aplica a los campos que la operación documenta en UTC o en hora civil.", + "schema": { + "type": "string" + }, + "example": "America/Mexico_City" + } + ], + "requestBody": { + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/UpdateCommand" + } + }, + "text/json": { + "schema": { + "$ref": "#/components/schemas/UpdateCommand" + } + }, + "application/*+json": { + "schema": { + "$ref": "#/components/schemas/UpdateCommand" + } + } + }, + "required": true + }, + "responses": { + "200": { + "description": "OK" + } + }, + "security": [ + { + "ApiKey": [ ], + "TenantKey": [ ] + } + ] + }, + "delete": { + "tags": [ + "Products" + ], + "summary": "Eliminar un producto", + "description": "Da de baja el producto: ya no aparece al listar y obtenerlo responde 404. Responde 404 si el producto no existe. Docs: [eliminar producto](https://docs.fiscalapi.com/products#eliminar-producto).", + "operationId": "deleteProduct", + "parameters": [ + { + "name": "id", + "in": "path", + "required": true, + "schema": { + "type": "string" + } + }, + { + "name": "X-TIME-ZONE", + "in": "header", + "description": "Zona horaria IANA de las fechas sin zona del cuerpo JSON: el request las lee y la respuesta las escribe en ella. Si falta o no es válida, America/Mexico_City. No aplica a los campos que la operación documenta en UTC o en hora civil.", + "schema": { + "type": "string" + }, + "example": "America/Mexico_City" + } + ], + "responses": { + "200": { + "description": "OK" + } + }, + "security": [ + { + "ApiKey": [ ], + "TenantKey": [ ] + } + ] + } + }, + "/api/v4/products/{id}/taxes": { + "get": { + "tags": [ + "Products" + ], + "summary": "Listar los impuestos de un producto", + "description": "Los impuestos del producto, cada uno con el impuesto (tax), el tipo de factor (taxType), si es trasladado o retenido (taxFlag) y la tasa o cuota (rate). Un id de producto que no existe responde 200 con la lista vacía, no 404. Docs: [modelo producto](https://docs.fiscalapi.com/products#modelo-producto).", + "operationId": "listProductTaxes", + "parameters": [ + { + "name": "id", + "in": "path", + "required": true, + "schema": { + "type": "string" + } + }, + { + "name": "X-TIME-ZONE", + "in": "header", + "description": "Zona horaria IANA de las fechas sin zona del cuerpo JSON: el request las lee y la respuesta las escribe en ella. Si falta o no es válida, America/Mexico_City. No aplica a los campos que la operación documenta en UTC o en hora civil.", + "schema": { + "type": "string" + }, + "example": "America/Mexico_City" + } + ], + "responses": { + "200": { + "description": "OK" + } + }, + "security": [ + { + "ApiKey": [ ], + "TenantKey": [ ] + } + ] + } + }, + "/api/v4/sat-validations": { + "get": { + "tags": [ + "SatValidations" + ], + "summary": "Listar tipos de validación SAT", + "description": "Los tipos de validación SAT activos (id y description) en el orden del catálogo, sin paginar. El id es el que se envía en validationTypes al validar un CFDI. No consume créditos de validación. Docs: [listar tipos de validación](https://docs.fiscalapi.com/sat-validations#listar-tipos-de-validacion).", + "operationId": "listSatValidationTypes", + "parameters": [ + { + "name": "X-TIME-ZONE", + "in": "header", + "description": "Zona horaria IANA de las fechas sin zona del cuerpo JSON: el request las lee y la respuesta las escribe en ella. Si falta o no es válida, America/Mexico_City. No aplica a los campos que la operación documenta en UTC o en hora civil.", + "schema": { + "type": "string" + }, + "example": "America/Mexico_City" + } + ], + "responses": { + "200": { + "description": "OK" + } + }, + "security": [ + { + "ApiKey": [ ], + "TenantKey": [ ] + } + ] + }, + "post": { + "tags": [ + "SatValidations" + ], + "summary": "Validar un CFDI o un RFC ante el SAT", + "description": "Ejecuta los tipos de validationTypes sobre un CFDI timbrado en base64 (xml, máximo 4,000,000 de caracteres) o solo sobre un RFC (tin), nunca ambos; con tin solo se admiten sat.blacklist.69b y sat.blacklist.69bbis. Los tipos son sat.xml.structure, sat.cfdi.sello y sat.tfd.sello (verificador de CFDI del SAT), sat.cfdi.status (estatus ante el SAT), sat.certificate.validity (vigencia del certificado del emisor al emitir) y las listas 69-B y 69-B Bis del último corte importado; en el ambiente de pruebas cada tipo responde un estatus aleatorio sin consultar al SAT. Cada tipo consume 1 crédito de validación de la persona autenticada, todo o nada: sin saldo suficiente, o si la suscripción del tenant no incluye Emisión CFDI, responde 403 y no ejecuta ninguno; un cuerpo inválido o un tipo desconocido responde 400. Devuelve un resultado por tipo (type, status, passed) en el orden del catálogo, y un resultado adverso es 200 con passed false. Docs: [validar un CFDI](https://docs.fiscalapi.com/sat-validations#validar) y [validar solo por RFC](https://docs.fiscalapi.com/sat-validations#validar-solo-por-rfc).", + "operationId": "runSatValidations", + "parameters": [ + { + "name": "X-TIME-ZONE", + "in": "header", + "description": "Zona horaria IANA de las fechas sin zona del cuerpo JSON: el request las lee y la respuesta las escribe en ella. Si falta o no es válida, America/Mexico_City. No aplica a los campos que la operación documenta en UTC o en hora civil.", + "schema": { + "type": "string" + }, + "example": "America/Mexico_City" + } + ], + "requestBody": { + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/RunSatValidationsCommand" + } + }, + "text/json": { + "schema": { + "$ref": "#/components/schemas/RunSatValidationsCommand" + } + }, + "application/*+json": { + "schema": { + "$ref": "#/components/schemas/RunSatValidationsCommand" + } + } + }, + "required": true + }, + "responses": { + "200": { + "description": "OK" + } + }, + "security": [ + { + "ApiKey": [ ], + "TenantKey": [ ] + } + ] + } + }, + "/api/v4/sat-validations/{id}": { + "get": { + "tags": [ + "SatValidations" + ], + "summary": "Obtener un tipo de validación SAT", + "description": "Un tipo de validación SAT activo (id y description) por su id, p. ej. sat.cfdi.status; responde 404 si no existe o está inactivo. Sus estatus posibles se listan con listSatValidationTypeStatuses. No consume créditos de validación. Docs: [obtener tipo de validación](https://docs.fiscalapi.com/sat-validations#obtener-tipo-de-validacion-por-id).", + "operationId": "getSatValidationType", + "parameters": [ + { + "name": "id", + "in": "path", + "required": true, + "schema": { + "type": "string" + } + }, + { + "name": "X-TIME-ZONE", + "in": "header", + "description": "Zona horaria IANA de las fechas sin zona del cuerpo JSON: el request las lee y la respuesta las escribe en ella. Si falta o no es válida, America/Mexico_City. No aplica a los campos que la operación documenta en UTC o en hora civil.", + "schema": { + "type": "string" + }, + "example": "America/Mexico_City" + } + ], + "responses": { + "200": { + "description": "OK" + } + }, + "security": [ + { + "ApiKey": [ ], + "TenantKey": [ ] + } + ] + } + }, + "/api/v4/sat-validations/{id}/statuses": { + "get": { + "tags": [ + "SatValidations" + ], + "summary": "Listar los estatus de un tipo de validación SAT", + "description": "Los estatus (id y description) que un tipo de validación SAT puede tomar al ejecutarse: primero los aprobatorios y, dentro de cada grupo, por id. No incluye passed, que llega en el resultado de cada validación; no ejecuta la validación ni consume créditos. Responde 404 si el tipo no existe o está inactivo. Docs: [listar estatus de un tipo](https://docs.fiscalapi.com/sat-validations#listar-estatus-de-un-tipo).", + "operationId": "listSatValidationTypeStatuses", + "parameters": [ + { + "name": "id", + "in": "path", + "required": true, + "schema": { + "type": "string" + } + }, + { + "name": "X-TIME-ZONE", + "in": "header", + "description": "Zona horaria IANA de las fechas sin zona del cuerpo JSON: el request las lee y la respuesta las escribe en ella. Si falta o no es válida, America/Mexico_City. No aplica a los campos que la operación documenta en UTC o en hora civil.", + "schema": { + "type": "string" + }, + "example": "America/Mexico_City" + } + ], + "responses": { + "200": { + "description": "OK" + } + }, + "security": [ + { + "ApiKey": [ ], + "TenantKey": [ ] + } + ] + } + }, + "/api/v4/stamps": { + "get": { + "tags": [ + "Stamps" + ], + "summary": "Listar movimientos de timbres", + "description": "Los movimientos de créditos de todas las personas del tenant (compras, transferencias, consumos, autofacturas y reversos), del más antiguo al más reciente, paginados con pageNumber y pageSize (10 por omisión, máximo 50). Cada movimiento pasa amount créditos de fromPerson a toPerson en el saldo de su creditType (1 timbres, 2 validaciones, 3 tickets). Docs: [listar movimientos](https://docs.fiscalapi.com/stamps#listar-movimientos).", + "operationId": "listStampTransactions", + "parameters": [ + { + "name": "PageNumber", + "in": "query", + "schema": { + "pattern": "^-?(?:0|[1-9]\\d*)$", + "type": [ + "integer", + "string" + ], + "format": "int32" + } + }, + { + "name": "PageSize", + "in": "query", + "schema": { + "pattern": "^-?(?:0|[1-9]\\d*)$", + "type": [ + "integer", + "string" + ], + "format": "int32" + } + }, + { + "name": "X-TIME-ZONE", + "in": "header", + "description": "Zona horaria IANA de las fechas sin zona del cuerpo JSON: el request las lee y la respuesta las escribe en ella. Si falta o no es válida, America/Mexico_City. No aplica a los campos que la operación documenta en UTC o en hora civil.", + "schema": { + "type": "string" + }, + "example": "America/Mexico_City" + } + ], + "responses": { + "200": { + "description": "OK" + } + }, + "security": [ + { + "ApiKey": [ ], + "TenantKey": [ ] + } + ] + }, + "post": { + "tags": [ + "Stamps" + ], + "summary": "Transferir o retirar timbres entre personas", + "description": "Resta amount (mayor que 0) del saldo de fromPersonId y lo suma a toPersonId en un solo movimiento de transferencia; retirar timbres es la misma operación en sentido inverso. creditType elige el saldo (1 timbres por omisión, 2 validaciones, 3 tickets) y, si el del origen no alcanza, responde 403 sin mover nada. Origen y destino deben ser personas distintas del tenant con id GUID (si no, 400) y solo el owner del tenant transfiere desde una persona que no es la autenticada (403). Docs: [transferir](https://docs.fiscalapi.com/stamps#transferir-timbres) y [retirar](https://docs.fiscalapi.com/stamps#retirar-timbres).", + "operationId": "transferStamps", + "parameters": [ + { + "name": "X-TIME-ZONE", + "in": "header", + "description": "Zona horaria IANA de las fechas sin zona del cuerpo JSON: el request las lee y la respuesta las escribe en ella. Si falta o no es válida, America/Mexico_City. No aplica a los campos que la operación documenta en UTC o en hora civil.", + "schema": { + "type": "string" + }, + "example": "America/Mexico_City" + } + ], + "requestBody": { + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/TransferStampsCommand" + } + }, + "text/json": { + "schema": { + "$ref": "#/components/schemas/TransferStampsCommand" + } + }, + "application/*+json": { + "schema": { + "$ref": "#/components/schemas/TransferStampsCommand" + } + } + }, + "required": true + }, + "responses": { + "200": { + "description": "OK" + } + }, + "security": [ + { + "ApiKey": [ ], + "TenantKey": [ ] + } + ] + } + }, + "/api/v4/stamps/{id}": { + "get": { + "tags": [ + "Stamps" + ], + "summary": "Obtener un movimiento de timbres", + "description": "Un movimiento por su id: consecutive, origen (fromPerson) y destino (toPerson) con su id, RFC (tin) y legalName, amount, transactionType (1 compra, 2 transferencia, 3 consumo, 4 autofactura, 5 reverso), transactionStatus, referenceId y creditType. Responde 404 si no existe en el tenant. Docs: [obtener movimiento](https://docs.fiscalapi.com/stamps#obtener-movimiento-por-id).", + "operationId": "getStampTransaction", + "parameters": [ + { + "name": "id", + "in": "path", + "required": true, + "schema": { + "type": "string" + } + }, + { + "name": "details", + "in": "query", + "schema": { + "type": "boolean", + "default": false + } + }, + { + "name": "X-TIME-ZONE", + "in": "header", + "description": "Zona horaria IANA de las fechas sin zona del cuerpo JSON: el request las lee y la respuesta las escribe en ella. Si falta o no es válida, America/Mexico_City. No aplica a los campos que la operación documenta en UTC o en hora civil.", + "schema": { + "type": "string" + }, + "example": "America/Mexico_City" + } + ], + "responses": { + "200": { + "description": "OK" + } + }, + "security": [ + { + "ApiKey": [ ], + "TenantKey": [ ] + } + ] + } + }, + "/api/v4/tax-files": { + "get": { + "tags": [ + "TaxFiles" + ], + "summary": "Listar certificados CSD y FIEL", + "description": "Los certificados (.cer) y las llaves privadas (.key) de CSD y FIEL que la persona autenticada puede ver, paginados con pageNumber y pageSize (10 por omisión, máximo 50). La contraseña nunca se devuelve. Docs: [listar certificados](https://docs.fiscalapi.com/tax-files#listar-certificados).", + "operationId": "listTaxFiles", + "parameters": [ + { + "name": "PageNumber", + "in": "query", + "schema": { + "pattern": "^-?(?:0|[1-9]\\d*)$", + "type": [ + "integer", + "string" + ], + "format": "int32" + } + }, + { + "name": "PageSize", + "in": "query", + "schema": { + "pattern": "^-?(?:0|[1-9]\\d*)$", + "type": [ + "integer", + "string" + ], + "format": "int32" + } + }, + { + "name": "X-TIME-ZONE", + "in": "header", + "description": "Zona horaria IANA de las fechas sin zona del cuerpo JSON: el request las lee y la respuesta las escribe en ella. Si falta o no es válida, America/Mexico_City. No aplica a los campos que la operación documenta en UTC o en hora civil.", + "schema": { + "type": "string" + }, + "example": "America/Mexico_City" + } + ], + "responses": { + "200": { + "description": "OK" + } + }, + "security": [ + { + "ApiKey": [ ], + "TenantKey": [ ] + } + ] + }, + "post": { + "tags": [ + "TaxFiles" + ], + "summary": "Subir un certificado CSD o FIEL", + "description": "Sube el certificado (.cer) o la llave privada (.key) del CSD o de la FIEL de una persona: personId, base64File, fileType y password, la contraseña de la llave. El certificado debe ser del RFC de la persona. Un par completo requiere dos peticiones con el mismo personId, una con el .cer y otra con la .key. Docs: [subir certificados](https://docs.fiscalapi.com/tax-files#subir-certificados).", + "operationId": "createTaxFile", + "parameters": [ + { + "name": "X-TIME-ZONE", + "in": "header", + "description": "Zona horaria IANA de las fechas sin zona del cuerpo JSON: el request las lee y la respuesta las escribe en ella. Si falta o no es válida, America/Mexico_City. No aplica a los campos que la operación documenta en UTC o en hora civil.", + "schema": { + "type": "string" + }, + "example": "America/Mexico_City" + } + ], + "requestBody": { + "content": { + "application/json": { + "schema": { + "oneOf": [ + { + "type": "null" + }, + { + "$ref": "#/components/schemas/CreateCommand" + } + ] + } + }, + "text/json": { + "schema": { + "oneOf": [ + { + "type": "null" + }, + { + "$ref": "#/components/schemas/CreateCommand" + } + ] + } + }, + "application/*+json": { + "schema": { + "oneOf": [ + { + "type": "null" + }, + { + "$ref": "#/components/schemas/CreateCommand" + } + ] + } + } + } + }, + "responses": { + "200": { + "description": "OK" + } + }, + "security": [ + { + "ApiKey": [ ], + "TenantKey": [ ] + } + ] + } + }, + "/api/v4/tax-files/{id}": { + "get": { + "tags": [ + "TaxFiles" + ], + "summary": "Obtener un certificado", + "description": "Un certificado o una llave privada por su id; el de otra persona responde 404, salvo para el owner. Docs: [obtener certificado](https://docs.fiscalapi.com/tax-files#obtener-certificado-por-id).", + "operationId": "getTaxFile", + "parameters": [ + { + "name": "id", + "in": "path", + "required": true, + "schema": { + "type": "string" + } + }, + { + "name": "X-TIME-ZONE", + "in": "header", + "description": "Zona horaria IANA de las fechas sin zona del cuerpo JSON: el request las lee y la respuesta las escribe en ella. Si falta o no es válida, America/Mexico_City. No aplica a los campos que la operación documenta en UTC o en hora civil.", + "schema": { + "type": "string" + }, + "example": "America/Mexico_City" + } + ], + "responses": { + "200": { + "description": "OK" + } + }, + "security": [ + { + "ApiKey": [ ], + "TenantKey": [ ] + } + ] + }, + "delete": { + "tags": [ + "TaxFiles" + ], + "summary": "Eliminar un certificado", + "description": "Da de baja un certificado o una llave privada; responde 400 (data false) si no existe o la persona autenticada no lo puede ver. Un certificado no se edita: sube uno nuevo y elimina el anterior. Docs: [eliminar certificado](https://docs.fiscalapi.com/tax-files#eliminar-certificado).", + "operationId": "deleteTaxFile", + "parameters": [ + { + "name": "id", + "in": "path", + "required": true, + "schema": { + "type": "string" + } + }, + { + "name": "X-TIME-ZONE", + "in": "header", + "description": "Zona horaria IANA de las fechas sin zona del cuerpo JSON: el request las lee y la respuesta las escribe en ella. Si falta o no es válida, America/Mexico_City. No aplica a los campos que la operación documenta en UTC o en hora civil.", + "schema": { + "type": "string" + }, + "example": "America/Mexico_City" + } + ], + "responses": { + "200": { + "description": "OK" + } + }, + "security": [ + { + "ApiKey": [ ], + "TenantKey": [ ] + } + ] + } + }, + "/api/v4/tax-files/{personId}/default-values": { + "get": { + "tags": [ + "TaxFiles" + ], + "summary": "Obtener el CSD vigente de una persona", + "description": "El último par .cer y .key vigente del CSD de la persona, con cada archivo en base64 y la contraseña descifrada. Solo pueden pedirlo la persona misma o el owner. Sin CSD vigente responde una lista vacía; 400 si la persona no tiene RFC y 404 si el par está incompleto. Docs: [certificados por defecto](https://docs.fiscalapi.com/tax-files#obtener-certificados-por-defecto).", + "operationId": "getTaxFileDefaultValues", + "parameters": [ + { + "name": "personId", + "in": "path", + "required": true, + "schema": { + "type": "string" + } + }, + { + "name": "X-TIME-ZONE", + "in": "header", + "description": "Zona horaria IANA de las fechas sin zona del cuerpo JSON: el request las lee y la respuesta las escribe en ella. Si falta o no es válida, America/Mexico_City. No aplica a los campos que la operación documenta en UTC o en hora civil.", + "schema": { + "type": "string" + }, + "example": "America/Mexico_City" + } + ], + "responses": { + "200": { + "description": "OK" + } + }, + "security": [ + { + "ApiKey": [ ], + "TenantKey": [ ] + } + ] + } + }, + "/api/v4/tickets": { + "get": { + "tags": [ + "Tickets" + ], + "summary": "Listar tickets", + "description": "Los tickets del tenant paginados y ordenados por fecha de pago. Cada filtro aplica solo si viene: estados, persona emisora, factura global y periodo de pago en fechas civiles de México. Con details=true cada ticket trae sus conceptos con sus impuestos. No exige el feature Auto CFDI. 400: un estado desconocido, una fecha inválida, un periodo invertido o una página fuera de rango. 401: sin API key o JWT válidos. 403: la persona no tiene el permiso Tickets.Read.", + "operationId": "listTickets", + "parameters": [ + { + "name": "Status", + "in": "query", + "description": "Uno o más estados (TicketStatus en camelCase), repitiendo el parámetro: status=pending&status=invoiced.", + "schema": { + "type": "array", + "items": { + "type": "string" + } + } + }, + { + "name": "PersonId", + "in": "query", + "description": "Id de la persona emisora; una persona de otro tenant deja la lista vacía.", + "schema": { + "type": "string" + } + }, + { + "name": "GlobalInvoiceId", + "in": "query", + "description": "Id de la factura global que ampara los tickets; una de otro tenant deja la lista vacía.", + "schema": { + "type": "string" + } + }, + { + "name": "From", + "in": "query", + "description": "Primer día del pago (incluido), fecha civil de México YYYY-MM-DD.", + "schema": { + "type": "string" + } + }, + { + "name": "To", + "in": "query", + "description": "Último día del pago (incluido), fecha civil de México YYYY-MM-DD.", + "schema": { + "type": "string" + } + }, + { + "name": "Details", + "in": "query", + "description": "Con true, cada ticket trae sus conceptos con sus impuestos.", + "schema": { + "type": "boolean" + } + }, + { + "name": "PageNumber", + "in": "query", + "description": "Número de página, desde 1 (por omisión 1).", + "schema": { + "pattern": "^-?(?:0|[1-9]\\d*)$", + "type": [ + "integer", + "string" + ], + "format": "int32" + } + }, + { + "name": "PageSize", + "in": "query", + "description": "Tickets por página, de 1 a 50 (por omisión 10).", + "schema": { + "pattern": "^-?(?:0|[1-9]\\d*)$", + "type": [ + "integer", + "string" + ], + "format": "int32" + } + }, + { + "name": "X-TIME-ZONE", + "in": "header", + "description": "Zona horaria IANA de las fechas sin zona del cuerpo JSON: el request las lee y la respuesta las escribe en ella. Si falta o no es válida, America/Mexico_City. No aplica a los campos que la operación documenta en UTC o en hora civil.", + "schema": { + "type": "string" + }, + "example": "America/Mexico_City" + } + ], + "responses": { + "200": { + "description": "OK", + "content": { + "text/plain": { + "schema": { + "$ref": "#/components/schemas/ApiResponseOfPagedListOfTicketDto" + } + }, + "application/json": { + "schema": { + "$ref": "#/components/schemas/ApiResponseOfPagedListOfTicketDto" + } + }, + "text/json": { + "schema": { + "$ref": "#/components/schemas/ApiResponseOfPagedListOfTicketDto" + } + } + } + }, + "400": { + "description": "Bad Request", + "content": { + "text/plain": { + "schema": { + "$ref": "#/components/schemas/ApiResponseOfObject" + } + }, + "application/json": { + "schema": { + "$ref": "#/components/schemas/ApiResponseOfObject" + } + }, + "text/json": { + "schema": { + "$ref": "#/components/schemas/ApiResponseOfObject" + } + } + } + }, + "401": { + "description": "Unauthorized", + "content": { + "text/plain": { + "schema": { + "$ref": "#/components/schemas/ApiResponseOfObject" + } + }, + "application/json": { + "schema": { + "$ref": "#/components/schemas/ApiResponseOfObject" + } + }, + "text/json": { + "schema": { + "$ref": "#/components/schemas/ApiResponseOfObject" + } + } + } + }, + "403": { + "description": "Forbidden", + "content": { + "text/plain": { + "schema": { + "$ref": "#/components/schemas/ApiResponseOfObject" + } + }, + "application/json": { + "schema": { + "$ref": "#/components/schemas/ApiResponseOfObject" + } + }, + "text/json": { + "schema": { + "$ref": "#/components/schemas/ApiResponseOfObject" + } + } + } + } + }, + "security": [ + { + "ApiKey": [ ], + "TenantKey": [ ] + } + ] + }, + "post": { + "tags": [ + "Tickets" + ], + "summary": "Crear un ticket", + "description": "Registra una venta pagada del punto de venta y cobra 1 crédito de ticket. Responde el código del ticket y la URL del portal para su QR. Exige el feature Auto CFDI. 201: el ticket nuevo. 200: el ticket original, porque la Idempotency-Key ya se usó con el mismo cuerpo (no cobra otra vez). 400: el cuerpo no tiene la estructura esperada o le faltan datos. 401: sin API key o JWT válidos. 403: la persona no tiene el permiso Tickets.Create, el tenant no tiene Auto CFDI o no hay saldo de créditos de ticket. 409: el emisor ya tiene un ticket activo con esa serie y folio (data.existingTicketId), u otro ticket con la misma serie y folio o la misma Idempotency-Key cambió al mismo tiempo. 422: una regla fiscal o aritmética no se cumple, el emisor no puede facturar, o la Idempotency-Key ya se usó con otro cuerpo.", + "operationId": "createTicket", + "parameters": [ + { + "name": "Idempotency-Key", + "in": "header", + "description": "Llave de idempotencia opcional: repetirla con el mismo cuerpo devuelve el ticket original.", + "schema": { + "type": "string" + } + }, + { + "name": "X-TIME-ZONE", + "in": "header", + "description": "Zona horaria IANA de las fechas sin zona del cuerpo JSON: el request las lee y la respuesta las escribe en ella. Si falta o no es válida, America/Mexico_City. No aplica a los campos que la operación documenta en UTC o en hora civil.", + "schema": { + "type": "string" + }, + "example": "America/Mexico_City" + } + ], + "requestBody": { + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/CreateTicketCommand" + } + }, + "text/json": { + "schema": { + "$ref": "#/components/schemas/CreateTicketCommand" + } + }, + "application/*+json": { + "schema": { + "$ref": "#/components/schemas/CreateTicketCommand" + } + } + }, + "required": true + }, + "responses": { + "201": { + "description": "Created", + "content": { + "text/plain": { + "schema": { + "$ref": "#/components/schemas/ApiResponseOfTicketIngestedDto" + } + }, + "application/json": { + "schema": { + "$ref": "#/components/schemas/ApiResponseOfTicketIngestedDto" + } + }, + "text/json": { + "schema": { + "$ref": "#/components/schemas/ApiResponseOfTicketIngestedDto" + } + } + } + }, + "200": { + "description": "OK", + "content": { + "text/plain": { + "schema": { + "$ref": "#/components/schemas/ApiResponseOfTicketIngestedDto" + } + }, + "application/json": { + "schema": { + "$ref": "#/components/schemas/ApiResponseOfTicketIngestedDto" + } + }, + "text/json": { + "schema": { + "$ref": "#/components/schemas/ApiResponseOfTicketIngestedDto" + } + } + } + }, + "400": { + "description": "Bad Request", + "content": { + "text/plain": { + "schema": { + "$ref": "#/components/schemas/ApiResponseOfObject" + } + }, + "application/json": { + "schema": { + "$ref": "#/components/schemas/ApiResponseOfObject" + } + }, + "text/json": { + "schema": { + "$ref": "#/components/schemas/ApiResponseOfObject" + } + } + } + }, + "401": { + "description": "Unauthorized", + "content": { + "text/plain": { + "schema": { + "$ref": "#/components/schemas/ApiResponseOfObject" + } + }, + "application/json": { + "schema": { + "$ref": "#/components/schemas/ApiResponseOfObject" + } + }, + "text/json": { + "schema": { + "$ref": "#/components/schemas/ApiResponseOfObject" + } + } + } + }, + "403": { + "description": "Forbidden", + "content": { + "text/plain": { + "schema": { + "$ref": "#/components/schemas/ApiResponseOfObject" + } + }, + "application/json": { + "schema": { + "$ref": "#/components/schemas/ApiResponseOfObject" + } + }, + "text/json": { + "schema": { + "$ref": "#/components/schemas/ApiResponseOfObject" + } + } + } + }, + "409": { + "description": "Conflict", + "content": { + "text/plain": { + "schema": { + "$ref": "#/components/schemas/ApiResponseOfExistingTicketData" + } + }, + "application/json": { + "schema": { + "$ref": "#/components/schemas/ApiResponseOfExistingTicketData" + } + }, + "text/json": { + "schema": { + "$ref": "#/components/schemas/ApiResponseOfExistingTicketData" + } + } + } + }, + "422": { + "description": "Unprocessable Entity", + "content": { + "text/plain": { + "schema": { + "$ref": "#/components/schemas/ApiResponseOfObject" + } + }, + "application/json": { + "schema": { + "$ref": "#/components/schemas/ApiResponseOfObject" + } + }, + "text/json": { + "schema": { + "$ref": "#/components/schemas/ApiResponseOfObject" + } + } + } + } + }, + "security": [ + { + "ApiKey": [ ], + "TenantKey": [ ] + } + ] + } + }, + "/api/v4/tickets/{id}": { + "get": { + "tags": [ + "Tickets" + ], + "summary": "Obtener un ticket", + "description": "Un ticket del tenant con su estado, sus facturas y su vigencia de autofactura. Con details=true trae además sus conceptos con sus impuestos y su historia (eventos). 401: sin API key o JWT válidos. 403: la persona no tiene el permiso Tickets.Read. 404: no existe un ticket con ese id en el tenant.", + "operationId": "getTicket", + "parameters": [ + { + "name": "id", + "in": "path", + "description": "Id del ticket.", + "required": true, + "schema": { + "type": "string" + } + }, + { + "name": "details", + "in": "query", + "description": "Con true, trae los conceptos y la historia del ticket.", + "schema": { + "type": "boolean", + "default": false + } + }, + { + "name": "X-TIME-ZONE", + "in": "header", + "description": "Zona horaria IANA de las fechas sin zona del cuerpo JSON: el request las lee y la respuesta las escribe en ella. Si falta o no es válida, America/Mexico_City. No aplica a los campos que la operación documenta en UTC o en hora civil.", + "schema": { + "type": "string" + }, + "example": "America/Mexico_City" + } + ], + "responses": { + "200": { + "description": "OK", + "content": { + "text/plain": { + "schema": { + "$ref": "#/components/schemas/ApiResponseOfTicketDto" + } + }, + "application/json": { + "schema": { + "$ref": "#/components/schemas/ApiResponseOfTicketDto" + } + }, + "text/json": { + "schema": { + "$ref": "#/components/schemas/ApiResponseOfTicketDto" + } + } + } + }, + "401": { + "description": "Unauthorized", + "content": { + "text/plain": { + "schema": { + "$ref": "#/components/schemas/ApiResponseOfObject" + } + }, + "application/json": { + "schema": { + "$ref": "#/components/schemas/ApiResponseOfObject" + } + }, + "text/json": { + "schema": { + "$ref": "#/components/schemas/ApiResponseOfObject" + } + } + } + }, + "403": { + "description": "Forbidden", + "content": { + "text/plain": { + "schema": { + "$ref": "#/components/schemas/ApiResponseOfObject" + } + }, + "application/json": { + "schema": { + "$ref": "#/components/schemas/ApiResponseOfObject" + } + }, + "text/json": { + "schema": { + "$ref": "#/components/schemas/ApiResponseOfObject" + } + } + } + }, + "404": { + "description": "Not Found", + "content": { + "text/plain": { + "schema": { + "$ref": "#/components/schemas/ApiResponseOfObject" + } + }, + "application/json": { + "schema": { + "$ref": "#/components/schemas/ApiResponseOfObject" + } + }, + "text/json": { + "schema": { + "$ref": "#/components/schemas/ApiResponseOfObject" + } + } + } + } + }, + "security": [ + { + "ApiKey": [ ], + "TenantKey": [ ] + } + ] + }, + "delete": { + "tags": [ + "Tickets" + ], + "summary": "Cancelar un ticket", + "description": "Cancela un ticket en estado pending y libera su serie y folio para volver a ingerirlo. El crédito de ticket de la ingesta no se devuelve. Exige el feature Auto CFDI. 401: sin API key o JWT válidos. 403: la persona no tiene el permiso Tickets.Cancel o el tenant no tiene Auto CFDI. 404: no existe un ticket con ese id en el tenant. 409: el ticket no está en pending o cambió de estado al mismo tiempo.", + "operationId": "cancelTicket", + "parameters": [ + { + "name": "id", + "in": "path", + "description": "Id del ticket.", + "required": true, + "schema": { + "type": "string" + } + }, + { + "name": "X-TIME-ZONE", + "in": "header", + "description": "Zona horaria IANA de las fechas sin zona del cuerpo JSON: el request las lee y la respuesta las escribe en ella. Si falta o no es válida, America/Mexico_City. No aplica a los campos que la operación documenta en UTC o en hora civil.", + "schema": { + "type": "string" + }, + "example": "America/Mexico_City" + } + ], + "responses": { + "200": { + "description": "OK", + "content": { + "text/plain": { + "schema": { + "$ref": "#/components/schemas/ApiResponseOfboolean" + } + }, + "application/json": { + "schema": { + "$ref": "#/components/schemas/ApiResponseOfboolean" + } + }, + "text/json": { + "schema": { + "$ref": "#/components/schemas/ApiResponseOfboolean" + } + } + } + }, + "401": { + "description": "Unauthorized", + "content": { + "text/plain": { + "schema": { + "$ref": "#/components/schemas/ApiResponseOfObject" + } + }, + "application/json": { + "schema": { + "$ref": "#/components/schemas/ApiResponseOfObject" + } + }, + "text/json": { + "schema": { + "$ref": "#/components/schemas/ApiResponseOfObject" + } + } + } + }, + "403": { + "description": "Forbidden", + "content": { + "text/plain": { + "schema": { + "$ref": "#/components/schemas/ApiResponseOfObject" + } + }, + "application/json": { + "schema": { + "$ref": "#/components/schemas/ApiResponseOfObject" + } + }, + "text/json": { + "schema": { + "$ref": "#/components/schemas/ApiResponseOfObject" + } + } + } + }, + "404": { + "description": "Not Found", + "content": { + "text/plain": { + "schema": { + "$ref": "#/components/schemas/ApiResponseOfObject" + } + }, + "application/json": { + "schema": { + "$ref": "#/components/schemas/ApiResponseOfObject" + } + }, + "text/json": { + "schema": { + "$ref": "#/components/schemas/ApiResponseOfObject" + } + } + } + }, + "409": { + "description": "Conflict", + "content": { + "text/plain": { + "schema": { + "$ref": "#/components/schemas/ApiResponseOfObject" + } + }, + "application/json": { + "schema": { + "$ref": "#/components/schemas/ApiResponseOfObject" + } + }, + "text/json": { + "schema": { + "$ref": "#/components/schemas/ApiResponseOfObject" + } + } + } + } + }, + "security": [ + { + "ApiKey": [ ], + "TenantKey": [ ] + } + ] + } + }, + "/api/v4/tickets/{id}/code": { + "get": { + "tags": [ + "Tickets" + ], + "summary": "Obtener el código de un ticket", + "description": "El código del ticket y la URL del portal que abre su QR, los mismos de la ingesta, para reimprimirlo. 401: sin API key o JWT válidos. 403: la persona no tiene el permiso Tickets.Read. 404: no existe un ticket con ese id en el tenant.", + "operationId": "getTicketCode", + "parameters": [ + { + "name": "id", + "in": "path", + "description": "Id del ticket.", + "required": true, + "schema": { + "type": "string" + } + }, + { + "name": "X-TIME-ZONE", + "in": "header", + "description": "Zona horaria IANA de las fechas sin zona del cuerpo JSON: el request las lee y la respuesta las escribe en ella. Si falta o no es válida, America/Mexico_City. No aplica a los campos que la operación documenta en UTC o en hora civil.", + "schema": { + "type": "string" + }, + "example": "America/Mexico_City" + } + ], + "responses": { + "200": { + "description": "OK", + "content": { + "text/plain": { + "schema": { + "$ref": "#/components/schemas/ApiResponseOfTicketCodeDto" + } + }, + "application/json": { + "schema": { + "$ref": "#/components/schemas/ApiResponseOfTicketCodeDto" + } + }, + "text/json": { + "schema": { + "$ref": "#/components/schemas/ApiResponseOfTicketCodeDto" + } + } + } + }, + "401": { + "description": "Unauthorized", + "content": { + "text/plain": { + "schema": { + "$ref": "#/components/schemas/ApiResponseOfObject" + } + }, + "application/json": { + "schema": { + "$ref": "#/components/schemas/ApiResponseOfObject" + } + }, + "text/json": { + "schema": { + "$ref": "#/components/schemas/ApiResponseOfObject" + } + } + } + }, + "403": { + "description": "Forbidden", + "content": { + "text/plain": { + "schema": { + "$ref": "#/components/schemas/ApiResponseOfObject" + } + }, + "application/json": { + "schema": { + "$ref": "#/components/schemas/ApiResponseOfObject" + } + }, + "text/json": { + "schema": { + "$ref": "#/components/schemas/ApiResponseOfObject" + } + } + } + }, + "404": { + "description": "Not Found", + "content": { + "text/plain": { + "schema": { + "$ref": "#/components/schemas/ApiResponseOfObject" + } + }, + "application/json": { + "schema": { + "$ref": "#/components/schemas/ApiResponseOfObject" + } + }, + "text/json": { + "schema": { + "$ref": "#/components/schemas/ApiResponseOfObject" + } + } + } + } + }, + "security": [ + { + "ApiKey": [ ], + "TenantKey": [ ] + } + ] + } + }, + "/api/v4/webhooks": { + "get": { + "tags": [ + "Webhooks" + ], + "summary": "Listar webhooks", + "description": "Los webhooks del tenant en orden de alta, sin su secreto. Las escuchas de CLI (kind cliListener) solo salen para su persona y para el owner. 400: una página fuera de rango. 401: sin API key o JWT válidos. 403: la persona no tiene el permiso Webhooks.Manage.", + "operationId": "listWebhooks", + "parameters": [ + { + "name": "PageNumber", + "in": "query", + "description": "Número de página, desde 1 (por omisión 1).", + "schema": { + "pattern": "^-?(?:0|[1-9]\\d*)$", + "type": [ + "integer", + "string" + ], + "format": "int32" + } + }, + { + "name": "PageSize", + "in": "query", + "description": "Webhooks por página, de 1 a 50 (por omisión 10).", + "schema": { + "pattern": "^-?(?:0|[1-9]\\d*)$", + "type": [ + "integer", + "string" + ], + "format": "int32" + } + }, + { + "name": "X-TIME-ZONE", + "in": "header", + "description": "Zona horaria IANA de las fechas sin zona del cuerpo JSON: el request las lee y la respuesta las escribe en ella. Si falta o no es válida, America/Mexico_City. No aplica a los campos que la operación documenta en UTC o en hora civil.", + "schema": { + "type": "string" + }, + "example": "America/Mexico_City" + } + ], + "responses": { + "200": { + "description": "OK", + "content": { + "text/plain": { + "schema": { + "$ref": "#/components/schemas/ApiResponseOfPagedListOfWebhookDto" + } + }, + "application/json": { + "schema": { + "$ref": "#/components/schemas/ApiResponseOfPagedListOfWebhookDto" + } + }, + "text/json": { + "schema": { + "$ref": "#/components/schemas/ApiResponseOfPagedListOfWebhookDto" + } + } + } + }, + "400": { + "description": "Bad Request", + "content": { + "text/plain": { + "schema": { + "$ref": "#/components/schemas/ApiResponseOfObject" + } + }, + "application/json": { + "schema": { + "$ref": "#/components/schemas/ApiResponseOfObject" + } + }, + "text/json": { + "schema": { + "$ref": "#/components/schemas/ApiResponseOfObject" + } + } + } + }, + "401": { + "description": "Unauthorized", + "content": { + "text/plain": { + "schema": { + "$ref": "#/components/schemas/ApiResponseOfObject" + } + }, + "application/json": { + "schema": { + "$ref": "#/components/schemas/ApiResponseOfObject" + } + }, + "text/json": { + "schema": { + "$ref": "#/components/schemas/ApiResponseOfObject" + } + } + } + }, + "403": { + "description": "Forbidden", + "content": { + "text/plain": { + "schema": { + "$ref": "#/components/schemas/ApiResponseOfObject" + } + }, + "application/json": { + "schema": { + "$ref": "#/components/schemas/ApiResponseOfObject" + } + }, + "text/json": { + "schema": { + "$ref": "#/components/schemas/ApiResponseOfObject" + } + } + } + } + }, + "security": [ + { + "ApiKey": [ ], + "TenantKey": [ ] + } + ] + }, + "post": { + "tags": [ + "Webhooks" + ], + "summary": "Crear un webhook", + "description": "Da de alta una URL que recibe los eventos suscritos del tenant (o de las personas indicadas). 201: el webhook con su secret whsec_…, la única respuesta que lo trae sin pedirlo; durante una suplantación no lo trae. 400: la URL no es https absoluta sin usuario ni contraseña, apunta a una dirección no permitida, un tipo de evento no existe, '*' va con otros tipos, una persona no es del tenant, son más de 100 personas o el tenant ya tiene 16 webhooks de URL. 401: sin API key o JWT válidos. 403: la persona no tiene el permiso Webhooks.Manage. 409: otro cambio a los webhooks del tenant está en curso; reintenta.", + "operationId": "createWebhook", + "parameters": [ + { + "name": "X-TIME-ZONE", + "in": "header", + "description": "Zona horaria IANA de las fechas sin zona del cuerpo JSON: el request las lee y la respuesta las escribe en ella. Si falta o no es válida, America/Mexico_City. No aplica a los campos que la operación documenta en UTC o en hora civil.", + "schema": { + "type": "string" + }, + "example": "America/Mexico_City" + } + ], + "requestBody": { + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/CreateWebhookCommand" + } + }, + "text/json": { + "schema": { + "$ref": "#/components/schemas/CreateWebhookCommand" + } + }, + "application/*+json": { + "schema": { + "$ref": "#/components/schemas/CreateWebhookCommand" + } + } + }, + "required": true + }, + "responses": { + "201": { + "description": "Created", + "content": { + "text/plain": { + "schema": { + "$ref": "#/components/schemas/ApiResponseOfWebhookWithSecretDto" + } + }, + "application/json": { + "schema": { + "$ref": "#/components/schemas/ApiResponseOfWebhookWithSecretDto" + } + }, + "text/json": { + "schema": { + "$ref": "#/components/schemas/ApiResponseOfWebhookWithSecretDto" + } + } + } + }, + "400": { + "description": "Bad Request", + "content": { + "text/plain": { + "schema": { + "$ref": "#/components/schemas/ApiResponseOfObject" + } + }, + "application/json": { + "schema": { + "$ref": "#/components/schemas/ApiResponseOfObject" + } + }, + "text/json": { + "schema": { + "$ref": "#/components/schemas/ApiResponseOfObject" + } + } + } + }, + "401": { + "description": "Unauthorized", + "content": { + "text/plain": { + "schema": { + "$ref": "#/components/schemas/ApiResponseOfObject" + } + }, + "application/json": { + "schema": { + "$ref": "#/components/schemas/ApiResponseOfObject" + } + }, + "text/json": { + "schema": { + "$ref": "#/components/schemas/ApiResponseOfObject" + } + } + } + }, + "403": { + "description": "Forbidden", + "content": { + "text/plain": { + "schema": { + "$ref": "#/components/schemas/ApiResponseOfObject" + } + }, + "application/json": { + "schema": { + "$ref": "#/components/schemas/ApiResponseOfObject" + } + }, + "text/json": { + "schema": { + "$ref": "#/components/schemas/ApiResponseOfObject" + } + } + } + }, + "409": { + "description": "Conflict", + "content": { + "text/plain": { + "schema": { + "$ref": "#/components/schemas/ApiResponseOfObject" + } + }, + "application/json": { + "schema": { + "$ref": "#/components/schemas/ApiResponseOfObject" + } + }, + "text/json": { + "schema": { + "$ref": "#/components/schemas/ApiResponseOfObject" + } + } + } + } + }, + "security": [ + { + "ApiKey": [ ], + "TenantKey": [ ] + } + ] + } + }, + "/api/v4/webhooks/{id}": { + "get": { + "tags": [ + "Webhooks" + ], + "summary": "Obtener un webhook", + "description": "Un webhook del tenant con su estado, sin su secreto. 401: sin API key o JWT válidos. 403: la persona no tiene el permiso Webhooks.Manage. 404: no existe un webhook con ese id en el tenant (una escucha de CLI solo existe para su persona y el owner), o el id no es un GUID.", + "operationId": "getWebhook", + "parameters": [ + { + "name": "id", + "in": "path", + "description": "Id del webhook.", + "required": true, + "schema": { + "type": "string", + "format": "uuid" + } + }, + { + "name": "X-TIME-ZONE", + "in": "header", + "description": "Zona horaria IANA de las fechas sin zona del cuerpo JSON: el request las lee y la respuesta las escribe en ella. Si falta o no es válida, America/Mexico_City. No aplica a los campos que la operación documenta en UTC o en hora civil.", + "schema": { + "type": "string" + }, + "example": "America/Mexico_City" + } + ], + "responses": { + "200": { + "description": "OK", + "content": { + "text/plain": { + "schema": { + "$ref": "#/components/schemas/ApiResponseOfWebhookDto" + } + }, + "application/json": { + "schema": { + "$ref": "#/components/schemas/ApiResponseOfWebhookDto" + } + }, + "text/json": { + "schema": { + "$ref": "#/components/schemas/ApiResponseOfWebhookDto" + } + } + } + }, + "401": { + "description": "Unauthorized", + "content": { + "text/plain": { + "schema": { + "$ref": "#/components/schemas/ApiResponseOfObject" + } + }, + "application/json": { + "schema": { + "$ref": "#/components/schemas/ApiResponseOfObject" + } + }, + "text/json": { + "schema": { + "$ref": "#/components/schemas/ApiResponseOfObject" + } + } + } + }, + "403": { + "description": "Forbidden", + "content": { + "text/plain": { + "schema": { + "$ref": "#/components/schemas/ApiResponseOfObject" + } + }, + "application/json": { + "schema": { + "$ref": "#/components/schemas/ApiResponseOfObject" + } + }, + "text/json": { + "schema": { + "$ref": "#/components/schemas/ApiResponseOfObject" + } + } + } + }, + "404": { + "description": "Not Found", + "content": { + "text/plain": { + "schema": { + "$ref": "#/components/schemas/ApiResponseOfObject" + } + }, + "application/json": { + "schema": { + "$ref": "#/components/schemas/ApiResponseOfObject" + } + }, + "text/json": { + "schema": { + "$ref": "#/components/schemas/ApiResponseOfObject" + } + } + } + } + }, + "security": [ + { + "ApiKey": [ ], + "TenantKey": [ ] + } + ] + }, + "put": { + "tags": [ + "Webhooks" + ], + "summary": "Reemplazar un webhook", + "description": "Reemplaza la URL, la descripción, los tipos de evento y las personas; status omitido conserva el estado. Habilitar uno deshabilitado limpia failingSince, disabledReason y disabledAt; deshabilitar cierra sus entregas pendientes y conserva el primer motivo. 400: los mismos del alta (salvo el tope), el id del cuerpo no es el de la ruta o es una escucha de CLI. 401: sin API key o JWT válidos. 403: la persona no tiene el permiso Webhooks.Manage. 404: no existe un webhook con ese id en el tenant (una escucha de CLI solo existe para su persona y el owner), o el id no es un GUID. 409: otro cambio a los webhooks del tenant está en curso; reintenta.", + "operationId": "updateWebhook", + "parameters": [ + { + "name": "id", + "in": "path", + "description": "Id del webhook.", + "required": true, + "schema": { + "type": "string", + "format": "uuid" + } + }, + { + "name": "X-TIME-ZONE", + "in": "header", + "description": "Zona horaria IANA de las fechas sin zona del cuerpo JSON: el request las lee y la respuesta las escribe en ella. Si falta o no es válida, America/Mexico_City. No aplica a los campos que la operación documenta en UTC o en hora civil.", + "schema": { + "type": "string" + }, + "example": "America/Mexico_City" + } + ], + "requestBody": { + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/UpdateWebhookCommand" + } + }, + "text/json": { + "schema": { + "$ref": "#/components/schemas/UpdateWebhookCommand" + } + }, + "application/*+json": { + "schema": { + "$ref": "#/components/schemas/UpdateWebhookCommand" + } + } + }, + "required": true + }, + "responses": { + "200": { + "description": "OK", + "content": { + "text/plain": { + "schema": { + "$ref": "#/components/schemas/ApiResponseOfWebhookDto" + } + }, + "application/json": { + "schema": { + "$ref": "#/components/schemas/ApiResponseOfWebhookDto" + } + }, + "text/json": { + "schema": { + "$ref": "#/components/schemas/ApiResponseOfWebhookDto" + } + } + } + }, + "400": { + "description": "Bad Request", + "content": { + "text/plain": { + "schema": { + "$ref": "#/components/schemas/ApiResponseOfObject" + } + }, + "application/json": { + "schema": { + "$ref": "#/components/schemas/ApiResponseOfObject" + } + }, + "text/json": { + "schema": { + "$ref": "#/components/schemas/ApiResponseOfObject" + } + } + } + }, + "401": { + "description": "Unauthorized", + "content": { + "text/plain": { + "schema": { + "$ref": "#/components/schemas/ApiResponseOfObject" + } + }, + "application/json": { + "schema": { + "$ref": "#/components/schemas/ApiResponseOfObject" + } + }, + "text/json": { + "schema": { + "$ref": "#/components/schemas/ApiResponseOfObject" + } + } + } + }, + "403": { + "description": "Forbidden", + "content": { + "text/plain": { + "schema": { + "$ref": "#/components/schemas/ApiResponseOfObject" + } + }, + "application/json": { + "schema": { + "$ref": "#/components/schemas/ApiResponseOfObject" + } + }, + "text/json": { + "schema": { + "$ref": "#/components/schemas/ApiResponseOfObject" + } + } + } + }, + "404": { + "description": "Not Found", + "content": { + "text/plain": { + "schema": { + "$ref": "#/components/schemas/ApiResponseOfObject" + } + }, + "application/json": { + "schema": { + "$ref": "#/components/schemas/ApiResponseOfObject" + } + }, + "text/json": { + "schema": { + "$ref": "#/components/schemas/ApiResponseOfObject" + } + } + } + }, + "409": { + "description": "Conflict", + "content": { + "text/plain": { + "schema": { + "$ref": "#/components/schemas/ApiResponseOfObject" + } + }, + "application/json": { + "schema": { + "$ref": "#/components/schemas/ApiResponseOfObject" + } + }, + "text/json": { + "schema": { + "$ref": "#/components/schemas/ApiResponseOfObject" + } + } + } + } + }, + "security": [ + { + "ApiKey": [ ], + "TenantKey": [ ] + } + ] + }, + "delete": { + "tags": [ + "Webhooks" + ], + "summary": "Borrar un webhook", + "description": "Da de baja el webhook (de URL o una escucha de CLI) y cierra sus entregas pendientes; su historial de entregas se conserva. 401: sin API key o JWT válidos. 403: la persona no tiene el permiso Webhooks.Manage. 404: no existe un webhook con ese id en el tenant (una escucha de CLI solo existe para su persona y el owner), o el id no es un GUID. 409: otro cambio a los webhooks del tenant está en curso; reintenta.", + "operationId": "deleteWebhook", + "parameters": [ + { + "name": "id", + "in": "path", + "description": "Id del webhook.", + "required": true, + "schema": { + "type": "string", + "format": "uuid" + } + }, + { + "name": "X-TIME-ZONE", + "in": "header", + "description": "Zona horaria IANA de las fechas sin zona del cuerpo JSON: el request las lee y la respuesta las escribe en ella. Si falta o no es válida, America/Mexico_City. No aplica a los campos que la operación documenta en UTC o en hora civil.", + "schema": { + "type": "string" + }, + "example": "America/Mexico_City" + } + ], + "responses": { + "200": { + "description": "OK", + "content": { + "text/plain": { + "schema": { + "$ref": "#/components/schemas/ApiResponseOfboolean" + } + }, + "application/json": { + "schema": { + "$ref": "#/components/schemas/ApiResponseOfboolean" + } + }, + "text/json": { + "schema": { + "$ref": "#/components/schemas/ApiResponseOfboolean" + } + } + } + }, + "401": { + "description": "Unauthorized", + "content": { + "text/plain": { + "schema": { + "$ref": "#/components/schemas/ApiResponseOfObject" + } + }, + "application/json": { + "schema": { + "$ref": "#/components/schemas/ApiResponseOfObject" + } + }, + "text/json": { + "schema": { + "$ref": "#/components/schemas/ApiResponseOfObject" + } + } + } + }, + "403": { + "description": "Forbidden", + "content": { + "text/plain": { + "schema": { + "$ref": "#/components/schemas/ApiResponseOfObject" + } + }, + "application/json": { + "schema": { + "$ref": "#/components/schemas/ApiResponseOfObject" + } + }, + "text/json": { + "schema": { + "$ref": "#/components/schemas/ApiResponseOfObject" + } + } + } + }, + "404": { + "description": "Not Found", + "content": { + "text/plain": { + "schema": { + "$ref": "#/components/schemas/ApiResponseOfObject" + } + }, + "application/json": { + "schema": { + "$ref": "#/components/schemas/ApiResponseOfObject" + } + }, + "text/json": { + "schema": { + "$ref": "#/components/schemas/ApiResponseOfObject" + } + } + } + }, + "409": { + "description": "Conflict", + "content": { + "text/plain": { + "schema": { + "$ref": "#/components/schemas/ApiResponseOfObject" + } + }, + "application/json": { + "schema": { + "$ref": "#/components/schemas/ApiResponseOfObject" + } + }, + "text/json": { + "schema": { + "$ref": "#/components/schemas/ApiResponseOfObject" + } + } + } + } + }, + "security": [ + { + "ApiKey": [ ], + "TenantKey": [ ] + } + ] + } + }, + "/api/v4/webhooks/{id}/deliveries": { + "get": { + "tags": [ + "Webhooks" + ], + "summary": "Listar las entregas de un webhook", + "description": "Las entregas del webhook, también si ya se dio de baja: el evento y su tipo, el origen (automatic, manual o test), el estado, los intentos y lo técnico del último (código HTTP, latencia, error y el primer KB de la respuesta). 400: una página fuera de rango. 401: sin API key o JWT válidos. 403: la persona no tiene el permiso Webhooks.Manage. 404: no existe un webhook con ese id en el tenant, ni dado de baja (una escucha de CLI solo existe para su persona y el owner), o el id no es un GUID.", + "operationId": "listWebhookDeliveries", + "parameters": [ + { + "name": "id", + "in": "path", + "description": "Id del webhook.", + "required": true, + "schema": { + "type": "string", + "format": "uuid" + } + }, + { + "name": "WebhookId", + "in": "query", + "schema": { + "type": "string" + } + }, + { + "name": "PageNumber", + "in": "query", + "description": "Número de página, desde 1 (por omisión 1).", + "schema": { + "pattern": "^-?(?:0|[1-9]\\d*)$", + "type": [ + "integer", + "string" + ], + "format": "int32" + } + }, + { + "name": "PageSize", + "in": "query", + "description": "Entregas por página, de 1 a 50 (por omisión 10).", + "schema": { + "pattern": "^-?(?:0|[1-9]\\d*)$", + "type": [ + "integer", + "string" + ], + "format": "int32" + } + }, + { + "name": "X-TIME-ZONE", + "in": "header", + "description": "Zona horaria IANA de las fechas sin zona del cuerpo JSON: el request las lee y la respuesta las escribe en ella. Si falta o no es válida, America/Mexico_City. No aplica a los campos que la operación documenta en UTC o en hora civil.", + "schema": { + "type": "string" + }, + "example": "America/Mexico_City" + } + ], + "responses": { + "200": { + "description": "OK", + "content": { + "text/plain": { + "schema": { + "$ref": "#/components/schemas/ApiResponseOfPagedListOfWebhookDeliveryDto" + } + }, + "application/json": { + "schema": { + "$ref": "#/components/schemas/ApiResponseOfPagedListOfWebhookDeliveryDto" + } + }, + "text/json": { + "schema": { + "$ref": "#/components/schemas/ApiResponseOfPagedListOfWebhookDeliveryDto" + } + } + } + }, + "400": { + "description": "Bad Request", + "content": { + "text/plain": { + "schema": { + "$ref": "#/components/schemas/ApiResponseOfObject" + } + }, + "application/json": { + "schema": { + "$ref": "#/components/schemas/ApiResponseOfObject" + } + }, + "text/json": { + "schema": { + "$ref": "#/components/schemas/ApiResponseOfObject" + } + } + } + }, + "401": { + "description": "Unauthorized", + "content": { + "text/plain": { + "schema": { + "$ref": "#/components/schemas/ApiResponseOfObject" + } + }, + "application/json": { + "schema": { + "$ref": "#/components/schemas/ApiResponseOfObject" + } + }, + "text/json": { + "schema": { + "$ref": "#/components/schemas/ApiResponseOfObject" + } + } + } + }, + "403": { + "description": "Forbidden", + "content": { + "text/plain": { + "schema": { + "$ref": "#/components/schemas/ApiResponseOfObject" + } + }, + "application/json": { + "schema": { + "$ref": "#/components/schemas/ApiResponseOfObject" + } + }, + "text/json": { + "schema": { + "$ref": "#/components/schemas/ApiResponseOfObject" + } + } + } + }, + "404": { + "description": "Not Found", + "content": { + "text/plain": { + "schema": { + "$ref": "#/components/schemas/ApiResponseOfObject" + } + }, + "application/json": { + "schema": { + "$ref": "#/components/schemas/ApiResponseOfObject" + } + }, + "text/json": { + "schema": { + "$ref": "#/components/schemas/ApiResponseOfObject" + } + } + } + } + }, + "security": [ + { + "ApiKey": [ ], + "TenantKey": [ ] + } + ] + } + }, + "/api/v4/webhooks/{id}/rotate-secret": { + "post": { + "tags": [ + "Webhooks" + ], + "summary": "Rotar el secreto de un webhook", + "description": "Genera un secreto de firma nuevo y lo responde en secret; el anterior sigue firmando cada entrega 24 horas para que el receptor cambie el suyo sin perder eventos. Durante una suplantación rota, pero no responde el secreto. 401: sin API key o JWT válidos. 403: la persona no tiene el permiso Webhooks.Manage. 404: no existe un webhook con ese id en el tenant (una escucha de CLI solo existe para su persona y el owner), o el id no es un GUID. 409: otro cambio a los webhooks del tenant está en curso; reintenta.", + "operationId": "rotateWebhookSecret", + "parameters": [ + { + "name": "id", + "in": "path", + "description": "Id del webhook.", + "required": true, + "schema": { + "type": "string", + "format": "uuid" + } + }, + { + "name": "X-TIME-ZONE", + "in": "header", + "description": "Zona horaria IANA de las fechas sin zona del cuerpo JSON: el request las lee y la respuesta las escribe en ella. Si falta o no es válida, America/Mexico_City. No aplica a los campos que la operación documenta en UTC o en hora civil.", + "schema": { + "type": "string" + }, + "example": "America/Mexico_City" + } + ], + "responses": { + "200": { + "description": "OK", + "content": { + "text/plain": { + "schema": { + "$ref": "#/components/schemas/ApiResponseOfWebhookWithSecretDto" + } + }, + "application/json": { + "schema": { + "$ref": "#/components/schemas/ApiResponseOfWebhookWithSecretDto" + } + }, + "text/json": { + "schema": { + "$ref": "#/components/schemas/ApiResponseOfWebhookWithSecretDto" + } + } + } + }, + "401": { + "description": "Unauthorized", + "content": { + "text/plain": { + "schema": { + "$ref": "#/components/schemas/ApiResponseOfObject" + } + }, + "application/json": { + "schema": { + "$ref": "#/components/schemas/ApiResponseOfObject" + } + }, + "text/json": { + "schema": { + "$ref": "#/components/schemas/ApiResponseOfObject" + } + } + } + }, + "403": { + "description": "Forbidden", + "content": { + "text/plain": { + "schema": { + "$ref": "#/components/schemas/ApiResponseOfObject" + } + }, + "application/json": { + "schema": { + "$ref": "#/components/schemas/ApiResponseOfObject" + } + }, + "text/json": { + "schema": { + "$ref": "#/components/schemas/ApiResponseOfObject" + } + } + } + }, + "404": { + "description": "Not Found", + "content": { + "text/plain": { + "schema": { + "$ref": "#/components/schemas/ApiResponseOfObject" + } + }, + "application/json": { + "schema": { + "$ref": "#/components/schemas/ApiResponseOfObject" + } + }, + "text/json": { + "schema": { + "$ref": "#/components/schemas/ApiResponseOfObject" + } + } + } + }, + "409": { + "description": "Conflict", + "content": { + "text/plain": { + "schema": { + "$ref": "#/components/schemas/ApiResponseOfObject" + } + }, + "application/json": { + "schema": { + "$ref": "#/components/schemas/ApiResponseOfObject" + } + }, + "text/json": { + "schema": { + "$ref": "#/components/schemas/ApiResponseOfObject" + } + } + } + } + }, + "security": [ + { + "ApiKey": [ ], + "TenantKey": [ ] + } + ] + } + }, + "/api/v4/webhooks/{id}/test": { + "post": { + "tags": [ + "Webhooks" + ], + "summary": "Probar un webhook", + "description": "Registra un evento webhook.test cuyo recurso es el webhook y lo entrega solo a él, firmado y en un solo intento, sin reintentos. 202: los ids del evento y de la entrega. 400: es una escucha de CLI o está deshabilitado. 401: sin API key o JWT válidos. 403: la persona no tiene el permiso Webhooks.Manage. 404: no existe un webhook con ese id en el tenant (una escucha de CLI solo existe para su persona y el owner), o el id no es un GUID. 429: demasiadas pruebas con la misma API key.", + "operationId": "testWebhook", + "parameters": [ + { + "name": "id", + "in": "path", + "description": "Id del webhook.", + "required": true, + "schema": { + "type": "string", + "format": "uuid" + } + }, + { + "name": "X-TIME-ZONE", + "in": "header", + "description": "Zona horaria IANA de las fechas sin zona del cuerpo JSON: el request las lee y la respuesta las escribe en ella. Si falta o no es válida, America/Mexico_City. No aplica a los campos que la operación documenta en UTC o en hora civil.", + "schema": { + "type": "string" + }, + "example": "America/Mexico_City" + } + ], + "responses": { + "202": { + "description": "Accepted", + "content": { + "text/plain": { + "schema": { + "$ref": "#/components/schemas/ApiResponseOfWebhookTestDto" + } + }, + "application/json": { + "schema": { + "$ref": "#/components/schemas/ApiResponseOfWebhookTestDto" + } + }, + "text/json": { + "schema": { + "$ref": "#/components/schemas/ApiResponseOfWebhookTestDto" + } + } + } + }, + "400": { + "description": "Bad Request", + "content": { + "text/plain": { + "schema": { + "$ref": "#/components/schemas/ApiResponseOfObject" + } + }, + "application/json": { + "schema": { + "$ref": "#/components/schemas/ApiResponseOfObject" + } + }, + "text/json": { + "schema": { + "$ref": "#/components/schemas/ApiResponseOfObject" + } + } + } + }, + "401": { + "description": "Unauthorized", + "content": { + "text/plain": { + "schema": { + "$ref": "#/components/schemas/ApiResponseOfObject" + } + }, + "application/json": { + "schema": { + "$ref": "#/components/schemas/ApiResponseOfObject" + } + }, + "text/json": { + "schema": { + "$ref": "#/components/schemas/ApiResponseOfObject" + } + } + } + }, + "403": { + "description": "Forbidden", + "content": { + "text/plain": { + "schema": { + "$ref": "#/components/schemas/ApiResponseOfObject" + } + }, + "application/json": { + "schema": { + "$ref": "#/components/schemas/ApiResponseOfObject" + } + }, + "text/json": { + "schema": { + "$ref": "#/components/schemas/ApiResponseOfObject" + } + } + } + }, + "404": { + "description": "Not Found", + "content": { + "text/plain": { + "schema": { + "$ref": "#/components/schemas/ApiResponseOfObject" + } + }, + "application/json": { + "schema": { + "$ref": "#/components/schemas/ApiResponseOfObject" + } + }, + "text/json": { + "schema": { + "$ref": "#/components/schemas/ApiResponseOfObject" + } + } + } + }, + "429": { + "description": "Too Many Requests", + "content": { + "text/plain": { + "schema": { + "$ref": "#/components/schemas/ApiResponseOfObject" + } + }, + "application/json": { + "schema": { + "$ref": "#/components/schemas/ApiResponseOfObject" + } + }, + "text/json": { + "schema": { + "$ref": "#/components/schemas/ApiResponseOfObject" + } + } + } + } + }, + "security": [ + { + "ApiKey": [ ], + "TenantKey": [ ] + } + ] + } + } + }, + "components": { + "schemas": { + "ApiKeyStatus": { + "type": "integer" + }, + "ApiResponseOfAutoInvoiceSettingDto": { + "type": "object", + "properties": { + "data": { + "oneOf": [ + { + "type": "null" + }, + { + "$ref": "#/components/schemas/AutoInvoiceSettingDto" + } + ] + }, + "succeeded": { + "type": "boolean" + }, + "message": { + "type": [ + "null", + "string" + ] + }, + "details": { + "type": [ + "null", + "string" + ] + }, + "httpStatusCode": { + "$ref": "#/components/schemas/HttpStatusCode" + }, + "traceIdentifier": { + "type": [ + "null", + "string" + ] + } + } + }, + "ApiResponseOfboolean": { + "type": "object", + "properties": { + "data": { + "type": "boolean" + }, + "succeeded": { + "type": "boolean" + }, + "message": { + "type": [ + "null", + "string" + ] + }, + "details": { + "type": [ + "null", + "string" + ] + }, + "httpStatusCode": { + "$ref": "#/components/schemas/HttpStatusCode" + }, + "traceIdentifier": { + "type": [ + "null", + "string" + ] + } + } + }, + "ApiResponseOfConsumptionReportDto": { + "type": "object", + "properties": { + "data": { + "oneOf": [ + { + "type": "null" + }, + { + "$ref": "#/components/schemas/ConsumptionReportDto" + } + ] + }, + "succeeded": { + "type": "boolean" + }, + "message": { + "type": [ + "null", + "string" + ] + }, + "details": { + "type": [ + "null", + "string" + ] + }, + "httpStatusCode": { + "$ref": "#/components/schemas/HttpStatusCode" + }, + "traceIdentifier": { + "type": [ + "null", + "string" + ] + } + } + }, + "ApiResponseOfExistingTicketData": { + "type": "object", + "properties": { + "data": { + "oneOf": [ + { + "type": "null" + }, + { + "$ref": "#/components/schemas/ExistingTicketData" + } + ] + }, + "succeeded": { + "type": "boolean" + }, + "message": { + "type": [ + "null", + "string" + ] + }, + "details": { + "type": [ + "null", + "string" + ] + }, + "httpStatusCode": { + "$ref": "#/components/schemas/HttpStatusCode" + }, + "traceIdentifier": { + "type": [ + "null", + "string" + ] + } + } + }, + "ApiResponseOfJsonElement": { + "type": "object", + "properties": { + "data": { + "$ref": "#/components/schemas/JsonElement" + }, + "succeeded": { + "type": "boolean" + }, + "message": { + "type": [ + "null", + "string" + ] + }, + "details": { + "type": [ + "null", + "string" + ] + }, + "httpStatusCode": { + "$ref": "#/components/schemas/HttpStatusCode" + }, + "traceIdentifier": { + "type": [ + "null", + "string" + ] + } + } + }, + "ApiResponseOfListOfPortalCfdiUseDto": { + "type": "object", + "properties": { + "data": { + "type": [ + "null", + "array" + ], + "items": { + "$ref": "#/components/schemas/PortalCfdiUseDto" + } + }, + "succeeded": { + "type": "boolean" + }, + "message": { + "type": [ + "null", + "string" + ] + }, + "details": { + "type": [ + "null", + "string" + ] + }, + "httpStatusCode": { + "$ref": "#/components/schemas/HttpStatusCode" + }, + "traceIdentifier": { + "type": [ + "null", + "string" + ] + } + } + }, + "ApiResponseOfListOfPortalTaxRegimeDto": { + "type": "object", + "properties": { + "data": { + "type": [ + "null", + "array" + ], + "items": { + "$ref": "#/components/schemas/PortalTaxRegimeDto" + } + }, + "succeeded": { + "type": "boolean" + }, + "message": { + "type": [ + "null", + "string" + ] + }, + "details": { + "type": [ + "null", + "string" + ] + }, + "httpStatusCode": { + "$ref": "#/components/schemas/HttpStatusCode" + }, + "traceIdentifier": { + "type": [ + "null", + "string" + ] + } + } + }, + "ApiResponseOfObject": { + "type": "object", + "properties": { + "data": { }, + "succeeded": { + "type": "boolean" + }, + "message": { + "type": [ + "null", + "string" + ] + }, + "details": { + "type": [ + "null", + "string" + ] + }, + "httpStatusCode": { + "$ref": "#/components/schemas/HttpStatusCode" + }, + "traceIdentifier": { + "type": [ + "null", + "string" + ] + } + } + }, + "ApiResponseOfPagedListOfJsonElement": { + "type": "object", + "properties": { + "data": { + "oneOf": [ + { + "type": "null" + }, + { + "$ref": "#/components/schemas/PagedListOfJsonElement" + } + ] + }, + "succeeded": { + "type": "boolean" + }, + "message": { + "type": [ + "null", + "string" + ] + }, + "details": { + "type": [ + "null", + "string" + ] + }, + "httpStatusCode": { + "$ref": "#/components/schemas/HttpStatusCode" + }, + "traceIdentifier": { + "type": [ + "null", + "string" + ] + } + } + }, + "ApiResponseOfPagedListOfTicketDto": { + "type": "object", + "properties": { + "data": { + "oneOf": [ + { + "type": "null" + }, + { + "$ref": "#/components/schemas/PagedListOfTicketDto" + } + ] + }, + "succeeded": { + "type": "boolean" + }, + "message": { + "type": [ + "null", + "string" + ] + }, + "details": { + "type": [ + "null", + "string" + ] + }, + "httpStatusCode": { + "$ref": "#/components/schemas/HttpStatusCode" + }, + "traceIdentifier": { + "type": [ + "null", + "string" + ] + } + } + }, + "ApiResponseOfPagedListOfWebhookDeliveryDto": { + "type": "object", + "properties": { + "data": { + "oneOf": [ + { + "type": "null" + }, + { + "$ref": "#/components/schemas/PagedListOfWebhookDeliveryDto" + } + ] + }, + "succeeded": { + "type": "boolean" + }, + "message": { + "type": [ + "null", + "string" + ] + }, + "details": { + "type": [ + "null", + "string" + ] + }, + "httpStatusCode": { + "$ref": "#/components/schemas/HttpStatusCode" + }, + "traceIdentifier": { + "type": [ + "null", + "string" + ] + } + } + }, + "ApiResponseOfPagedListOfWebhookDto": { + "type": "object", + "properties": { + "data": { + "oneOf": [ + { + "type": "null" + }, + { + "$ref": "#/components/schemas/PagedListOfWebhookDto" + } + ] + }, + "succeeded": { + "type": "boolean" + }, + "message": { + "type": [ + "null", + "string" + ] + }, + "details": { + "type": [ + "null", + "string" + ] + }, + "httpStatusCode": { + "$ref": "#/components/schemas/HttpStatusCode" + }, + "traceIdentifier": { + "type": [ + "null", + "string" + ] + } + } + }, + "ApiResponseOfPortalBrandingDto": { + "type": "object", + "properties": { + "data": { + "oneOf": [ + { + "type": "null" + }, + { + "$ref": "#/components/schemas/PortalBrandingDto" + } + ] + }, + "succeeded": { + "type": "boolean" + }, + "message": { + "type": [ + "null", + "string" + ] + }, + "details": { + "type": [ + "null", + "string" + ] + }, + "httpStatusCode": { + "$ref": "#/components/schemas/HttpStatusCode" + }, + "traceIdentifier": { + "type": [ + "null", + "string" + ] + } + } + }, + "ApiResponseOfPortalInvoiceDto": { + "type": "object", + "properties": { + "data": { + "oneOf": [ + { + "type": "null" + }, + { + "$ref": "#/components/schemas/PortalInvoiceDto" + } + ] + }, + "succeeded": { + "type": "boolean" + }, + "message": { + "type": [ + "null", + "string" + ] + }, + "details": { + "type": [ + "null", + "string" + ] + }, + "httpStatusCode": { + "$ref": "#/components/schemas/HttpStatusCode" + }, + "traceIdentifier": { + "type": [ + "null", + "string" + ] + } + } + }, + "ApiResponseOfPortalNameAvailabilityDto": { + "type": "object", + "properties": { + "data": { + "oneOf": [ + { + "type": "null" + }, + { + "$ref": "#/components/schemas/PortalNameAvailabilityDto" + } + ] + }, + "succeeded": { + "type": "boolean" + }, + "message": { + "type": [ + "null", + "string" + ] + }, + "details": { + "type": [ + "null", + "string" + ] + }, + "httpStatusCode": { + "$ref": "#/components/schemas/HttpStatusCode" + }, + "traceIdentifier": { + "type": [ + "null", + "string" + ] + } + } + }, + "ApiResponseOfPortalNameDto": { + "type": "object", + "properties": { + "data": { + "oneOf": [ + { + "type": "null" + }, + { + "$ref": "#/components/schemas/PortalNameDto" + } + ] + }, + "succeeded": { + "type": "boolean" + }, + "message": { + "type": [ + "null", + "string" + ] + }, + "details": { + "type": [ + "null", + "string" + ] + }, + "httpStatusCode": { + "$ref": "#/components/schemas/HttpStatusCode" + }, + "traceIdentifier": { + "type": [ + "null", + "string" + ] + } + } + }, + "ApiResponseOfPortalRecoveryDto": { + "type": "object", + "properties": { + "data": { + "oneOf": [ + { + "type": "null" + }, + { + "$ref": "#/components/schemas/PortalRecoveryDto" + } + ] + }, + "succeeded": { + "type": "boolean" + }, + "message": { + "type": [ + "null", + "string" + ] + }, + "details": { + "type": [ + "null", + "string" + ] + }, + "httpStatusCode": { + "$ref": "#/components/schemas/HttpStatusCode" + }, + "traceIdentifier": { + "type": [ + "null", + "string" + ] + } + } + }, + "ApiResponseOfPortalResendDto": { + "type": "object", + "properties": { + "data": { + "oneOf": [ + { + "type": "null" + }, + { + "$ref": "#/components/schemas/PortalResendDto" + } + ] + }, + "succeeded": { + "type": "boolean" + }, + "message": { + "type": [ + "null", + "string" + ] + }, + "details": { + "type": [ + "null", + "string" + ] + }, + "httpStatusCode": { + "$ref": "#/components/schemas/HttpStatusCode" + }, + "traceIdentifier": { + "type": [ + "null", + "string" + ] + } + } + }, + "ApiResponseOfPortalTicketDto": { + "type": "object", + "properties": { + "data": { + "oneOf": [ + { + "type": "null" + }, + { + "$ref": "#/components/schemas/PortalTicketDto" + } + ] + }, + "succeeded": { + "type": "boolean" + }, + "message": { + "type": [ + "null", + "string" + ] + }, + "details": { + "type": [ + "null", + "string" + ] + }, + "httpStatusCode": { + "$ref": "#/components/schemas/HttpStatusCode" + }, + "traceIdentifier": { + "type": [ + "null", + "string" + ] + } + } + }, + "ApiResponseOfTicketCodeDto": { + "type": "object", + "properties": { + "data": { + "oneOf": [ + { + "type": "null" + }, + { + "$ref": "#/components/schemas/TicketCodeDto" + } + ] + }, + "succeeded": { + "type": "boolean" + }, + "message": { + "type": [ + "null", + "string" + ] + }, + "details": { + "type": [ + "null", + "string" + ] + }, + "httpStatusCode": { + "$ref": "#/components/schemas/HttpStatusCode" + }, + "traceIdentifier": { + "type": [ + "null", + "string" + ] + } + } + }, + "ApiResponseOfTicketDto": { + "type": "object", + "properties": { + "data": { + "oneOf": [ + { + "type": "null" + }, + { + "$ref": "#/components/schemas/TicketDto" + } + ] + }, + "succeeded": { + "type": "boolean" + }, + "message": { + "type": [ + "null", + "string" + ] + }, + "details": { + "type": [ + "null", + "string" + ] + }, + "httpStatusCode": { + "$ref": "#/components/schemas/HttpStatusCode" + }, + "traceIdentifier": { + "type": [ + "null", + "string" + ] + } + } + }, + "ApiResponseOfTicketIngestedDto": { + "type": "object", + "properties": { + "data": { + "oneOf": [ + { + "type": "null" + }, + { + "$ref": "#/components/schemas/TicketIngestedDto" + } + ] + }, + "succeeded": { + "type": "boolean" + }, + "message": { + "type": [ + "null", + "string" + ] + }, + "details": { + "type": [ + "null", + "string" + ] + }, + "httpStatusCode": { + "$ref": "#/components/schemas/HttpStatusCode" + }, + "traceIdentifier": { + "type": [ + "null", + "string" + ] + } + } + }, + "ApiResponseOfWebhookDto": { + "type": "object", + "properties": { + "data": { + "oneOf": [ + { + "type": "null" + }, + { + "$ref": "#/components/schemas/WebhookDto" + } + ] + }, + "succeeded": { + "type": "boolean" + }, + "message": { + "type": [ + "null", + "string" + ] + }, + "details": { + "type": [ + "null", + "string" + ] + }, + "httpStatusCode": { + "$ref": "#/components/schemas/HttpStatusCode" + }, + "traceIdentifier": { + "type": [ + "null", + "string" + ] + } + } + }, + "ApiResponseOfWebhookEventResendDto": { + "type": "object", + "properties": { + "data": { + "oneOf": [ + { + "type": "null" + }, + { + "$ref": "#/components/schemas/WebhookEventResendDto" + } + ] + }, + "succeeded": { + "type": "boolean" + }, + "message": { + "type": [ + "null", + "string" + ] + }, + "details": { + "type": [ + "null", + "string" + ] + }, + "httpStatusCode": { + "$ref": "#/components/schemas/HttpStatusCode" + }, + "traceIdentifier": { + "type": [ + "null", + "string" + ] + } + } + }, + "ApiResponseOfWebhookEventTriggerDto": { + "type": "object", + "properties": { + "data": { + "oneOf": [ + { + "type": "null" + }, + { + "$ref": "#/components/schemas/WebhookEventTriggerDto" + } + ] + }, + "succeeded": { + "type": "boolean" + }, + "message": { + "type": [ + "null", + "string" + ] + }, + "details": { + "type": [ + "null", + "string" + ] + }, + "httpStatusCode": { + "$ref": "#/components/schemas/HttpStatusCode" + }, + "traceIdentifier": { + "type": [ + "null", + "string" + ] + } + } + }, + "ApiResponseOfWebhookTestDto": { + "type": "object", + "properties": { + "data": { + "oneOf": [ + { + "type": "null" + }, + { + "$ref": "#/components/schemas/WebhookTestDto" + } + ] + }, + "succeeded": { + "type": "boolean" + }, + "message": { + "type": [ + "null", + "string" + ] + }, + "details": { + "type": [ + "null", + "string" + ] + }, + "httpStatusCode": { + "$ref": "#/components/schemas/HttpStatusCode" + }, + "traceIdentifier": { + "type": [ + "null", + "string" + ] + } + } + }, + "ApiResponseOfWebhookWithSecretDto": { + "type": "object", + "properties": { + "data": { + "oneOf": [ + { + "type": "null" + }, + { + "$ref": "#/components/schemas/WebhookWithSecretDto" + } + ] + }, + "succeeded": { + "type": "boolean" + }, + "message": { + "type": [ + "null", + "string" + ] + }, + "details": { + "type": [ + "null", + "string" + ] + }, + "httpStatusCode": { + "$ref": "#/components/schemas/HttpStatusCode" + }, + "traceIdentifier": { + "type": [ + "null", + "string" + ] + } + } + }, + "AutoInvoiceSettingDto": { + "required": [ + "allowBillingOutsideMonth", + "allowBillingOutsideYear", + "privacyNoticeUrl" + ], + "type": "object", + "properties": { + "allowBillingOutsideMonth": { + "type": "boolean" + }, + "allowBillingOutsideYear": { + "type": "boolean" + }, + "privacyNoticeUrl": { + "type": [ + "null", + "string" + ] + } + } + }, + "BalanceCompensationRequest": { + "type": "object", + "properties": { + "favorableBalance": { + "pattern": "^-?(?:0|[1-9]\\d*)(?:\\.\\d+)?$", + "type": [ + "number", + "string" + ], + "format": "double" + }, + "year": { + "pattern": "^-?(?:0|[1-9]\\d*)$", + "type": [ + "integer", + "string" + ], + "format": "int16" + }, + "remainingFavorableBalance": { + "pattern": "^-?(?:0|[1-9]\\d*)(?:\\.\\d+)?$", + "type": [ + "number", + "string" + ], + "format": "double" + } + } + }, + "CancelInvoiceCommand": { + "type": "object", + "properties": { + "id": { + "type": [ + "null", + "string" + ] + }, + "invoiceUuid": { + "type": [ + "null", + "string" + ] + }, + "tin": { + "type": [ + "null", + "string" + ] + }, + "cancellationReasonCode": { + "type": [ + "null", + "string" + ] + }, + "replacementUuid": { + "type": [ + "null", + "string" + ] + }, + "taxCredentials": { + "type": [ + "null", + "array" + ], + "items": { + "$ref": "#/components/schemas/CredentialInputModel" + } + } + } + }, + "CartaPorteAutotransporteRequest": { + "type": "object", + "properties": { + "permSCTId": { + "type": "string" + }, + "numPermisoSCT": { + "type": "string" + }, + "configVehicularId": { + "type": "string" + }, + "pesoBrutoVehicular": { + "pattern": "^-?(?:0|[1-9]\\d*)(?:\\.\\d+)?$", + "type": [ + "number", + "string" + ], + "format": "double" + }, + "placaVM": { + "type": "string" + }, + "anioModeloVM": { + "pattern": "^-?(?:0|[1-9]\\d*)$", + "type": [ + "integer", + "string" + ], + "format": "int32" + }, + "aseguraRespCivil": { + "type": "string" + }, + "polizaRespCivil": { + "type": "string" + }, + "aseguraMedAmbiente": { + "type": [ + "null", + "string" + ] + }, + "polizaMedAmbiente": { + "type": [ + "null", + "string" + ] + }, + "aseguraCarga": { + "type": [ + "null", + "string" + ] + }, + "polizaCarga": { + "type": [ + "null", + "string" + ] + }, + "primaSeguro": { + "pattern": "^-?(?:0|[1-9]\\d*)(?:\\.\\d+)?$", + "type": [ + "null", + "number", + "string" + ], + "format": "double" + }, + "remolques": { + "type": [ + "null", + "array" + ], + "items": { + "$ref": "#/components/schemas/CartaPorteRemolqueRequest" + } + } + } + }, + "CartaPorteCantidadTransportaRequest": { + "type": "object", + "properties": { + "cantidad": { + "pattern": "^-?(?:0|[1-9]\\d*)(?:\\.\\d+)?$", + "type": [ + "number", + "string" + ], + "format": "double" + }, + "idOrigen": { + "type": "string" + }, + "idDestino": { + "type": "string" + }, + "cvesTransporteId": { + "type": [ + "null", + "string" + ] + } + } + }, + "CartaPorteCarroContenedorRequest": { + "type": "object", + "properties": { + "tipoContenedorId": { + "type": "string" + }, + "pesoContenedorVacio": { + "pattern": "^-?(?:0|[1-9]\\d*)(?:\\.\\d+)?$", + "type": [ + "number", + "string" + ], + "format": "double" + }, + "pesoNetoMercancia": { + "pattern": "^-?(?:0|[1-9]\\d*)(?:\\.\\d+)?$", + "type": [ + "number", + "string" + ], + "format": "double" + } + } + }, + "CartaPorteCarroRequest": { + "type": "object", + "properties": { + "tipoCarroId": { + "type": "string" + }, + "matriculaCarro": { + "type": "string" + }, + "guiaCarro": { + "type": "string" + }, + "toneladasNetasCarro": { + "pattern": "^-?(?:0|[1-9]\\d*)(?:\\.\\d+)?$", + "type": [ + "number", + "string" + ], + "format": "double" + }, + "contenedores": { + "type": [ + "null", + "array" + ], + "items": { + "$ref": "#/components/schemas/CartaPorteCarroContenedorRequest" + } + } + } + }, + "CartaPorteComplementRequest": { + "type": "object", + "properties": { + "transpInternacId": { + "type": "string" + }, + "entradaSalidaMercId": { + "type": [ + "null", + "string" + ] + }, + "paisOrigenDestinoId": { + "type": [ + "null", + "string" + ] + }, + "viaEntradaSalidaId": { + "type": [ + "null", + "string" + ] + }, + "totalDistRec": { + "pattern": "^-?(?:0|[1-9]\\d*)(?:\\.\\d+)?$", + "type": [ + "null", + "number", + "string" + ], + "format": "double" + }, + "registroISTMOId": { + "type": [ + "null", + "string" + ] + }, + "ubicacionPoloOrigenId": { + "type": [ + "null", + "string" + ] + }, + "ubicacionPoloDestinoId": { + "type": [ + "null", + "string" + ] + }, + "unidadPesoId": { + "type": "string" + }, + "pesoNetoTotal": { + "pattern": "^-?(?:0|[1-9]\\d*)(?:\\.\\d+)?$", + "type": [ + "null", + "number", + "string" + ], + "format": "double" + }, + "cargoPorTasacion": { + "pattern": "^-?(?:0|[1-9]\\d*)(?:\\.\\d+)?$", + "type": [ + "null", + "number", + "string" + ], + "format": "double" + }, + "logisticaInversaRecoleccionDevolucionId": { + "type": [ + "null", + "string" + ] + }, + "regimenAduaneros": { + "type": [ + "null", + "array" + ], + "items": { + "$ref": "#/components/schemas/CartaPorteRegimenAduaneroRequest" + } + }, + "ubicaciones": { + "type": "array", + "items": { + "$ref": "#/components/schemas/CartaPorteUbicacionRequest" + } + }, + "mercancias": { + "type": "array", + "items": { + "$ref": "#/components/schemas/CartaPorteMercanciaRequest" + } + }, + "autotransporte": { + "oneOf": [ + { + "type": "null" + }, + { + "$ref": "#/components/schemas/CartaPorteAutotransporteRequest" + } + ] + }, + "transporteMaritimo": { + "oneOf": [ + { + "type": "null" + }, + { + "$ref": "#/components/schemas/CartaPorteTransporteMaritimoRequest" + } + ] + }, + "transporteAereo": { + "oneOf": [ + { + "type": "null" + }, + { + "$ref": "#/components/schemas/CartaPorteTransporteAereoRequest" + } + ] + }, + "transporteFerroviario": { + "oneOf": [ + { + "type": "null" + }, + { + "$ref": "#/components/schemas/CartaPorteTransporteFerroviarioRequest" + } + ] + }, + "tiposFigura": { + "type": [ + "null", + "array" + ], + "items": { + "$ref": "#/components/schemas/CartaPorteTiposFiguraRequest" + } + } + } + }, + "CartaPorteContenedorMaritimoRequest": { + "type": "object", + "properties": { + "tipoContenedorId": { + "type": "string" + }, + "matriculaContenedor": { + "type": [ + "null", + "string" + ] + }, + "numPrecinto": { + "type": [ + "null", + "string" + ] + }, + "idCCPRelacionado": { + "type": [ + "null", + "string" + ] + }, + "placaVMCCP": { + "type": [ + "null", + "string" + ] + }, + "fechaCertificacionCCP": { + "type": [ + "null", + "string" + ], + "format": "date-time" + }, + "remolquesCCP": { + "type": [ + "null", + "array" + ], + "items": { + "$ref": "#/components/schemas/CartaPorteRemolqueCCPMaritimoRequest" + } + } + } + }, + "CartaPorteDerechosDePasoRequest": { + "type": "object", + "properties": { + "tipoDerechoDePasoId": { + "type": "string" + }, + "kilometrajePagado": { + "pattern": "^-?(?:0|[1-9]\\d*)(?:\\.\\d+)?$", + "type": [ + "number", + "string" + ], + "format": "double" + } + } + }, + "CartaPorteDetalleMercanciaRequest": { + "type": "object", + "properties": { + "unidadPesoMercId": { + "type": "string" + }, + "pesoBruto": { + "pattern": "^-?(?:0|[1-9]\\d*)(?:\\.\\d+)?$", + "type": [ + "number", + "string" + ], + "format": "double" + }, + "pesoNeto": { + "pattern": "^-?(?:0|[1-9]\\d*)(?:\\.\\d+)?$", + "type": [ + "number", + "string" + ], + "format": "double" + }, + "pesoTara": { + "pattern": "^-?(?:0|[1-9]\\d*)(?:\\.\\d+)?$", + "type": [ + "number", + "string" + ], + "format": "double" + }, + "numPiezas": { + "pattern": "^-?(?:0|[1-9]\\d*)$", + "type": [ + "null", + "integer", + "string" + ], + "format": "int32" + } + } + }, + "CartaPorteDocumentacionAduaneraRequest": { + "type": "object", + "properties": { + "tipoDocumentoId": { + "type": "string" + }, + "numPedimento": { + "type": [ + "null", + "string" + ] + }, + "identDocAduanero": { + "type": [ + "null", + "string" + ] + }, + "rfcImpo": { + "type": [ + "null", + "string" + ] + } + } + }, + "CartaPorteGuiaIdentificacionRequest": { + "type": "object", + "properties": { + "numeroGuiaIdentificacion": { + "type": "string" + }, + "descripGuiaIdentificacion": { + "type": "string" + }, + "pesoGuiaIdentificacion": { + "pattern": "^-?(?:0|[1-9]\\d*)(?:\\.\\d+)?$", + "type": [ + "number", + "string" + ], + "format": "double" + } + } + }, + "CartaPorteMercanciaRequest": { + "type": "object", + "properties": { + "bienesTranspId": { + "type": "string" + }, + "claveSTCCId": { + "type": [ + "null", + "string" + ] + }, + "descripcion": { + "type": "string" + }, + "cantidad": { + "pattern": "^-?(?:0|[1-9]\\d*)(?:\\.\\d+)?$", + "type": [ + "number", + "string" + ], + "format": "double" + }, + "claveUnidadId": { + "type": "string" + }, + "unidad": { + "type": [ + "null", + "string" + ] + }, + "dimensiones": { + "type": [ + "null", + "string" + ] + }, + "materialPeligrosoId": { + "type": [ + "null", + "string" + ] + }, + "cveMaterialPeligrosoId": { + "type": [ + "null", + "string" + ] + }, + "embalajeId": { + "type": [ + "null", + "string" + ] + }, + "descripEmbalaje": { + "type": [ + "null", + "string" + ] + }, + "sectorCOFEPRISId": { + "type": [ + "null", + "string" + ] + }, + "nombreIngredienteActivo": { + "type": [ + "null", + "string" + ] + }, + "nomQuimico": { + "type": [ + "null", + "string" + ] + }, + "denominacionGenericaProd": { + "type": [ + "null", + "string" + ] + }, + "denominacionDistintivaProd": { + "type": [ + "null", + "string" + ] + }, + "fabricante": { + "type": [ + "null", + "string" + ] + }, + "fechaCaducidad": { + "type": [ + "null", + "string" + ], + "format": "date-time" + }, + "loteMedicamento": { + "type": [ + "null", + "string" + ] + }, + "formaFarmaceuticaId": { + "type": [ + "null", + "string" + ] + }, + "condicionesEspTranspId": { + "type": [ + "null", + "string" + ] + }, + "registroSanitarioFolioAutorizacion": { + "type": [ + "null", + "string" + ] + }, + "permisoImportacion": { + "type": [ + "null", + "string" + ] + }, + "folioImpoVUCEM": { + "type": [ + "null", + "string" + ] + }, + "numCAS": { + "type": [ + "null", + "string" + ] + }, + "razonSocialEmpImp": { + "type": [ + "null", + "string" + ] + }, + "numRegSanPlagCOFEPRIS": { + "type": [ + "null", + "string" + ] + }, + "datosFabricante": { + "type": [ + "null", + "string" + ] + }, + "datosFormulador": { + "type": [ + "null", + "string" + ] + }, + "datosMaquilador": { + "type": [ + "null", + "string" + ] + }, + "usoAutorizado": { + "type": [ + "null", + "string" + ] + }, + "pesoEnKg": { + "pattern": "^-?(?:0|[1-9]\\d*)(?:\\.\\d+)?$", + "type": [ + "number", + "string" + ], + "format": "double" + }, + "valorMercancia": { + "pattern": "^-?(?:0|[1-9]\\d*)(?:\\.\\d+)?$", + "type": [ + "null", + "number", + "string" + ], + "format": "double" + }, + "monedaId": { + "type": [ + "null", + "string" + ] + }, + "fraccionArancelariaId": { + "type": [ + "null", + "string" + ] + }, + "uuidComercioExt": { + "type": [ + "null", + "string" + ] + }, + "tipoMateriaId": { + "type": [ + "null", + "string" + ] + }, + "descripcionMateria": { + "type": [ + "null", + "string" + ] + }, + "documentacionAduanera": { + "type": [ + "null", + "array" + ], + "items": { + "$ref": "#/components/schemas/CartaPorteDocumentacionAduaneraRequest" + } + }, + "guiasIdentificacion": { + "type": [ + "null", + "array" + ], + "items": { + "$ref": "#/components/schemas/CartaPorteGuiaIdentificacionRequest" + } + }, + "cantidadTransporta": { + "type": [ + "null", + "array" + ], + "items": { + "$ref": "#/components/schemas/CartaPorteCantidadTransportaRequest" + } + }, + "detalleMercancia": { + "oneOf": [ + { + "type": "null" + }, + { + "$ref": "#/components/schemas/CartaPorteDetalleMercanciaRequest" + } + ] + } + } + }, + "CartaPortePartesTransporteRequest": { + "type": "object", + "properties": { + "parteTransporteId": { + "type": "string" + } + } + }, + "CartaPorteRegimenAduaneroRequest": { + "type": "object", + "properties": { + "regimenAduaneroId": { + "type": "string" + } + } + }, + "CartaPorteRemolqueCCPMaritimoRequest": { + "type": "object", + "properties": { + "subTipoRemCCPId": { + "type": "string" + }, + "placaCCP": { + "type": "string" + } + } + }, + "CartaPorteRemolqueRequest": { + "type": "object", + "properties": { + "subTipoRemId": { + "type": "string" + }, + "placa": { + "type": "string" + } + } + }, + "CartaPorteTiposFiguraDomicilioRequest": { + "type": "object", + "properties": { + "calle": { + "type": [ + "null", + "string" + ] + }, + "numeroExterior": { + "type": [ + "null", + "string" + ] + }, + "numeroInterior": { + "type": [ + "null", + "string" + ] + }, + "coloniaId": { + "type": [ + "null", + "string" + ] + }, + "localidadId": { + "type": [ + "null", + "string" + ] + }, + "referencia": { + "type": [ + "null", + "string" + ] + }, + "municipioId": { + "type": [ + "null", + "string" + ] + }, + "estadoId": { + "type": "string" + }, + "paisId": { + "type": "string" + }, + "codigoPostalId": { + "type": "string" + } + } + }, + "CartaPorteTiposFiguraRequest": { + "type": "object", + "properties": { + "tipoFiguraId": { + "type": "string" + }, + "rfcFigura": { + "type": [ + "null", + "string" + ] + }, + "numLicencia": { + "type": [ + "null", + "string" + ] + }, + "nombreFigura": { + "type": "string" + }, + "numRegIdTribFigura": { + "type": [ + "null", + "string" + ] + }, + "residenciaFiscalFiguraId": { + "type": [ + "null", + "string" + ] + }, + "partesTransporte": { + "type": [ + "null", + "array" + ], + "items": { + "$ref": "#/components/schemas/CartaPortePartesTransporteRequest" + } + }, + "domicilio": { + "oneOf": [ + { + "type": "null" + }, + { + "$ref": "#/components/schemas/CartaPorteTiposFiguraDomicilioRequest" + } + ] + } + } + }, + "CartaPorteTransporteAereoRequest": { + "type": "object", + "properties": { + "permSCTId": { + "type": "string" + }, + "numPermisoSCT": { + "type": "string" + }, + "matriculaAeronave": { + "type": [ + "null", + "string" + ] + }, + "nombreAseg": { + "type": [ + "null", + "string" + ] + }, + "numPolizaSeguro": { + "type": [ + "null", + "string" + ] + }, + "numeroGuia": { + "type": "string" + }, + "lugarContrato": { + "type": [ + "null", + "string" + ] + }, + "codigoTransportistaId": { + "type": "string" + }, + "rfcEmbarcador": { + "type": [ + "null", + "string" + ] + }, + "numRegIdTribEmbarc": { + "type": [ + "null", + "string" + ] + }, + "residenciaFiscalEmbarcId": { + "type": [ + "null", + "string" + ] + }, + "nombreEmbarcador": { + "type": [ + "null", + "string" + ] + } + } + }, + "CartaPorteTransporteFerroviarioRequest": { + "type": "object", + "properties": { + "tipoDeServicioId": { + "type": "string" + }, + "tipoDeTraficoId": { + "type": "string" + }, + "nombreAseg": { + "type": [ + "null", + "string" + ] + }, + "numPolizaSeguro": { + "type": [ + "null", + "string" + ] + }, + "derechosDePaso": { + "type": [ + "null", + "array" + ], + "items": { + "$ref": "#/components/schemas/CartaPorteDerechosDePasoRequest" + } + }, + "carros": { + "type": "array", + "items": { + "$ref": "#/components/schemas/CartaPorteCarroRequest" + } + } + } + }, + "CartaPorteTransporteMaritimoRequest": { + "type": "object", + "properties": { + "permSCTId": { + "type": [ + "null", + "string" + ] + }, + "numPermisoSCT": { + "type": [ + "null", + "string" + ] + }, + "nombreAseg": { + "type": [ + "null", + "string" + ] + }, + "numPolizaSeguro": { + "type": [ + "null", + "string" + ] + }, + "tipoEmbarcacionId": { + "type": "string" + }, + "matricula": { + "type": "string" + }, + "numeroOMI": { + "type": "string" + }, + "anioEmbarcacion": { + "pattern": "^-?(?:0|[1-9]\\d*)$", + "type": [ + "null", + "integer", + "string" + ], + "format": "int32" + }, + "nombreEmbarc": { + "type": [ + "null", + "string" + ] + }, + "nacionalidadEmbarcId": { + "type": "string" + }, + "unidadesDeArqBruto": { + "pattern": "^-?(?:0|[1-9]\\d*)(?:\\.\\d+)?$", + "type": [ + "number", + "string" + ], + "format": "double" + }, + "tipoCargaId": { + "type": "string" + }, + "eslora": { + "pattern": "^-?(?:0|[1-9]\\d*)(?:\\.\\d+)?$", + "type": [ + "null", + "number", + "string" + ], + "format": "double" + }, + "manga": { + "pattern": "^-?(?:0|[1-9]\\d*)(?:\\.\\d+)?$", + "type": [ + "null", + "number", + "string" + ], + "format": "double" + }, + "calado": { + "pattern": "^-?(?:0|[1-9]\\d*)(?:\\.\\d+)?$", + "type": [ + "null", + "number", + "string" + ], + "format": "double" + }, + "puntal": { + "pattern": "^-?(?:0|[1-9]\\d*)(?:\\.\\d+)?$", + "type": [ + "null", + "number", + "string" + ], + "format": "double" + }, + "lineaNaviera": { + "type": [ + "null", + "string" + ] + }, + "nombreAgenteNaviero": { + "type": "string" + }, + "numAutorizacionNavieroId": { + "type": "string" + }, + "numViaje": { + "type": [ + "null", + "string" + ] + }, + "numConocEmbarc": { + "type": [ + "null", + "string" + ] + }, + "permisoTempNavegacion": { + "type": [ + "null", + "string" + ] + }, + "contenedores": { + "type": [ + "null", + "array" + ], + "items": { + "$ref": "#/components/schemas/CartaPorteContenedorMaritimoRequest" + } + } + } + }, + "CartaPorteUbicacionDomicilioRequest": { + "type": "object", + "properties": { + "calle": { + "type": [ + "null", + "string" + ] + }, + "numeroExterior": { + "type": [ + "null", + "string" + ] + }, + "numeroInterior": { + "type": [ + "null", + "string" + ] + }, + "coloniaId": { + "type": [ + "null", + "string" + ] + }, + "localidadId": { + "type": [ + "null", + "string" + ] + }, + "referencia": { + "type": [ + "null", + "string" + ] + }, + "municipioId": { + "type": [ + "null", + "string" + ] + }, + "estadoId": { + "type": "string" + }, + "paisId": { + "type": "string" + }, + "codigoPostalId": { + "type": "string" + } + } + }, + "CartaPorteUbicacionRequest": { + "type": "object", + "properties": { + "tipoUbicacion": { + "type": "string" + }, + "idUbicacion": { + "type": [ + "null", + "string" + ] + }, + "rfcRemitenteDestinatario": { + "type": "string" + }, + "nombreRemitenteDestinatario": { + "type": [ + "null", + "string" + ] + }, + "numRegIdTrib": { + "type": [ + "null", + "string" + ] + }, + "residenciaFiscalId": { + "type": [ + "null", + "string" + ] + }, + "numEstacionId": { + "type": [ + "null", + "string" + ] + }, + "nombreEstacion": { + "type": [ + "null", + "string" + ] + }, + "navegacionTraficoId": { + "type": [ + "null", + "string" + ] + }, + "fechaHoraSalidaLlegada": { + "type": "string", + "format": "date-time" + }, + "tipoEstacionId": { + "type": [ + "null", + "string" + ] + }, + "distanciaRecorrida": { + "pattern": "^-?(?:0|[1-9]\\d*)(?:\\.\\d+)?$", + "type": [ + "null", + "number", + "string" + ], + "format": "double" + }, + "domicilio": { + "oneOf": [ + { + "type": "null" + }, + { + "$ref": "#/components/schemas/CartaPorteUbicacionDomicilioRequest" + } + ] + } + } + }, + "ComercioExteriorComplementRequest": { + "type": "object", + "properties": { + "motivoTrasladoId": { + "type": [ + "null", + "string" + ] + }, + "claveDePedimentoId": { + "type": "string" + }, + "certificadoOrigen": { + "pattern": "^-?(?:0|[1-9]\\d*)$", + "type": [ + "integer", + "string" + ], + "format": "int32" + }, + "numCertificadoOrigen": { + "type": [ + "null", + "string" + ] + }, + "numeroExportadorConfiable": { + "type": [ + "null", + "string" + ] + }, + "incotermId": { + "type": [ + "null", + "string" + ] + }, + "observaciones": { + "type": [ + "null", + "string" + ] + }, + "tipoCambioUSD": { + "pattern": "^-?(?:0|[1-9]\\d*)(?:\\.\\d+)?$", + "type": [ + "number", + "string" + ], + "format": "double" + }, + "emisor": { + "oneOf": [ + { + "type": "null" + }, + { + "$ref": "#/components/schemas/ComercioExteriorEmisorRequest" + } + ] + }, + "receptor": { + "oneOf": [ + { + "type": "null" + }, + { + "$ref": "#/components/schemas/ComercioExteriorReceptorRequest" + } + ] + }, + "propietarios": { + "type": [ + "null", + "array" + ], + "items": { + "$ref": "#/components/schemas/ComercioExteriorPropietarioRequest" + } + }, + "destinatarios": { + "type": [ + "null", + "array" + ], + "items": { + "$ref": "#/components/schemas/ComercioExteriorDestinatarioRequest" + } + }, + "mercancias": { + "type": "array", + "items": { + "$ref": "#/components/schemas/ComercioExteriorMercanciaRequest" + } + } + } + }, + "ComercioExteriorDestinatarioDomicilioRequest": { + "type": "object", + "properties": { + "calle": { + "type": "string" + }, + "numeroExterior": { + "type": [ + "null", + "string" + ] + }, + "numeroInterior": { + "type": [ + "null", + "string" + ] + }, + "colonia": { + "type": [ + "null", + "string" + ] + }, + "localidad": { + "type": [ + "null", + "string" + ] + }, + "referencia": { + "type": [ + "null", + "string" + ] + }, + "municipio": { + "type": [ + "null", + "string" + ] + }, + "estado": { + "type": "string" + }, + "paisId": { + "type": "string" + }, + "codigoPostal": { + "type": "string" + } + } + }, + "ComercioExteriorDestinatarioRequest": { + "type": "object", + "properties": { + "numRegIdTrib": { + "type": [ + "null", + "string" + ] + }, + "nombre": { + "type": [ + "null", + "string" + ] + }, + "domicilios": { + "type": "array", + "items": { + "$ref": "#/components/schemas/ComercioExteriorDestinatarioDomicilioRequest" + } + } + } + }, + "ComercioExteriorEmisorDomicilioRequest": { + "type": "object", + "properties": { + "calle": { + "type": "string" + }, + "numeroExterior": { + "type": [ + "null", + "string" + ] + }, + "numeroInterior": { + "type": [ + "null", + "string" + ] + }, + "coloniaId": { + "type": [ + "null", + "string" + ] + }, + "localidadId": { + "type": [ + "null", + "string" + ] + }, + "referencia": { + "type": [ + "null", + "string" + ] + }, + "municipioId": { + "type": [ + "null", + "string" + ] + }, + "estadoId": { + "type": "string" + }, + "paisId": { + "type": "string" + }, + "codigoPostalId": { + "type": "string" + } + } + }, + "ComercioExteriorEmisorRequest": { + "type": "object", + "properties": { + "curp": { + "type": [ + "null", + "string" + ] + }, + "domicilio": { + "$ref": "#/components/schemas/ComercioExteriorEmisorDomicilioRequest" + } + } + }, + "ComercioExteriorMercanciaDescripcionesEspecificasRequest": { + "type": "object", + "properties": { + "marca": { + "type": "string" + }, + "modelo": { + "type": [ + "null", + "string" + ] + }, + "subModelo": { + "type": [ + "null", + "string" + ] + }, + "numeroSerie": { + "type": [ + "null", + "string" + ] + } + } + }, + "ComercioExteriorMercanciaRequest": { + "type": "object", + "properties": { + "noIdentificacion": { + "type": "string" + }, + "fraccionArancelariaId": { + "type": [ + "null", + "string" + ] + }, + "cantidadAduana": { + "pattern": "^-?(?:0|[1-9]\\d*)(?:\\.\\d+)?$", + "type": [ + "null", + "number", + "string" + ], + "format": "double" + }, + "unidadAduanaId": { + "type": [ + "null", + "string" + ] + }, + "valorUnitarioAduana": { + "pattern": "^-?(?:0|[1-9]\\d*)(?:\\.\\d+)?$", + "type": [ + "null", + "number", + "string" + ], + "format": "double" + }, + "valorDolares": { + "pattern": "^-?(?:0|[1-9]\\d*)(?:\\.\\d+)?$", + "type": [ + "number", + "string" + ], + "format": "double" + }, + "descripcionesEspecificas": { + "type": [ + "null", + "array" + ], + "items": { + "$ref": "#/components/schemas/ComercioExteriorMercanciaDescripcionesEspecificasRequest" + } + } + } + }, + "ComercioExteriorPropietarioRequest": { + "type": "object", + "properties": { + "numRegIdTrib": { + "type": "string" + }, + "residenciaFiscalId": { + "type": "string" + } + } + }, + "ComercioExteriorReceptorDomicilioRequest": { + "type": "object", + "properties": { + "calle": { + "type": "string" + }, + "numeroExterior": { + "type": [ + "null", + "string" + ] + }, + "numeroInterior": { + "type": [ + "null", + "string" + ] + }, + "colonia": { + "type": [ + "null", + "string" + ] + }, + "localidad": { + "type": [ + "null", + "string" + ] + }, + "referencia": { + "type": [ + "null", + "string" + ] + }, + "municipio": { + "type": [ + "null", + "string" + ] + }, + "estado": { + "type": "string" + }, + "paisId": { + "type": "string" + }, + "codigoPostal": { + "type": "string" + } + } + }, + "ComercioExteriorReceptorRequest": { + "type": "object", + "properties": { + "numRegIdTrib": { + "type": [ + "null", + "string" + ] + }, + "domicilio": { + "oneOf": [ + { + "type": "null" + }, + { + "$ref": "#/components/schemas/ComercioExteriorReceptorDomicilioRequest" + } + ] + } + } + }, + "ComplementRequest": { + "type": "object", + "properties": { + "localTaxes": { + "oneOf": [ + { + "type": "null" + }, + { + "$ref": "#/components/schemas/LocalTaxesComplementRequest" + } + ] + }, + "payment": { + "oneOf": [ + { + "type": "null" + }, + { + "$ref": "#/components/schemas/PaymentComplementRequest" + } + ] + }, + "payroll": { + "oneOf": [ + { + "type": "null" + }, + { + "$ref": "#/components/schemas/PayrollComplementRequest" + } + ] + }, + "cartaPorte": { + "oneOf": [ + { + "type": "null" + }, + { + "$ref": "#/components/schemas/CartaPorteComplementRequest" + } + ] + }, + "comercioExterior": { + "oneOf": [ + { + "type": "null" + }, + { + "$ref": "#/components/schemas/ComercioExteriorComplementRequest" + } + ] + } + } + }, + "ConsumptionReportDto": { + "required": [ + "from", + "to", + "issuers", + "payers", + "totals" + ], + "type": "object", + "properties": { + "from": { + "type": "string", + "format": "date" + }, + "to": { + "type": "string", + "format": "date" + }, + "issuers": { + "type": "array", + "items": { + "$ref": "#/components/schemas/IssuerConsumptionDto" + } + }, + "payers": { + "type": "array", + "items": { + "$ref": "#/components/schemas/PayerConsumptionDto" + } + }, + "totals": { + "$ref": "#/components/schemas/ConsumptionTotalsDto" + } + } + }, + "ConsumptionTotalsDto": { + "required": [ + "ingested", + "pending", + "inProgress", + "invoiced", + "globalized", + "cancelled", + "ticketCreditsConsumed" + ], + "type": "object", + "properties": { + "ingested": { + "pattern": "^-?(?:0|[1-9]\\d*)$", + "type": [ + "integer", + "string" + ], + "format": "int32" + }, + "pending": { + "pattern": "^-?(?:0|[1-9]\\d*)$", + "type": [ + "integer", + "string" + ], + "format": "int32" + }, + "inProgress": { + "pattern": "^-?(?:0|[1-9]\\d*)$", + "type": [ + "integer", + "string" + ], + "format": "int32" + }, + "invoiced": { + "pattern": "^-?(?:0|[1-9]\\d*)$", + "type": [ + "integer", + "string" + ], + "format": "int32" + }, + "globalized": { + "pattern": "^-?(?:0|[1-9]\\d*)$", + "type": [ + "integer", + "string" + ], + "format": "int32" + }, + "cancelled": { + "pattern": "^-?(?:0|[1-9]\\d*)$", + "type": [ + "integer", + "string" + ], + "format": "int32" + }, + "ticketCreditsConsumed": { + "pattern": "^-?(?:0|[1-9]\\d*)$", + "type": [ + "integer", + "string" + ], + "format": "int32" + } + } + }, + "CreateApiKeyCommand": { + "type": "object", + "properties": { + "personId": { + "type": [ + "null", + "string" + ] + }, + "description": { + "type": [ + "null", + "string" + ] + } + } + }, + "CreateCommand": { + "type": "object", + "properties": { + "description": { + "type": [ + "null", + "string" + ] + }, + "unitPrice": { + "pattern": "^-?(?:0|[1-9]\\d*)(?:\\.\\d+)?$", + "type": [ + "number", + "string" + ], + "format": "double" + }, + "satUnitMeasurementId": { + "type": [ + "null", + "string" + ] + }, + "satTaxObjectId": { + "type": [ + "null", + "string" + ] + }, + "satProductCodeId": { + "type": [ + "null", + "string" + ] + }, + "productTaxes": { + "type": [ + "null", + "array" + ], + "items": { + "$ref": "#/components/schemas/CreateCommand" + } + } + } + }, + "CreateDownloadRequestCommand": { + "type": "object", + "properties": { + "downloadRuleId": { + "type": [ + "null", + "string" + ] + }, + "downloadRequestTypeId": { + "type": [ + "null", + "string" + ] + }, + "startDate": { + "type": "string", + "format": "date-time" + }, + "endDate": { + "type": "string", + "format": "date-time" + }, + "sourcePackageId": { + "type": [ + "null", + "string" + ] + } + } + }, + "CreateDownloadRuleCommand": { + "type": "object", + "properties": { + "personId": { + "type": [ + "null", + "string" + ] + }, + "description": { + "type": [ + "null", + "string" + ] + }, + "satQueryTypeId": { + "type": [ + "null", + "string" + ] + }, + "downloadTypeId": { + "type": [ + "null", + "string" + ] + }, + "satInvoiceStatusId": { + "type": [ + "null", + "string" + ] + } + } + }, + "CreateEmployeeDataCommand": { + "type": "object", + "properties": { + "employerPersonId": { + "type": "string" + }, + "employeePersonId": { + "type": "string" + }, + "socialSecurityNumber": { + "type": [ + "null", + "string" + ] + }, + "laborRelationStartDate": { + "type": [ + "null", + "string" + ], + "format": "date-time" + }, + "seniority": { + "type": [ + "null", + "string" + ] + }, + "satContractTypeId": { + "type": "string" + }, + "satUnionizedStatusId": { + "type": [ + "null", + "string" + ] + }, + "satTaxRegimeTypeId": { + "type": "string" + }, + "satWorkdayTypeId": { + "type": [ + "null", + "string" + ] + }, + "satJobRiskId": { + "type": [ + "null", + "string" + ] + }, + "satPaymentPeriodicityId": { + "type": "string" + }, + "employeeNumber": { + "type": "string" + }, + "satBankId": { + "type": [ + "null", + "string" + ] + }, + "satPayrollStateId": { + "type": "string" + }, + "department": { + "type": [ + "null", + "string" + ] + }, + "position": { + "type": [ + "null", + "string" + ] + }, + "bankAccount": { + "type": [ + "null", + "string" + ] + }, + "baseSalaryForContributions": { + "pattern": "^-?(?:0|[1-9]\\d*)(?:\\.\\d+)?$", + "type": [ + "null", + "number", + "string" + ], + "format": "double" + }, + "integratedDailySalary": { + "pattern": "^-?(?:0|[1-9]\\d*)(?:\\.\\d+)?$", + "type": [ + "null", + "number", + "string" + ], + "format": "double" + }, + "subcontractorRfc": { + "type": [ + "null", + "string" + ] + }, + "timePercentage": { + "pattern": "^-?(?:0|[1-9]\\d*)(?:\\.\\d+)?$", + "type": [ + "null", + "number", + "string" + ], + "format": "double" + } + } + }, + "CreateEmployerDataCommand": { + "type": "object", + "properties": { + "personId": { + "type": [ + "null", + "string" + ] + }, + "employerRegistration": { + "type": [ + "null", + "string" + ] + }, + "originEmployerTin": { + "type": [ + "null", + "string" + ] + }, + "satFundSourceId": { + "type": [ + "null", + "string" + ] + }, + "ownResourceAmount": { + "pattern": "^-?(?:0|[1-9]\\d*)(?:\\.\\d+)?$", + "type": [ + "null", + "number", + "string" + ], + "format": "double" + } + } + }, + "CreatePdfCommand": { + "type": "object", + "properties": { + "invoiceId": { + "type": [ + "null", + "string" + ] + }, + "base64Logo": { + "type": [ + "null", + "string" + ] + }, + "bandColor": { + "type": [ + "null", + "string" + ] + }, + "fontColor": { + "type": [ + "null", + "string" + ] + } + } + }, + "CreateTicketCommand": { + "type": "object", + "properties": { + "issuer": { + "oneOf": [ + { + "type": "null" + }, + { + "$ref": "#/components/schemas/TicketIssuerRequest" + } + ] + }, + "folio": { + "type": [ + "null", + "string" + ] + }, + "series": { + "type": [ + "null", + "string" + ] + }, + "paymentDate": { + "format": "date-time" + }, + "paymentFormCode": { + "type": [ + "null", + "string" + ] + }, + "currencyCode": { + "type": [ + "null", + "string" + ] + }, + "exchangeRate": { + "pattern": "^-?(?:0|[1-9]\\d*)(?:\\.\\d+)?$", + "type": [ + "null", + "number", + "string" + ], + "format": "double" + }, + "expeditionZipCode": { + "type": [ + "null", + "string" + ] + }, + "total": { + "pattern": "^-?(?:0|[1-9]\\d*)(?:\\.\\d+)?$", + "type": [ + "number", + "string" + ], + "format": "double" + }, + "items": { + "type": [ + "null", + "array" + ], + "items": { + "$ref": "#/components/schemas/TicketItemRequest" + } + } + } + }, + "CreateUserCommand": { + "type": "object", + "properties": { + "legalName": { + "type": [ + "null", + "string" + ] + }, + "email": { + "type": [ + "null", + "string" + ] + }, + "password": { + "type": [ + "null", + "string" + ] + }, + "phoneNumber": { + "type": [ + "null", + "string" + ] + }, + "satTaxRegimeId": { + "type": [ + "null", + "string" + ] + }, + "userTypeId": { + "type": [ + "null", + "string" + ] + }, + "satCfdiUseId": { + "type": [ + "null", + "string" + ] + }, + "tin": { + "type": [ + "null", + "string" + ] + }, + "zipCode": { + "type": [ + "null", + "string" + ] + }, + "base64Photo": { + "type": [ + "null", + "string" + ] + }, + "taxPassword": { + "type": [ + "null", + "string" + ] + }, + "curp": { + "type": [ + "null", + "string" + ] + }, + "countryId": { + "type": [ + "null", + "string" + ] + }, + "foreignTin": { + "type": [ + "null", + "string" + ] + } + } + }, + "CreateWebhookCommand": { + "type": "object", + "properties": { + "url": { + "type": [ + "null", + "string" + ], + "description": "URL https absoluta que recibe las entregas, sin usuario ni contraseña." + }, + "description": { + "type": [ + "null", + "string" + ], + "description": "Texto libre del tenant, hasta 256 caracteres." + }, + "enabledEvents": { + "type": [ + "null", + "array" + ], + "items": { + "type": "string" + }, + "description": "Tipos de evento suscritos, o solo \"*\" para recibirlos todos." + }, + "personIds": { + "type": [ + "null", + "array" + ], + "items": { + "type": "string" + }, + "description": "Personas del tenant cuyos eventos recibe; sin ellas (o vacío), las de todas." + } + } + }, + "CredentialInputModel": { + "type": "object", + "properties": { + "id": { + "type": [ + "null", + "string" + ] + }, + "base64File": { + "type": [ + "null", + "string" + ] + }, + "fileType": { + "oneOf": [ + { + "type": "null" + }, + { + "$ref": "#/components/schemas/FileType" + } + ] + }, + "password": { + "type": [ + "null", + "string" + ] + } + } + }, + "CreditTypeId": { + "type": "integer", + "format": "int32" + }, + "CustomsInfoRequest": { + "type": "object", + "properties": { + "customsNumber": { + "type": [ + "null", + "string" + ] + } + } + }, + "DeductionRequest": { + "type": "object", + "properties": { + "deductionTypeCode": { + "type": "string" + }, + "code": { + "type": "string" + }, + "concept": { + "type": "string" + }, + "amount": { + "pattern": "^-?(?:0|[1-9]\\d*)(?:\\.\\d+)?$", + "type": [ + "number", + "string" + ], + "format": "double" + } + } + }, + "DisabilityRequest": { + "type": "object", + "properties": { + "disabilityDays": { + "pattern": "^-?(?:0|[1-9]\\d*)$", + "type": [ + "integer", + "string" + ], + "format": "int32" + }, + "disabilityTypeCode": { + "type": "string" + }, + "monetaryAmount": { + "pattern": "^-?(?:0|[1-9]\\d*)(?:\\.\\d+)?$", + "type": [ + "number", + "string" + ], + "format": "double" + } + } + }, + "EarningRequest": { + "type": "object", + "properties": { + "earningTypeCode": { + "type": "string" + }, + "code": { + "type": "string" + }, + "concept": { + "type": "string" + }, + "taxedAmount": { + "pattern": "^-?(?:0|[1-9]\\d*)(?:\\.\\d+)?$", + "type": [ + "number", + "string" + ], + "format": "double" + }, + "exemptAmount": { + "pattern": "^-?(?:0|[1-9]\\d*)(?:\\.\\d+)?$", + "type": [ + "number", + "string" + ], + "format": "double" + }, + "stockOptions": { + "oneOf": [ + { + "type": "null" + }, + { + "$ref": "#/components/schemas/StockOptionsRequest" + } + ] + }, + "overtime": { + "type": [ + "null", + "array" + ], + "items": { + "$ref": "#/components/schemas/OvertimeRequest" + } + } + } + }, + "EarningsRequest": { + "type": "object", + "properties": { + "earnings": { + "type": [ + "null", + "array" + ], + "items": { + "$ref": "#/components/schemas/EarningRequest" + } + }, + "otherPayments": { + "type": [ + "null", + "array" + ], + "items": { + "$ref": "#/components/schemas/OtherPaymentRequest" + } + }, + "retirement": { + "oneOf": [ + { + "type": "null" + }, + { + "$ref": "#/components/schemas/RetirementRequest" + } + ] + }, + "severance": { + "oneOf": [ + { + "type": "null" + }, + { + "$ref": "#/components/schemas/SeveranceRequest" + } + ] + } + }, + "additionalProperties": false + }, + "EmployeeDataRequest": { + "type": "object", + "properties": { + "curp": { + "type": [ + "null", + "string" + ] + }, + "socialSecurityNumber": { + "type": [ + "null", + "string" + ] + }, + "laborRelationStartDate": { + "type": [ + "null", + "string" + ], + "format": "date-time" + }, + "seniority": { + "type": [ + "null", + "string" + ] + }, + "satContractTypeId": { + "type": [ + "null", + "string" + ] + }, + "satUnionizedStatusId": { + "type": [ + "null", + "string" + ] + }, + "satWorkdayTypeId": { + "type": [ + "null", + "string" + ] + }, + "satTaxRegimeTypeId": { + "type": [ + "null", + "string" + ] + }, + "employeeNumber": { + "type": [ + "null", + "string" + ] + }, + "department": { + "type": [ + "null", + "string" + ] + }, + "position": { + "type": [ + "null", + "string" + ] + }, + "satJobRiskId": { + "type": [ + "null", + "string" + ] + }, + "satPaymentPeriodicityId": { + "type": [ + "null", + "string" + ] + }, + "satBankId": { + "type": [ + "null", + "string" + ] + }, + "bankAccount": { + "type": [ + "null", + "string" + ] + }, + "baseSalaryForContributions": { + "pattern": "^-?(?:0|[1-9]\\d*)(?:\\.\\d+)?$", + "type": [ + "null", + "number", + "string" + ], + "format": "double" + }, + "integratedDailySalary": { + "pattern": "^-?(?:0|[1-9]\\d*)(?:\\.\\d+)?$", + "type": [ + "null", + "number", + "string" + ], + "format": "double" + }, + "satPayrollStateId": { + "type": [ + "null", + "string" + ] + }, + "subcontractorRfc": { + "type": [ + "null", + "string" + ] + }, + "timePercentage": { + "pattern": "^-?(?:0|[1-9]\\d*)(?:\\.\\d+)?$", + "type": [ + "number", + "string" + ], + "format": "double" + } + } + }, + "EmployerDataRequest": { + "type": "object", + "properties": { + "curp": { + "type": [ + "null", + "string" + ] + }, + "employerRegistration": { + "type": [ + "null", + "string" + ] + }, + "originEmployerTin": { + "type": [ + "null", + "string" + ] + }, + "satFundSourceId": { + "type": [ + "null", + "string" + ] + }, + "ownResourceAmount": { + "pattern": "^-?(?:0|[1-9]\\d*)(?:\\.\\d+)?$", + "type": [ + "null", + "number", + "string" + ], + "format": "double" + } + } + }, + "EntityTagHeaderValue": { + "type": "object", + "properties": { + "tag": { + "$ref": "#/components/schemas/StringSegment" + }, + "isWeak": { + "type": "boolean" + } + } + }, + "ExistingTicketData": { + "required": [ + "existingTicketId" + ], + "type": "object", + "properties": { + "existingTicketId": { + "type": "string" + } + } + }, + "FileContentResult": { + "type": "object", + "properties": { + "fileContents": { + "type": "string", + "format": "byte" + }, + "contentType": { + "type": [ + "null", + "string" + ] + }, + "fileDownloadName": { + "type": [ + "null", + "string" + ] + }, + "lastModified": { + "type": [ + "null", + "string" + ], + "format": "date-time" + }, + "entityTag": { + "oneOf": [ + { + "type": "null" + }, + { + "$ref": "#/components/schemas/EntityTagHeaderValue" + } + ] + }, + "enableRangeProcessing": { + "type": "boolean" + } + } + }, + "FileType": { + "type": "integer" + }, + "GlobalInfoRequest": { + "type": "object", + "properties": { + "periodicityCode": { + "type": [ + "null", + "string" + ] + }, + "monthCode": { + "type": [ + "null", + "string" + ] + }, + "year": { + "pattern": "^-?(?:0|[1-9]\\d*)$", + "type": [ + "integer", + "string" + ], + "format": "int32" + } + } + }, + "HttpStatusCode": { + "type": "integer" + }, + "InvoiceRequest": { + "type": "object", + "properties": { + "versionCode": { + "type": [ + "null", + "string" + ] + }, + "paymentFormCode": { + "type": [ + "null", + "string" + ] + }, + "paymentMethodCode": { + "type": [ + "null", + "string" + ] + }, + "currencyCode": { + "type": [ + "null", + "string" + ] + }, + "typeCode": { + "type": [ + "null", + "string" + ] + }, + "expeditionZipCode": { + "type": [ + "null", + "string" + ] + }, + "pacConfirmation": { + "type": [ + "null", + "string" + ] + }, + "series": { + "type": [ + "null", + "string" + ] + }, + "number": { + "type": [ + "null", + "string" + ] + }, + "date": { + "type": "string", + "format": "date-time" + }, + "paymentConditions": { + "type": [ + "null", + "string" + ] + }, + "exchangeRate": { + "pattern": "^-?(?:0|[1-9]\\d*)(?:\\.\\d+)?$", + "type": [ + "number", + "string" + ], + "format": "double" + }, + "exportCode": { + "type": [ + "null", + "string" + ] + }, + "issuer": { + "oneOf": [ + { + "type": "null" + }, + { + "$ref": "#/components/schemas/IssuerRequest" + } + ] + }, + "recipient": { + "oneOf": [ + { + "type": "null" + }, + { + "$ref": "#/components/schemas/RecipientRequest" + } + ] + }, + "items": { + "type": [ + "null", + "array" + ], + "items": { + "$ref": "#/components/schemas/ItemRequest" + } + }, + "globalInformation": { + "oneOf": [ + { + "type": "null" + }, + { + "$ref": "#/components/schemas/GlobalInfoRequest" + } + ] + }, + "relatedInvoices": { + "type": [ + "null", + "array" + ], + "items": { + "$ref": "#/components/schemas/RelatedInvoiceRequest" + } + }, + "complement": { + "oneOf": [ + { + "type": "null" + }, + { + "$ref": "#/components/schemas/ComplementRequest" + } + ] + }, + "metadata": { + "type": [ + "null", + "object" + ], + "additionalProperties": { + "type": "string" + } + } + } + }, + "IssuerConsumptionDto": { + "required": [ + "personId", + "tin", + "legalName", + "ingested", + "pending", + "inProgress", + "invoiced", + "globalized", + "cancelled" + ], + "type": "object", + "properties": { + "personId": { + "type": "string" + }, + "tin": { + "type": [ + "null", + "string" + ] + }, + "legalName": { + "type": [ + "null", + "string" + ] + }, + "ingested": { + "pattern": "^-?(?:0|[1-9]\\d*)$", + "type": [ + "integer", + "string" + ], + "format": "int32" + }, + "pending": { + "pattern": "^-?(?:0|[1-9]\\d*)$", + "type": [ + "integer", + "string" + ], + "format": "int32" + }, + "inProgress": { + "pattern": "^-?(?:0|[1-9]\\d*)$", + "type": [ + "integer", + "string" + ], + "format": "int32" + }, + "invoiced": { + "pattern": "^-?(?:0|[1-9]\\d*)$", + "type": [ + "integer", + "string" + ], + "format": "int32" + }, + "globalized": { + "pattern": "^-?(?:0|[1-9]\\d*)$", + "type": [ + "integer", + "string" + ], + "format": "int32" + }, + "cancelled": { + "pattern": "^-?(?:0|[1-9]\\d*)$", + "type": [ + "integer", + "string" + ], + "format": "int32" + } + } + }, + "IssuerRequest": { + "type": "object", + "properties": { + "id": { + "type": [ + "null", + "string" + ] + }, + "tin": { + "type": [ + "null", + "string" + ] + }, + "legalName": { + "type": [ + "null", + "string" + ] + }, + "taxRegimeCode": { + "type": [ + "null", + "string" + ] + }, + "employerData": { + "oneOf": [ + { + "type": "null" + }, + { + "$ref": "#/components/schemas/EmployerDataRequest" + } + ] + }, + "taxCredentials": { + "type": [ + "null", + "array" + ], + "items": { + "$ref": "#/components/schemas/CredentialInputModel" + } + } + } + }, + "ItemPartRequest": { + "type": "object", + "properties": { + "itemCode": { + "type": [ + "null", + "string" + ] + }, + "itemSku": { + "type": [ + "null", + "string" + ] + }, + "quantity": { + "pattern": "^-?(?:0|[1-9]\\d*)(?:\\.\\d+)?$", + "type": [ + "number", + "string" + ], + "format": "double" + }, + "unitOfMeasurementCode": { + "type": [ + "null", + "string" + ] + }, + "description": { + "type": [ + "null", + "string" + ] + }, + "unitPrice": { + "pattern": "^-?(?:0|[1-9]\\d*)(?:\\.\\d+)?$", + "type": [ + "number", + "string" + ], + "format": "double" + }, + "customsInfo": { + "type": [ + "null", + "array" + ], + "items": { + "$ref": "#/components/schemas/CustomsInfoRequest" + } + } + } + }, + "ItemRequest": { + "type": "object", + "properties": { + "id": { + "type": [ + "null", + "string" + ] + }, + "itemCode": { + "type": [ + "null", + "string" + ] + }, + "itemSku": { + "type": [ + "null", + "string" + ] + }, + "quantity": { + "pattern": "^-?(?:0|[1-9]\\d*)(?:\\.\\d+)?$", + "type": [ + "number", + "string" + ], + "format": "double" + }, + "unitOfMeasurementCode": { + "type": [ + "null", + "string" + ] + }, + "description": { + "type": [ + "null", + "string" + ] + }, + "unitPrice": { + "pattern": "^-?(?:0|[1-9]\\d*)(?:\\.\\d+)?$", + "type": [ + "number", + "string" + ], + "format": "double" + }, + "discount": { + "pattern": "^-?(?:0|[1-9]\\d*)(?:\\.\\d+)?$", + "type": [ + "number", + "string" + ], + "format": "double" + }, + "taxObjectCode": { + "type": [ + "null", + "string" + ] + }, + "itemTaxes": { + "type": "array", + "items": { + "$ref": "#/components/schemas/ItemTaxRequest" + } + }, + "onBehalfOf": { + "oneOf": [ + { + "type": "null" + }, + { + "$ref": "#/components/schemas/OnBehalfOfRequest" + } + ] + }, + "customsInfo": { + "type": [ + "null", + "array" + ], + "items": { + "$ref": "#/components/schemas/CustomsInfoRequest" + } + }, + "propertyInfo": { + "type": [ + "null", + "array" + ], + "items": { + "$ref": "#/components/schemas/PropertyInfoRequest" + } + }, + "parts": { + "type": [ + "null", + "array" + ], + "items": { + "$ref": "#/components/schemas/ItemPartRequest" + } + }, + "ticketId": { + "type": [ + "null", + "string" + ] + } + } + }, + "ItemTaxRequest": { + "type": "object", + "properties": { + "taxCode": { + "type": [ + "null", + "string" + ] + }, + "taxTypeCode": { + "type": [ + "null", + "string" + ] + }, + "taxRate": { + "pattern": "^-?(?:0|[1-9]\\d*)(?:\\.\\d+)?$", + "type": [ + "number", + "string" + ], + "format": "double" + }, + "taxFlagCode": { + "type": [ + "null", + "string" + ] + } + } + }, + "JsonElement": { }, + "LocalTaxesComplementRequest": { + "type": "object", + "properties": { + "taxes": { + "type": "array", + "items": { + "$ref": "#/components/schemas/LocalTaxRequest" + } + } + } + }, + "LocalTaxRequest": { + "type": "object", + "properties": { + "taxName": { + "type": [ + "null", + "string" + ] + }, + "taxRate": { + "pattern": "^-?(?:0|[1-9]\\d*)(?:\\.\\d+)?$", + "type": [ + "number", + "string" + ], + "format": "double" + }, + "taxAmount": { + "pattern": "^-?(?:0|[1-9]\\d*)(?:\\.\\d+)?$", + "type": [ + "number", + "string" + ], + "format": "double" + }, + "taxFlagCode": { + "type": [ + "null", + "string" + ] + } + } + }, + "OnBehalfOfRequest": { + "type": "object", + "properties": { + "tin": { + "type": [ + "null", + "string" + ] + }, + "legalName": { + "type": [ + "null", + "string" + ] + }, + "taxRegimeCode": { + "type": [ + "null", + "string" + ] + }, + "zipCode": { + "type": [ + "null", + "string" + ] + } + } + }, + "OtherPaymentRequest": { + "type": "object", + "properties": { + "otherPaymentTypeCode": { + "type": "string" + }, + "code": { + "type": "string" + }, + "concept": { + "type": "string" + }, + "amount": { + "pattern": "^-?(?:0|[1-9]\\d*)(?:\\.\\d+)?$", + "type": [ + "number", + "string" + ], + "format": "double" + }, + "subsidyCaused": { + "pattern": "^-?(?:0|[1-9]\\d*)(?:\\.\\d+)?$", + "type": [ + "null", + "number", + "string" + ], + "format": "double" + }, + "balanceCompensation": { + "oneOf": [ + { + "type": "null" + }, + { + "$ref": "#/components/schemas/BalanceCompensationRequest" + } + ] + } + } + }, + "OvertimeRequest": { + "type": "object", + "properties": { + "days": { + "pattern": "^-?(?:0|[1-9]\\d*)$", + "type": [ + "integer", + "string" + ], + "format": "int32" + }, + "hoursTypeCode": { + "type": "string" + }, + "extraHours": { + "pattern": "^-?(?:0|[1-9]\\d*)$", + "type": [ + "integer", + "string" + ], + "format": "int32" + }, + "amountPaid": { + "pattern": "^-?(?:0|[1-9]\\d*)(?:\\.\\d+)?$", + "type": [ + "number", + "string" + ], + "format": "double" + } + } + }, + "PagedListOfJsonElement": { + "required": [ + "items", + "pageNumber" + ], + "type": "object", + "properties": { + "items": { + "type": "array" + }, + "pageNumber": { + "pattern": "^-?(?:0|[1-9]\\d*)$", + "type": [ + "integer", + "string" + ], + "format": "int32" + }, + "totalPages": { + "pattern": "^-?(?:0|[1-9]\\d*)$", + "type": [ + "integer", + "string" + ], + "format": "int32" + }, + "totalCount": { + "pattern": "^-?(?:0|[1-9]\\d*)$", + "type": [ + "integer", + "string" + ], + "format": "int32" + }, + "hasPreviousPage": { + "type": "boolean" + }, + "hasNextPage": { + "type": "boolean" + } + } + }, + "PagedListOfTicketDto": { + "required": [ + "items", + "pageNumber" + ], + "type": "object", + "properties": { + "items": { + "type": "array", + "items": { + "$ref": "#/components/schemas/TicketDto" + } + }, + "pageNumber": { + "pattern": "^-?(?:0|[1-9]\\d*)$", + "type": [ + "integer", + "string" + ], + "format": "int32" + }, + "totalPages": { + "pattern": "^-?(?:0|[1-9]\\d*)$", + "type": [ + "integer", + "string" + ], + "format": "int32" + }, + "totalCount": { + "pattern": "^-?(?:0|[1-9]\\d*)$", + "type": [ + "integer", + "string" + ], + "format": "int32" + }, + "hasPreviousPage": { + "type": "boolean" + }, + "hasNextPage": { + "type": "boolean" + } + } + }, + "PagedListOfWebhookDeliveryDto": { + "required": [ + "items", + "pageNumber" + ], + "type": "object", + "properties": { + "items": { + "type": "array", + "items": { + "$ref": "#/components/schemas/WebhookDeliveryDto" + } + }, + "pageNumber": { + "pattern": "^-?(?:0|[1-9]\\d*)$", + "type": [ + "integer", + "string" + ], + "format": "int32" + }, + "totalPages": { + "pattern": "^-?(?:0|[1-9]\\d*)$", + "type": [ + "integer", + "string" + ], + "format": "int32" + }, + "totalCount": { + "pattern": "^-?(?:0|[1-9]\\d*)$", + "type": [ + "integer", + "string" + ], + "format": "int32" + }, + "hasPreviousPage": { + "type": "boolean" + }, + "hasNextPage": { + "type": "boolean" + } + } + }, + "PagedListOfWebhookDto": { + "required": [ + "items", + "pageNumber" + ], + "type": "object", + "properties": { + "items": { + "type": "array", + "items": { + "$ref": "#/components/schemas/WebhookDto" + } + }, + "pageNumber": { + "pattern": "^-?(?:0|[1-9]\\d*)$", + "type": [ + "integer", + "string" + ], + "format": "int32" + }, + "totalPages": { + "pattern": "^-?(?:0|[1-9]\\d*)$", + "type": [ + "integer", + "string" + ], + "format": "int32" + }, + "totalCount": { + "pattern": "^-?(?:0|[1-9]\\d*)$", + "type": [ + "integer", + "string" + ], + "format": "int32" + }, + "hasPreviousPage": { + "type": "boolean" + }, + "hasNextPage": { + "type": "boolean" + } + } + }, + "PaidInvoice": { + "type": "object", + "properties": { + "id": { + "type": [ + "null", + "string" + ] + }, + "uuid": { + "type": [ + "null", + "string" + ] + }, + "series": { + "type": [ + "null", + "string" + ] + }, + "number": { + "type": [ + "null", + "string" + ] + }, + "currencyCode": { + "type": [ + "null", + "string" + ] + }, + "partialityNumber": { + "pattern": "^-?(?:0|[1-9]\\d*)$", + "type": [ + "integer", + "string" + ], + "format": "int32" + }, + "subTotal": { + "pattern": "^-?(?:0|[1-9]\\d*)(?:\\.\\d+)?$", + "type": [ + "number", + "string" + ], + "format": "double" + }, + "previousBalance": { + "pattern": "^-?(?:0|[1-9]\\d*)(?:\\.\\d+)?$", + "type": [ + "number", + "string" + ], + "format": "double" + }, + "paymentAmount": { + "pattern": "^-?(?:0|[1-9]\\d*)(?:\\.\\d+)?$", + "type": [ + "number", + "string" + ], + "format": "double" + }, + "remainingBalance": { + "pattern": "^-?(?:0|[1-9]\\d*)(?:\\.\\d+)?$", + "type": [ + "number", + "string" + ], + "format": "double" + }, + "taxObjectCode": { + "type": [ + "null", + "string" + ] + }, + "equivalence": { + "pattern": "^-?(?:0|[1-9]\\d*)(?:\\.\\d+)?$", + "type": [ + "number", + "string" + ], + "format": "double" + }, + "exchangeRate": { + "pattern": "^-?(?:0|[1-9]\\d*)(?:\\.\\d+)?$", + "type": [ + "number", + "string" + ], + "format": "double" + }, + "paidInvoiceTaxes": { + "type": [ + "null", + "array" + ], + "items": { + "$ref": "#/components/schemas/PaidInvoiceTax" + } + } + } + }, + "PaidInvoiceTax": { + "type": "object", + "properties": { + "base": { + "pattern": "^-?(?:0|[1-9]\\d*)(?:\\.\\d+)?$", + "type": [ + "number", + "string" + ], + "format": "double" + }, + "taxCode": { + "type": [ + "null", + "string" + ] + }, + "taxTypeCode": { + "type": [ + "null", + "string" + ] + }, + "taxRate": { + "pattern": "^-?(?:0|[1-9]\\d*)(?:\\.\\d+)?$", + "type": [ + "number", + "string" + ], + "format": "double" + }, + "taxFlagCode": { + "type": [ + "null", + "string" + ] + } + } + }, + "PatchUserCommand": { + "type": "object", + "properties": { + "id": { + "type": [ + "null", + "string" + ] + }, + "legalName": { + "type": [ + "null", + "string" + ] + }, + "email": { + "type": [ + "null", + "string" + ] + }, + "phoneNumber": { + "type": [ + "null", + "string" + ] + }, + "password": { + "type": [ + "null", + "string" + ] + }, + "satTaxRegimeId": { + "type": [ + "null", + "string" + ] + }, + "satCfdiUseId": { + "type": [ + "null", + "string" + ] + }, + "userTypeId": { + "type": [ + "null", + "string" + ] + }, + "tin": { + "type": [ + "null", + "string" + ] + }, + "zipCode": { + "type": [ + "null", + "string" + ] + }, + "base64Photo": { + "type": [ + "null", + "string" + ] + }, + "taxPassword": { + "type": [ + "null", + "string" + ] + }, + "curp": { + "type": [ + "null", + "string" + ] + }, + "countryId": { + "type": [ + "null", + "string" + ] + }, + "foreignTin": { + "type": [ + "null", + "string" + ] + } + } + }, + "PayerConsumptionDto": { + "required": [ + "personId", + "tin", + "legalName", + "ticketCreditsConsumed" + ], + "type": "object", + "properties": { + "personId": { + "type": "string" + }, + "tin": { + "type": [ + "null", + "string" + ] + }, + "legalName": { + "type": [ + "null", + "string" + ] + }, + "ticketCreditsConsumed": { + "pattern": "^-?(?:0|[1-9]\\d*)$", + "type": [ + "integer", + "string" + ], + "format": "int32" + } + } + }, + "PaymentComplementRequest": { + "type": "object", + "properties": { + "paymentDate": { + "type": "string", + "format": "date-time" + }, + "paymentFormCode": { + "type": [ + "null", + "string" + ] + }, + "currencyCode": { + "type": "string" + }, + "exchangeRate": { + "pattern": "^-?(?:0|[1-9]\\d*)(?:\\.\\d+)?$", + "type": [ + "number", + "string" + ], + "format": "double" + }, + "amount": { + "pattern": "^-?(?:0|[1-9]\\d*)(?:\\.\\d+)?$", + "type": [ + "number", + "string" + ], + "format": "double" + }, + "operationNumber": { + "type": [ + "null", + "string" + ] + }, + "sourceBankTin": { + "type": [ + "null", + "string" + ] + }, + "sourceBankAccount": { + "type": [ + "null", + "string" + ] + }, + "targetBankTin": { + "type": [ + "null", + "string" + ] + }, + "targetBankAccount": { + "type": [ + "null", + "string" + ] + }, + "foreignBankName": { + "type": [ + "null", + "string" + ] + }, + "paymentTypeCode": { + "type": [ + "null", + "string" + ] + }, + "base64PaymentCertificate": { + "type": [ + "null", + "string" + ] + }, + "paymentOriginalString": { + "type": [ + "null", + "string" + ] + }, + "signatureValue": { + "type": [ + "null", + "string" + ] + }, + "paidInvoices": { + "type": "array", + "items": { + "$ref": "#/components/schemas/PaidInvoice" + } + } + } + }, + "PayrollComplementRequest": { + "type": "object", + "properties": { + "version": { + "type": "string" + }, + "payrollTypeCode": { + "type": [ + "null", + "string" + ] + }, + "paymentDate": { + "type": "string", + "format": "date-time" + }, + "initialPaymentDate": { + "type": "string", + "format": "date-time" + }, + "finalPaymentDate": { + "type": "string", + "format": "date-time" + }, + "daysPaid": { + "pattern": "^-?(?:0|[1-9]\\d*)(?:\\.\\d+)?$", + "type": [ + "number", + "string" + ], + "format": "double" + }, + "earnings": { + "oneOf": [ + { + "type": "null" + }, + { + "$ref": "#/components/schemas/EarningsRequest" + } + ] + }, + "deductions": { + "type": [ + "null", + "array" + ], + "items": { + "$ref": "#/components/schemas/DeductionRequest" + } + }, + "disabilities": { + "type": [ + "null", + "array" + ], + "items": { + "$ref": "#/components/schemas/DisabilityRequest" + } + } + }, + "additionalProperties": false + }, + "PortalBrand": { + "required": [ + "logo", + "bandColor", + "fontColor" + ], + "type": "object", + "properties": { + "logo": { + "type": [ + "null", + "string" + ] + }, + "bandColor": { + "type": [ + "null", + "string" + ] + }, + "fontColor": { + "type": [ + "null", + "string" + ] + } + } + }, + "PortalBrandingDto": { + "required": [ + "canonicalName", + "logo", + "bandColor", + "fontColor", + "portalStatus", + "privacyNoticeUrl" + ], + "type": "object", + "properties": { + "canonicalName": { + "type": "string" + }, + "logo": { + "type": [ + "null", + "string" + ] + }, + "bandColor": { + "type": [ + "null", + "string" + ] + }, + "fontColor": { + "type": [ + "null", + "string" + ] + }, + "portalStatus": { + "$ref": "#/components/schemas/PortalStatus" + }, + "privacyNoticeUrl": { + "type": [ + "null", + "string" + ] + } + } + }, + "PortalCfdiUseDto": { + "required": [ + "code", + "description", + "appliesToPhysicalPerson", + "appliesToLegalEntity", + "recipientTaxRegimes" + ], + "type": "object", + "properties": { + "code": { + "type": "string" + }, + "description": { + "type": "string" + }, + "appliesToPhysicalPerson": { + "type": "boolean" + }, + "appliesToLegalEntity": { + "type": "boolean" + }, + "recipientTaxRegimes": { + "type": "array", + "items": { + "type": "string" + } + } + } + }, + "PortalIneligibleReason": { + "enum": [ + "invoiced", + "invoicing", + "globalized", + "portalDisabled", + "outsideWindow", + null + ], + "description": "Por qué el receptor no puede autofacturar el ticket. invoiced: ya tiene su factura; se puede recuperar. invoicing: alguien lo está facturando en este momento. globalized: está en una factura global o entrando a una. portalDisabled: el comercio no ofrece facturación en línea por el momento. outsideWindow: ya pasó el plazo para autofacturarlo." + }, + "PortalInvoiceDto": { + "type": "object", + "properties": { + "uuid": { + "type": [ + "null", + "string" + ] + }, + "downloadToken": { + "type": [ + "null", + "string" + ] + }, + "emailSent": { + "type": [ + "null", + "boolean" + ] + }, + "maskedEmail": { + "type": [ + "null", + "string" + ] + }, + "status": { + "oneOf": [ + { + "type": "null" + }, + { + "$ref": "#/components/schemas/TicketStatus" + } + ] + }, + "errorCode": { + "type": [ + "null", + "string" + ] + }, + "field": { + "type": [ + "null", + "string" + ] + } + } + }, + "PortalInvoiceTicketBody": { + "required": [ + "recipient", + "recaptchaToken" + ], + "type": "object", + "properties": { + "recipient": { + "oneOf": [ + { + "type": "null" + }, + { + "$ref": "#/components/schemas/SelfInvoiceRecipient" + } + ] + }, + "recaptchaToken": { + "type": [ + "null", + "string" + ] + } + } + }, + "PortalIssuerDto": { + "required": [ + "legalName", + "tin" + ], + "type": "object", + "properties": { + "legalName": { + "type": [ + "null", + "string" + ] + }, + "tin": { + "type": [ + "null", + "string" + ] + } + } + }, + "PortalNameAvailabilityDto": { + "required": [ + "name", + "available", + "reason" + ], + "type": "object", + "properties": { + "name": { + "type": "string" + }, + "available": { + "type": "boolean" + }, + "reason": { + "oneOf": [ + { + "type": "null" + }, + { + "$ref": "#/components/schemas/PortalNameUnavailableReason" + } + ] + } + } + }, + "PortalNameDto": { + "required": [ + "name", + "assignedAt", + "nextChangeAllowedAt" + ], + "type": "object", + "properties": { + "name": { + "type": [ + "null", + "string" + ] + }, + "assignedAt": { + "type": [ + "null", + "string" + ], + "format": "date-time" + }, + "nextChangeAllowedAt": { + "type": [ + "null", + "string" + ], + "format": "date-time" + } + } + }, + "PortalNameUnavailableReason": { + "enum": [ + "invalidFormat", + "reserved", + "taken", + null + ], + "description": "Por qué el nombre de portal no está disponible. invalidFormat: no cumple el formato (largo, caracteres o guiones). reserved: es un nombre reservado. taken: otro tenant lo tiene." + }, + "PortalRecoverTicketBody": { + "required": [ + "email", + "recaptchaToken" + ], + "type": "object", + "properties": { + "email": { + "type": [ + "null", + "string" + ] + }, + "recaptchaToken": { + "type": [ + "null", + "string" + ] + } + } + }, + "PortalRecoveryDto": { + "type": "object", + "properties": { + "downloadToken": { + "type": [ + "null", + "string" + ] + }, + "errorCode": { + "type": [ + "null", + "string" + ] + }, + "field": { + "type": [ + "null", + "string" + ] + } + } + }, + "PortalResendDto": { + "type": "object", + "properties": { + "errorCode": { + "type": [ + "null", + "string" + ] + }, + "retryAfterSeconds": { + "pattern": "^-?(?:0|[1-9]\\d*)$", + "type": [ + "null", + "integer", + "string" + ], + "format": "int32" + } + } + }, + "PortalResendTicketBody": { + "required": [ + "downloadToken", + "recaptchaToken" + ], + "type": "object", + "properties": { + "downloadToken": { + "type": [ + "null", + "string" + ] + }, + "recaptchaToken": { + "type": [ + "null", + "string" + ] + } + } + }, + "PortalStatus": { + "enum": [ + "enabled", + "disabled" + ], + "description": "Si el portal de autofactura atiende. enabled: localiza y timbra tickets. disabled: no timbra (el comercio no tiene Auto CFDI o le falta el aviso de privacidad); la recuperación y la descarga siguen disponibles." + }, + "PortalTaxRegimeDto": { + "required": [ + "code", + "description", + "appliesToPhysicalPerson", + "appliesToLegalEntity" + ], + "type": "object", + "properties": { + "code": { + "type": "string" + }, + "description": { + "type": "string" + }, + "appliesToPhysicalPerson": { + "type": "boolean" + }, + "appliesToLegalEntity": { + "type": "boolean" + } + } + }, + "PortalTicketDto": { + "type": "object", + "properties": { + "issuer": { + "$ref": "#/components/schemas/PortalIssuerDto" + }, + "branding": { + "$ref": "#/components/schemas/PortalBrand" + }, + "folio": { + "type": "string" + }, + "series": { + "type": [ + "null", + "string" + ] + }, + "paymentDate": { + "format": "date-time" + }, + "currencyCode": { + "type": "string" + }, + "items": { + "type": "array", + "items": { + "$ref": "#/components/schemas/PortalTicketItemDto" + } + }, + "subtotal": { + "format": "double" + }, + "discount": { + "format": "double" + }, + "transferredTaxes": { + "type": "array", + "items": { + "$ref": "#/components/schemas/PortalTransferredTaxDto" + } + }, + "withheldTaxes": { + "type": "array", + "items": { + "$ref": "#/components/schemas/PortalWithheldTaxDto" + } + }, + "totalTransferredTaxes": { + "format": "double" + }, + "totalWithheldTaxes": { + "format": "double" + }, + "total": { + "format": "double" + }, + "status": { + "$ref": "#/components/schemas/TicketStatus" + }, + "selfInvoiceEligible": { + "type": "boolean" + }, + "ineligibleReason": { + "oneOf": [ + { + "type": "null" + }, + { + "$ref": "#/components/schemas/PortalIneligibleReason" + } + ] + }, + "eligibleUntil": { + "type": [ + "null", + "string" + ], + "format": "date-time" + }, + "maskedEmail": { + "type": [ + "null", + "string" + ] + }, + "privacyNoticeUrl": { + "type": [ + "null", + "string" + ] + } + } + }, + "PortalTicketItemDto": { + "type": "object", + "properties": { + "description": { + "type": "string" + }, + "quantity": { + "format": "double" + }, + "unitOfMeasurementCode": { + "type": "string" + }, + "unitPrice": { + "format": "double" + }, + "discount": { + "format": "double" + }, + "amount": { + "format": "double" + } + } + }, + "PortalTransferredTaxDto": { + "type": "object", + "properties": { + "taxCode": { + "type": "string" + }, + "taxTypeCode": { + "type": "string" + }, + "taxRate": { + "format": "double" + }, + "base": { + "format": "double" + }, + "amount": { + "format": "double" + } + } + }, + "PortalWithheldTaxDto": { + "type": "object", + "properties": { + "taxCode": { + "type": "string" + }, + "amount": { + "format": "double" + } + } + }, + "PropertyInfoRequest": { + "type": "object", + "properties": { + "number": { + "type": [ + "null", + "string" + ] + } + } + }, + "RecipientRequest": { + "type": "object", + "properties": { + "id": { + "type": [ + "null", + "string" + ] + }, + "tin": { + "type": [ + "null", + "string" + ] + }, + "legalName": { + "type": [ + "null", + "string" + ] + }, + "zipCode": { + "type": [ + "null", + "string" + ] + }, + "taxRegimeCode": { + "type": [ + "null", + "string" + ] + }, + "cfdiUseCode": { + "type": [ + "null", + "string" + ] + }, + "email": { + "type": [ + "null", + "string" + ] + }, + "countryId": { + "type": [ + "null", + "string" + ] + }, + "foreignTin": { + "type": [ + "null", + "string" + ] + }, + "employeeData": { + "oneOf": [ + { + "type": "null" + }, + { + "$ref": "#/components/schemas/EmployeeDataRequest" + } + ] + } + } + }, + "RelatedInvoiceRequest": { + "type": "object", + "properties": { + "relationshipTypeCode": { + "type": [ + "null", + "string" + ] + }, + "uuid": { + "type": [ + "null", + "string" + ] + } + } + }, + "ResendWebhookEventCommand": { + "type": "object", + "properties": { + "webhookId": { + "type": [ + "null", + "string" + ], + "description": "El webhook que recibe el reenvío; si se omite, la escucha de CLI de la persona autenticada (fiscalapi listen)." + } + }, + "additionalProperties": false + }, + "RetirementRequest": { + "type": "object", + "properties": { + "totalOneTime": { + "pattern": "^-?(?:0|[1-9]\\d*)(?:\\.\\d+)?$", + "type": [ + "number", + "string" + ], + "format": "double" + }, + "totalInstallments": { + "pattern": "^-?(?:0|[1-9]\\d*)(?:\\.\\d+)?$", + "type": [ + "number", + "string" + ], + "format": "double" + }, + "dailyAmount": { + "pattern": "^-?(?:0|[1-9]\\d*)(?:\\.\\d+)?$", + "type": [ + "number", + "string" + ], + "format": "double" + }, + "accumulableIncome": { + "pattern": "^-?(?:0|[1-9]\\d*)(?:\\.\\d+)?$", + "type": [ + "number", + "string" + ], + "format": "double" + }, + "nonAccumulableIncome": { + "pattern": "^-?(?:0|[1-9]\\d*)(?:\\.\\d+)?$", + "type": [ + "number", + "string" + ], + "format": "double" + } + } + }, + "RunSatValidationsCommand": { + "type": "object", + "properties": { + "xml": { + "type": [ + "null", + "string" + ] + }, + "tin": { + "type": [ + "null", + "string" + ] + }, + "validationTypes": { + "type": "array", + "items": { + "type": "string" + } + } + } + }, + "SelfInvoiceRecipient": { + "required": [ + "tin", + "legalName", + "taxRegimeCode", + "cfdiUseCode", + "zipCode", + "email" + ], + "type": "object", + "properties": { + "tin": { + "type": [ + "null", + "string" + ] + }, + "legalName": { + "type": [ + "null", + "string" + ] + }, + "taxRegimeCode": { + "type": [ + "null", + "string" + ] + }, + "cfdiUseCode": { + "type": [ + "null", + "string" + ] + }, + "zipCode": { + "type": [ + "null", + "string" + ] + }, + "email": { + "type": [ + "null", + "string" + ] + }, + "countryId": { + "type": [ + "null", + "string" + ] + }, + "foreignTin": { + "type": [ + "null", + "string" + ] + } + } + }, + "SendInvoiceByEmailCommand": { + "type": "object", + "properties": { + "invoiceId": { + "type": [ + "null", + "string" + ] + }, + "base64Logo": { + "type": [ + "null", + "string" + ] + }, + "bandColor": { + "type": [ + "null", + "string" + ] + }, + "fontColor": { + "type": [ + "null", + "string" + ] + }, + "toEmail": { + "type": [ + "null", + "string" + ] + } + } + }, + "SeveranceRequest": { + "type": "object", + "properties": { + "totalPaid": { + "pattern": "^-?(?:0|[1-9]\\d*)(?:\\.\\d+)?$", + "type": [ + "number", + "string" + ], + "format": "double" + }, + "yearsOfService": { + "pattern": "^-?(?:0|[1-9]\\d*)$", + "type": [ + "integer", + "string" + ], + "format": "int32" + }, + "lastMonthlySalary": { + "pattern": "^-?(?:0|[1-9]\\d*)(?:\\.\\d+)?$", + "type": [ + "number", + "string" + ], + "format": "double" + }, + "accumulableIncome": { + "pattern": "^-?(?:0|[1-9]\\d*)(?:\\.\\d+)?$", + "type": [ + "number", + "string" + ], + "format": "double" + }, + "nonAccumulableIncome": { + "pattern": "^-?(?:0|[1-9]\\d*)(?:\\.\\d+)?$", + "type": [ + "number", + "string" + ], + "format": "double" + } + } + }, + "SignManifestCommand": { + "type": "object", + "properties": { + "base64Cer": { + "type": [ + "null", + "string" + ] + }, + "base64Key": { + "type": [ + "null", + "string" + ] + }, + "password": { + "type": [ + "null", + "string" + ] + } + } + }, + "StatusCheckCommand": { + "type": "object", + "properties": { + "id": { + "type": [ + "null", + "string" + ] + }, + "issuerTin": { + "type": [ + "null", + "string" + ] + }, + "recipientTin": { + "type": [ + "null", + "string" + ] + }, + "invoiceTotal": { + "pattern": "^-?(?:0|[1-9]\\d*)(?:\\.\\d+)?$", + "type": [ + "number", + "string" + ], + "format": "double" + }, + "invoiceUuid": { + "type": [ + "null", + "string" + ] + }, + "last8DigitsIssuerSignature": { + "type": [ + "null", + "string" + ] + } + } + }, + "StockOptionsRequest": { + "type": "object", + "properties": { + "marketPrice": { + "pattern": "^-?(?:0|[1-9]\\d*)(?:\\.\\d+)?$", + "type": [ + "number", + "string" + ], + "format": "double" + }, + "grantPrice": { + "pattern": "^-?(?:0|[1-9]\\d*)(?:\\.\\d+)?$", + "type": [ + "number", + "string" + ], + "format": "double" + } + } + }, + "StringSegment": { + "type": "object", + "properties": { + "buffer": { + "type": [ + "null", + "string" + ] + }, + "offset": { + "pattern": "^-?(?:0|[1-9]\\d*)$", + "type": [ + "integer", + "string" + ], + "format": "int32" + }, + "length": { + "pattern": "^-?(?:0|[1-9]\\d*)$", + "type": [ + "integer", + "string" + ], + "format": "int32" + }, + "value": { + "type": [ + "null", + "string" + ] + }, + "hasValue": { + "type": "boolean" + } + } + }, + "TicketActorType": { + "enum": [ + "tenant", + "portal", + "system" + ], + "description": "Quién provocó el hecho. tenant: una persona del tenant por la API o el dashboard. portal: el receptor en el portal de autofactura. system: un proceso en segundo plano de FiscalAPI." + }, + "TicketCodeDto": { + "required": [ + "ticketCode", + "qrUrl" + ], + "type": "object", + "properties": { + "ticketCode": { + "type": "string" + }, + "qrUrl": { + "type": "string" + } + } + }, + "TicketDto": { + "type": "object", + "properties": { + "id": { + "type": "string" + }, + "personId": { + "type": "string" + }, + "folio": { + "type": "string" + }, + "series": { + "type": [ + "null", + "string" + ] + }, + "paymentDate": { + "format": "date-time" + }, + "paymentFormCode": { + "type": "string" + }, + "currencyCode": { + "type": "string" + }, + "exchangeRate": { + "format": "double" + }, + "expeditionZipCode": { + "type": "string" + }, + "subtotal": { + "format": "double" + }, + "totalTransferredTaxes": { + "format": "double" + }, + "totalWithheldTaxes": { + "format": "double" + }, + "total": { + "format": "double" + }, + "status": { + "$ref": "#/components/schemas/TicketStatus" + }, + "selfInvoiceEligible": { + "type": "boolean" + }, + "eligibleUntil": { + "type": [ + "null", + "string" + ], + "format": "date-time" + }, + "invoiceId": { + "type": [ + "null", + "string" + ] + }, + "globalInvoiceId": { + "type": [ + "null", + "string" + ] + }, + "createdAt": { + "type": "string", + "format": "date-time" + }, + "items": { + "type": [ + "null", + "array" + ], + "items": { + "$ref": "#/components/schemas/TicketItemDto" + } + }, + "events": { + "type": [ + "null", + "array" + ], + "items": { + "$ref": "#/components/schemas/TicketEventDto" + } + } + } + }, + "TicketEventDto": { + "type": "object", + "properties": { + "type": { + "$ref": "#/components/schemas/TicketEventType" + }, + "occurredAt": { + "type": "string", + "format": "date-time" + }, + "actorType": { + "$ref": "#/components/schemas/TicketActorType" + }, + "invoiceId": { + "type": [ + "null", + "string" + ] + } + } + }, + "TicketEventType": { + "enum": [ + "ingested", + "invoicingStarted", + "invoiced", + "invoiceRejected", + "outcomeUnknown", + "leaseExpired", + "globalizing", + "globalized", + "releasedFromGlobal", + "cancelled", + "recoveryFailed", + "recovered", + "resent", + "returnedToPending", + "leaseUnresolved", + "cancellationRequested", + "substituted", + "ownerNotified" + ], + "description": "Hecho de la historia del ticket. ingested: alta. invoicingStarted: el receptor empezó a facturarlo. invoiced: quedó timbrada su factura individual. invoiceRejected: el PAC rechazó el CFDI o no hubo saldo; vuelve a su estado anterior. outcomeUnknown: no se conoce el resultado del timbrado; queda para atención manual. leaseExpired: venció la reserva sin CFDI; vuelve a su estado anterior. globalizing: una global lo está timbrando. globalized: quedó en la global. releasedFromGlobal: se canceló su global y quedó libre. cancelled: el tenant lo canceló. recoveryFailed: un intento fallido de recuperar la factura en el portal. recovered: el receptor recuperó su factura. resent: se reenvió la factura por correo. returnedToPending: se canceló su factura individual y volvió a pending. leaseUnresolved: la reserva venció y no se puede resolver sin riesgo de un segundo CFDI; queda para atención manual. cancellationRequested: el PAC aceptó la solicitud de cancelación de su factura. substituted: quedó ligado a la factura que sustituye a la cancelada. ownerNotified: se avisó al owner que una autofactura se bloqueó por una causa del comercio." + }, + "TicketIngestedDto": { + "required": [ + "id", + "ticketCode", + "qrUrl", + "status", + "selfInvoiceEligible", + "eligibleUntil", + "subtotal", + "totalTransferredTaxes", + "totalWithheldTaxes", + "total" + ], + "type": "object", + "properties": { + "id": { + "type": "string" + }, + "ticketCode": { + "type": "string" + }, + "qrUrl": { + "type": "string" + }, + "status": { + "$ref": "#/components/schemas/TicketStatus" + }, + "selfInvoiceEligible": { + "type": "boolean" + }, + "eligibleUntil": { + "type": [ + "null", + "string" + ], + "format": "date-time" + }, + "subtotal": { + "pattern": "^-?(?:0|[1-9]\\d*)(?:\\.\\d+)?$", + "type": [ + "number", + "string" + ], + "format": "double" + }, + "totalTransferredTaxes": { + "pattern": "^-?(?:0|[1-9]\\d*)(?:\\.\\d+)?$", + "type": [ + "number", + "string" + ], + "format": "double" + }, + "totalWithheldTaxes": { + "pattern": "^-?(?:0|[1-9]\\d*)(?:\\.\\d+)?$", + "type": [ + "number", + "string" + ], + "format": "double" + }, + "total": { + "pattern": "^-?(?:0|[1-9]\\d*)(?:\\.\\d+)?$", + "type": [ + "number", + "string" + ], + "format": "double" + } + } + }, + "TicketIssuerRequest": { + "type": "object", + "properties": { + "id": { + "type": [ + "null", + "string" + ] + } + } + }, + "TicketItemDto": { + "type": "object", + "properties": { + "itemCode": { + "type": "string" + }, + "itemSku": { + "type": [ + "null", + "string" + ] + }, + "quantity": { + "format": "double" + }, + "unitOfMeasurementCode": { + "type": "string" + }, + "description": { + "type": "string" + }, + "unitPrice": { + "format": "double" + }, + "discount": { + "format": "double" + }, + "taxObjectCode": { + "type": "string" + }, + "itemTaxes": { + "type": "array", + "items": { + "$ref": "#/components/schemas/TicketItemTaxDto" + } + } + } + }, + "TicketItemRequest": { + "type": "object", + "properties": { + "itemCode": { + "type": [ + "null", + "string" + ] + }, + "itemSku": { + "type": [ + "null", + "string" + ] + }, + "quantity": { + "pattern": "^-?(?:0|[1-9]\\d*)(?:\\.\\d+)?$", + "type": [ + "number", + "string" + ], + "format": "double" + }, + "unitOfMeasurementCode": { + "type": [ + "null", + "string" + ] + }, + "description": { + "type": [ + "null", + "string" + ] + }, + "unitPrice": { + "pattern": "^-?(?:0|[1-9]\\d*)(?:\\.\\d+)?$", + "type": [ + "number", + "string" + ], + "format": "double" + }, + "discount": { + "pattern": "^-?(?:0|[1-9]\\d*)(?:\\.\\d+)?$", + "type": [ + "number", + "string" + ], + "format": "double" + }, + "taxObjectCode": { + "type": [ + "null", + "string" + ] + }, + "itemTaxes": { + "type": [ + "null", + "array" + ], + "items": { + "$ref": "#/components/schemas/ItemTaxRequest" + } + } + } + }, + "TicketItemTaxDto": { + "type": "object", + "properties": { + "taxCode": { + "type": "string" + }, + "taxTypeCode": { + "type": "string" + }, + "taxRate": { + "format": "double" + }, + "taxFlagCode": { + "type": "string" + } + } + }, + "TicketStatus": { + "enum": [ + "pending", + "invoicing", + "invoiced", + "globalizing", + "globalized", + "cancelled" + ], + "description": "Estado del ticket. pending: sin factura; el receptor lo puede autofacturar o el tenant incluirlo en una global. invoicing: el receptor lo está timbrando. invoiced: tiene su factura individual (invoiceId). globalizing: una factura global lo está timbrando. globalized: está en una factura global (globalInvoiceId). cancelled: el tenant lo canceló antes de facturarlo; es final y libera la serie y el folio." + }, + "TransferStampsCommand": { + "type": "object", + "properties": { + "fromPersonId": { + "type": [ + "null", + "string" + ] + }, + "toPersonId": { + "type": [ + "null", + "string" + ] + }, + "amount": { + "pattern": "^-?(?:0|[1-9]\\d*)$", + "type": [ + "integer", + "string" + ], + "format": "int32" + }, + "comments": { + "type": [ + "null", + "string" + ] + }, + "creditType": { + "$ref": "#/components/schemas/CreditTypeId" + } + } + }, + "TriggerWebhookEventCommand": { + "type": "object", + "properties": { + "type": { + "type": [ + "null", + "string" + ], + "description": "El tipo de evento, uno de los que se suscriben (p. ej. invoice.canceled)." + }, + "relatedObjectId": { + "type": [ + "null", + "string" + ], + "description": "El id del recurso del evento (data.relatedObject.id); si se omite, un id sintético cuyo GET responde 404." + }, + "personId": { + "type": [ + "null", + "string" + ], + "description": "La persona del tenant dueña del recurso (personId del evento); si se omite, el evento va sin persona." + }, + "webhookId": { + "type": [ + "null", + "string" + ], + "description": "El webhook que recibe el evento; si se omite, la escucha de CLI de la persona autenticada (fiscalapi listen)." + } + }, + "additionalProperties": false + }, + "UpdateApiKeyCommand": { + "type": "object", + "properties": { + "id": { + "type": [ + "null", + "string" + ] + }, + "description": { + "type": [ + "null", + "string" + ] + }, + "apiKeyStatus": { + "oneOf": [ + { + "type": "null" + }, + { + "$ref": "#/components/schemas/ApiKeyStatus" + } + ] + } + } + }, + "UpdateAutoInvoiceSettingCommand": { + "type": "object", + "properties": { + "allowBillingOutsideMonth": { + "type": "boolean" + }, + "allowBillingOutsideYear": { + "type": "boolean" + }, + "privacyNoticeUrl": { + "type": [ + "null", + "string" + ] + } + } + }, + "UpdateCommand": { + "type": "object", + "properties": { + "id": { + "type": [ + "null", + "string" + ] + }, + "description": { + "type": [ + "null", + "string" + ] + }, + "unitPrice": { + "pattern": "^-?(?:0|[1-9]\\d*)(?:\\.\\d+)?$", + "type": [ + "number", + "string" + ], + "format": "double" + }, + "satUnitMeasurementId": { + "type": [ + "null", + "string" + ] + }, + "satTaxObjectId": { + "type": [ + "null", + "string" + ] + }, + "satProductCodeId": { + "type": [ + "null", + "string" + ] + }, + "productTaxes": { + "type": [ + "null", + "array" + ], + "items": { + "$ref": "#/components/schemas/UpdateCommand" + } + } + } + }, + "UpdateDownloadRuleCommand": { + "type": "object", + "properties": { + "id": { + "type": [ + "null", + "string" + ] + }, + "description": { + "type": [ + "null", + "string" + ] + } + } + }, + "UpdateEmployeeDataCommand": { + "type": "object", + "properties": { + "employerPersonId": { + "type": "string" + }, + "employeePersonId": { + "type": "string" + }, + "socialSecurityNumber": { + "type": [ + "null", + "string" + ] + }, + "laborRelationStartDate": { + "type": [ + "null", + "string" + ], + "format": "date-time" + }, + "seniority": { + "type": [ + "null", + "string" + ] + }, + "satContractTypeId": { + "type": "string" + }, + "satUnionizedStatusId": { + "type": [ + "null", + "string" + ] + }, + "satTaxRegimeTypeId": { + "type": "string" + }, + "satWorkdayTypeId": { + "type": [ + "null", + "string" + ] + }, + "satJobRiskId": { + "type": [ + "null", + "string" + ] + }, + "satPaymentPeriodicityId": { + "type": "string" + }, + "employeeNumber": { + "type": "string" + }, + "satBankId": { + "type": [ + "null", + "string" + ] + }, + "satPayrollStateId": { + "type": "string" + }, + "department": { + "type": [ + "null", + "string" + ] + }, + "position": { + "type": [ + "null", + "string" + ] + }, + "bankAccount": { + "type": [ + "null", + "string" + ] + }, + "baseSalaryForContributions": { + "pattern": "^-?(?:0|[1-9]\\d*)(?:\\.\\d+)?$", + "type": [ + "null", + "number", + "string" + ], + "format": "double" + }, + "integratedDailySalary": { + "pattern": "^-?(?:0|[1-9]\\d*)(?:\\.\\d+)?$", + "type": [ + "null", + "number", + "string" + ], + "format": "double" + }, + "subcontractorRfc": { + "type": [ + "null", + "string" + ] + }, + "timePercentage": { + "pattern": "^-?(?:0|[1-9]\\d*)(?:\\.\\d+)?$", + "type": [ + "null", + "number", + "string" + ], + "format": "double" + } + } + }, + "UpdateEmployerDataCommand": { + "type": "object", + "properties": { + "personId": { + "type": [ + "null", + "string" + ] + }, + "employerRegistration": { + "type": [ + "null", + "string" + ] + }, + "originEmployerTin": { + "type": [ + "null", + "string" + ] + }, + "satFundSourceId": { + "type": [ + "null", + "string" + ] + }, + "ownResourceAmount": { + "pattern": "^-?(?:0|[1-9]\\d*)(?:\\.\\d+)?$", + "type": [ + "null", + "number", + "string" + ], + "format": "double" + } + } + }, + "UpdatePortalNameCommand": { + "type": "object", + "properties": { + "name": { + "type": [ + "null", + "string" + ] + } + } + }, + "UpdateUserCommand": { + "type": "object", + "properties": { + "id": { + "type": [ + "null", + "string" + ] + }, + "legalName": { + "type": [ + "null", + "string" + ] + }, + "email": { + "type": [ + "null", + "string" + ] + }, + "phoneNumber": { + "type": [ + "null", + "string" + ] + }, + "password": { + "type": [ + "null", + "string" + ] + }, + "satTaxRegimeId": { + "type": [ + "null", + "string" + ] + }, + "satCfdiUseId": { + "type": [ + "null", + "string" + ] + }, + "userTypeId": { + "type": [ + "null", + "string" + ] + }, + "tin": { + "type": [ + "null", + "string" + ] + }, + "zipCode": { + "type": [ + "null", + "string" + ] + }, + "base64Photo": { + "type": [ + "null", + "string" + ] + }, + "taxPassword": { + "type": [ + "null", + "string" + ] + }, + "curp": { + "type": [ + "null", + "string" + ] + }, + "countryId": { + "type": [ + "null", + "string" + ] + }, + "foreignTin": { + "type": [ + "null", + "string" + ] + } + } + }, + "UpdateWebhookCommand": { + "type": "object", + "properties": { + "id": { + "type": [ + "null", + "string" + ], + "description": "Id del webhook; debe ser el de la ruta." + }, + "url": { + "type": [ + "null", + "string" + ], + "description": "URL https absoluta que recibe las entregas, sin usuario ni contraseña." + }, + "description": { + "type": [ + "null", + "string" + ], + "description": "Texto libre del tenant, hasta 256 caracteres." + }, + "enabledEvents": { + "type": [ + "null", + "array" + ], + "items": { + "type": "string" + }, + "description": "Tipos de evento suscritos, o solo \"*\" para recibirlos todos." + }, + "personIds": { + "type": [ + "null", + "array" + ], + "items": { + "type": "string" + }, + "description": "Personas del tenant cuyos eventos recibe; sin ellas (o vacío), las de todas." + }, + "status": { + "oneOf": [ + { + "type": "null" + }, + { + "description": "enabled o disabled; omitido, conserva el estado. Habilitar uno deshabilitado limpia failingSince, disabledReason y disabledAt; deshabilitar cierra sus entregas pendientes.", + "$ref": "#/components/schemas/WebhookStatus" + } + ] + } + } + }, + "WebhookDeliveryDto": { + "type": "object", + "properties": { + "id": { + "type": "string" + }, + "eventId": { + "type": "string" + }, + "eventType": { + "type": "string" + }, + "kind": { + "$ref": "#/components/schemas/WebhookDeliveryKind" + }, + "status": { + "$ref": "#/components/schemas/WebhookDeliveryStatus" + }, + "attempts": { + "pattern": "^-?(?:0|[1-9]\\d*)$", + "type": [ + "integer", + "string" + ], + "format": "int32" + }, + "nextAttemptAt": { + "type": "string", + "format": "date-time" + }, + "lastAttemptAt": { + "type": [ + "null", + "string" + ], + "format": "date-time" + }, + "lastStatusCode": { + "pattern": "^-?(?:0|[1-9]\\d*)$", + "type": [ + "null", + "integer", + "string" + ], + "format": "int32" + }, + "lastLatencyMs": { + "pattern": "^-?(?:0|[1-9]\\d*)$", + "type": [ + "null", + "integer", + "string" + ], + "format": "int32" + }, + "lastError": { + "type": [ + "null", + "string" + ] + }, + "lastResponseExcerpt": { + "type": [ + "null", + "string" + ] + }, + "createdAt": { + "type": "string", + "format": "date-time" + } + } + }, + "WebhookDeliveryKind": { + "enum": [ + "automatic", + "manual", + "test" + ] + }, + "WebhookDeliveryStatus": { + "enum": [ + "pending", + "succeeded", + "failed" + ] + }, + "WebhookDisabledReason": { + "enum": [ + "manual", + "gone", + "failingTooLong", + null + ] + }, + "WebhookDto": { + "type": "object", + "properties": { + "id": { + "type": "string" + }, + "kind": { + "$ref": "#/components/schemas/WebhookKind" + }, + "url": { + "type": [ + "null", + "string" + ] + }, + "description": { + "type": "string" + }, + "enabledEvents": { + "type": "array", + "items": { + "type": "string" + } + }, + "personIds": { + "type": [ + "null", + "array" + ], + "items": { + "type": "string" + } + }, + "status": { + "$ref": "#/components/schemas/WebhookStatus" + }, + "disabledReason": { + "oneOf": [ + { + "type": "null" + }, + { + "$ref": "#/components/schemas/WebhookDisabledReason" + } + ] + }, + "disabledAt": { + "type": [ + "null", + "string" + ], + "format": "date-time" + }, + "failingSince": { + "type": [ + "null", + "string" + ], + "format": "date-time" + }, + "listenerPersonId": { + "type": [ + "null", + "string" + ] + }, + "sessionExpiresAt": { + "type": [ + "null", + "string" + ], + "format": "date-time" + }, + "createdAt": { + "type": "string", + "format": "date-time" + }, + "updatedAt": { + "type": [ + "null", + "string" + ], + "format": "date-time" + } + } + }, + "WebhookEventResendDto": { + "required": [ + "deliveryId", + "webhookId" + ], + "type": "object", + "properties": { + "deliveryId": { + "type": "string" + }, + "webhookId": { + "type": "string" + } + } + }, + "WebhookEventTriggerDto": { + "required": [ + "eventId", + "webhookId" + ], + "type": "object", + "properties": { + "eventId": { + "type": "string" + }, + "webhookId": { + "type": "string" + } + } + }, + "WebhookKind": { + "enum": [ + "url", + "cliListener" + ] + }, + "WebhookStatus": { + "enum": [ + "enabled", + "disabled" + ] + }, + "WebhookTestDto": { + "required": [ + "eventId", + "deliveryId" + ], + "type": "object", + "properties": { + "eventId": { + "type": "string" + }, + "deliveryId": { + "type": "string" + } + } + }, + "WebhookWithSecretDto": { + "type": "object", + "properties": { + "secret": { + "type": [ + "null", + "string" + ] + }, + "id": { + "type": "string" + }, + "kind": { + "$ref": "#/components/schemas/WebhookKind" + }, + "url": { + "type": [ + "null", + "string" + ] + }, + "description": { + "type": "string" + }, + "enabledEvents": { + "type": "array", + "items": { + "type": "string" + } + }, + "personIds": { + "type": [ + "null", + "array" + ], + "items": { + "type": "string" + } + }, + "status": { + "$ref": "#/components/schemas/WebhookStatus" + }, + "disabledReason": { + "oneOf": [ + { + "type": "null" + }, + { + "$ref": "#/components/schemas/WebhookDisabledReason" + } + ] + }, + "disabledAt": { + "type": [ + "null", + "string" + ], + "format": "date-time" + }, + "failingSince": { + "type": [ + "null", + "string" + ], + "format": "date-time" + }, + "listenerPersonId": { + "type": [ + "null", + "string" + ] + }, + "sessionExpiresAt": { + "type": [ + "null", + "string" + ], + "format": "date-time" + }, + "createdAt": { + "type": "string", + "format": "date-time" + }, + "updatedAt": { + "type": [ + "null", + "string" + ], + "format": "date-time" + } + } + } + }, + "securitySchemes": { + "ApiKey": { + "type": "apiKey", + "name": "X-API-KEY", + "in": "header" + }, + "TenantKey": { + "type": "apiKey", + "name": "X-TENANT-KEY", + "in": "header" + } + } + }, + "tags": [ + { + "name": "ApiKeys" + }, + { + "name": "AutoInvoice" + }, + { + "name": "Catalogs" + }, + { + "name": "DownloadCatalogs" + }, + { + "name": "DownloadRequests" + }, + { + "name": "DownloadRules" + }, + { + "name": "Events" + }, + { + "name": "Invoices" + }, + { + "name": "Manifests" + }, + { + "name": "People" + }, + { + "name": "Portal" + }, + { + "name": "Products" + }, + { + "name": "SatValidations" + }, + { + "name": "Stamps" + }, + { + "name": "TaxFiles" + }, + { + "name": "Tickets" + }, + { + "name": "Webhooks" + } + ] +} \ No newline at end of file diff --git a/mcp/test/global-setup.ts b/mcp/test/global-setup.ts new file mode 100644 index 0000000..d5d3d3a --- /dev/null +++ b/mcp/test/global-setup.ts @@ -0,0 +1,38 @@ +import { mkdtempSync, rmSync } from 'node:fs'; +import { tmpdir } from 'node:os'; +import path from 'node:path'; +import { fileURLToPath, pathToFileURL } from 'node:url'; + +import type { TestProject } from 'vitest/node'; + +import { readDocsCorpus, serveDocs } from './docs-server.js'; +import { runBuildData } from './run-build-data.js'; + +export const packageDir = fileURLToPath(new URL('../', import.meta.url)); +export const fixturePath = path.join(packageDir, 'test', 'fixtures', 'openapi-v4.json'); + +// Los datos de prueba van a un directorio temporal: npm test nunca pisa el data/ que se publica. +// La documentación sale del fixture servido en local, así que el build de prueba no usa la red. +export default async function setup(project: TestProject): Promise<() => void> { + const workDir = mkdtempSync(path.join(tmpdir(), 'fiscalapi-mcp-')); + const dataDir = path.join(workDir, 'data'); + const docs = await serveDocs(readDocsCorpus()); + let build: { code: number | null; stderr: string }; + try { + build = await runBuildData([fixturePath, '--docs-origin', docs.origin, '--out', dataDir]); + } finally { + await docs.close(); + } + if (build.code !== 0) { + rmSync(workDir, { recursive: true, force: true }); + throw new Error(`build-data falló con el fixture:\n${build.stderr}`); + } + project.provide('dataDir', pathToFileURL(dataDir + path.sep).href); + return () => rmSync(workDir, { recursive: true, force: true }); +} + +declare module 'vitest' { + export interface ProvidedContext { + dataDir: string; + } +} diff --git a/mcp/test/helpers.ts b/mcp/test/helpers.ts new file mode 100644 index 0000000..157c5b1 --- /dev/null +++ b/mcp/test/helpers.ts @@ -0,0 +1,37 @@ +import { Client, InMemoryTransport } from '@modelcontextprotocol/client'; +import type { CallToolResult } from '@modelcontextprotocol/client'; +import { inject } from 'vitest'; + +import { OperationCatalog } from '../src/catalog.js'; +import { DocsIndex } from '../src/docs.js'; +import { createServer } from '../src/server.js'; + +export function dataDirUrl(): URL { + return new URL(inject('dataDir')); +} + +/** Cliente MCP en proceso conectado al servidor por el transporte en memoria del SDK. */ +export async function connectClient(): Promise<{ client: Client; close: () => Promise }> { + const server = createServer(OperationCatalog.load(dataDirUrl()), DocsIndex.load(dataDirUrl()), '0.0.0-test'); + const [clientTransport, serverTransport] = InMemoryTransport.createLinkedPair(); + const client = new Client({ name: 'fiscalapi-mcp-test', version: '1.0.0' }); + await server.connect(serverTransport); + await client.connect(clientTransport); + return { + client, + close: async () => { + await client.close(); + await server.close(); + }, + }; +} + +export async function callTool(client: Client, name: string, args: Record = {}): Promise { + return (await client.callTool({ name, arguments: args })) as CallToolResult; +} + +export function textOf(result: CallToolResult): string { + const [block] = result.content; + if (block?.type !== 'text') throw new Error('Se esperaba un bloque de texto.'); + return block.text; +} diff --git a/mcp/test/offline.test.ts b/mcp/test/offline.test.ts new file mode 100644 index 0000000..3ae06de --- /dev/null +++ b/mcp/test/offline.test.ts @@ -0,0 +1,31 @@ +import { afterEach, expect, it, vi } from 'vitest'; + +import { callTool, connectClient, textOf } from './helpers.js'; + +afterEach(() => { + vi.unstubAllGlobals(); +}); + +// El cliente se conecta después de cortar la red: así cargar el catálogo y el índice de la +// documentación también queda cubierto, no solo las llamadas a las herramientas. +it('arranca y responde sin red: nunca llama a fetch', async () => { + const fetch = vi.fn(() => { + throw new Error('Sin red: el MCP no debe descargar nada'); + }); + vi.stubGlobal('fetch', fetch); + + const { client, close } = await connectClient(); + try { + const { tools } = await client.listTools(); + const operation = await callTool(client, 'obtener_operacion', { operationId: 'createInvoice' }); + const docs = await callTool(client, 'buscar_docs', { consulta: 'cancelar factura' }); + + expect(tools).toHaveLength(3); + expect(JSON.parse(textOf(operation)).operationId).toBe('createInvoice'); + expect(docs.isError).toBeFalsy(); + expect(textOf(docs)).toContain('https://docs.fiscalapi.com/'); + expect(fetch).not.toHaveBeenCalled(); + } finally { + await close(); + } +}); diff --git a/mcp/test/run-build-data.ts b/mcp/test/run-build-data.ts new file mode 100644 index 0000000..a2b3c68 --- /dev/null +++ b/mcp/test/run-build-data.ts @@ -0,0 +1,18 @@ +import { spawn } from 'node:child_process'; +import { fileURLToPath } from 'node:url'; + +const packageDir = fileURLToPath(new URL('../', import.meta.url)); + +/** + * Corre scripts/build-data.mjs en otro proceso. Es asíncrono a propósito: el servidor de la + * documentación de prueba vive en este proceso y con spawnSync no podría responder. + */ +export function runBuildData(args: readonly string[]): Promise<{ code: number | null; stderr: string }> { + return new Promise((resolve, reject) => { + const child = spawn(process.execPath, ['scripts/build-data.mjs', ...args], { cwd: packageDir }); + let stderr = ''; + child.stderr.on('data', (chunk) => (stderr += chunk)); + child.on('error', reject); + child.on('close', (code) => resolve({ code, stderr })); + }); +} diff --git a/mcp/test/search-regression.test.ts b/mcp/test/search-regression.test.ts new file mode 100644 index 0000000..cc0d138 --- /dev/null +++ b/mcp/test/search-regression.test.ts @@ -0,0 +1,56 @@ +import { describe, expect, it } from 'vitest'; + +import { DocsIndex } from '../src/docs.js'; +import { dataDirUrl } from './helpers.js'; + +const TOP = 3; +const MIN_HITS = 15; + +// Caso conocido: el primer resultado es la página general "Catálogos SAT" (catalogs-info.md), que +// remite al recurso de catálogos; el endpoint "Buscar en un catálogo" queda lejos porque su texto no +// dice "producto" ni "clave". No tiene su propio it: solo cuenta para el total. +const KNOWN_MISS = 'buscar clave de producto sat'; + +// Consultas reales de un agente con la sección que debe aparecer entre los primeros 3 resultados. +// "estatus ante el sat" acepta sat-validations/validar: la validación sat.cfdi.status ("Estatus del +// comprobante") responde esa consulta. +const CASES: ReadonlyArray = [ + ['cancelar una factura', /\/cancelar-factura\.md$/], + ['complemento de pago', /crear-factura-de-pago|errores-pagos/], + ['descargar xml', /obtener-xml-por-id|listar-xmls-por-id|descargar-xml-y-pdf/], + [KNOWN_MISS, /catalogs\/buscar-en-un-catalogo|productos-y-servicios/], + ['crear cliente receptor', /recipients\/crear-receptor/], + ['subir certificado csd', /subir-certificados/], + ['nómina empleado', /employee-data\/|crear-factura-de-nomina/], + ['webhook', /\/webhooks\//], + ['saldo de timbres', /\/stamps\//], + ['estatus ante el sat', /consultar-estado-de-factura|sat-validations\/validar\.md|los-siete-criterios/], + ['descarga masiva', /download-info|download-requests\//], + ['lista negra 69b', /sat-validations/], + ['factura global', /factura-global|global-invoice/], + ['enviar factura por correo', /enviar-factura-por-correo/], + ['CFDI40147', /errores-cfdi-4-0/], + ['objeto de impuesto', /objeto-imp/], +]; + +const docs = DocsIndex.load(dataDirUrl()); + +function topUrls(query: string): string[] { + return docs.search(query, TOP).map((result) => result.url); +} + +function hits(query: string, expected: RegExp): boolean { + return topUrls(query).some((url) => expected.test(url)); +} + +describe('buscar_docs: regresión de relevancia', () => { + it.each(CASES.filter(([query]) => query !== KNOWN_MISS))('"%s" trae la sección esperada en el top 3', (query, expected) => { + expect(topUrls(query)).toEqual(expect.arrayContaining([expect.stringMatching(expected)])); + }); + + it(`acierta al menos ${MIN_HITS} de ${CASES.length} consultas`, () => { + const total = CASES.filter(([query, expected]) => hits(query, expected)).length; + + expect(total).toBeGreaterThanOrEqual(MIN_HITS); + }); +}); diff --git a/mcp/test/tools.test.ts b/mcp/test/tools.test.ts new file mode 100644 index 0000000..cb09e44 --- /dev/null +++ b/mcp/test/tools.test.ts @@ -0,0 +1,138 @@ +import { readFileSync } from 'node:fs'; + +import type { Client } from '@modelcontextprotocol/client'; +import { afterAll, beforeAll, describe, expect, it } from 'vitest'; + +import { callTool, connectClient, dataDirUrl, textOf } from './helpers.js'; + +// Límites duros de RNF-05: unos 3k tokens para el catálogo y unos 10k para una operación. +const MAX_CATALOG_CHARS = 12_000; +const MAX_OPERATION_CHARS = 40_000; +const OPERATION_LINE = /^\w+ {2}(GET|POST|PUT|PATCH|DELETE) \/\S* {2}.+$/; + +interface CatalogEntry { + id: string; + tag: string; +} + +const catalog = JSON.parse(readFileSync(new URL('catalog.json', dataDirUrl()), 'utf8')) as CatalogEntry[]; + +let client: Client; +let close: () => Promise; + +beforeAll(async () => { + ({ client, close } = await connectClient()); +}); + +afterAll(async () => { + await close(); +}); + +function operationLines(text: string): string[] { + return text.split('\n').filter((line) => OPERATION_LINE.test(line)); +} + +describe('tools/list', () => { + it('expone exactamente buscar_docs, listar_operaciones y obtener_operacion con descripciones en español', async () => { + const { tools } = await client.listTools(); + + expect(tools.map((tool) => tool.name).sort()).toEqual(['buscar_docs', 'listar_operaciones', 'obtener_operacion']); + const byName = Object.fromEntries(tools.map((tool) => [tool.name, tool])); + expect(byName.listar_operaciones?.description).toContain('el siguiente paso es llamar a obtener_operacion'); + expect(byName.obtener_operacion?.description).toContain('Usa el operationId exacto que da listar_operaciones'); + expect(byName.obtener_operacion?.inputSchema.required).toEqual(['operationId']); + for (const tool of tools) { + expect(tool.annotations?.readOnlyHint).toBe(true); + expect(tool.description).not.toMatch(/—/); + } + }); +}); + +describe('listar_operaciones', () => { + it('sin tag lista las 100 operaciones agrupadas por tag dentro del límite de tamaño', async () => { + const result = await callTool(client, 'listar_operaciones'); + const text = textOf(result); + + expect(result.isError).toBeFalsy(); + expect(operationLines(text)).toHaveLength(100); + expect(text).toContain('createInvoice POST /api/v4/invoices Emitir (timbrar) una factura CFDI 4.0'); + expect(text).toContain('## Invoices'); + expect(text.length).toBeLessThanOrEqual(MAX_CATALOG_CHARS); + }); + + it('con tag filtra sin distinguir mayúsculas', async () => { + const invoices = catalog.filter((entry) => entry.tag === 'Invoices').map((entry) => entry.id); + + const text = textOf(await callTool(client, 'listar_operaciones', { tag: 'invoices' })); + + const ids = operationLines(text).map((line) => line.split(' ')[0]); + expect(ids).toEqual(invoices); + expect(text).not.toContain('## People'); + }); + + it('con un tag inexistente devuelve error con los tags disponibles', async () => { + const result = await callTool(client, 'listar_operaciones', { tag: 'Facturas' }); + + expect(result.isError).toBe(true); + expect(textOf(result)).toContain('No hay operaciones con el tag "Facturas"'); + expect(textOf(result)).toContain('Invoices'); + }); +}); + +describe('obtener_operacion', () => { + it('devuelve createInvoice con su cuerpo resuelto y dentro del límite de tamaño', async () => { + const result = await callTool(client, 'obtener_operacion', { operationId: 'createInvoice' }); + const text = textOf(result); + const operation = JSON.parse(text); + + expect(result.isError).toBeFalsy(); + expect(operation).toMatchObject({ operationId: 'createInvoice', method: 'POST', path: '/api/v4/invoices' }); + expect(operation.description).toContain('https://docs.fiscalapi.com/'); + const body = operation.requestBody.content['application/json'].schema; + expect(body.properties.items.items.properties).toHaveProperty('itemCode'); + expect(text).not.toContain('"$ref"'); + expect(text.length).toBeLessThanOrEqual(MAX_OPERATION_CHARS); + }); + + it.each(catalog.map((entry) => entry.id))('%s cabe en el límite y no deja $ref sin resolver', async (operationId) => { + const result = await callTool(client, 'obtener_operacion', { operationId }); + const text = textOf(result); + + expect(result.isError).toBeFalsy(); + expect(JSON.parse(text).operationId).toBe(operationId); + expect(text).not.toContain('"$ref"'); + expect(text.length).toBeLessThanOrEqual(MAX_OPERATION_CHARS); + }); + + it('con un id mal escrito devuelve error y sugiere el correcto', async () => { + const result = await callTool(client, 'obtener_operacion', { operationId: 'createInvoce' }); + + expect(result.isError).toBe(true); + expect(textOf(result)).toContain('No existe la operación "createInvoce"'); + expect(textOf(result)).toContain('¿Quisiste decir createInvoice?'); + }); + + it('sin parecido suficiente no sugiere nada y remite a listar_operaciones', async () => { + const result = await callTool(client, 'obtener_operacion', { operationId: 'xyz' }); + + expect(result.isError).toBe(true); + expect(textOf(result)).toBe('No existe la operación "xyz". Usa listar_operaciones para ver los operationId disponibles.'); + }); + + it('sugiere primero los ids que empiezan con lo enviado y luego los que lo contienen', async () => { + const result = await callTool(client, 'obtener_operacion', { operationId: 'getinvoice' }); + + expect(result.isError).toBe(true); + expect(textOf(result)).toContain('¿Quisiste decir getInvoice, getInvoicePdf, getInvoiceStatus, getInvoiceXml?'); + }); + + it.each(['../package', '../catalog', '..\\catalog', '/etc/passwd'])('rechaza %s sin leer archivos fuera de data/operations', async (operationId) => { + const result = await callTool(client, 'obtener_operacion', { operationId }); + const text = textOf(result); + + expect(result.isError).toBe(true); + expect(text).toContain('No existe la operación'); + expect(text).not.toContain('"summary"'); + expect(text).not.toContain('@fiscalapi/mcp'); + }); +}); diff --git a/mcp/tsconfig.json b/mcp/tsconfig.json new file mode 100644 index 0000000..402c028 --- /dev/null +++ b/mcp/tsconfig.json @@ -0,0 +1,18 @@ +{ + "compilerOptions": { + "target": "ES2023", + "lib": ["ES2023"], + "module": "NodeNext", + "moduleResolution": "NodeNext", + "types": ["node"], + "strict": true, + "noUncheckedIndexedAccess": true, + "noImplicitOverride": true, + "verbatimModuleSyntax": true, + "erasableSyntaxOnly": true, + "skipLibCheck": true, + "rootDir": "src", + "outDir": "dist" + }, + "include": ["src"] +} diff --git a/mcp/vitest.config.ts b/mcp/vitest.config.ts new file mode 100644 index 0000000..17cae7d --- /dev/null +++ b/mcp/vitest.config.ts @@ -0,0 +1,8 @@ +import { defineConfig } from 'vitest/config'; + +export default defineConfig({ + test: { + include: ['test/**/*.test.ts'], + globalSetup: ['test/global-setup.ts'], + }, +}); diff --git a/scripts/check-skills.mjs b/scripts/check-skills.mjs new file mode 100644 index 0000000..fece6f9 --- /dev/null +++ b/scripts/check-skills.mjs @@ -0,0 +1,172 @@ +// Revisa lo que publica el plugin antes de que llegue a un integrador: +// - el frontmatter de cada SKILL.md sigue el estándar de agentskills.io; +// - cada enlace a docs.fiscalapi.com responde 200 y, si lleva ancla, el ancla existe en la página; +// - lo mismo para los enlaces "Docs:" de las descripciones del OpenAPI público (/api/openapi/v4.json), que +// fapi-back escribe a mano y que un título renombrado en fapi-docs rompería sin que nadie lo note. +// Las skills enlazan las docs en vez de copiarlas (D-03): un enlace roto deja a la skill sin su fuente. +// +// Uso: node scripts/check-skills.mjs [carpeta ...] (por omisión, skills y commands) +// FISCALAPI_DOCS_ORIGIN=http://localhost:3100 revisa los enlaces contra un build local de fapi-docs, para +// validar secciones que todavía no están publicadas. +// FISCALAPI_OPENAPI_URL= lee el OpenAPI de un back local o de un archivo, para +// validar descripciones que todavía no están en producción. +// Sale con código 1 si hay algún hallazgo. + +import { readdir, readFile, stat } from 'node:fs/promises' +import path from 'node:path' + +const DOCS_ORIGIN = 'https://docs.fiscalapi.com' +const CHECK_ORIGIN = process.env.FISCALAPI_DOCS_ORIGIN ?? DOCS_ORIGIN +const OPENAPI_SOURCE = process.env.FISCALAPI_OPENAPI_URL ?? 'https://live.fiscalapi.com/api/openapi/v4.json' +const HTTP_METHODS = ['get', 'put', 'post', 'delete', 'options', 'head', 'patch', 'trace'] +const DOCS_URL = /https:\/\/docs\.fiscalapi\.com[^\s)<>"'`\]]*/g +const SKILL_NAME = /^[a-z0-9]+(-[a-z0-9]+)*$/ +const MAX_NAME = 64 +const MAX_DESCRIPTION = 1024 +const MAX_SKILL_LINES = 500 + +const roots = process.argv.slice(2).length > 0 ? process.argv.slice(2) : ['skills', 'commands'] + +async function exists(target) { + return stat(target).then(() => true, () => false) +} + +async function markdownFiles(dir) { + const entries = await readdir(dir, { withFileTypes: true }) + const nested = await Promise.all( + entries.map((entry) => { + const full = path.join(dir, entry.name) + if (entry.isDirectory()) return markdownFiles(full) + return entry.name.endsWith('.md') ? [full] : [] + }), + ) + return nested.flat() +} + +/** Las claves de una línea del frontmatter YAML (`clave: valor`, con o sin comillas). */ +function frontmatter(source) { + const match = source.replace(/\r\n/g, '\n').match(/^---\n([\s\S]*?)\n---\n/) + if (!match) return null + const fields = {} + for (const line of match[1].split('\n')) { + const field = line.match(/^([A-Za-z-]+):\s*(.*)$/) + if (field) fields[field[1]] = field[2].trim().replace(/^(['"])([\s\S]*)\1$/, '$2') + } + return fields +} + +function skillProblems(file, source) { + const problems = [] + const folder = path.basename(path.dirname(file)) + const fields = frontmatter(source) + if (!fields) return [`${file}: falta el frontmatter (--- … ---) al inicio`] + + const { name, description } = fields + if (!name) problems.push(`${file}: falta 'name'`) + else { + if (name !== folder) problems.push(`${file}: 'name' (${name}) no es el nombre de la carpeta (${folder})`) + if (!SKILL_NAME.test(name) || name.length > MAX_NAME) + problems.push(`${file}: 'name' debe ser minúsculas, dígitos y guiones, de ${MAX_NAME} caracteres como máximo`) + } + if (!description) problems.push(`${file}: falta 'description'`) + else if (/^[>|]/.test(description)) + problems.push(`${file}: 'description' va en una sola línea (este lector no interpreta bloques YAML > o |)`) + else if (description.length > MAX_DESCRIPTION) + problems.push(`${file}: 'description' tiene ${description.length} caracteres (máximo ${MAX_DESCRIPTION})`) + + const lines = source.split('\n').length + if (lines >= MAX_SKILL_LINES) problems.push(`${file}: tiene ${lines} líneas (debe tener menos de ${MAX_SKILL_LINES})`) + return problems +} + +/** Las anclas de la página HTML: el `id` que el sitio genera para cada H2 y los `` explícitos. */ +async function pageAnchors(pageUrl, cache) { + if (!cache.has(pageUrl)) { + cache.set( + pageUrl, + fetch(pageUrl).then(async (response) => + response.ok ? new Set([...(await response.text()).matchAll(/\s(?:id|name)="([^"]+)"/g)].map((m) => m[1])) : null, + ), + ) + } + return cache.get(pageUrl) +} + +async function linkProblems(url, anchorsCache) { + const [published, anchor] = url.split('#') + const address = published.replace(DOCS_ORIGIN, CHECK_ORIGIN) + const response = await fetch(address, { method: 'GET', redirect: 'manual' }) + if (response.status !== 200) return [`${url}: responde ${response.status}`] + if (!anchor) return [] + + // El ancla vive en la página HTML; la copia .md de una página tiene las mismas secciones. + const anchors = await pageAnchors(address.replace(/\.md$/, ''), anchorsCache) + if (!anchors) return [`${url}: la página HTML de su ancla no responde`] + return anchors.has(anchor) ? [] : [`${url}: el ancla #${anchor} no existe`] +} + +/** Agrega a `urls` cada enlace a las docs de `text`, recordando dónde aparece (`where`). */ +function collectDocsUrls(text, where, urls) { + for (const [url] of (text ?? '').matchAll(DOCS_URL)) { + const clean = url.replace(/[.,;:]+$/, '') + urls.set(clean, [...(urls.get(clean) ?? []), where]) + } +} + +async function readOpenApi(source) { + if (!/^https?:\/\//.test(source)) return JSON.parse(await readFile(source, 'utf8')) + const response = await fetch(source) + if (!response.ok) throw new Error(`${source} responde ${response.status}`) + return response.json() +} + +/** Los enlaces a las docs de la descripción del documento y de cada operación, por operationId. */ +async function openApiDocsUrls(source) { + const document = await readOpenApi(source) + const urls = new Map() + collectDocsUrls(document.info?.description, 'OpenAPI info.description', urls) + for (const [route, item] of Object.entries(document.paths ?? {})) { + for (const method of HTTP_METHODS) { + const operation = item[method] + if (operation) collectDocsUrls(operation.description, `OpenAPI ${operation.operationId ?? `${method} ${route}`}`, urls) + } + } + return urls +} + +async function main() { + const files = [] + for (const root of roots) if (await exists(root)) files.push(...(await markdownFiles(root))) + + const problems = [] + const urls = new Map() + for (const file of files) { + const source = await readFile(file, 'utf8') + if (path.basename(file) === 'SKILL.md') problems.push(...skillProblems(file, source)) + collectDocsUrls(source, file, urls) + } + const skillUrlCount = urls.size + + let openApiUrlCount = 0 + try { + const openApiUrls = await openApiDocsUrls(OPENAPI_SOURCE) + openApiUrlCount = openApiUrls.size + for (const [url, places] of openApiUrls) urls.set(url, [...(urls.get(url) ?? []), ...places]) + } catch (error) { + problems.push(`${OPENAPI_SOURCE}: no se pudo leer el OpenAPI (${error.message})`) + } + + const anchorsCache = new Map() + for (const [url, places] of urls) { + for (const problem of await linkProblems(url, anchorsCache)) problems.push(`${places.join(', ')}: ${problem}`) + } + + console.log( + `${files.length} archivos con ${skillUrlCount} enlaces y el OpenAPI (${OPENAPI_SOURCE}) con ${openApiUrlCount}: ` + + `${urls.size} enlaces a ${DOCS_ORIGIN} revisados en ${CHECK_ORIGIN}, ${problems.length} hallazgos`, + ) + for (const problem of problems) console.log(`- ${problem}`) + process.exitCode = problems.length > 0 ? 1 : 0 +} + +await main() diff --git a/skills/fiscalapi-carta-porte/SKILL.md b/skills/fiscalapi-carta-porte/SKILL.md new file mode 100644 index 0000000..5c1ada6 --- /dev/null +++ b/skills/fiscalapi-carta-porte/SKILL.md @@ -0,0 +1,48 @@ +--- +name: fiscalapi-carta-porte +description: Emite con FiscalAPI un CFDI con complemento Carta Porte 3.1 por autotransporte, de traslado (tipo T) o de ingreso por el flete (tipo I). Decide moneda, receptor y conceptos según el tipo, arma ubicaciones de origen y destino con domicilio y distancias, mercancías con su clave, peso y material peligroso, el autotransporte (permiso SCT, vehículo, seguros y remolques) y las figuras de transporte, y evita los rechazos CP más comunes. Úsala cuando vayas a timbrar una carta porte o a transportar mercancías por carretera con CFDI. Para un error CP que ya devolvió la API, usa fiscalapi-diagnostico. +--- + +# Carta Porte 3.1 por autotransporte + +Antes de escribir el cuerpo, lee [Crear factura de carta porte](https://docs.fiscalapi.com/invoices-by-value/crear-factura-de-carta-porte.md) (por referencias: [esta sección](https://docs.fiscalapi.com/invoices-by-reference/crear-factura-de-carta-porte.md)) y toma su ejemplo como base. El ambiente, las llaves y el SDK están en la skill `fiscalapi-integracion`. Esta skill cubre autotransporte; marítimo, aéreo y ferroviario no. + +## 1. Traslado (T) o ingreso (I) + +| | Traslado `T` | Ingreso `I` | +|---|---|---| +| Cuándo | Mueves mercancía propia; no cobras el flete | Cobras el servicio de transporte a un cliente | +| `currencyCode` | `XXX` (CP103) | La del cobro, por ejemplo `MXN` (CP104) | +| Receptor | El mismo emisor (CP107) | El cliente | +| Uso CFDI | FiscalAPI fija `S01` | El del cliente | +| Forma y método de pago | No llevan | Los del cobro | +| Conceptos (`items`) | Obligatorios; `taxObjectCode` `01` | Clave de servicio de transporte, por ejemplo `78101800` (CP109) | + +En un traslado, FiscalAPI deja subtotal y total en 0 aunque los conceptos tengan importe. FiscalAPI no valida la moneda ni que el receptor sea el emisor: si te equivocas, rechaza el PAC. + +## 2. `complement.cartaPorte` + +FiscalAPI genera solos el `IdCCP`, el peso bruto total y el número total de mercancías: no los mandes. Los nombres de los campos son los del ejemplo de las docs; un campo mal escrito se ignora sin error, así que cópialos de ahí. + +- **Raíz:** `transpInternacId` (`No` para nacional, con acento en `Sí`), `unidadPesoId` ([unidad de peso](https://docs.fiscalapi.com/catalogs-info/carta-porte-31/clave-unidad-peso.md), por ejemplo `KGM`) y `totalDistRec`, la suma de las distancias de los destinos (CP125, CP126). +- **Ubicaciones** (al menos un origen y un destino, CP130): `tipoUbicacion` (`Origen` o `Destino`), `idUbicacion` (`OR` o `DE` más 6 dígitos), RFC y nombre del remitente o destinatario, `fechaHoraSalidaLlegada` y, **solo en los destinos**, `distanciaRecorrida` (CP141). +- **Domicilio de cada ubicación:** `estadoId` (clave de 3 letras del SAT, por ejemplo `CMX`, `JAL`, `COA`), `paisId` `MEX` y `codigoPostalId`; colonia, municipio y localidad deben corresponder a ese código postal y estado (CP143 a CP147). FiscalAPI no lo valida. +- **Mercancías:** `bienesTranspId` ([clave de producto de carta porte](https://docs.fiscalapi.com/catalogs-info/carta-porte-31/clave-prod-serv-cp.md)), `descripcion`, `cantidad`, `claveUnidadId`, `pesoEnKg` (hasta 3 decimales) y `materialPeligrosoId` (`Sí` o `No`, según lo que la clave permita, CP155). Con material peligroso, `cveMaterialPeligrosoId`, `embalajeId` y el seguro de medio ambiente son obligatorios (CP182). +- **Autotransporte:** `permSCTId` ([tipo de permiso](https://docs.fiscalapi.com/catalogs-info/carta-porte-31/tipo-permiso.md)), `numPermisoSCT`, `configVehicularId` ([configuración vehicular](https://docs.fiscalapi.com/catalogs-info/carta-porte-31/config-autotransporte.md)), `pesoBrutoVehicular`, `placaVM` (5 a 7 caracteres sin guiones ni espacios), `anioModeloVM`, `aseguraRespCivil` y `polizaRespCivil`. Los `remolques` (máximo 2, con `subTipoRemId` y `placa`) van o no según la configuración vehicular (CP184): un tractocamión los exige, un camión unitario no. +- **Figuras de transporte:** al menos el operador (`tipoFiguraId` `01`) con `rfcFigura`, `nombreFigura` y `numLicencia` (CP193, CP194). + +## 3. Catálogos + +Los catálogos de carta porte se consultan con la API de catálogos ([tabla de nombres](https://docs.fiscalapi.com/catalogs/catalogos-de-carta-porte-3-1.md)): `SatClaveUnidadPeso`, `SatMaterialPeligroso`, `SatTipoEmbalaje`, `SatTipoPermiso`, `SatConfigAutotransporte`, `SatSubTipoRem`, `SatFiguraTransporte`, `SatColonia`, `SatMunicipio` y `SatLocalidad`. Ten en cuenta sus límites: + +- `SatStates` usa claves numéricas, no las de 3 letras que pide `estadoId`. +- No hay catálogo de códigos postales, y colonia, municipio y localidad no traen su código postal: la coherencia entre ellos no se puede comprobar con la API. Toma esos datos de una fuente confiable (el domicilio fiscal real) y no los adivines. + +## 4. Antes de timbrar + +1. Tipo, moneda y receptor según la tabla de la sección 1. +2. `totalDistRec` igual a la suma de las distancias de los destinos; ninguna distancia en los orígenes. +3. Domicilios con estado de 3 letras y colonia, municipio, localidad y código postal congruentes. +4. Remolques según la configuración vehicular y un operador con licencia. + +Si el PAC rechaza, busca el código en [Errores Carta Porte 3.1](https://docs.fiscalapi.com/errors/carta-porte-31/errores-carta-porte-3-1.md) o usa la skill `fiscalapi-diagnostico`. Para consultar el estatus o cancelar, usa `fiscalapi-ciclo-de-vida`. diff --git a/skills/fiscalapi-ciclo-de-vida/SKILL.md b/skills/fiscalapi-ciclo-de-vida/SKILL.md new file mode 100644 index 0000000..022fecb --- /dev/null +++ b/skills/fiscalapi-ciclo-de-vida/SKILL.md @@ -0,0 +1,49 @@ +--- +name: fiscalapi-ciclo-de-vida +description: Maneja lo que pasa con una factura CFDI de FiscalAPI después de timbrarla. Cubre la cancelación ante el SAT (motivos 01-04, UUID sustituto y aceptación del receptor), el estatus ante el SAT, la descarga del XML y del PDF, el envío por correo y los webhooks (registro, eventos, verificación de la firma, reintentos y pruebas en local con fiscalapi listen). Úsala cuando vayas a cancelar, consultar, descargar o enviar una factura ya emitida, o a recibir eventos de FiscalAPI en tu sistema. Para emitir usa fiscalapi-ingreso-egreso o fiscalapi-pagos; para revisar una integración completa, el comando revisar-integracion. +--- + +# Después de timbrar + +Cada operación tiene su sección en las docs (por valores; cambia `invoices-by-value` por `invoices-by-reference` si emitiste por referencias). Léela antes de escribir el cuerpo. El ambiente, las llaves y el SDK están en la skill `fiscalapi-integracion`. + +## 1. Cancelar + +`DELETE /api/v4/invoices` ([Cancelar factura](https://docs.fiscalapi.com/invoices-by-value/cancelar-factura.md)). + +- **Por referencias:** `id` de la factura en FiscalAPI. Firma con el CSD vigente del emisor guardado; una factura emitida por valores no se cancela por referencias. +- **Por valores:** `invoiceUuid` (el UUID, no el `id`), `tin` del emisor y `taxCredentials` con su `.cer` y su `.key` en base64. +- `cancellationReasonCode` ([motivos](https://docs.fiscalapi.com/catalogs-info/cfdi-40/motivos-de-cancelacion.md)): + - `01` comprobante emitido con errores con relación: exige `replacementUuid`, el UUID de la factura que la sustituye. Emite primero la sustituta. + - `02` comprobante emitido con errores sin relación. + - `03` no se llevó a cabo la operación. + - `04` operación nominativa relacionada en una factura global. + +**La respuesta no confirma la cancelación.** Que el PAC acepte la solicitud (201 o 202 en `invoiceUuids`) solo significa que llegó al SAT: la factura queda en "Cancelando". FiscalAPI consulta al SAT en segundo plano y la deja en "Cancelado" o la regresa a "Vigente" si el receptor la rechaza o no es cancelable. Si el SAT pide la aceptación del receptor, este tiene 3 días hábiles para responder. Entérate del resultado con los webhooks `invoice.canceled` e `invoice.cancellation_rejected`, o consultando el estatus. No marques la factura como cancelada en tu sistema con la respuesta del DELETE. + +## 2. Estatus ante el SAT + +`POST /api/v4/invoices/status` ([Consultar estado de factura](https://docs.fiscalapi.com/invoices-by-value/consultar-estado-de-factura.md)): por referencias con `id`; por valores con `issuerTin`, `recipientTin`, `invoiceTotal`, `invoiceUuid` y `last8DigitsIssuerSignature`. + +La respuesta trae `status` (Vigente, Cancelado o No Encontrado), `cancelableStatus` (sin aceptación, con aceptación o no cancelable) y `cancellationStatus` (en proceso, rechazada, plazo vencido, cancelado) ([modelo de respuesta](https://docs.fiscalapi.com/invoices-by-value/modelo-de-respuesta-a-la-consulta-del-estado-de-una-factura.md)). En el ambiente de pruebas la respuesta es simulada y `details` lo avisa: no sirve para probar lógica que dependa del valor. + +## 3. XML, PDF y correo + +- XML timbrado: `GET /api/v4/invoices/{id}/xml` ([Obtener XML](https://docs.fiscalapi.com/invoices-by-value/obtener-xml-por-id.md)). +- PDF: `POST /api/v4/invoices/pdf` con `invoiceId` y, opcionales, `base64Logo`, `bandColor` y `fontColor` ([Generar PDF](https://docs.fiscalapi.com/invoices-by-value/generar-pdf-por-id.md)). Para un diseño fijo por RFC, usa [reglas de reporte](https://docs.fiscalapi.com/report-rules-info/como-funcionan-las-reglas-de-reporte.md), que se configuran en el dashboard. +- Correo con el PDF y el XML: `POST /api/v4/invoices/send` con `invoiceId` y `toEmail` ([Enviar factura por correo](https://docs.fiscalapi.com/invoices-by-value/enviar-factura-por-correo.md)). El envío se programa y puede tardar unos minutos. + +Los archivos llegan en base64 (`base64File`) con `fileName`. Decodifícalos antes de guardarlos. + +## 4. Webhooks + +- **Registro:** `POST /api/v4/webhooks` con `url` (https, no localhost), `enabledEvents` (o `*`) y, opcional, `personIds` ([Registrar un webhook](https://docs.fiscalapi.com/webhooks-info/registrar-un-webhook.md)). La respuesta trae el `secret` (`whsec_...`): guárdalo como secreto; con la API key no se vuelve a ver salvo al rotarlo. +- **Eventos:** `invoice.cancellation_requested`, `invoice.canceled`, `invoice.cancellation_rejected`, `download_request.completed`, `download_request.abandoned` y `download_rule.suspended` ([Tipos de evento](https://docs.fiscalapi.com/webhooks-info/tipos-de-evento.md)). Un evento de una factura emitida por valores no trae persona: solo lo reciben los webhooks sin filtro `personIds`. +- **Verifica la firma de cada entrega** antes de procesarla ([Verificar la firma](https://docs.fiscalapi.com/webhooks-info/verificar-la-firma.md)): usa el helper del SDK (`Webhook.verify` y equivalentes) sobre el **cuerpo crudo** de la petición, sin parsearlo antes. La firma es Standard Webhooks (cabeceras `webhook-id`, `webhook-timestamp` y `webhook-signature`, HMAC-SHA256) y se rechaza con más de 5 minutos de diferencia. +- **Entrega al menos una vez y sin orden garantizado:** deduplica por `webhook-id`, responde 2xx rápido, procesa después, y consulta el recurso (`relatedObject`) para conocer su estado actual ([Reintentos](https://docs.fiscalapi.com/webhooks-info/reintentos.md)). Si no respondes 2xx, FiscalAPI reintenta hasta 10 veces en unas 75 horas. +- **Rotación:** al rotar el secreto, el anterior sigue firmando 24 horas y cada entrega lleva las dos firmas ([Rotar el secreto](https://docs.fiscalapi.com/webhooks-info/rotar-el-secreto.md)). +- **En local:** `fiscalapi listen --forward-to http://localhost:/` recibe los eventos de tu cuenta de pruebas y `fiscalapi trigger ` dispara uno ([Recibir eventos en tu servidor local](https://docs.fiscalapi.com/webhooks-info/local-testing/recibir-eventos-en-tu-servidor-local.md)). La escucha tiene su propio secreto. + +## 5. Fechas + +Manda siempre `X-TIME-ZONE`. Sin él, FiscalAPI usa `America/Mexico_City` para leer y escribir las fechas sin zona, y tu sistema puede interpretarlas en otra. El sobre de los eventos de webhook va siempre en UTC con `Z`. diff --git a/skills/fiscalapi-comercio-exterior/SKILL.md b/skills/fiscalapi-comercio-exterior/SKILL.md new file mode 100644 index 0000000..ab4d095 --- /dev/null +++ b/skills/fiscalapi-comercio-exterior/SKILL.md @@ -0,0 +1,52 @@ +--- +name: fiscalapi-comercio-exterior +description: Emite con FiscalAPI un CFDI de exportación definitiva con complemento Comercio Exterior 2.0. Decide exportación 02, moneda y tipo de cambio, el tipo de cambio USD del DOF, el receptor extranjero (XEXX010101000, NumRegIdTrib, residencia fiscal), la clave de pedimento A1, el Incoterm, los domicilios, y las mercancías con fracción arancelaria, unidad y cantidad de aduana y valor en dólares ligadas a cada concepto. También evita los rechazos CCE más comunes. Úsala cuando vayas a facturar una venta al extranjero o una exportación de mercancías. Para un error CCE que ya devolvió la API, usa fiscalapi-diagnostico. +--- + +# Comercio Exterior 2.0 (exportación definitiva) + +Antes de escribir el cuerpo, lee [Crear factura de comercio exterior](https://docs.fiscalapi.com/invoices-by-value/crear-factura-de-comercio-exterior.md) (por referencias: [esta sección](https://docs.fiscalapi.com/invoices-by-reference/crear-factura-de-comercio-exterior.md)). **No copies la mercancía de su ejemplo cURL**: la factura es de 400 USD y la mercancía declara 23.47 en `valorDolares`, que no cuadra. El ambiente, las llaves y el SDK están en la skill `fiscalapi-integracion`. + +## 1. El comprobante + +- `typeCode` `I`, `exportCode` `02` (exportación definitiva) y `complement.comercioExterior`. Sin el complemento, `02` lo rechaza el PAC (CCE104). FiscalAPI no exige el `02`: mándalo tú. +- **Moneda:** la de la venta. Con una moneda extranjera manda siempre `exchangeRate` (por ejemplo `currencyCode` `USD` y `exchangeRate` 17.1995): si lo omites, FiscalAPI escribe `TipoCambio` 1. +- **Receptor extranjero:** `tin` `XEXX010101000`, `legalName` real, `foreignTin` (su número de identificación fiscal; con Estados Unidos o Canadá, 9 dígitos), `countryId` (país de residencia, por ejemplo `USA`), `taxRegimeCode` `616`, `cfdiUseCode` `S01` y `zipCode` igual a `expeditionZipCode` (CFDI40149, CFDI40159). Sin `foreignTin` el PAC rechaza (CCE133). +- **Cada concepto** necesita `itemSku`: se escribe como `NoIdentificacion` y es la llave que liga el concepto con su mercancía en el complemento (CCE151, CCE152). Por referencias, el `NoIdentificacion` es el `id` del producto. + +## 2. `complement.comercioExterior` + +- `claveDePedimentoId` `A1` (exportación definitiva) y `certificadoOrigen` `0` (o `1` con `numCertificadoOrigen`). +- `incotermId`: obligatorio con exportación `02` (CCE120), por ejemplo `FOB`, `CIF` o `EXW`. +- `tipoCambioUSD`: pesos por dólar (sección 3). +- `emisor.domicilio`: `calle`, `estadoId` (clave de 3 letras, por ejemplo `QUE`), `paisId` `MEX`, `codigoPostalId`, y colonia, localidad y municipio de catálogo que correspondan a ese código postal. +- `receptor.domicilio`: `paisId` del receptor, distinto de `MEX` (CCE134). Si el país tiene estados en el catálogo `c_Estado` (Estados Unidos, Canadá), `estado` es su clave, por ejemplo `TX`, no "Texas" (CCE139); en otro país es texto libre. `codigoPostal` sigue el formato de códigos postales del país (CCE140), y municipio y colonia van en texto libre. El `numRegIdTrib` del receptor lo toma FiscalAPI del `foreignTin` del CFDI. +- `mercancias[]`, una por concepto: + - `noIdentificacion`: igual al `itemSku` del concepto. + - `fraccionArancelariaId`: la fracción vigente de la tarifa ([catálogos](https://docs.fiscalapi.com/catalogs/catalogos-de-comercio-exterior-2-0.md)). + - `unidadAduanaId`: la unidad de medida de la tarifa (UMT) de esa fracción (CCE161, CCE167). El catálogo de la API no trae la UMT de cada fracción: tómala de la tarifa oficial o del agente aduanal. + - `cantidadAduana` y `valorUnitarioAduana`: van juntas con la unidad (CCE165, CCE166). + - `valorDolares`: el importe del concepto en dólares (sección 3). + +FiscalAPI calcula solo `TotalUSD` y no valida la congruencia entre conceptos y mercancías: la valida el PAC. + +## 3. Tipo de cambio y valor en dólares + +- `tipoCambioUSD` es el tipo de cambio que publica el Banco de México en el DOF el día anterior a la fecha de la factura (o el de adquisición de la moneda); si no coincide, el PAC rechaza (CCE121). FiscalAPI no lo consulta: obtenlo del DOF o de Banxico en tu sistema. +- `valorDolares` de cada mercancía (CCE158, CCE172): + - factura en USD: el importe del concepto; + - factura en MXN: importe ÷ `tipoCambioUSD`; + - otra moneda: importe × `exchangeRate` ÷ `tipoCambioUSD`. + +## 4. Catálogos + +Por la API de catálogos: `SatClavePedimento`, `SatINCOTERM`, `SatFraccionArancelaria`, `SatUnidadAduana`, `SatMotivoTraslado`, `SatTipoOperacion` y `SatCountries`. El nombre del catálogo distingue mayúsculas. No hay catálogo de códigos postales, y la fracción no trae su unidad: valida esos datos con su fuente oficial. + +## 5. Antes de timbrar + +1. `exportCode` `02`, complemento presente e `incotermId`. +2. Moneda extranjera con `exchangeRate`; `tipoCambioUSD` del DOF del día anterior. +3. Receptor `XEXX010101000` con `foreignTin`, `countryId`, `616`, `S01` y el código postal de expedición. +4. Cada concepto con `itemSku` igual al `noIdentificacion` de su mercancía, y `valorDolares` calculado con la sección 3. + +Si el PAC rechaza, busca el código en [Errores Comercio Exterior 2.0](https://docs.fiscalapi.com/errors/comercio-exterior-20/errores-comercio-exterior-2-0.md) o usa la skill `fiscalapi-diagnostico`. Para consultar el estatus o cancelar, usa `fiscalapi-ciclo-de-vida`. diff --git a/skills/fiscalapi-diagnostico/SKILL.md b/skills/fiscalapi-diagnostico/SKILL.md new file mode 100644 index 0000000..174f829 --- /dev/null +++ b/skills/fiscalapi-diagnostico/SKILL.md @@ -0,0 +1,47 @@ +--- +name: fiscalapi-diagnostico +description: Diagnostica una respuesta de error de la API de FiscalAPI (CFDI 4.0) y propone la corrección. Distingue una validación de FiscalAPI (400 con propertyName), un rechazo del PAC o del SAT (código como CFDI40147, CRP20101, NOM5 o CP101 en message) y un error del servidor (5xx), y lleva a la página de docs.fiscalapi.com con la causa. Úsala cuando una llamada a FiscalAPI falla al timbrar, cancelar o consultar. No la uses para diseñar una factura nueva ni para errores de tu propio código que no vienen de la API. +--- + +# Diagnóstico de errores de FiscalAPI + +Toda respuesta de FiscalAPI es un `ApiResponse` con `succeeded`, `message`, `details`, `data`, `httpStatusCode` y, en los errores del servidor, `traceIdentifier`. El modelo completo está en [ApiResponse](https://docs.fiscalapi.com/api-response.md). + +Lee la respuesta completa antes de proponer un cambio. No inventes la causa: la tabla o la sección enlazada es la fuente. + +## Árbol de decisión + +1. **¿`data` es una lista de fallas con `propertyName`?** Es una **validación de FiscalAPI**: la solicitud no llegó al PAC y no se cobró timbre. + - `propertyName` es la ruta del campo en el request (por ejemplo, `Items[0].ItemTaxes[0].TaxCode`). Corrige ese campo; no te guíes por el texto. + - Modelo de cada falla: [ValidationFailure](https://docs.fiscalapi.com/api-response.md#validationfailure). + - Mensajes propios del timbrado: [Mensajes de validación del timbrado](https://docs.fiscalapi.com/errors.md#mensajes-de-validacion-del-timbrado). + - Decimales, longitudes y claves de catálogo: [Decimales](https://docs.fiscalapi.com/errors.md#decimales-de-importes-cantidades-y-tasas), [Valores que no caben en la factura](https://docs.fiscalapi.com/errors.md#valores-que-no-caben-en-la-factura) y [Claves de catálogo del SAT](https://docs.fiscalapi.com/errors.md#claves-de-catalogo-del-sat). + +2. **¿`message` empieza con un código seguido de ` - ` y `data` es `null`?** Es un **rechazo del PAC o del SAT**: no se timbró y el timbre se reembolsó. El código es lo que va antes de ` - `; `details` trae el detalle del PAC. Busca la fila del código en su tabla: + + | El código empieza con | Tabla | + |---|---| + | `CFDI40` | [Errores CFDI 4.0](https://docs.fiscalapi.com/errors/cfdi-40.md) | + | `CRP` | [Errores Pagos 2.0](https://docs.fiscalapi.com/errors/pagos-20.md) | + | `NOM` | [Errores Nómina 1.2](https://docs.fiscalapi.com/errors/nomina-12.md) | + | `CP` | [Errores Carta Porte 3.1](https://docs.fiscalapi.com/errors/carta-porte-31.md) | + | `CCE` | [Errores Comercio Exterior 2.0](https://docs.fiscalapi.com/errors/comercio-exterior-20.md) | + + - Si la tabla no tiene el código, o es un rechazo de cancelación (por ejemplo, `CACFDI33`), la causa es el texto de `message` y `details`. Revisa [Cancelar factura](https://docs.fiscalapi.com/invoices-by-value#cancelar-factura) si fallaba una cancelación. + - Corrige el dato y vuelve a timbrar: un rechazo es definitivo para esa solicitud. + +3. **¿Es un 5xx?** Es un **error del servidor**: no se resuelve cambiando los datos. Ver [Errores del servidor](https://docs.fiscalapi.com/errors.md#errores-del-servidor). + - Un 500 al timbrar puede ser un **resultado incierto**: el CFDI pudo quedar timbrado. **No repitas la factura de inmediato**: búscala con [Listar facturas](https://docs.fiscalapi.com/invoices-by-value#listar-facturas) y, si no aparece, escribe a soporte con el `traceIdentifier`. + - Un 5xx con un código de rechazo en `message` sigue la regla del paso 2. + +4. **¿Otro 4xx?** Lee `details`: + - 404: [Recurso no encontrado](https://docs.fiscalapi.com/errors.md#recurso-no-encontrado). + - 400 sin fallas de validación o 409: las restricciones de los datos y la concurrencia están en [Errores del servidor](https://docs.fiscalapi.com/errors.md#errores-del-servidor). + - Certificados y llaves privadas: [Certificados y llaves privadas](https://docs.fiscalapi.com/errors.md#certificados-y-llaves-privadas). + +## Cómo responder + +- Di de qué tipo es el error (validación, rechazo del PAC, error del servidor u otro 4xx) y por qué. +- Cita el campo (`propertyName`) o el código, y la fila o sección enlazada que explica la causa. +- Propón el cambio concreto en el código del integrador. +- Nunca pidas ni escribas una API key de producción: las llaves van en variables de entorno. diff --git a/skills/fiscalapi-ingreso-egreso/SKILL.md b/skills/fiscalapi-ingreso-egreso/SKILL.md new file mode 100644 index 0000000..d04331d --- /dev/null +++ b/skills/fiscalapi-ingreso-egreso/SKILL.md @@ -0,0 +1,75 @@ +--- +name: fiscalapi-ingreso-egreso +description: Arma con FiscalAPI una factura de ingreso, una nota de crédito (egreso) o una factura global CFDI 4.0 sin rechazos del SAT. Decide régimen fiscal y uso CFDI del receptor, objeto de impuesto e impuestos del concepto, método y forma de pago (PUE o PPD), la relación de la nota de crédito y la información global de público en general, y busca las claves del SAT con la API de catálogos. Úsala cuando vayas a emitir o corregir una factura de ingreso, egreso o global. Para el complemento de pago usa fiscalapi-pagos; para un error que ya devolvió la API, fiscalapi-diagnostico. +--- + +# Ingreso, nota de crédito y factura global + +Las tres se emiten con `POST /api/v4/invoices`; cambian `typeCode`, el receptor, `relatedInvoices` y `globalInformation`. Antes de escribir el cuerpo, lee la sección del caso (por valores; cambia `invoices-by-value` por `invoices-by-reference` si usas referencias): + +| Caso | Sección | +|---|---| +| Factura de ingreso | [Crear factura ingreso](https://docs.fiscalapi.com/invoices-by-value/crear-factura-ingreso.md) | +| Nota de crédito (egreso) | [Crear nota de crédito](https://docs.fiscalapi.com/invoices-by-value/crear-nota-de-credito.md) | +| Factura global (público en general) | [Crear factura global](https://docs.fiscalapi.com/invoices-by-value/crear-factura-global.md) y [Aspectos clave de la factura global](https://docs.fiscalapi.com/global-invoice-info/aspectos-clave-de-la-factura-global.md) | +| Todos los campos | [Modelo factura](https://docs.fiscalapi.com/invoices-by-value/modelo-factura.md) | + +El ambiente, las llaves y el SDK están en la skill `fiscalapi-integracion`. + +## 1. Claves del SAT: búscalas, no las inventes + +Cada `...Code` es una clave de un catálogo del SAT ([equivalencias](https://docs.fiscalapi.com/catalogs/catalogos-de-facturacion.md)). Con la API de pruebas: + +- Buscar por descripción (mínimo 4 caracteres): `GET /api/v4/catalogs/{catalogo}/{texto}` ([Buscar en un catálogo](https://docs.fiscalapi.com/catalogs/buscar-en-un-catalogo.md)). +- Confirmar una clave: `GET /api/v4/catalogs/{catalogo}/key/{clave}` ([Obtener registro](https://docs.fiscalapi.com/catalogs/obtener-registro-por-id.md)); 404 si no existe. +- El nombre del catálogo distingue mayúsculas: `SatProductCodes` (producto o servicio), `SatUnitMeasurements` (unidad), `SatTaxRegimes`, `SatCfdiUses`, `SatPaymentForms`, `SatPaymentMethods`, `SatTaxObjects`, `SatPeriodicities`, `SatMonths`, `SatRelationshipTypes`. + +Una clave que no existe responde 400 en su campo antes de llegar al PAC ([Claves de catálogo del SAT](https://docs.fiscalapi.com/errors/claves-de-catalogo-del-sat.md)). + +## 2. Receptor: régimen fiscal y uso CFDI + +1. El tipo de persona sale del RFC: 12 caracteres es persona moral, 13 es física. +2. `recipient.taxRegimeCode` debe aplicar a ese tipo de persona ([regímenes](https://docs.fiscalapi.com/catalogs-info/cfdi-40/regimenes-fiscales.md)): 601 solo moral; 612 y 616 solo física; 626 ambas. Si no, CFDI40158. +3. `recipient.cfdiUseCode` debe aplicar al tipo de persona y admitir el régimen del receptor ([usos de CFDI](https://docs.fiscalapi.com/catalogs-info/cfdi-40/usos-de-cfdi.md)). Si no, CFDI40161. G01 y G03 no admiten 616 ni 605; S01 y CP01 admiten cualquier régimen. +4. `recipient.zipCode` es el código postal del domicilio fiscal del receptor, 5 dígitos, el de su constancia. + +FiscalAPI valida 2 y 3 antes del PAC y responde 400 en `Recipient.TaxRegimeCode` o `Recipient.CfdiUseCode` ([Mensajes de validación del timbrado](https://docs.fiscalapi.com/errors/mensajes-de-validacion-del-timbrado.md)). Pide los datos fiscales al cliente; no los deduzcas. + +## 3. Conceptos e impuestos + +- `itemCode` es la clave de producto o servicio (8 caracteres) y `unitOfMeasurementCode` la de unidad; búscalas en el catálogo. Por valores, `itemSku` es obligatorio. +- `taxObjectCode` ([objeto de impuesto](https://docs.fiscalapi.com/catalogs-info/cfdi-40/objeto-imp.md)): + - `02` (sí objeto de impuesto) exige al menos un impuesto en `itemTaxes`; sin él, FiscalAPI rechaza el concepto. + - `01`, `03` y `04` no llevan impuestos: FiscalAPI ignora `itemTaxes` con esos valores. + - `07` exige IEPS (`003`) y no admite IVA. +- Cada impuesto: `taxCode` (`001` ISR, `002` IVA, `003` IEPS), `taxTypeCode` (`Tasa`, `Cuota` o `Exento`), `taxRate` y `taxFlagCode` (`T` traslado, `R` retención). Una retención no puede ser `Exento`. +- Decimales: `quantity`, `unitPrice`, `discount` y `taxRate` admiten hasta 6; FiscalAPI no redondea ([Decimales](https://docs.fiscalapi.com/errors/decimales-de-importes-cantidades-y-tasas.md)). En JavaScript, redondea antes de enviar: `0.1 + 0.2` llega como `0.30000000000000004` y se rechaza. + +## 4. Método y forma de pago + +- **PUE** (pago en una sola exhibición): ya se pagó o se paga al emitir. `paymentFormCode` es la forma real (`01` efectivo, `03` transferencia, `04` tarjeta de crédito, ...). +- **PPD** (pago en parcialidades o diferido): todavía no se paga. `paymentFormCode` es `99` (por definir), y cada pago posterior se registra con un complemento de pago (skill `fiscalapi-pagos`). El PAC rechaza PPD con otra forma (CFDI40105) y FiscalAPI no lo valida antes: revísalo tú. +- Catálogos: [formas de pago](https://docs.fiscalapi.com/catalogs-info/cfdi-40/formas-de-pago.md) y [métodos de pago](https://docs.fiscalapi.com/catalogs-info/cfdi-40/metodos-de-pago.md). + +## 5. Nota de crédito (egreso) + +- `typeCode` `E` y `relatedInvoices: [{ "relationshipTypeCode": "...", "uuid": "" }]`; todos los elementos llevan el mismo tipo de relación ([tipos de relación](https://docs.fiscalapi.com/catalogs-info/cfdi-40/tipo-relacion.md)). + - `01`: descuento o bonificación sobre la factura relacionada. + - `03`: devolución de mercancía. + - `07`: aplicación de anticipo (con forma de pago `30`). +- Método `PUE`; la forma de pago es la del reembolso o descuento (`15` condonación si el ingreso aún no se pagó). +- Concepto típico: `itemCode` `84111506` (servicios de facturación), unidad `ACT`, cantidad 1. +- Primero emite y guarda el `uuid` del ingreso: la nota de crédito no existe sin él. + +## 6. Factura global (público en general) + +- Receptor: `tin` `XAXX010101000`, `legalName` `PUBLICO EN GENERAL`, `taxRegimeCode` `616`, `cfdiUseCode` `S01` y `zipCode` igual a `expeditionZipCode`. Con el RFC genérico, otro régimen da CFDI40159 y otro código postal CFDI40149. +- `globalInformation` es obligatoria en un ingreso a público en general (CFDI40130): `periodicityCode` (`01` diario, `02` semanal, `03` quincenal, `04` mensual, `05` bimestral), `monthCode` y `year` ([periodo de facturación](https://docs.fiscalapi.com/global-invoice-info/periodo-de-facturacion.md)). + - `year` es el de la fecha de la factura o hasta 5 años antes (CFDI40136). + - `monthCode` va de `01` a `12`; solo con `05` (bimestral, emisor en régimen 621) va de `13` a `18`. FiscalAPI no valida esta combinación antes del PAC (CFDI40132, CFDI40134, CFDI40135). +- Método `PUE`. Conceptos: `itemCode` `01010101`, unidad `ACT`, cantidad 1, un concepto por venta o ticket con su folio en `itemSku`. +- Si las ventas son tickets de Auto CFDI, usa [Crear factura global de tickets](https://docs.fiscalapi.com/tickets/crear-factura-global-de-tickets.md). + +## 7. Después de emitir + +Guarda `id` y `uuid`. Para consultar el estatus ante el SAT, cancelar o descargar el PDF y el XML, usa la skill `fiscalapi-ciclo-de-vida`. En el ambiente de pruebas el estatus del SAT se simula. diff --git a/skills/fiscalapi-integracion/SKILL.md b/skills/fiscalapi-integracion/SKILL.md new file mode 100644 index 0000000..23b0ec1 --- /dev/null +++ b/skills/fiscalapi-integracion/SKILL.md @@ -0,0 +1,75 @@ +--- +name: fiscalapi-integracion +description: Integra FiscalAPI (facturación electrónica CFDI 4.0 en México) en un ERP, POS o SaaS. Elige el SDK según el lenguaje del proyecto, configura el ambiente de pruebas y las llaves (X-API-KEY, X-TENANT-KEY, X-TIME-ZONE), decide entre el modo por valores y por referencias, y lleva a la sección exacta de docs.fiscalapi.com y a la skill de cada tipo de factura. Úsala cuando te pidan integrar FiscalAPI, timbrar o emitir facturas CFDI desde código, o empezar con FiscalAPI desde cero. No la uses para diagnosticar un error que ya devolvió la API (usa fiscalapi-diagnostico). +--- + +# Integrar FiscalAPI + +FiscalAPI es una API REST para emitir CFDI 4.0 y sus complementos, cancelarlos y consultarlos ante el SAT. Esta skill decide cómo integrarla; el detalle de cada endpoint vive en las docs, en copias Markdown por sección (`https://docs.fiscalapi.com//.md`). Lee la sección que corresponda antes de escribir el cuerpo de una petición: no lo inventes. + +Para timbrar la primera factura de punta a punta en un proyecto vacío, usa el comando `primer-timbre`. + +## 1. Ambiente y llaves + +- Trabaja siempre en **pruebas**: `https://test.fiscalapi.com`. Producción (`https://live.fiscalapi.com`) es otra cuenta con otras llaves; pasar a producción cambia solo la URL, la API key y el tenant ([Ambientes](https://docs.fiscalapi.com/environments.md)). +- Cada petición lleva `X-API-KEY` y `X-TENANT-KEY` ([Encabezados requeridos](https://docs.fiscalapi.com/authentication/encabezados-http-requeridos.md)) y `X-TIME-ZONE` con una zona IANA, por ejemplo `America/Mexico_City`: las fechas sin zona del JSON se leen y se escriben en esa zona, y si falta se usa `America/Mexico_City`. Los SDK mandan los tres headers. +- Las llaves se leen de variables de entorno (`FISCALAPI_API_URL`, `FISCALAPI_API_KEY`, `FISCALAPI_TENANT`). Nunca las escribas en el código, en un archivo versionado ni en el chat, y nunca uses una llave de producción para probar. + +Si el integrador todavía no tiene llaves de pruebas, guíalo por el flujo de [Pasos para comenzar](https://docs.fiscalapi.com/quickstart/pasos-para-comenzar.md): sin suscripción activa y sin timbres no se puede timbrar. + +1. Crear la cuenta en `https://test.fiscalapi.com/create-account` y confirmar el correo. +2. [Activar la suscripción de prueba](https://docs.fiscalapi.com/subscription-info/activar-suscripcion-de-prueba.md) y [comprar timbres de prueba](https://docs.fiscalapi.com/subscription-info/activar-timbres-de-prueba.md) con una [tarjeta de prueba](https://docs.fiscalapi.com/testing-data/tarjetas-de-prueba.md). Los pagos se hacen solo desde el dashboard. +3. [Generar la API key](https://docs.fiscalapi.com/credentials-info/generar-una-api-key.md) y copiar el [tenant (TID)](https://docs.fiscalapi.com/credentials-info/obtener-el-tenant.md) del perfil. + +## 2. SDK + +Usa el SDK oficial del lenguaje del proyecto; el paquete, la creación del cliente y las trampas de cada README están en [references/sdks.md](references/sdks.md). Si el lenguaje no tiene SDK, llama a la API por HTTP con los tres headers. + +| El proyecto tiene | SDK | +|---|---| +| `*.csproj`, `*.sln` | .NET: `Fiscalapi` | +| `package.json` | Node: `fiscalapi` | +| `pyproject.toml`, `requirements.txt` | Python: `fiscalapi` | +| `composer.json` | PHP: `fiscalapi/fiscalapi` | +| `pom.xml`, `build.gradle` | Java: `com.fiscalapi:fiscalapi` | + +## 3. Por valores o por referencias + +Los dos modos usan el mismo `POST /api/v4/invoices` y dan el mismo CFDI ([Diferencias entre modos](https://docs.fiscalapi.com/modes-of-operation/diferencias-entre-modos-de-operacion.md)). + +- **Por valores:** el cuerpo lleva todos los datos del emisor, del receptor y de los conceptos, incluidos los certificados CSD del emisor en base64 (`taxCredentials`). No hay que registrar nada antes. Conviene para la primera prueba y cuando el sistema del integrador ya guarda esos datos. +- **Por referencias:** el cuerpo lleva los `id` de personas y productos ya registrados en FiscalAPI ([emisores](https://docs.fiscalapi.com/issuers.md), [receptores](https://docs.fiscalapi.com/recipients.md), [productos](https://docs.fiscalapi.com/products.md), [certificados](https://docs.fiscalapi.com/tax-files.md)). Peticiones más chicas y sin certificados en cada llamada; es el modo recomendado para producción. + +Las secciones de facturas existen en las dos variantes con la misma ancla: basta cambiar `invoices-by-value` por `invoices-by-reference` en la URL. + +## 4. Datos de prueba + +- El SAT exige usar sus [personas de prueba](https://docs.fiscalapi.com/testing-data/personas-de-prueba.md) en este ambiente (RFC, razón social y código postal); no inventes RFC. +- Los CSD de prueba vienen de SW ([Certificados de prueba](https://docs.fiscalapi.com/testing-data/certificados-de-prueba.md)); todas las llaves usan la contraseña `12345678a`. Cada persona trae también su FIEL: para timbrar usa el `.cer` y el `.key` de la subcarpeta `CSD_*`, no el `.cer` ni el `Claveprivada_FIEL_*.key` de la carpeta de la persona. +- El ejemplo de [Crear factura ingreso por valores](https://docs.fiscalapi.com/invoices-by-value/crear-factura-ingreso.md) usa al emisor `FUNK671228PH6` (régimen 621) y al receptor `EKU9003173C9` (régimen 601, uso G01, CP 42501). + +## 5. Qué vas a construir + +| Necesidad | Sección (por valores; cambia a `invoices-by-reference` si usas referencias) | Skill | +|---|---|---| +| Factura de ingreso o nota de crédito (egreso) | [crear-factura-ingreso](https://docs.fiscalapi.com/invoices-by-value/crear-factura-ingreso.md), [crear-nota-de-credito](https://docs.fiscalapi.com/invoices-by-value/crear-nota-de-credito.md) | `fiscalapi-ingreso-egreso` | +| Complemento de pago (PPD) | [crear-factura-de-pago](https://docs.fiscalapi.com/invoices-by-value/crear-factura-de-pago.md) | `fiscalapi-pagos` | +| Recibo de nómina | [crear-factura-de-nomina](https://docs.fiscalapi.com/invoices-by-value/crear-factura-de-nomina.md) | `fiscalapi-nomina` | +| Carta porte | [crear-factura-de-carta-porte](https://docs.fiscalapi.com/invoices-by-value/crear-factura-de-carta-porte.md) | `fiscalapi-carta-porte` | +| Comercio exterior | [crear-factura-de-comercio-exterior](https://docs.fiscalapi.com/invoices-by-value/crear-factura-de-comercio-exterior.md) | `fiscalapi-comercio-exterior` | +| Factura global | [crear-factura-global](https://docs.fiscalapi.com/invoices-by-value/crear-factura-global.md) | `fiscalapi-ingreso-egreso` | +| Cancelar, PDF, XML, correo o estatus ante el SAT | [cancelar-factura](https://docs.fiscalapi.com/invoices-by-value/cancelar-factura.md), [generar-pdf-por-id](https://docs.fiscalapi.com/invoices-by-value/generar-pdf-por-id.md), [obtener-xml-por-id](https://docs.fiscalapi.com/invoices-by-value/obtener-xml-por-id.md), [enviar-factura-por-correo](https://docs.fiscalapi.com/invoices-by-value/enviar-factura-por-correo.md), [consultar-estado-de-factura](https://docs.fiscalapi.com/invoices-by-value/consultar-estado-de-factura.md) | `fiscalapi-ciclo-de-vida` | +| Un error que ya devolvió la API | [Errores](https://docs.fiscalapi.com/errors.md) | `fiscalapi-diagnostico` | +| Catálogos del SAT | [Catálogos](https://docs.fiscalapi.com/catalogs.md) | — | +| Notificaciones de eventos (webhooks) | [Webhooks](https://docs.fiscalapi.com/webhooks-info.md) | `fiscalapi-ciclo-de-vida` | +| Descarga masiva de CFDI del SAT | [Descarga masiva](https://docs.fiscalapi.com/download-info.md) | — | +| Validaciones ante el SAT (69-B, sellos, estatus) | [Validaciones SAT](https://docs.fiscalapi.com/sat-validations-info.md) | — | +| Autofactura de tickets | [Auto CFDI](https://docs.fiscalapi.com/auto-invoice.md) | — | + +## 6. Reglas que no se negocian + +- **Un 5xx o un timeout al timbrar no se reintenta.** El CFDI pudo quedar timbrado y un reintento lo duplicaría. `POST /api/v4/invoices` no es idempotente. Registra la respuesta completa (incluido `traceIdentifier`), marca la factura como "resultado incierto" en el sistema del integrador y sigue [Errores del servidor](https://docs.fiscalapi.com/errors/errores-del-servidor.md): búscala con [Listar facturas](https://docs.fiscalapi.com/invoices-by-value/listar-facturas.md) y, si no aparece, escribe a soporte con el `traceIdentifier` antes de volver a emitir. +- **Un 400 con fallas de validación o un rechazo del PAC no consumen timbre**: corrige el dato y vuelve a emitir. Para interpretarlos usa `fiscalapi-diagnostico`. +- **Lee `succeeded`, no solo el código HTTP.** Toda respuesta es un [ApiResponse](https://docs.fiscalapi.com/api-response.md); el CFDI emitido trae su `id` en FiscalAPI y su `uuid` (folio fiscal). Guarda los dos. +- **Las fechas van sin zona** (`yyyy-MM-ddTHH:mm:ss`), en la zona de `X-TIME-ZONE`. +- **Nada en producción** desde un agente: timbrar en `live` es un acto fiscal irreversible. diff --git a/skills/fiscalapi-integracion/references/sdks.md b/skills/fiscalapi-integracion/references/sdks.md new file mode 100644 index 0000000..0974b2d --- /dev/null +++ b/skills/fiscalapi-integracion/references/sdks.md @@ -0,0 +1,48 @@ +# SDK oficiales de FiscalAPI + +Los cinco SDK tienen la misma forma: un objeto de configuración con la URL base, la API key, el tenant y la zona horaria; un cliente; un servicio de facturas con `create`; y una respuesta con `succeeded` y `data`. La URL base es libre (`https://test.fiscalapi.com` o la que indique `FISCALAPI_API_URL`) y el SDK agrega `/api/v4/...`. El repo de cada uno tiene su README con más ejemplos. + +Revisado contra la versión 4.0.450 de los cinco paquetes. + +## .NET: [fiscalapi-net](https://github.com/FiscalAPI/fiscalapi-net) + +- Instalar: `dotnet add package Fiscalapi` (netstandard2.0 y .NET Framework 4.6.1 en adelante). +- Namespaces: `Fiscalapi.Common` (configuración y respuesta), `Fiscalapi.Services` (cliente) y `Fiscalapi.Models` (factura). +- Cliente: `var client = FiscalApiClient.Create(new FiscalapiSettings { ApiUrl = ..., ApiKey = ..., Tenant = ..., TimeZone = "America/Mexico_City" });`. Con inyección de dependencias: `builder.Services.AddFiscalApi()` lee la sección `FiscalapiSettings` y requiere `using FiscalApi;` (con A mayúscula, a diferencia del resto del SDK). +- Emitir: `var response = await client.Invoices.CreateAsync(invoice);` con `Invoice`, `InvoiceIssuer` (`TaxCredentials` con `FileType.CertificateCsd` y `FileType.PrivateKeyCsd`), `InvoiceRecipient`, `InvoiceItem` e `InvoiceItemTax`. +- Respuesta: `response.Succeeded`, `response.Data.Id`, `response.Data.Uuid`, `response.Message`, `response.Details`, `response.TraceIdentifier`. +- Timeout fijo de 30 s. + +## Node: [fiscalapi-node](https://github.com/FiscalAPI/fiscalapi-node) + +- Instalar: `npm install fiscalapi`. +- Cliente: `import { FiscalapiClient } from 'fiscalapi'` y `FiscalapiClient.create({ apiUrl, apiKey, tenant, timeZone: 'America/Mexico_City' })`. **La clase es `FiscalapiClient`** (con `a` minúscula); los ejemplos del README escriben `FiscalApiClient`, que no existe. +- Emitir: `const response = await client.invoices.create(invoice)` con un objeto plano; `taxCredentials` lleva `fileType: 0` (`.cer`) y `fileType: 1` (`.key`). +- Respuesta: `response.succeeded`, `response.data.id`, `response.data.uuid`, `response.message`, `response.details`. +- Fechas: manda `date` como texto `yyyy-MM-ddTHH:mm:ss` (por ejemplo con `DateTime.now().toFormat("yyyy-MM-dd'T'HH:mm:ss")` de luxon). Un `new Date()` viaja en UTC con `Z` y deja de ser la hora local de `X-TIME-ZONE`. +- `debug: true` imprime los logs y además desactiva la validación SSL: no lo uses fuera de tu máquina. + +## Python: [fiscalapi-python](https://github.com/FiscalAPI/fiscalapi-python) + +- Instalar: `pip install fiscalapi` (Python 3.9 o más reciente). +- Cliente: `from fiscalapi import FiscalApiClient, FiscalApiSettings` y `client = FiscalApiClient(settings=FiscalApiSettings(api_url=..., api_key=..., tenant=..., time_zone="America/Mexico_City"))`. +- Emitir: `response = client.invoices.create(invoice)` con `Invoice`, `InvoiceIssuer` (`tax_credentials=[TaxCredential(base64_file=..., file_type=0, password=...), TaxCredential(..., file_type=1, ...)]`), `InvoiceRecipient`, `InvoiceItem` e `ItemTax`. +- Respuesta: `response.succeeded`, `response.data.id`, `response.data.uuid`, `response.message`, `response.details`, `response.trace_identifier`. +- Fechas: `date=datetime.now().strftime("%Y-%m-%dT%H:%M:%S")`; un `datetime` sin formatear manda microsegundos. +- El SDK no fija timeout en `requests`. + +## PHP: [fiscalapi-php](https://github.com/FiscalAPI/fiscalapi-php) + +- Instalar: `composer require fiscalapi/fiscalapi` (PHP 7.4 o más reciente). +- Cliente: `$settings = new \Fiscalapi\Http\FiscalApiSettings($apiUrl, $apiKey, $tenant);` y `$client = new \Fiscalapi\Services\FiscalApiClient($settings);`. Los argumentos son posicionales: después vienen `debug`, `verifySsl`, `apiVersion` y `timeZone`. +- Emitir: `$response = $client->getInvoiceService()->create($invoice);` con un arreglo asociativo con las mismas claves del JSON de las docs. +- Respuesta: `$json = $response->getJson();` y luego `$json['succeeded']`, `$json['data']['id']`, `$json['data']['uuid']`. `isSuccessful()` solo revisa que el HTTP sea 2xx: usa `succeeded`. + +## Java: [fiscalapi-java](https://github.com/FiscalAPI/fiscalapi-java) + +- Instalar: `com.fiscalapi:fiscalapi:4.0.450` en Maven o Gradle (Java 8 o más reciente). +- Paquetes: `com.fiscalapi.common` (configuración y respuesta), `com.fiscalapi.services` (cliente) y `com.fiscalapi.models.invoicing` (factura). +- Cliente: `FiscalApiSettings` con `setApiUrl`, `setApiKey`, `setTenant` y `setTimeZone`; `FiscalApiClient client = FiscalApiClient.create(settings);`. +- Emitir: `ApiResponse response = client.getInvoiceService().create(invoice);`. `TaxCredential` usa `setFileType(0)` y `setFileType(1)`. +- Los importes, cantidades y tasas son `BigDecimal` (`new BigDecimal("0.160000")`); los ejemplos del README que pasan `double` no compilan. +- Respuesta: `response.isSucceeded()`, `response.getData().getId()`, `response.getData().getUuid()`. diff --git a/skills/fiscalapi-nomina/SKILL.md b/skills/fiscalapi-nomina/SKILL.md new file mode 100644 index 0000000..3c3b5c6 --- /dev/null +++ b/skills/fiscalapi-nomina/SKILL.md @@ -0,0 +1,58 @@ +--- +name: fiscalapi-nomina +description: Emite con FiscalAPI un recibo de nómina CFDI 4.0 con complemento de nómina 1.2 (factura tipo N), ordinaria o extraordinaria. Decide los datos del empleado y del empleador (por referencias en la persona o por valores en el request), el tipo de nómina, el periodo y los días pagados, las percepciones, las deducciones (ISR, IMSS), los otros pagos con el subsidio para el empleo y las incapacidades, y evita los rechazos NOM más comunes. Úsala cuando vayas a timbrar nómina o recibos de pago de sueldos, o a cargar los datos laborales de empleados y empleadores. Para un error NOM que ya devolvió la API, usa fiscalapi-diagnostico. +--- + +# Recibo de nómina 1.2 + +Antes de escribir el cuerpo, lee [Crear factura de nómina](https://docs.fiscalapi.com/invoices-by-value/crear-factura-de-nomina.md) (por referencias: [esta sección](https://docs.fiscalapi.com/invoices-by-reference/crear-factura-de-nomina.md)). El ambiente, las llaves y el SDK están en la skill `fiscalapi-integracion`. Ojo con los ejemplos de C# y Python de esa página: usan un receptor 601/G01 y `PaymentFormCode`, que la nómina no admite; sigue esta skill. + +## 1. El comprobante + +`POST /api/v4/invoices` con: + +- `typeCode` `N`, `currencyCode` `MXN`, `exportCode` `01`, `paymentMethodCode` `PUE`, `series`, `date` y `expeditionZipCode`. +- **No mandes** `paymentFormCode` (el CFDI de nómina no lleva forma de pago y FiscalAPI lo copiaría al XML), `items`, `paymentConditions` ni `exchangeRate`. FiscalAPI arma solo el concepto (84111505, ACT, "Pago de nómina") con importe = percepciones + otros pagos y descuento = deducciones. +- **Emisor** (empleador): si es persona física, necesita su CURP (NOM5); una persona moral no la lleva. +- **Receptor** (empleado): RFC de persona física (13 caracteres, NOM8), `taxRegimeCode` `605` (sueldos y salarios, NOM11) y `cfdiUseCode` `CN01` (nómina, NOM12). FiscalAPI no los fuerza: mándalos. + +## 2. Datos laborales: por referencias o por valores + +- **Por referencias:** registra una vez los [datos de empleado](https://docs.fiscalapi.com/employee-data/crear-datos-de-empleado.md) (`POST /api/v4/people/{personId}/employee`) y de [empleador](https://docs.fiscalapi.com/employer-data/crear-datos-de-empleador.md) (`POST /api/v4/people/{personId}/employer`), y emite con `issuer.id` y `recipient.id`. La CURP sale de la persona ([emisor en nómina](https://docs.fiscalapi.com/issuers/emisor-en-facturas-de-nomina.md), [receptor en nómina](https://docs.fiscalapi.com/recipients/receptor-en-facturas-de-nomina.md)). Cada persona tiene un solo registro de cada tipo: si ya existe, actualízalo con PUT. +- **Por valores:** los mismos datos van en `issuer.employerData` y `recipient.employeeData`, con la `curp` dentro. + +Datos del empleado que más fallan: + +- Obligatorios: `curp`, `employeeNumber` (1-15 caracteres), `satContractTypeId`, `satTaxRegimeTypeId`, `satPaymentPeriodicityId` y `satPayrollStateId` (estado donde trabaja, por ejemplo `JAL`). +- Contrato `01` a `08` (con relación laboral): el emisor necesita `employerRegistration` (registro patronal) y el empleado `socialSecurityNumber`, `laborRelationStartDate`, `seniority`, `satJobRiskId` e `integratedDailySalary` (NOM42, NOM44); el régimen debe ser `02`, `03` o `04` (NOM57). Contratos `09`, `10` y `99` no llevan registro patronal (NOM43). +- `seniority` es la antigüedad al final del periodo: `P{semanas}W` (por ejemplo `P54W`, sin pasarte de las semanas cumplidas, NOM54) o `P{a}Y{m}M{d}D` exacta (NOM55). +- Los catálogos de nómina distinguen acentos y mayúsculas: sindicalizado es `Sí` o `No`, no `Si` ni `SÍ` ([catálogos de nómina](https://docs.fiscalapi.com/catalogs-info/nomina-12/sindicalizado.md)). + +## 3. `complement.payroll` + +| Campo | Qué es | +|---|---| +| `payrollTypeCode` | `O` ordinaria (periodicidad distinta de `99`) o `E` extraordinaria (periodicidad `99`) (NOM33, NOM34) | +| `paymentDate`, `initialPaymentDate`, `finalPaymentDate` | `yyyy-MM-dd`; el inicio no puede ser posterior al fin (NOM35) | +| `daysPaid` | Días pagados del periodo | +| `earnings.earnings[]` | Percepciones: `earningTypeCode` ([tipos](https://docs.fiscalapi.com/catalogs-info/nomina-12/tipo-percepcion.md); `001` sueldos), `code`, `concept`, `taxedAmount`, `exemptAmount` | +| `earnings.otherPayments[]` | Otros pagos: `otherPaymentTypeCode` ([tipos](https://docs.fiscalapi.com/catalogs-info/nomina-12/tipo-otro-pago.md)), `amount`; el subsidio para el empleo (`002`) lleva además `subsidyCaused` | +| `deductions[]` | Deducciones: `deductionTypeCode` ([tipos](https://docs.fiscalapi.com/catalogs-info/nomina-12/tipo-deduccion.md); `001` seguridad social, `002` ISR), `code`, `concept`, `amount` | +| `disabilities[]` | Incapacidades: `disabilityDays`, `disabilityTypeCode`, `monetaryAmount` | + +`otherPayments` va **dentro** de `earnings`; `deductions` y `disabilities` van en la raíz de `payroll`. FiscalAPI calcula todos los totales (percepciones, gravado, exento, deducciones, impuestos retenidos y otros pagos): no los mandes. FiscalAPI no calcula el ISR: el monto de la deducción `002` lo calcula el sistema de nómina del integrador. + +## 4. Subsidio para el empleo + +- Con régimen `02` (sueldos), el recibo lleva el otro pago `002` aunque sea en 0 (`amount` 0 y `subsidyCaused` 0), salvo que haya `007` o `008` (NOM105). Otros regímenes no lo llevan (NOM106). +- `amount` (subsidio entregado) no puede pasar de `subsidyCaused` (subsidio causado) (NOM107). +- Si en un periodo se entregó subsidio de más, se descuenta con la deducción `071`. + +## 5. Antes de timbrar + +1. Receptor con RFC de 13 caracteres, `605` y `CN01`; sin `paymentFormCode` en la raíz. +2. Contrato, régimen y registro patronal congruentes (sección 2). +3. Antigüedad que no exceda el periodo trabajado. +4. Subsidio `002` presente con régimen `02`. + +Si el PAC rechaza, busca el código en [Errores Nómina 1.2](https://docs.fiscalapi.com/errors/nomina-12/errores-nomina-1-2.md) o usa la skill `fiscalapi-diagnostico`. Para consultar el estatus, cancelar o enviar el recibo, usa `fiscalapi-ciclo-de-vida`. diff --git a/skills/fiscalapi-pagos/SKILL.md b/skills/fiscalapi-pagos/SKILL.md new file mode 100644 index 0000000..c6cbce4 --- /dev/null +++ b/skills/fiscalapi-pagos/SKILL.md @@ -0,0 +1,58 @@ +--- +name: fiscalapi-pagos +description: Emite con FiscalAPI el complemento de recepción de pagos 2.0 (REP, factura de pago tipo P) de una factura PPD, en una o varias parcialidades. Decide cuándo hace falta (PUE frente a PPD), arma complement.payment con saldo anterior, importe pagado y saldo insoluto, el número de parcialidad, el tipo de cambio y la equivalencia cuando las monedas difieren, y los impuestos del documento pagado. Úsala cuando un cliente pague una factura emitida con método PPD o cuando te pidan un complemento de pago o un REP. Para la factura original usa fiscalapi-ingreso-egreso; para un error CRP que ya devolvió la API, fiscalapi-diagnostico. +--- + +# Complemento de pago (Pagos 2.0) + +Una factura **PUE** ya está pagada: no lleva complemento. Una factura **PPD** (forma de pago `99`) se paga después, y cada pago se registra con un CFDI de tipo `P` que relaciona el UUID de la factura. El SAT pide emitirlo a más tardar el día 5 del mes siguiente al pago. + +Antes de escribir el cuerpo, lee [Crear factura de pago](https://docs.fiscalapi.com/invoices-by-value/crear-factura-de-pago.md) (por referencias: [esta sección](https://docs.fiscalapi.com/invoices-by-reference/crear-factura-de-pago.md)). Los campos de `complement.payment` están en [Modelo factura](https://docs.fiscalapi.com/invoices-by-value/modelo-factura.md). El ambiente y el SDK, en la skill `fiscalapi-integracion`. + +## 1. El comprobante de pago + +Se emite con `POST /api/v4/invoices`. Manda: + +- `typeCode` `P`, `currencyCode` `XXX` (cualquier otra responde 400), `exchangeRate` 1 u omitido, `exportCode` `01`, `series`, `date`, `expeditionZipCode`, `issuer` y `recipient` (el mismo receptor de la factura). +- `complement.payment` con el pago (sección 2). + +**No mandes** `paymentFormCode`, `paymentMethodCode` ni `paymentConditions` en la raíz: el CFDI de pago no los lleva y el PAC lo rechaza (CRP20105, CRP20106, CRP20107). La forma de pago va dentro de `complement.payment`. Tampoco mandes `items`: FiscalAPI arma solo el concepto (84111506, ACT, "Pago", importe 0), fija el uso CFDI en `CP01` y calcula los totales del pago. + +## 2. El pago y los documentos que paga + +`complement.payment`: + +| Campo | Qué es | +|---|---| +| `paymentDate` | Fecha y hora en que se recibió el pago (sin zona, `yyyy-MM-ddTHH:mm:ss`) | +| `paymentFormCode` | Forma real del pago (`03` transferencia, `01` efectivo, ...); nunca `99` (CRP20212) | +| `currencyCode` y `exchangeRate` | Moneda del pago y su tipo de cambio: con `MXN` exactamente 1 | +| `amount` | Monto total recibido | +| `paidInvoices` | Una entrada por factura que cubre este pago | + +Cada `paidInvoices[]`: + +- `uuid` de la factura PPD; `series` y `number` son opcionales. +- `currencyCode` de la factura y `equivalence`: 1 si es la misma moneda del pago; si no, las unidades de la moneda de la factura por cada unidad de la del pago. +- `partialityNumber`: 1 en el primer pago de esa factura y consecutivo en los siguientes. +- `previousBalance`: en la primera parcialidad, el total de la factura; después, el saldo insoluto del pago anterior. Mayor que 0. +- `paymentAmount`: lo que este pago abona a esa factura. Mayor que 0. (La tabla de las docs lo llama `amount`; el campo real es `paymentAmount`.) +- `remainingBalance` = `previousBalance` − `paymentAmount`, nunca negativo (CRP20244). +- `taxObjectCode` y `paidInvoiceTaxes`: ver la sección 3. + +**FiscalAPI no busca la factura ni calcula saldos.** Copia `uuid`, `partialityNumber` y los saldos tal como llegan, y el PAC rechaza lo incongruente. El sistema del integrador lleva el saldo y el número de parcialidad de cada factura PPD: guárdalos al emitir cada pago. Con la misma moneda, la suma de los `paymentAmount` no puede pasar de `amount` (CRP20217). + +## 3. Impuestos del documento pagado + +- `taxObjectCode` repite el objeto de impuesto de la factura: con `02`, `paidInvoiceTaxes` es obligatorio (CRP20246); con `01`, `03` o `04` no debe venir (CRP20247). +- Cada impuesto lleva `base`, `taxCode`, `taxTypeCode`, `taxRate` y `taxFlagCode` como en la factura. La `base` es **proporcional a lo pagado**: si se paga la mitad de una factura de 1,000 + IVA, la base del IVA es 500. FiscalAPI calcula el importe de cada impuesto y los totales del pago a partir de esa base. +- Decimales: hasta 6 en importes y tasas, 10 en `equivalence`; FiscalAPI no redondea. + +## 4. Antes de timbrar, revisa + +1. La factura es PPD y tienes su `uuid`, su total y los pagos previos. +2. `partialityNumber` y `previousBalance` siguen al pago anterior; `remainingBalance` cuadra. +3. La forma del pago no es `99` y, si mandas cuentas o RFC bancarios, la forma los admite (CRP20221 a CRP20228). +4. No hay `paymentFormCode` ni `paymentMethodCode` en la raíz. + +Si el PAC rechaza, busca el código en [Errores Pagos 2.0](https://docs.fiscalapi.com/errors/pagos-20/errores-pagos-2-0.md) o usa la skill `fiscalapi-diagnostico`. Para consultar el estatus del pago ante el SAT o cancelarlo, usa la skill `fiscalapi-ciclo-de-vida`.