├── CODE_OF_CONDUCT.md ├── CONTRIBUTING.md ├── LICENSE ├── README.md ├── composer.json ├── docs ├── CHANGELOG.md ├── Cancelación.md ├── Ejemplos │ ├── CancelacionFirmada.md │ ├── ConsultarEstadoCFDI.md │ └── Timbrado.md ├── ListadoDeServicios.md ├── PruebasDeIntegracion.md ├── PruebasDeIntegracionContinua.md ├── RegistroDeClientes.md ├── SEMVER.md ├── Servicios.md ├── TODO.md └── issues │ ├── AcuseCancelacionNoCoincidente.md │ ├── CancelSignatureServiceCancelarRecienCreado.md │ ├── CancelacionRetencionesCodEstatus.md │ ├── CancelacionRetencionesError1308.md │ ├── QueryPendingServiceUuidNoExistente.md │ ├── RegistrationGetNoList.md │ ├── StampAmpersand.md │ └── StampServiceDobleEstampado.md └── src ├── Definitions ├── CancelAnswer.php ├── CancelStorePending.php ├── Environment.php ├── EnvironmentManifest.php ├── ReceiptType.php ├── RfcRole.php ├── Services.php └── SignedDocumentFormat.php ├── Exceptions ├── FinkokException.php └── InvalidArgumentException.php ├── Finkok.php ├── FinkokEnvironment.php ├── FinkokSettings.php ├── Helpers ├── AcceptRejectSigner.php ├── CancelSigner.php ├── DocumentSigner.php ├── FileLogger.php ├── GetRelatedSigner.php ├── GetSatStatusExtractor.php └── JsonDecoderLogger.php ├── QuickFinkok.php ├── Services ├── AbstractCollection.php ├── AbstractResult.php ├── Cancel │ ├── AcceptRejectSignatureCommand.php │ ├── AcceptRejectSignatureResult.php │ ├── AcceptRejectSignatureService.php │ ├── AcceptRejectUuidItem.php │ ├── AcceptRejectUuidList.php │ ├── AcceptRejectUuidStatus.php │ ├── CancelSignatureCommand.php │ ├── CancelSignatureResult.php │ ├── CancelSignatureService.php │ ├── CancelledDocument.php │ ├── CancelledDocuments.php │ ├── GetPendingCommand.php │ ├── GetPendingResult.php │ ├── GetPendingService.php │ ├── GetReceiptCommand.php │ ├── GetReceiptResult.php │ ├── GetReceiptService.php │ ├── GetRelatedSignatureCommand.php │ ├── GetRelatedSignatureResult.php │ ├── GetRelatedSignatureService.php │ ├── GetSatStatusCommand.php │ ├── GetSatStatusResult.php │ ├── GetSatStatusService.php │ ├── RelatedItem.php │ └── RelatedItems.php ├── Manifest │ ├── GetContractsCommand.php │ ├── GetContractsResult.php │ ├── GetContractsService.php │ ├── GetSignedContractsCommand.php │ ├── GetSignedContractsResult.php │ ├── GetSignedContractsService.php │ ├── SignContractsCommand.php │ ├── SignContractsResult.php │ └── SignContractsService.php ├── Registration │ ├── AddCommand.php │ ├── AddResult.php │ ├── AddService.php │ ├── AssignCommand.php │ ├── AssignResult.php │ ├── AssignService.php │ ├── Customer.php │ ├── CustomerStatus.php │ ├── CustomerType.php │ ├── Customers.php │ ├── EditCommand.php │ ├── EditResult.php │ ├── EditService.php │ ├── ObtainCommand.php │ ├── ObtainCustomersCommand.php │ ├── ObtainCustomersResult.php │ ├── ObtainCustomersService.php │ ├── ObtainResult.php │ ├── ObtainService.php │ ├── PageInformation.php │ ├── SwitchCommand.php │ ├── SwitchResult.php │ └── SwitchService.php ├── Retentions │ ├── CancelSignatureCommand.php │ ├── CancelSignatureResult.php │ ├── CancelSignatureService.php │ ├── StampCommand.php │ ├── StampResult.php │ ├── StampService.php │ ├── StampedCommand.php │ ├── StampedResult.php │ └── StampedService.php ├── Stamping │ ├── QueryPendingCommand.php │ ├── QueryPendingResult.php │ ├── QueryPendingService.php │ ├── QuickStampService.php │ ├── StampService.php │ ├── StampedService.php │ ├── StampingAlert.php │ ├── StampingAlerts.php │ ├── StampingCommand.php │ └── StampingResult.php └── Utilities │ ├── DatetimeCommand.php │ ├── DatetimeResult.php │ ├── DatetimeService.php │ ├── DownloadXmlCommand.php │ ├── DownloadXmlResult.php │ ├── DownloadXmlService.php │ ├── ReportCreditCommand.php │ ├── ReportCreditResult.php │ ├── ReportCreditService.php │ ├── ReportTotalCommand.php │ ├── ReportTotalResult.php │ ├── ReportTotalService.php │ ├── ReportUuidCommand.php │ ├── ReportUuidResult.php │ └── ReportUuidService.php ├── SoapCaller.php └── SoapFactory.php /CODE_OF_CONDUCT.md: -------------------------------------------------------------------------------- 1 | # Código de Conducta Convenido para Contribuyentes 2 | 3 | ## Nuestro compromiso 4 | 5 | Nosotros, como miembros, contribuyentes y administradores nos comprometemos a hacer de la participación en nuestra comunidad una experiencia libre de acoso para todo el mundo, independientemente de la edad, dimensión corporal, minusvalía visible o invisible, etnicidad, características sexuales, identidad y expresión de género, nivel de experiencia, educación, nivel socioeconómico, nacionalidad, apariencia personal, raza, religión, o identidad u orientación sexual. 6 | 7 | Nos comprometemos a actuar e interactuar de maneras que contribuyan a una comunidad abierta, acogedora, diversa, inclusiva y sana. 8 | 9 | ## Nuestros estándares 10 | 11 | Ejemplos de comportamiento que contribuyen a crear un ambiente positivo para nuestra comunidad: 12 | 13 | * Demostrar empatía y amabilidad ante otras personas 14 | * Respeto a diferentes opiniones, puntos de vista y experiencias 15 | * Dar y aceptar adecuadamente retroalimentación constructiva 16 | * Aceptar la responsabilidad y disculparse ante quienes se vean afectados por nuestros errores, aprendiendo de la experiencia 17 | * Centrarse en lo que sea mejor no solo para nosotros como individuos, sino para la comunidad en general 18 | 19 | Ejemplos de comportamiento inaceptable: 20 | 21 | * El uso de lenguaje o imágenes sexualizadas, y aproximaciones o 22 | atenciones sexuales de cualquier tipo 23 | * Comentarios despectivos (_trolling_), insultantes o derogatorios, y ataques personales o políticos 24 | * El acoso en público o privado 25 | * Publicar información privada de otras personas, tales como direcciones físicas o de correo 26 | electrónico, sin su permiso explícito 27 | * Otras conductas que puedan ser razonablemente consideradas como inapropiadas en un 28 | entorno profesional 29 | 30 | ## Aplicación de las responsabilidades 31 | 32 | Los administradores de la comunidad son responsables de aclarar y hacer cumplir nuestros estándares de comportamiento aceptable y tomarán acciones apropiadas y correctivas de forma justa en respuesta a cualquier comportamiento que consideren inapropiado, amenazante, ofensivo o dañino. 33 | 34 | Los administradores de la comunidad tendrán el derecho y la responsabilidad de eliminar, editar o rechazar comentarios, _commits_, código, ediciones de páginas de wiki, _issues_ y otras contribuciones que no se alineen con este Código de Conducta, y comunicarán las razones para sus decisiones de moderación cuando sea apropiado. 35 | 36 | ## Alcance 37 | 38 | Este código de conducta aplica tanto a espacios del proyecto como a espacios públicos donde un individuo esté en representación del proyecto o comunidad. Ejemplos de esto incluyen el uso de la cuenta oficial de correo electrónico, publicaciones a través de las redes sociales oficiales, o presentaciones con personas designadas en eventos en línea o no. 39 | 40 | ## Aplicación 41 | 42 | Instancias de comportamiento abusivo, acosador o inaceptable de otro modo podrán ser reportadas a los administradores de la comunidad responsables del cumplimiento a través de [coc@phpcfdi.com](). Todas las quejas serán evaluadas e investigadas de una manera puntual y justa. 43 | 44 | Todos los administradores de la comunidad están obligados a respetar la privacidad y la seguridad de quienes reporten incidentes. 45 | 46 | ## Guías de Aplicación 47 | 48 | Los administradores de la comunidad seguirán estas Guías de Impacto en la Comunidad para determinar las consecuencias de cualquier acción que juzguen como un incumplimiento de este Código de Conducta: 49 | 50 | ### 1. Corrección 51 | 52 | **Impacto en la Comunidad**: El uso de lenguaje inapropiado u otro comportamiento considerado no profesional o no acogedor en la comunidad. 53 | 54 | **Consecuencia**: Un aviso escrito y privado por parte de los administradores de la comunidad, proporcionando claridad alrededor de la naturaleza de este incumplimiento y una explicación de por qué el comportamiento es inaceptable. Una disculpa pública podría ser solicitada. 55 | 56 | ### 2. Aviso 57 | 58 | **Impacto en la Comunidad**: Un incumplimiento causado por un único incidente o por una cadena de acciones. 59 | 60 | **Consecuencia**: Un aviso con consecuencias por comportamiento prolongado. No se interactúa con las personas involucradas, incluyendo interacción no solicitada con quienes se encuentran aplicando el Código de Conducta, por un periodo especificado de tiempo. Esto incluye evitar las interacciones en espacios de la comunidad, así como a través de canales externos como las redes sociales. Incumplir estos términos puede conducir a una expulsión temporal o permanente. 61 | 62 | ### 3. Expulsión temporal 63 | 64 | **Impacto en la Comunidad**: Una serie de incumplimientos de los estándares de la comunidad, incluyendo comportamiento inapropiado continuo. 65 | 66 | **Consecuencia**: Una expulsión temporal de cualquier forma de interacción o comunicación pública con la comunidad durante un intervalo de tiempo especificado. No se permite interactuar de manera pública o privada con las personas involucradas, incluyendo interacciones no solicitadas con quienes se encuentran aplicando el Código de Conducta, durante este periodo. Incumplir estos términos puede conducir a una expulsión permanente. 67 | 68 | ### 4. Expulsión permanente 69 | 70 | **Impacto en la Comunidad**: Demostrar un patrón sistemático de incumplimientos de los estándares de la comunidad, incluyendo conductas inapropiadas prolongadas en el tiempo, acoso de individuos, o agresiones o menosprecio a grupos de individuos. 71 | 72 | **Consecuencia**: Una expulsión permanente de cualquier tipo de interacción pública con la comunidad del proyecto. 73 | 74 | ## Atribución 75 | 76 | Este Código de Conducta es una adaptación del [Contributor Covenant][homepage], versión 2.0, 77 | disponible en . 78 | 79 | Las Guías de Impacto en la Comunidad están inspiradas en la [escalera de aplicación del código de conducta de Mozilla](https://github.com/mozilla/diversity). 80 | 81 | [homepage]: https://www.contributor-covenant.org 82 | 83 | Para respuestas a las preguntas frecuentes de este código de conducta, consulta las FAQ en . 84 | Hay traducciones disponibles en . 85 | -------------------------------------------------------------------------------- /CONTRIBUTING.md: -------------------------------------------------------------------------------- 1 | # Contribuciones 2 | 3 | Las contribuciones son bienvenidas. Aceptamos *Pull Requests* en [el repositorio GitHub][project]. 4 | 5 | Este proyecto se apega al siguiente [Código de Conducta][coc]. 6 | Al participar en este proyecto y en su comunidad, deberás seguir este código. 7 | 8 | ## Miembros del equipo 9 | 10 | * [phpCfdi][] - Organización que mantiene el proyecto. 11 | * [Contribuidores][contributors]. 12 | 13 | ## Canales de comunicación 14 | 15 | Puedes encontrar ayuda y comentar asuntos relacionados con este proyecto en estos lugares: 16 | 17 | * Comunidad Discord: 18 | * GitHub Issues: 19 | 20 | ## Reportar Bugs 21 | 22 | Publica los *Bugs* en la sección [GitHub Issues][issues] del proyecto. 23 | 24 | Sigue las recomendaciones generales de [phpCfdi][] para reportar problemas 25 | . 26 | 27 | Cuando se reporte un *Bug*, por favor incluye la mayor información posible para reproducir el problema, preferentemente 28 | con ejemplos de código o cualquier otra información técnica que nos pueda ayudar a identificar el caso. 29 | 30 | **Recuerda no incluir contraseñas, información personal o confidencial.** 31 | 32 | ## Corrección de Bugs 33 | 34 | Apreciamos mucho los *Pull Request* para corregir Bugs. 35 | 36 | Si encuentras un reporte de Bug y te gustaría solucionarlo siéntete libre de hacerlo. 37 | Sigue las directrices de "Agregar nuevas funcionalidades" a continuación. 38 | 39 | ## Agregar nuevas funcionalidades 40 | 41 | Si tienes una idea para una nueva funcionalidad revisa primero que existan discusiones o *Pull Requests* 42 | en donde ya se esté trabajando en la funcionalidad. 43 | 44 | Antes de trabajar en la nueva característica, utiliza los "Canales de comunicación" mencionados 45 | anteriormente para platicar acerca de tu idea. Si dialogas tus ideas con la comunidad y los 46 | mantenedores del proyecto, podrás ahorrar mucho esfuerzo de desarrollo y prevenir que tu 47 | *Pull Request* sea rechazado. No nos gusta rechazar contribuciones, pero algunas características 48 | o la forma de desarrollarlas puede que no estén alineadas con el proyecto. 49 | 50 | Considera las siguientes directrices: 51 | 52 | * Usa una rama única que se desprenda de la rama principal. 53 | No mezcles dos diferentes funcionalidades en una misma rama o *Pull Request*. 54 | * Describe claramente y en detalle los cambios que hiciste. 55 | * **Escribe pruebas** para la funcionalidad que deseas agregar. 56 | * **Asegúrate que las pruebas pasan** antes de enviar tu contribución. 57 | Usamos integración contínua donde se hace esta verificación, pero es mucho mejor si lo pruebas localmente. 58 | * Intenta enviar una historia coherente, entenderemos cómo cambia el código si los *commits* tienen significado. 59 | * La documentación es parte del proyecto. 60 | Realiza los cambios en los archivos de ayuda para que reflejen los cambios en el código. 61 | 62 | ## Proceso de construcción 63 | 64 | ```shell 65 | # Actualiza tus dependencias 66 | composer update 67 | phive update 68 | 69 | # Verificación de estilo de código 70 | composer dev:check-style 71 | 72 | # Corrección de estilo de código 73 | composer dev:fix-style 74 | 75 | # Ejecución de pruebas unitarias 76 | composer dev:test 77 | 78 | # Ejecución todo en uno: corregir estilo, verificar estilo y correr pruebas 79 | composer dev:build 80 | ``` 81 | 82 | ## Pruebas de integración 83 | 84 | Lee y configura tu proyecto de acuerdo a la guía de configuración del [entorno de pruebas](docs/PruebasDeIntegracion.md). 85 | 86 | Una vez correctamente configurado, ejecuta las pruebas de integración: 87 | 88 | ```shell 89 | vendor/bin/phpunit tests/Integration --testdox --verbose 90 | ``` 91 | 92 | ## Ejecutar GitHub Actions localmente 93 | 94 | Puedes usar [`act`](https://github.com/nektos/act) para ejecutar GitHub Actions localmente, tal como se 95 | muestra en [`actions/setup-php-action`](https://github.com/marketplace/actions/setup-php-action#local-testing-setup) 96 | puedes ejecutar el siguiente comando: 97 | 98 | ```shell 99 | act -P ubuntu-latest=shivammathur/node:latest -W .github/workflows/build.yml 100 | act -P ubuntu-latest=shivammathur/node:latest -W .github/workflows/functional-tests.yml -s ENV_GPG_SECRET=********** 101 | ``` 102 | 103 | 104 | [phpCfdi]: https://github.com/phpcfdi/ 105 | [project]: https://github.com/phpcfdi/finkok 106 | [contributors]: https://github.com/phpcfdi/finkok/graphs/contributors 107 | [coc]: https://github.com/phpcfdi/finkok/blob/main/CODE_OF_CONDUCT.md 108 | [issues]: https://github.com/phpcfdi/finkok/issues 109 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | The MIT License (MIT) 2 | 3 | Copyright (c) 2019 - 2024 PhpCfdi https://www.phpcfdi.com/ 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | -------------------------------------------------------------------------------- /composer.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "phpcfdi/finkok", 3 | "description": "Librería para conectar con la API de servicios de FINKOK", 4 | "license": "MIT", 5 | "keywords": [ 6 | "phpcfdi", 7 | "sat", 8 | "cfdi", 9 | "finkok" 10 | ], 11 | "authors": [ 12 | { 13 | "name": "Carlos C Soto", 14 | "email": "eclipxe13@gmail.com", 15 | "homepage": "https://eclipxe.com.mx/" 16 | } 17 | ], 18 | "homepage": "https://github.com/phpcfdi/finkok", 19 | "support": { 20 | "issues": "https://github.com/phpcfdi/finkok/issues", 21 | "source": "https://github.com/phpcfdi/finkok" 22 | }, 23 | "require": { 24 | "php": ">=7.3", 25 | "ext-dom": "*", 26 | "ext-json": "*", 27 | "ext-openssl": "*", 28 | "ext-soap": "*", 29 | "eclipxe/enum": "^0.2.0", 30 | "eclipxe/micro-catalog": "^0.1.0", 31 | "phpcfdi/cfdi-expresiones": "^3.2", 32 | "phpcfdi/credentials": "^1.0.1", 33 | "phpcfdi/xml-cancelacion": "^2.0.2", 34 | "psr/log": "^1.1 || ^2.0 || ^3.0", 35 | "robrichards/xmlseclibs": "^3.0.4" 36 | }, 37 | "require-dev": { 38 | "ext-fileinfo": "*", 39 | "eclipxe/cfdiutils": "^2.23.2", 40 | "phpcfdi/rfc": "^1.1", 41 | "phpunit/phpunit": "^9.5.10", 42 | "symfony/dotenv": "^5.1 || ^6.0 || ^7.0" 43 | }, 44 | "prefer-stable": true, 45 | "autoload": { 46 | "psr-4": { 47 | "PhpCfdi\\Finkok\\": "src/" 48 | } 49 | }, 50 | "autoload-dev": { 51 | "psr-4": { 52 | "PhpCfdi\\Finkok\\Tests\\": "tests/" 53 | } 54 | }, 55 | "config": { 56 | "optimize-autoloader": true, 57 | "preferred-install": { 58 | "*": "dist" 59 | } 60 | }, 61 | "scripts": { 62 | "dev:build": [ 63 | "@dev:fix-style", 64 | "@dev:check-style", 65 | "@dev:test" 66 | ], 67 | "dev:check-style": [ 68 | "@php tools/composer-normalize normalize --dry-run", 69 | "@php tools/php-cs-fixer fix --dry-run --verbose", 70 | "@php tools/phpcs --colors -sp" 71 | ], 72 | "dev:coverage": [ 73 | "@php -dzend_extension=xdebug.so -dxdebug.mode=coverage vendor/bin/phpunit --verbose --coverage-html build/coverage/html/" 74 | ], 75 | "dev:fix-style": [ 76 | "@php tools/composer-normalize normalize", 77 | "@php tools/php-cs-fixer fix --verbose", 78 | "@php tools/phpcbf --colors -sp" 79 | ], 80 | "dev:test": [ 81 | "@php vendor/bin/phpunit --testdox --verbose --stop-on-failure tests/Unit", 82 | "@php tools/phpstan analyse --no-progress --verbose" 83 | ] 84 | }, 85 | "scripts-descriptions": { 86 | "dev:build": "DEV: run dev:fix-style dev:check-style and dev:tests, run before pull request", 87 | "dev:check-style": "DEV: search for code style errors using composer-normalize, php-cs-fixer and phpcs", 88 | "dev:coverage": "DEV: run phpunit with xdebug and storage coverage in build/coverage/html/", 89 | "dev:fix-style": "DEV: fix code style errors using composer-normalize, php-cs-fixer and phpcbf", 90 | "dev:test": "DEV: run phpunit and phpstan" 91 | } 92 | } 93 | -------------------------------------------------------------------------------- /docs/Cancelación.md: -------------------------------------------------------------------------------- 1 | # Cancelación de CFDI Finkok 2 | 3 | Algunos de los servicios de cancelación en realidad son un puente para conectarse con el SAT. 4 | A diferencia del timbrado que la puede hacer el PAC, la cancelación únicamente la puede hacer el SAT. 5 | 6 | Los servicios de paso son: 7 | 8 | - `cancel_signature`: Manda cancelar usando una solicitud de cancelación firmada. 9 | - `get_sat_status`: Consulta el estado de un CFDI. 10 | - `get_pending`: consultar cuantas solicitudes de cancelación tiene pendientes un receptor. 11 | - `accept_reject`: permite al receptor de una factura Aceptar o Rechazar una determinada cancelación. 12 | (*no recomendado*, requiere certificado, llave privada y contraseña compártida) 13 | - `get_related`: obtener una lista de los UUID relacionados del CFDI que se está intentando cancelar. 14 | (*no recomendado*, requiere certificado, llave privada y contraseña compártida) 15 | 16 | Los servicios de ayuda son: 17 | 18 | - `cancel`: (*no recomendado*) Manda cancelar, pero requiere del certificado, llave privada y contraseña compartida. 19 | La cancelación firmada la elabora Finkok en tu nombre y realiza `cancel_signature`. 20 | - `get_receipt`: Devuelve el acuse de recibo asociado a un UUID. 21 | - `query_pending_cancellation`: Consulta el *pending buffer*. 22 | 23 | Otros servicios: 24 | 25 | - `sign_cancel`: (*no recomendado*) cancelar uno o varios CFDI, las credenciales se cargaron en el panel de Finkok. 26 | 27 | Métodos especiales para trabajar con cancelaciones hechas por terceros: 28 | 29 | - `get_out_pending` 30 | - `get_out_related` 31 | - `get_out_sat_status` 32 | - `out_accept_reject` 33 | - `out_cancel` 34 | 35 | ### Documentación 36 | 37 | Documentación del servicio: 38 | 39 | ### Respuestas de cancelación por UUID 40 | 41 | Estas son las respuestas que puede dar el SAT para cada uno de los UUID incluídos en la solicitud. 42 | 43 | 44 | * no_cancelable - El UUID contiene CFDI relacionados 45 | * 201 - Petición de cancelación realizada exitosamente 46 | * 202 - Petición de cancelación realizada Previamente 47 | * 203 - No corresponde el RFC del Emisor y de quien solicita la cancelación 48 | * 205 - UUID No encontrado 49 | 50 | Si hubiera un problema en la solicitud, por ejemplo, un error de conexión con el SAT, devolverá 51 | para toda la solicitud y se considera como no presentada: 52 | 53 | * 708: No se pudo conectar al SAT (ver *pending buffer*) 54 | * 711: Error con el certificado al cancelar 55 | 56 | ### Pending buffer 57 | 58 | Finkok tiene una característica adicional llamado *Pending buffer*, que trataré de explicar a continuación 59 | como una *cola de reintentos*: 60 | 61 | Cuando se intenta hacer una cancelación, si por algún motivo no se pudieron contactar los servicios del SAT 62 | `708: No se pudo conectar al SAT` entonces se puede almacenar la solicitud de cancelación en una 63 | *cola de reintentos*. Esta cola de reintentos será procesada y se dejará de reintentar hasta que se deje de 64 | presentar el error `708`. 65 | 66 | Si deseas usar esta característica, al enviar la solicitud de cancelación debes establecer el parámetro 67 | `store_pending` a `true` disponible en los métodos `cancel_signature` y `cancel`. 68 | 69 | Siempre que uses el *Pending buffer* deberás utilizar el servicio `query_pending_cancellation`, 70 | que precísamente consulta el *pending buffer* para obtener el estado de la cancelación de una 71 | solicitud que se quedó pendiente de cancelar debido a una falla en el sistema de SAT. 72 | 73 | ### Cancelación de múltiples folios 74 | 75 | Aunque es posible, no lo hagas. Cancela un folio a la vez. 76 | 77 | A qué te enfrentas si cancelas múltiples folios en una sola petición: 78 | 79 | - Pierdes el control de la cancelación de un CFDI y su acuse. 80 | - Se desconoce qué puede ocurrir cuando se envía una solicitud con múltiples folios y 81 | uno es cancelable y otro es no cancelable. 82 | - El servicio del SAT frecuentemente se cuelga con peticiones de múltiples folios. 83 | - No existe un ahorro significativo. 84 | 85 | ### Validaciones de cancelación 86 | 87 | Los servicios de cancelación `sign_cancel`, `cancel` y `cancel_signature` tienen una validación previa 88 | a contactar al SAT para presentar la solicitud de cancelación: 89 | 90 | > Se verifica el estado de todos los folios enviados, si alguno es no cancelable no presenta la solicitud. 91 | 92 | Se le ha comentado a Finkok la posibilidad de incluir una bandera para excluir esta validación, porque se 93 | pueden presentar casos en donde deseas volver a presentar la solicitud simplemente para obtener un acuse 94 | de cancelación firmado por el SAT. 95 | 96 | ### Acuses 97 | 98 | Existen dos tipos de acuses: 99 | 100 | - Recepción: El que ocurre cuando el PAC presenta un CFDI firmado al SAT. 101 | - Cancelación: El que ocurre cuando se presenta una solicitud de cancelación al SAT. 102 | 103 | Estos acuses son firmados por el SAT, por lo que son inviolables, infalsificables e irrepudiables. 104 | 105 | No encuentro el caso en donde pudiera requerir un acuse de recepción. El PAC firma el CFDI y *es su deber* 106 | enviarlo al SAT, el SAT le responde con este acuse. Si por alguna extraña razón, el SAT diera por desconocido 107 | el CFDI, yo muestro la firma del PAC y con eso sería suficiente. El PAC puede utilizar ese comprobante 108 | para asegurarle al SAT que se lo entregó *y que lo recibió*. 109 | 110 | El acuse de cancelación representa la respuesta del SAT a una solicitud de cancelación. 111 | Dicha solicitud no la hace el PAC (como el Timbre Fiscal Digital), esta solicitud es hecha por el contribuyente 112 | y se firma con su llave privada e incluye el certificado y la llave pública. 113 | Si por alguna extraña razón, el SAT diera por desconocida una cancelación o por cancelado un CFDI, 114 | la única forma de poder argumentar contra el SAT es con el acuse. 115 | Por lo tanto, por seguridad fiscal, sí es muy importante almacenar el acuse, y no es responsabilidad del PAC 116 | almacenarlos por el contribuyente, es responsabilidad del contribuyente contar con ellos. 117 | 118 | ### Servicio Finkok Get_Sat_Status 119 | 120 | Este servicio no se encuentra debidamente documentado. 121 | 122 | Si se encuentra un error que reporta que la expresión no se encuentra bien formada puede ser porque alguno 123 | de los componentes que conforman esta operación es incorrecto. 124 | 125 | En una prueba, estableciendo el valor de total a un valor incorrecto, la respuesta encontrada indica: 126 | `CodigoEstatus: N 601 - La expresión impresa proporcionada no es válida.`, `Estado: Vigente` y 127 | `EsCancelable: Cancelable sin aceptación`. El problema es que `Estado` debería decir `No encontrado`. 128 | 129 | Desconozco por qué en los parámetros de consulta no se solicitan los últimos 8 caracteres del sello digital 130 | del emisor del comprobante (parte de la expresión impresa en `fe`). Esto indicaría que al PAC no le exigen 131 | todos los datos o bien el PAC los completa con la información que tiene del CFDI, en ese caso, me queda la 132 | duda de ¿por qué entonces no completa toda la expresión y requiere únicamente el UUID?. 133 | 134 | ### Servicio Finkok Cancel get_pending 135 | 136 | Obtiene un listado de UUID que están pendientes por aprobar o denegar. La lista puede estar vacía. 137 | 138 | En la documentación de Finkok solo está documentado 139 | el arreglo `uuids`, sin embargo, también existe la variable `error`. 140 | 141 | Al revisar las pruebas de integración, es muy difícil crear un caso automatizado, básicamente porque 142 | toma alrededor de 16 minutos el crear un CFDI y que este aparezca como "Cancelable con autorización". 143 | 144 | Desde 2019-05-14 que comencé la implementación, la lista devuelve los UUID 145 | 8096FF0F-6C49-41D3-B041-940A9DBBB5F2 y 4B2430D3-9714-4ED8-8084-6347914F93D6. 146 | Lo desconozco, pero podría ser, que esta fuera una respuesta predeterminada. 147 | 148 | ### Dudas de funcionamiento 149 | 150 | Suponiendo que se presenta la solicitud de cancelación por dos folios (A y B), 151 | donde A es cancelable sin autorización y B es no cancelable. 152 | 153 | - ¿El SAT responderá con un acuse de cancelación cancelando A (201), pero rechazando B (no_cancelable)? 154 | R: Se desconoce, se podría hacer una prueba al respecto. 155 | R: En la primera prueba realizada regresó estado de 201 para ambos CFDI, se está investigando. 156 | R: La respuesta 201 significa que la *solicitud* fue recibida, no que el CFDI fue cancelado. 157 | 158 | Acerca del servicio `Get_Receipt`: 159 | 160 | - Solo almacenan los acuses positivos o almacenan todos los acuses 161 | R: Finkok solo almacena los acuses positivos (estados 201 y 202). 162 | 163 | - ¿Cuál es el acuse de tipo "R - Recepción" y "C - Cancelación"? 164 | R es el acuse de recepción de un CFDI timbrado. 165 | C es el acuse de recepción de un CFDI cancelado. 166 | 167 | - Si se hubiera presentado la cancelación múltiples veces, se generaría un acuse de cancelación 168 | por cada solicitud con estados `201` y `202`. 169 | ¿Se devuelve solo el último acuse con respuesta 202 o el acuse con respuesta 201 donde se canceló por primera vez? 170 | R: Se devuelve solo el último. 171 | 172 | Para los servicios de pasarela, si no se pudo contactar al SAT, se devuelve `708`? 173 | R: No, existen varios mensajes de error e incluso excepciones. Finkok está analizando el tema para unificarlas. 174 | 175 | ### Servicios que requieren certificado, llave y contraseña compartida 176 | 177 | Los servicios `accept_reject` y `get_related` requieren que se les envíe certificado, llave y contraseña compartida. 178 | En Finkok preparan la solicitud al SAT, la firman usando los datos y luego la envían al SAT. 179 | 180 | Se podría simplemente recibir la solicitud ya firmada, sin embargo, no cuentan con los métodos implementados. 181 | He puesto el ticket #20586 esperando que los puedan implementar. 182 | 183 | Nunca compartas tu llave privada y contraseña, nunca se la entregues al PAC. No solo es un problema de seguridad. 184 | Es un problema fiscal y legal con consecuencias terribles. Si alguien pudiera tener acceso a estos datos podría 185 | utilizarlos para crear CFDI en tu nombre que legalmente no podrás reclamar que no fueron creadas por ti. 186 | 187 | No implementaré estos servicios, mi mejor recomendación es que consideres usar otro PAC o solicitarle a Finkok 188 | que fabrique estos métodos. 189 | -------------------------------------------------------------------------------- /docs/Ejemplos/CancelacionFirmada.md: -------------------------------------------------------------------------------- 1 | # Ejemplo de cancelación 2 | 3 | Para este ejemplo de cancelación partiremos del CSD `certificado.cer`, 4 | `llave-privada.key.pem` y la contraseña `12345678a` y enviaremos la solicitud 5 | de cancelación del CFDI `11111111-2222-3333-4444-000000000001` del RFC `EKU9003173C9`. 6 | 7 | ## Ejemplo usando `QuickFinkok` 8 | 9 | ```php 10 | use PhpCfdi\Credentials\Credential; 11 | use PhpCfdi\Finkok\FinkokEnvironment; 12 | use PhpCfdi\Finkok\FinkokSettings; 13 | use PhpCfdi\Finkok\QuickFinkok; 14 | use PhpCfdi\XmlCancelacion\Models\CancelDocument; 15 | 16 | // Crear el objeto QuickFinkok 17 | $credential = Credential::openFiles('certificado.cer', 'llave-privada.key.pem', '12345678a'); 18 | $quickFinkok = new QuickFinkok(new FinkokSettings('finkok-usuario', 'finkok-password', FinkokEnvironment::makeProduction())); 19 | 20 | // Crear el documento a cancelar (cancelado con relación) 21 | $documentToCancel = CancelDocument::newWithErrorsRelated( 22 | '12345678-1234-1234-1234-000000000001', // el UUID a cancelar 23 | '12345678-1234-1234-1234-000000000AAA' // el UUID que lo sustituye 24 | ); 25 | 26 | // Presentar la solicitud de cancelación 27 | $result = $quickFinkok->cancel($credential, $documentToCancel); 28 | $documentInfo = $result->documents()->first(); 29 | 30 | // Trabajar con la respuesta 31 | echo 'Código de estado de la solicitud de cancelación: ', $result->statusCode(); 32 | echo 'UUID: ', $documentInfo->uuid(); 33 | echo 'Estado del CFDI: ', $documentInfo->documentStatus(); 34 | echo 'Estado de cancelación: ', $documentInfo->cancellationStatus(); 35 | ``` 36 | 37 | ## Ejemplo usando `Finkok` y `CancelSigner` 38 | 39 | ```shell 40 | composer require phpcfdi/finkok 41 | composer require phpcfdi/credentials 42 | ``` 43 | 44 | ```php 45 | use PhpCfdi\Credentials\Credential; 46 | use PhpCfdi\Finkok\Finkok; 47 | use PhpCfdi\Finkok\FinkokEnvironment; 48 | use PhpCfdi\Finkok\FinkokSettings; 49 | use PhpCfdi\Finkok\Helpers\CancelSigner; 50 | use PhpCfdi\Finkok\Services\Cancel\CancelSignatureCommand; 51 | use PhpCfdi\XmlCancelacion\Models\CancelDocument; 52 | use PhpCfdi\XmlCancelacion\Models\CancelDocuments; 53 | 54 | $cancelHelper = new CancelSigner( 55 | new CancelDocuments(CancelDocument::newWithErrorsUnrelated('11111111-2222-3333-4444-000000000001')) 56 | ); 57 | $credential = Credential::openFiles('certificado.cer', 'llave-privada.key.pem', '12345678a'); 58 | $cancelXml = $cancelHelper->sign($credential); 59 | 60 | $finkok = new Finkok(new FinkokSettings('finkok-usuario', 'finkok-password', FinkokEnvironment::makeProduction())); 61 | $result = $finkok->cancelSignature(new CancelSignatureCommand($cancelXml)); 62 | echo $result->statusCode(); // código de estado de la solucitud de cancelación 63 | ``` 64 | 65 | ## Ejemplo usando phpcfdi/xml-cancelacion 66 | 67 | Para crear el XML de cancelación está usando [`phpcfdi/xml-cancelacion`](https://github.com/phpcfdi/xml-cancelacion). 68 | 69 | ```shell 70 | composer require phpcfdi/finkok 71 | composer require phpcfdi/xml-cancelacion 72 | ``` 73 | 74 | ```php 75 | $cancelXml = (new XmlCancelacionHelper()) 76 | ->setNewCredentials('certificado.cer', 'llave-privada.key.pem', '12345678a') 77 | ->signCancellation(CancelDocument::newWithErrorsUnrelated('11111111-2222-3333-4444-000000000001'), new DateTimeImmutable()); 78 | 79 | $finkok = new Finkok(new FinkokSettings('finkok-usuario', 'finkok-password', FinkokEnvironment::makeProduction())); 80 | $result = $finkok->cancelSignature(new CancelSignatureCommand($cancelXml)); 81 | echo $result->statusCode(); // código de estado de la solucitud de cancelación 82 | ``` 83 | -------------------------------------------------------------------------------- /docs/Ejemplos/ConsultarEstadoCFDI.md: -------------------------------------------------------------------------------- 1 | # Ejemplo de consulta de estado de un CFDI 2 | 3 | Para este ejemplo partiremos de que tenemos un CFDI en el archivo `cfdi.xml`. 4 | Y que los datos son: RFC emisor `EKU9003173C9`, RFC receptor `JES900109Q90`, 5 | total `12345.67` y UUID `11111111-2222-3333-4444-000000000001`. 6 | 7 | Nota: El servicio `get_sat_status` solo puede obtener datos de CFDI 3.3 y CFDI 3.2. 8 | 9 | ## Usando QuickFinkok con el CFDI 10 | 11 | Los datos de RFC emisor, receptor, total y UUID se obtienen directamente del CFDI. 12 | 13 | ```php 14 | use PhpCfdi\Finkok\FinkokEnvironment; 15 | use PhpCfdi\Finkok\FinkokSettings; 16 | use PhpCfdi\Finkok\QuickFinkok; 17 | 18 | $finkok = new QuickFinkok(new FinkokSettings('finkok-usuario', 'finkok-password', FinkokEnvironment::makeProduction())); 19 | $satStatus = $finkok->satStatusXml(file_get_contents(`cfdi.xml`)); 20 | 21 | echo $satStatus->query(); // S - Comprobante obtenido satisfactoriamente. 22 | echo $satStatus->cfdi(); // Vigente 23 | echo $satStatus->cancellable(); // Cancelable sin aceptación 24 | echo $satStatus->cancellation(); // (vacío) 25 | ``` 26 | 27 | ## Usando QuickFinkok con los datos 28 | 29 | ```php 30 | use PhpCfdi\Finkok\FinkokEnvironment; 31 | use PhpCfdi\Finkok\FinkokSettings; 32 | use PhpCfdi\Finkok\QuickFinkok; 33 | 34 | $rfcEmisor = 'EKU9003173C9'; 35 | $rfcReceptor = 'JES900109Q90'; 36 | $uuid = '11111111-2222-3333-4444-000000000001'; 37 | $total = '12345.67'; 38 | 39 | $finkok = new QuickFinkok(new FinkokSettings('finkok-usuario', 'finkok-password', FinkokEnvironment::makeProduction())); 40 | $satStatus = $finkok->satStatus($rfcEmisor, $rfcReceptor, $uuid, $total); 41 | 42 | echo $satStatus->query(); // S - Comprobante obtenido satisfactoriamente. 43 | echo $satStatus->cfdi(); // Vigente 44 | echo $satStatus->cancellable(); // Cancelable sin aceptación 45 | echo $satStatus->cancellation(); // (vacío) 46 | ``` 47 | 48 | ## Usando Finkok 49 | 50 | La fachada `Finkok` es un poco más compleja de usar y funciona mejor si se está implementando un *command bus*. 51 | Para cualquier otro caso se recomienda usar `QuickFinkok`. 52 | 53 | ```php 54 | use PhpCfdi\Finkok\FinkokEnvironment; 55 | use PhpCfdi\Finkok\FinkokSettings; 56 | use PhpCfdi\Finkok\Finkok; 57 | use PhpCfdi\Finkok\Services\Cancel\GetSatStatusCommand; 58 | 59 | // datos de origen 60 | $rfcEmisor = 'EKU9003173C9'; 61 | $rfcReceptor = 'JES900109Q90'; 62 | $uuid = '11111111-2222-3333-4444-000000000001'; 63 | $total = '12345.67'; 64 | 65 | // comando 66 | $cmdGetSatStatus = new GetSatStatusCommand($rfcEmisor, $rfcReceptor, $uuid, $total); 67 | 68 | // ejecución del comando 69 | $finkok = new Finkok(new FinkokSettings('finkok-usuario', 'finkok-password', FinkokEnvironment::makeProduction())); 70 | $satStatus = $finkok->getSatStatus($cmdGetSatStatus); 71 | ``` 72 | 73 | -------------------------------------------------------------------------------- /docs/Ejemplos/Timbrado.md: -------------------------------------------------------------------------------- 1 | # Ejemplo de timbrado 2 | 3 | Para este ejemplo se asume que ya existe un PreCFDI (CFDI sin timbre fiscal digital) en `$precfdi`. 4 | 5 | El resultado del firmado está en `$result` que es de tipo `PhpCfdi\Finkok\Services\Stamping\StampingResult` 6 | y se pueden extraer diferentes propiedades de este firmado como el xml firmado o el listado de alertas. 7 | 8 | ```php 9 | use PhpCfdi\Finkok\FinkokEnvironment; 10 | use PhpCfdi\Finkok\FinkokSettings; 11 | use PhpCfdi\Finkok\QuickFinkok; 12 | 13 | /** 14 | * @var string $precfdi Para este ejemplo esta variable contiene el CFDI sellado sin el Timbre Fiscal Digital 15 | */ 16 | 17 | $finkok = new QuickFinkok(new FinkokSettings('finkok-usuario', 'finkok-password', FinkokEnvironment::makeProduction())); 18 | $result = $finkok->stamp($precfdi); 19 | 20 | echo $result->xml(), PHP_EOL; // precfdi firmado 21 | 22 | foreach ($result->alerts() as $alert) { 23 | echo $alert->errorCode(), ': ', $alert->message(), PHP_EOL; // mensaje de incidencia 24 | } 25 | ``` 26 | -------------------------------------------------------------------------------- /docs/ListadoDeServicios.md: -------------------------------------------------------------------------------- 1 | # Listado de servicios de Finkok 2 | 3 | ## Timbrado de CFDI 4 | 5 | Servicios implementados de estampado: 6 | 7 | - [X] `Stamp`: Firma un CFDI, si fue firmado previamente retorna (a veces) el timbrado previo 8 | - [X] `QuickStamp`: Firma un CFDI, si fue firmado previamente retorna un error 9 | - [X] `QueryPending`: Consultar el estatus de una factura que se quedo pendiente de enviar al SAT (falla o quickstamp) 10 | - [X] `Stamped`: Regresa la información de un XML timbrado previamente 11 | 12 | ## Cancelación 13 | 14 | Servicios implementados de cancelación: 15 | 16 | - [X] `cancel_signature`: Manda cancelar usando una solicitud de cancelación firmada. 17 | - [X] `get_receipt`: Devuelve el acuse de recibo asociado a un UUID. 18 | - [X] `get_sat_status`: Consulta el estado de un CFDI. 19 | 20 | Servicios para trabajar con solicitudes de cancelación: 21 | 22 | - [X] `accept_reject_signature`: permite al receptor de una factura Aceptar o Rechazar una determinada cancelación. 23 | - [X] `get_pending`: consultar la lista de los UUID pendientes por cancelar que tiene el receptor. 24 | - [X] `get_related_signature`: obtener una lista de los UUID relacionados del CFDI que se está intentando cancelar. 25 | 26 | Servicios para trabajar cancelaciones con CFDI de otro PAC 27 | 28 | - [ ] `get_out_pending` 29 | - [ ] `get_out_related` 30 | - [ ] `get_out_sat_status` 31 | - [ ] `out_accept_reject` 32 | - [ ] `out_cancel` 33 | 34 | ## Utilerías 35 | 36 | - [X] `datetime`: Obtiene la hora de los servidores de Finkok 37 | - [X] `get_xml`: Obtiene un CFDI firmado usando su UUID (de los últimos 3 meses) 38 | - [X] `report_credit`: Obtener un reporte por RFC de los créditos añadidos 39 | - [X] `report_total`: Obtener un reporte por RFC del total de timbres consumidos por fechas 40 | - [X] `report_uuid`: Obtener un reporte de UUID con fecha de emisión por RFC 41 | 42 | ## Registro de clientes 43 | 44 | - [X] `assign`: Asignar créditos a un cliente que va a timbrar bajo la cuenta de un socio de negocios de Finkok 45 | - [X] `add`: Agregar un cliente que va a timbrar bajo la cuenta de un socio de negocios de Finkok 46 | - [X] `edit`: Editar el estatus de un cliente, como lo es suspender o activar 47 | - [X] `get`: Listado o el status del RFC Emisor que esté ingresando y tenga registrado en su cuenta 48 | - [X] `switch`: Cambia el tipo de cliente de prepago a ilimitado y viceversa 49 | 50 | ## Tokens 51 | 52 | - [ ] `add_token`: crea un token (usuario) 53 | - [ ] `reset_token`: cambia el passphrase token 54 | - [ ] `update_token`: cambia el estado del token a activo o inactivo 55 | 56 | ## Retenciones 57 | 58 | Estos servicios son de CFDI de retenciones e información de pagos (RET). 59 | 60 | - [X] `stamp`: Firma un CFDI RET, si fue firmado previamente retorna el timbrado previo. 61 | - [ ] `stamped`: Regresa la información de un XML timbrado previamente. 62 | - [ ] `cancel_signature`: Cancela un CFDI RET (el modelo de cancelación es el de CFDI 3.2). 63 | - [ ] `get_receipt`: Devuelve el acuse de recibo asociado a un UUID. 64 | 65 | ## Manifiesto de Finkok 66 | 67 | - [X] `get_contracts_snid`: Obtiene los textos para ser firmados 68 | - [X] `sign_contract`: Envía los textos firmados con la FIEL 69 | 70 | ## Servicios que no se implementarán 71 | 72 | No se implementan estos servicios porque utilizan la llave privada y contraseña de un CSD. 73 | 74 | Se han implementado los servicios análogos que permiten realizar estas tareas enviando los XML firmados. 75 | 76 | - `Sing_Stamp` (timbrado cfdi): Crea el sello y firma un CFDI con llave privada y contraseña compartida. 77 | - `cancel` (cancelación cfdi): Cancelación de CFDI regular. 78 | - `cancel` (retenciones): Cancelación de CFDI de retenciones. 79 | - `sign_cancel` (cancelación cfdi): Cancelación de CFDI regular con llave privada y contraseña compartida. 80 | - `accept_reject` (cancelación cfdi): Aceptar o rechazar la cancelación de un UUID. 81 | - `get_related` (cancelación cfdi): Obtiene los UUID relacionados de un UUID. 82 | -------------------------------------------------------------------------------- /docs/PruebasDeIntegracion.md: -------------------------------------------------------------------------------- 1 | # Pruebas de integración 2 | 3 | Finkok requiere que tengas una cuenta con ellos. Por lo que es importante que tengas tus datos a la mano. 4 | 5 | ## Certificado de pruebas 6 | 7 | Para las pruebas se está utilizando el certificado de pruebas que corresponde a `ESCUELA KEMPER URGATE SA DE CV` 8 | [EKU9003173C9](https://wiki.finkok.com/lib/exe/fetch.php?media=csd_eku9003173c9_20190617131829.zip) y caduca 9 | `2023-06-17`, antes se estaba usando TCM970625MB1, pero el SAT lo ha revocado. 10 | 11 | Los datos se encuentran en `tests/_files/certs/`: 12 | 13 | - `EKU9003173C9.cer` Archivo de certificado (formato DER) 14 | - `EKU9003173C9.key` Archivo de llave privada (formato DER) 15 | - `EKU9003173C9.password.bin` Archivo con el password del certificado 16 | 17 | Esta información es pública, por lo tanto no hay problema en publicarla aquí. 18 | 19 | Recuerda registrar este RFC en tu panel de 20 | Si no lo haces verás errores como estos: 21 | - `No ha registrado el RFC emisor bajo la cuenta de Finkok` 22 | - `Sorry there was an error when validating the reseller and user` 23 | 24 | ## Archivo de entorno `.env` 25 | 26 | Si ya registraste el RFC EKU9003173C9 en tu panel de Finkok, entonces ahora debes configurar 27 | el archivo `test/.env` de entorno. Este tipo de archivos se usa con mucha frecuencia para configurar 28 | entornos de ejecución. Puedes usar el archivo `test/.env-example` como base. 29 | 30 | Una vez que lo configures te recomiendo ejecutar el test inocuo de `datetime`. 31 | 32 | ```text 33 | php vendor/bin/phpunit --verbose --testdox tests/Integration/Services/Utilities/DatetimeServiceTest.php 34 | Services/Utilities/DatetimeServiceTest.php 35 | PHPUnit 9.5.3 by Sebastian Bergmann and contributors. 36 | 37 | Runtime: PHP 8.0.3 38 | Configuration: /home/eclipxe/work/PhpCfdi/finkok/phpunit.xml.dist 39 | 40 | Datetime Service (PhpCfdi\Finkok\Tests\Integration\Services\Utilities\DatetimeService) 41 | ✔ Two well known different postal codes 1173 ms 42 | ✔ Consume date time service 355 ms 43 | ✔ Consume date time service using invalid username password 314 ms 44 | 45 | Time: 00:01.843, Memory: 6.00 MB 46 | ``` 47 | 48 | ## Ejecución de pruebas 49 | 50 | Las pruebas de integración no están incluidas en el comando `composer dev:test`. Hay que correrlas a mano ejecutando: 51 | 52 | ```shell 53 | vendor/bin/phpunit tests/Integration --testdox --verbose 54 | ``` 55 | 56 | Lee la [guía de contribuciones](../CONTRIBUTING.md) para más información. 57 | -------------------------------------------------------------------------------- /docs/PruebasDeIntegracionContinua.md: -------------------------------------------------------------------------------- 1 | # Pruebas de integración contínua 2 | 3 | Se ha configurado este proyecto para correr las pruebas de integración contínua utilizando la plataforma 4 | de GitHub Actions. 5 | 6 | Hay dos trabajos de ejecución que ejecutan al hacer un push o un pull request sobre la rama principal: 7 | 8 | - Prueba general de contrucción `build.yml`. 9 | - Pruebas funcionales `functional-test.yml`. 10 | 11 | ## Pruebas generales 12 | 13 | Las pruebas generales que se ejecutan tienen que ver con el estilo de código, pruebas unitarias (no funcionales), 14 | y anásis estático de código. Estas pruebas son las que están vinculadas con el estado de la contrucción 15 | en el *badge* *build*. 16 | 17 | Adicionalmente, se ejecutan estas pruebas todos los domingos a las 16:00 horas. 18 | 19 | ## Pruebas funcionales 20 | 21 | Las pruebas funcionales consisten en la ejecución de todas las pruebas con la excepción de aquellas que estén 22 | marcadas con la etiqueta `@group large`. 23 | 24 | Este tipo de pruebas hace contacto con la plataforma de pruebas de Finkok, por lo que es necesario contar 25 | con una cuenta y configurar correctamente el archivo de configuración de entorno `.env`. 26 | Lee el archivo de [Pruebas de Integracion](PruebasDeIntegracion.md) para más información. 27 | 28 | ### Protección de los datos de configuración de entorno 29 | 30 | Las pruebas funcionales dependen del archivo de configuración de entorno `tests/.env`, el cual contiene 31 | información sensible como la credencial de Finkok. Para protegerlo se utiliza el almacenamiento de secretos 32 | de GitHub y GPG para encriptar y desencriptar 33 | el archivo de configuración de entorno. 34 | 35 | El secreto en cuestión está almacenado en `secrets.ENV_GPG_SECRET` a nivel repositorio. 36 | 37 | Para encriptar el archivo de configuración `tests/.env -> tests/.env-testing.enc`, al ejecutar el comando 38 | se solicitará la frase de contraseña, que es lo que se debe almacenar en el secreto. 39 | 40 | ```shell 41 | gpg --no-symkey-cache --symmetric --cipher-algo AES256 --output tests/.env-testing.enc tests/.env 42 | ``` 43 | 44 | Para desencriptar el archivo de configuración `tests/.env-testing.enc -> tests/.env` se puede usar el siguiente 45 | comando. Esta operación es la que se ejecuta en `functional-test.yml` usando el secreto `secrets.ENV_GPG_SECRET`. 46 | 47 | ```shell 48 | gpg --quiet --batch --yes --decrypt --output - tests/.env-testing.enc 49 | ``` 50 | 51 | ### Cobertura de código 52 | 53 | Las pruebas de funcionales son las que establecen la mayor cobertura de código, entonces, en su ejecución 54 | se genera el archivo de cobertura y se publica en Scrutinizer. 55 | -------------------------------------------------------------------------------- /docs/RegistroDeClientes.md: -------------------------------------------------------------------------------- 1 | # Manejo de clientes de Finkok 2 | 3 | El manejo de clientes en Finkok no es nada parecido a una implementación limpia como uno podría esperar 4 | de otros servicios u otras API como las de tipo REST. 5 | 6 | Los métodos son: get (implementado como obtain), add, edit y assign. 7 | 8 | ## Método add 9 | 10 | Existen dos parámetros que no tienen uso real: added y coupon. Al momento no se piensa implementarlos. 11 | Estos campos no se muestran en la interfaz del portal ni se pueden obtener por el método get. 12 | Fueron creados para un cliente de Finkok y por lo visto no piensan documentar claramente 13 | su uso o su omisión (ticket #19340). 14 | 15 | No está documentado lo que devuelve por respuesta, solo dice que devuelve success y message. 16 | Cuando se agrega un cliente que ya existe previamente, en lugar de devolver FALSE en success, devuelve TRUE. 17 | Por lo que podría entenderse success como que el cliente existe o no. 18 | 19 | ## Método get (implementado como obtain) 20 | 21 | Según la documentación el parámetro taxpayer_id es opcional, pero no lo es. 22 | En su lugar puede estar vacío (que no es lo mismo que opcional). 23 | Cuando se manda vacío significa que se desea obtener todo el listado de clientes registrados. 24 | 25 | ## Método assign 26 | 27 | Este método tiene el propósito de incrementar o decrementar créditos a cuentas de tipo prepago. 28 | 29 | Tome en cuenta las siguientes consideraciones: 30 | 31 | - Para agregar créditos use un número positivo, para reducir use un número negativo. 32 | - En caso de usarlo con una cuenta ilimitada (on-demand) devuelve un estado de error. 33 | - En caso de decrementar a un número negativo devuelve un error y se conserva la cantidad de créditos previos. 34 | 35 | ## Método switch 36 | 37 | Este método tiene el propósito de cambiar el tipo de cliente de prepago (prepaid) a iliminado (on-demand). 38 | 39 | No genera error si se intenta cambiar al estado actual (ilimitado a ilimitado, o prepago a prepago). 40 | 41 | ## Parámetros username/password 42 | 43 | Finkok considera una buena idea que para los métodos `add`, `edit` y `get` los parámetros de usuario y contraseña 44 | no son username/password como los demás. Los parámetros en estos casos son `username_reseller/password_reseller`. 45 | 46 | Pero, para los métodos assign y switch, los parámetros sí son username/password. 47 | 48 | ## Eliminar un cliente 49 | 50 | No existe método para eliminar un cliente (Ticket #19372) 51 | 52 | No funciona la eliminación de un cliente en el portal de finkok (Ticket #19524) 53 | 54 | Por las respuestas en los tickets, no hay una explicación de por qué no se puede eliminar 55 | (la única razón es "nuestras políticas internas de diseño y desarrollo de Finkok") 56 | y confirman que no se puede (ni se podrá) eliminar un cliente, ni por webservice ni por el portal. 57 | 58 | La única opción ofrecida es enviar un correo a soporte solicitando remover los clientes. 59 | Supongo que no les importa mucho la automatización de pruebas. 60 | 61 | ## Tipo de cliente 62 | 63 | En la creación de un cliente, no se puede especificar el crédito, sin embargo sí se puede establecer 64 | si la cuenta es de prepago (prepaid) o ilimitada (ondemand). 65 | 66 | Con el método switch es con el que se puede cambiar el tipo de cuenta (prepaid/ondemand). 67 | -------------------------------------------------------------------------------- /docs/SEMVER.md: -------------------------------------------------------------------------------- 1 | # SEMVER 2 | 3 | Respetamos el estándar [Versionado Semántico 2.0.0](https://semver.org/lang/es/). 4 | 5 | En resumen, [SemVer](https://semver.org/) es un sistema de versiones de tres componentes `X.Y.Z` 6 | que nombraremos así: ` Breaking . Feature . Fix `, donde: 7 | 8 | - `Breaking`: Rompe la compatibilidad de código con versiones anteriores. 9 | - `Feature`: Agrega una nueva característica que es compatible con lo anterior. 10 | - `Fix`: Incluye algún cambio (generalmente correcciones) que no agregan nueva funcionalidad. 11 | 12 | ## Composer 13 | 14 | La herramienta [Composer](https://getcomposer.org/) es un gestor de dependencias en proyectos para PHP. 15 | Este gestor usa las [reglas](https://getcomposer.org/doc/articles/versions.md) 16 | de versionado semántico para instalar y actualizar paquetes. 17 | 18 | Te recomendamos instalar dependencias de librerías (no frameworks) con *Caret Version Range*. 19 | Por ejemplo: `"vendor/package": "^2.5"`. 20 | 21 | Esto significa que: 22 | 23 | - no debe actualizar a versiones `3.x.x` 24 | - no debe utilizar ninguna versión menor a `2.5.0` 25 | 26 | ## Versiones 0.x.y no rompen compatibilidad 27 | 28 | Las versiones que inician con cero, por ejemplo `0.y.z`, no se ajustan a las reglas de versionado. 29 | Se considera que estas versiones son previas a la madurez del proyecto y por lo tanto 30 | introducen cambios sin previo aviso. 31 | 32 | ## `@internal` no rompe compatibilidad 33 | 34 | Si la librería contiene elementos marcados como `@internal` significa que no deben ser utilizados 35 | por tu código. Son partes de código internos de la librería. 36 | Por lo tanto, no se consideran breaking changes. 37 | 38 | Cuando un elemento es `@internal`, dicho elemento: 39 | 40 | - no debe ser una entrada (parámetro) 41 | - no debe ser una salida (retorno) 42 | - no debe exponer funcionalidades en los objetos públicos (rasgos) 43 | -------------------------------------------------------------------------------- /docs/Servicios.md: -------------------------------------------------------------------------------- 1 | # Servicios 2 | 3 | La librería de Finkok debe cubrir la mayor cantidad de servicios ofrecidos por la API de Finkok. 4 | Para acceder a la documentación de Finkok requieres usuario y contraseña (bad finkok, bad!). 5 | 6 | ## Implementaciones 7 | 8 | Para cada servicio que vayamos a consumir usar el patrón de diseño **command handler** con el que: 9 | - El *comando* es un objeto que contiene los parámetros de la acción, lo llamaremos `Command`. 10 | - El *handler* es un objeto que realiza la acción, lo llamaremos `Service`. 11 | - El *invoker* es quien llama a la acción (en este caso, nuestros tests). 12 | 13 | Los parámetros de configuración de conexión con finkok deben estar en una clase especializada FinkokSettings 14 | y esta clase debe ser inyectada al `Service`, nunca al `Command`. 15 | 16 | Como todas las comunicaciones son usando SOAP, los `Service` también requieren de una fábrica de 17 | objetos de tipo `\SoapClient`. 18 | 19 | Por lo tanto, parece que tenemos dos objetos que son relevantes y parece que serán usados siempre: 20 | `SoapClientFactory` y `FinkokSettings`. 21 | 22 | Voy a evitar los servicios que signifiquen enviar a Finkok el certificado o llave privada. 23 | Nunca compartas tus certificados y llaves privadas, ni con tu PAC. 24 | 25 | ## Entornos de producción y pruebas 26 | 27 | Otra característica importante es que Finkok tiene dos entornos de trabajo 28 | y en cada uno tiene su réplica de los servicios que ofrece. 29 | 30 | - Producción: `https://facturacion.finkok.com` 31 | - Pruebas: `https://demo-facturacion.finkok.com` 32 | -------------------------------------------------------------------------------- /docs/TODO.md: -------------------------------------------------------------------------------- 1 | # phpcfdi/finkok To Do List 2 | 3 | - Modificar la clase `SoapCaller` para que no dependa de `LoggerInterface` y en su lugar introducir 4 | una interfaz para capturar los eventos de llamada exitosa y llamada con error. 5 | 6 | - Agregar la ejecución de test de integración al flujo de trabajo `.github/workflows/build.yml`; 7 | es necesario entender cómo funcionan los secretos para poder crear un archivo de entorno seguro. 8 | 9 | - Investigar cómo validar firma en acuses y respuestas del SAT 10 | 11 | - Crear un namespace común porque hay clases que están interrelacionadas entre el estampado y cancelación 12 | de CFDI y de retenciones. Así como las clases abstractas de colecciones y resultados. 13 | Esto creará una incompatibilidad con versiones previas. 14 | 15 | - Agregar la integración de CFDI de retenciones y pagos. 16 | 17 | - Poder transformar un objeto de tipo `GetSatStatusResult` a `PhpCfdi\SatEstadoCfdi\CfdiStatus`. 18 | 19 | - Los reportes que devuelven una cuenta deberían retornar un entero. 20 | 21 | - La forma en que están hechos los objetos result es mezclada, algunas propiedades las obtiene cuando se solicitan 22 | y otras propiedades las obtiene en la creación del objeto. El problema es que se guarda la referencia al objeto 23 | stdClass de entrada, por lo que podría ser manipulado externamente y devolver resultados diferentes. 24 | Esto al fin de cuentas es un error de consistencia, pues o bien todas las propiedades se deben establecer en 25 | el constructor o bien las propiedades deben consultarse al momento de leerlas. 26 | La primera opción genera duplicidad de memoria (los valores están en el objeto result copiados del input). 27 | La segunda opción genera mutabilidad al poderse manipular el input. 28 | La tercera opción es no permitir manipular en input una vez que está dentro del resultado. 29 | 30 | - Fortalecer los comandos como DownloadXml (get_xml) que el tipo solo puede ser I - CFDI o R - Retenciones. 31 | 32 | - Poder configurar en Travis CI la ejecución de tests de integración. 33 | 34 | - AcceptRejectSigner debería permitir aceptar y/o rechazar más de 1 solo UUID a la vez. 35 | 36 | - Agregar un caso para hacer una prueba positiva de AcceptRejectSignatureService. 37 | Para hacer esta prueba se requieren 2 RFC (A y B), en este momento solo tenemos 1. 38 | Crear un CFDI donde A es Emisor y B es Receptor que requiera autorización (por ejemplo, por monto) 39 | A hace la solicitud de cancelación 40 | B hace la consulta de pendientes y ve el UUID 41 | B acepta la cancelación 42 | B hace la consulta de pendientes y ya no ve el UUID 43 | Se consulta el estado del UUID y está cancelado 44 | 45 | - Las pruebas de servicios no están verificando a dónde se está enviando la solicitud, por lo que podría existir un 46 | error al crear el endpoint, el error saldría a la luz en pruebas de integración pero no en pruebas unitarias. 47 | Se puede agregar un código como el siguiente (en `...\Tests\Unit\Services\Retentions\CancelSignatureServiceTest`): 48 | `$this->assertStringEndsWith(Services::retentions()->value(), $soapFactory->latestWsdlLocation);` 49 | 50 | - En el entorno de pruebas, en el objeto `FakeSoapFactory` permitir una cola de respuestas para simular el consumo 51 | de varias páginas del servicio `Registration#Customers`. 52 | 53 | ## Documentación 54 | 55 | - Documentar los métodos de `QuickFinkok` 56 | - Servicios: 57 | - Servicios que reintentan por errores de Finkok 58 | - Parámetros added y coupon de registration add 59 | -------------------------------------------------------------------------------- /docs/issues/AcuseCancelacionNoCoincidente.md: -------------------------------------------------------------------------------- 1 | # El acuse de cancelación entregado al cancelar y al solicitar el acuse no coinciden 2 | 3 | > *Este error de encuenta solucionado* 4 | 5 | Cuando se realiza una cancelación (vía `cancel_signature`) una de las respuestas es el acuse de cancelación 6 | entregado por el SAT. Dicho *acuse* se puede consultar en la respuesta de cancelación como el *voucher*. 7 | 8 | Finkok tiene también el método `get_receipt`, el problema es que el resultado devuelto no coincide 9 | con el del servicio `cancel_signature`, cuando deberían ser idénticos. 10 | 11 | Específicamente la diferencia se encuentra en el atributo `CancelaCFDResponse/CancelaCFDResult@Fecha` donde, 12 | como ejemplo, el método `get_receipt` devuelve *"2020-01-13·14:11:05"* 13 | y `cancelSignature` devuelve *"2020-01-13T14:11:05.3366563"*. 14 | 15 | Existe otra diferencia en el órden de los atributos del nodo `CancelaCFDResponse/CancelaCFDResult/Signature`, 16 | sin embargo no lo considero relevante para efectos de validación. 17 | 18 | Esto es importante porque altera el valor de la firma (la respuesta es un mensaje firmado), y deja la duda de que 19 | Finkok no solo está almacenando la respuesta, si no además la está alterando. 20 | 21 | A modo personal, creo que es importante que no se alteren los mensajes de respuestas del SAT, pero por otro lado 22 | aconsejo no almacenar este *acuse*, dado que se malentiende generalmente su uso. El acuse no significa que el 23 | CFDI fuese cancelado, el acuse contiene solamente la respuesta a la petición presentada ante el SAT. 24 | 25 | ## Actualizaciones 26 | 27 | 2019-01-14: Se creó el ticket #41435 documentando esta situación 28 | y se publicará la actualización a pesar de este error, si Finkok decidiera no actualizar su servicio entonces 29 | se eliminará la comprobación en el test de integración. 30 | 31 | 2019-01-15: Se confirmó que se sufría de un bug de carrera, cuando se solicita el acuse, pero aún no ha sido almacenado 32 | entonces se devuelve un acuse "fabricado". El problema lo tenían al fabricarlo y esto ha sido corregido. 33 | Personalmente considero que, al tratarse de un documento "oficial" con firma XML, deberían evitar fabricarlo y mejor 34 | retornar un error de tipo "Acuse no disponible en este momento, intente más tarde". 35 | Sin embargo, hay que considerar la poca utilidad del acuse y que en realidad no es relevante, 36 | siempre que el acuse sea idéntico al retornado por el SAT entonces no debería considerarse un problema. 37 | -------------------------------------------------------------------------------- /docs/issues/CancelacionRetencionesError1308.md: -------------------------------------------------------------------------------- 1 | # Error de cancelación de retenciones 1308 - Certificado revocado o caduco 2 | 3 | Al momento de hacer pruebas de integración sobre el servicio de cancelación de CFDI de tipo retenciones e información 4 | de pagos, se encuentra que al enviar la solicitud el servidor de pruebas del SAT responde en `CodEstatus` el 5 | error `1308 - Certificado revocado o caduco`. 6 | 7 | Se ha reportado a Finkok con el [ticket #41610](https://support.finkok.com/support/tickets/41610) donde nos pudieron 8 | responder ciertos cuestionamientos, pero el problema sigue sin resolverse: 9 | 10 | - ¿Hay algún RFC en especial que deba utilizar para poder hacer pruebas de CFDI de retenciones y pagos? 11 | 12 | Como tal no existe un RFC en específico, ya que se pueden realizar con los RFC que se encuentran actualmente 13 | disponible para realizar pruebas, sin embargo en este momento el SAT no está respondiendo de manera satisfactoria 14 | al utilizar cualquiera de los RFC que se tienen publicados en el wiki y que ellos mismos proporcionaron. 15 | 16 | - ¿Por qué está devolviendo el código 1308? 17 | 18 | Al parecer la incidencia se presenta porque el SAT no tiene habilitados los RFC de prueba. 19 | 20 | - Si es un error del SAT, ¿se ha reportado? 21 | 22 | Sí, sin embargo no hemos obtenido una respuesta favorable de su parte y en estos momentos nos encontramos 23 | dando seguimiento al caso. 24 | 25 | - ¿Tienen ustedes pruebas de integración que confirmen que los servicios de pruebas del SAT están funcionando? 26 | 27 | En su momento cuando se implementó el método de cancelación de retenciones funcionaba de forma correcta sin embargo 28 | debido a la actualización de los certificados de pruebas ya no fue posible efectuar pruebas de manera satisfactoria. 29 | De nuestra parte también hemos efectuado pruebas sin embargo obtenemos la misma respuesta que usted obtiene. 30 | 31 | ## Actualizaciones 32 | 33 | 2020-01-24: Se encontró el problema y se obtuvo respuesta de las preguntas. 34 | 35 | 2020-01-27: El problema persiste. 36 | 37 | 2021-03-20: Las pruebas de integración ya no están marcando este problema. 38 | -------------------------------------------------------------------------------- /docs/issues/QueryPendingServiceUuidNoExistente.md: -------------------------------------------------------------------------------- 1 | # Consumir `queryPending` con un CFDI recién creado 2 | 3 | ## Descripción 4 | 5 | El servicio [`Query_Pending`](https://wiki.finkok.com/doku.php?id=query_pending) *se usa para consultar el 6 | estatus de una factura que se quedó pendiente de enviar al SAT debido a una falla en el sistema del SAT 7 | o bien que se envió a través del método `Quick_Stamp`*. 8 | 9 | Por lo tanto, se podría llegar a ocupar con cualquier método de estampado. 10 | 11 | Al enviar un UUID mal formado como `"foo"` regresa el error `"UUID con formato invalido"`. 12 | Así, con errores de ortografía. 13 | 14 | Al enviar un UUID recién timbrado con `Quick_Stamp` regresa algunos (no todos) de los valores, 15 | supongo que esto depende del estado en que se encuentra. 16 | 17 | ## Error encontrado 18 | 19 | Al enviar un UUID que no se mandó timbrar, digamos, por ejemplo, timbrado con otro PAC o simplemente falso, como 20 | `01234567-0123-0123-0123-012345678901` lo que devuelve es un error 500 de SOAP: 21 | 22 | - Request headers 23 | 24 | ```text 25 | POST /servicios/soap/stamp HTTP/1.1 26 | Host: demo-facturacion.finkok.com 27 | Connection: Keep-Alive 28 | User-Agent: PHP-SOAP/7.3.3-1 29 | Content-Type: text/xml; charset=utf-8 30 | SOAPAction: "query_pending" 31 | Content-Length: 416 32 | ``` 33 | 34 | - Request body 35 | 36 | ```xml 37 | 38 | 39 | 40 | 41 | user@example.com 42 | secret 43 | 01234567-0123-0123-0123-012345678901 44 | 45 | 46 | 47 | ``` 48 | 49 | - Response headers 50 | 51 | ```text 52 | HTTP/1.1 500 INTERNAL SERVER ERROR 53 | Server: nginx/1.12.2 54 | Date: Sat, 06 Apr 2019 01:40:14 GMT 55 | Content-Type: text/xml; charset=utf-8 56 | Content-Length: 940 57 | Connection: close 58 | x-xss-protection: 1; mode=block 59 | x-content-type-options: nosniff 60 | Vary: Cookie 61 | x-frame-options: DENY 62 | Set-Cookie: sessionid=2f06b59dbf811e2b68be49a930692fe7; httponly; Path=/; secure 63 | ``` 64 | 65 | - Response body (se omiten namespaces que devuelve, pero no se usan) 66 | 67 | ```xml 68 | 69 | 70 | 71 | 72 | senv:Server 73 | local variable 'invoice' referenced before assignment 74 | 75 | 76 | 77 | 78 | ``` 79 | 80 | Lo que **se espera que retorne** es un error, del tipo `"UUID no existente"` 81 | tal como en una llamada con un UUID mal formado. 82 | 83 | ## Reporte 84 | 85 | 2019-04-05 20:10 86 | 87 | ## Actualización 2019-04-08.1 88 | 89 | Respondieron con una corrección al servicio en entorno de pruebas, ahora devuelve el mensaje: 90 | `UUID 01234567-0123-0123-0123-012345678901 No Encontrado`. 91 | -------------------------------------------------------------------------------- /docs/issues/RegistrationGetNoList.md: -------------------------------------------------------------------------------- 1 | # El método `Registration#Get` con `taxpayer_id` vacío no devuelve el listado de clientes 2 | 3 | ## Descripción 4 | 5 | El método `Registration#Get` debería devolver todos los clientes relacionados con la cuenta 6 | cuando no se envía el parámetro `taxpayer_id`. 7 | 8 | Así está documentado en el webservice: 9 | 10 | - https://facturacion.finkok.com/servicios/soap/registration.wsdl 11 | 12 | > This function lists all the user of the account if no taxpayer_id is passed 13 | > otherwise will return the taxpayer_id and status of the given user. 14 | 15 | ```xml 16 | 17 | 18 | This function lists all the user of the account if no taxpayer_id is passed otherwise will return the taxpayer_id and status of the given user. 19 | 20 | 21 | 22 | 23 | ``` 24 | 25 | Y en la documentación: 26 | 27 | - 28 | 29 | > Este método tiene como finalidad la de otorgar al socio de negocios un listado 30 | > o el status del RFC Emisor que esté ingresando y tenga registrado en su cuenta. 31 | 32 | Al correr pruebas de integración, hemos notado que este ya no es el caso, y en su lugar, en vez de devolver el listado, 33 | devuelve el mensaje `RFC Invalido` (así, sin acento). Anteriormente, sí devolvía el listado de clientes. 34 | 35 | ## Reporte 36 | 37 | Se reportó a Finkok en el [ticket #66516](https://support.finkok.com/support/tickets/66516), sin embargo, 38 | la respuesta fue que —efectivamente— este método se comportaba de la manera documentada y esperada en el 39 | entorno de pruebas, pero que el cambio nunca llegó a producción. Actualmente, no devuelve el listado de 40 | clientes ni en el entorno de pruebas ni en producción. 41 | 42 | También comentan que se agregará otro método diferente para poder obtener el listado, sin embargo, 43 | todavía no tienen fecha estimada para la implementación. 44 | 45 | Finkok debería considerar esto como un fallo en su aplicación, no como una nueva funcionalidad a agregar. 46 | Y, consecuentemente, darle prioridad alta para repararlo. 47 | 48 | ### Reporte 2022-01-03 49 | 50 | A pesar de la implementación del método `Registration#Customers`, el método `Registration#Get` sigue retornando 51 | un conjunto de `ResellerUser`, cuando debería de regresar cero o uno. Asimismo, se cambió la documentación 52 | para que ni diga que devuelve un listado de clientes. 53 | 54 | ## Solución 55 | 56 | Finkok implementó el método `Registration#Customers` con el que se puede obtener el listado de clientes. 57 | 58 | Al 2022-01-03 se hizo la implementación del método `Registration#Customers`. 59 | 60 | ### Implementación 61 | 62 | Este método devuelve un **listado paginado** —cosa que no habían hecho en ningún otro método— 63 | y se implementa de dos formas diferentes: 64 | 65 | #### `QuickFinkok::customersObtainAll(): Customers` 66 | 67 | Al llamarlo consume tantas páginas sea necesario y retorna el listado completo de clientes en el objeto `Customers`. 68 | No contiene los datos originales de las consultas, a diferencia de la mayoría de los valores retornados. 69 | 70 | #### `Finkok::registrationCustomers(ObtainCustomersCommand $command): ObtainCustomersResult` 71 | 72 | Al llamarlo consume el servicio y obtiene la página especificada. 73 | Este método sí contiene un objeto `ObtainCustomersResult` con toda la respuesta de la consulta. 74 | Solo devuelve los datos de la consulta especificada. 75 | 76 | ## Actualizaciones 77 | 78 | 2022-12-20: Se reportó y documentó el problema. 79 | 80 | 2022-12-27: Se agregó el método `Registration#Customers` en entorno de desarrollo y producción. 81 | 82 | 2022-01-03: Se implementó el consumo del método `Registration#Customers`. 83 | -------------------------------------------------------------------------------- /docs/issues/StampAmpersand.md: -------------------------------------------------------------------------------- 1 | # Al timbrar con un texto `&` devuelve `705 - XML Estructura inválida` 2 | 3 | ## Descripción 4 | 5 | Al momento de enviar una solicitud de timbrado (métodos `stamp`) Finkok verifica la existencia de `&`, 6 | y en caso de encontrarla lo reconoce como un error de tipo `705 - XML Estructura inválida`. 7 | 8 | Esto es un error, pues un texto válido como `Camiseta con la leyenda: & is XML` es rechazado. 9 | El texto anterior, codificado como XML es `Camiseta con la leyenda: &amp; is XML`. 10 | 11 | La validación es arbitraria en su naturaleza, dado que en ningún momento el SAT en su documentación técnica 12 | (*Anexo 20*, *Matriz de errores* o *Guías de llenado*) ha establecido la invalidez de esta información. 13 | 14 | Si bien la existencia de tener en un texto de origen la cadena de caracteres `&` *supone un error de codificación*, 15 | no necesariamente se trata de un error. También es cierto que, al generar un CFDI con un texto que no se apegue 16 | fielmente a la operación que refleja, puede ser una causa para considerarlo con errores. 17 | 18 | Es probable que si el texto es `Piezas de plástico` se trate de un *error de codificación*, 19 | donde en realidad el texto debería ser `Piezas de plástico`. 20 | Siendo totalmente estrictos, el texto del CFDI no es correcto y no refleja fielmente la operación. 21 | 22 | Un texto como `P&G` termina codificándose como `P&amp;G`, y es *muy probable* que se trate de un error. 23 | Un texto como `P&G` termina codificándose como `P&G`, y no es un error. 24 | 25 | También es importante considerar que, dada la probabilidad de que el texto `&` aparezca en un texto es muy baja, 26 | nuestros sistemas informáticos deberían poder validar estos casos y/o hacer las correcciones oportunas para que 27 | se envíen textos que reflejen fielmente las operaciones, sin errores de codificación. 28 | 29 | Personalmente, considero que esto se trata de una situación que se puede prevenir e incluso corregir sin causar grandes 30 | inconvenientes a los usuarios. Esto no se trata de un error ortográfico, se trata de un problema de codificación. 31 | 32 | Por otro lado, también considero que si un CFDI contiene estos errores es un tema entre el SAT, el emisor y el receptor. 33 | Pero no es un tema del PAC. Sobre todo: **el PAC no debería establecer validaciones sin fundamento legal**. 34 | 35 | ## Reporte 36 | 37 | Se reportó a Finkok en el [ticket #97947](https://support.finkok.com/support/tickets/97947) en donde se explica el caso. 38 | 39 | ### Respuesta 2024-03-13 40 | 41 | La respuesta inicial ha sido que, al momento de entregar el CFDI firmado al SAT, el SAT lo recibe, 42 | pero genera una incidencia para el PAC, y estas incidencias se consideran una métrica negativa. 43 | 44 | Se les ha solicitado compartir la evidencia de esta incidencia (anonimizada), pero se está evaluando la petición. 45 | 46 | También Finkok se ha comprometido a levantar el caso con el SAT, para poder timbrar sin que esta situación sea 47 | considerada una incidencia, en cuyo caso podrían omitir la validación. 48 | 49 | ### Pruebas 2024-04-12 50 | 51 | He corrido pruebas de integración y he encontrado que ya no se está realizando la validación al momento de timbrar. 52 | Al parecer los reportes levantados por la comunidad han tenido un buen resultado. 53 | 54 | Si Finkok decide quitar la validación está haciendo bien al mantenerse al margen de no validar sin fundamento legal. 55 | Ahora corresponde a los usuarios de su servicio hacer las correctas validaciones y prevenir entradas con `&`, 56 | que terminan en el CFDI como `&amp;`, y que en realidad seguramente se tratan de *errores de codificación* 57 | -------------------------------------------------------------------------------- /docs/issues/StampServiceDobleEstampado.md: -------------------------------------------------------------------------------- 1 | # Consumir `stamp` para generar un doble estampado no devuelve los datos 2 | 3 | ## Descripción 4 | 5 | Se espera que al consumir el servicio de estampado `stamp` con un mismo precfdi 6 | se devuelva el CFDI timbrado previamente (mismo uuid). 7 | 8 | Tal como dice la documentación en : 9 | 10 | > Este método recupera un XML que fue timbrado con anterioridad, 11 | > cuando se presenta la incidencia “307 El CFDI Contiene un timbre previo”. 12 | 13 | Y en 14 | 15 | > Cuando ocurre esta incidencia el web sevices de forma automática recupera el XML. 16 | 17 | **Solucionado en 2019-04-05** 18 | 19 | ## Error encontrado 20 | 21 | La respuesta no contiene la información del UUID timbrado previamente. 22 | 23 | En su lugar contiene un `stampResult` con `Incidencia:CodigoError` `307`, el valor de `xml` está vacío. 24 | Los demás valores no son reportados. Es un misterio para mí por qué retorna dos veces la incidencia. 25 | 26 | ```json 27 | { 28 | "stampResult": { 29 | "xml": "", 30 | "Incidencias": { 31 | "Incidencia": [ 32 | { 33 | "IdIncidencia": "ID_incidencia", 34 | "Uuid": "", 35 | "CodigoError": "307", 36 | "WorkProcessId": "WorkProcessId", 37 | "MensajeIncidencia": "El CFDI contiene un timbre previo", 38 | "ExtraInfo": "", 39 | "NoCertificadoPac": "", 40 | "FechaRegistro": "2019-03-31T15:50:14" 41 | }, 42 | { 43 | "IdIncidencia": "ID_incidencia", 44 | "Uuid": "", 45 | "CodigoError": "307", 46 | "WorkProcessId": "WorkProcessId", 47 | "MensajeIncidencia": "El CFDI contiene un timbre previo", 48 | "ExtraInfo": "", 49 | "NoCertificadoPac": "", 50 | "FechaRegistro": "2019-03-31T15:50:14" 51 | } 52 | ] 53 | } 54 | } 55 | } 56 | ``` 57 | 58 | El servicio `stamped` tampoco puede encontrar el primer CFDI recién estampado. 59 | Debe reintentarlo aproximadamente 4 segundos hasta que lo recupera correctamente. 60 | Mientras tanto devuelve: `603: El CFDI no contiene un timbre previo`. 61 | 62 | Esto parece dar más claridad al error: 63 | 64 | - Se genera el estampado del PRECFDI e inmediatamente se llama a: 65 | - `stamped`: El CFDI no contiene un timbre previo 66 | - `stamp`: El CFDI contiene un timbre previo 67 | - ...después de algunos segundos (me ha tocado esperar hasta 8): 68 | - `stamped`: Lo encuentra y lo devuelve 69 | - `stamp`: Algunas veces lo encuentra y lo devuelve 70 | 71 | Por lo tanto, parece que en realidad el problema consiste en que internamente Finkok 72 | sí reporta que el CFDI fue creado, pero no lo pone a disposición para poderlo recuperar. 73 | 74 | Incluso he creado un test que encadena las pruebas, en resumen: 75 | llama a `stamp` por primera vez, 76 | llama a `stamped` hasta que devuelve el resultado 77 | llama a `stamp` por segunda vez. 78 | Y lo que sucede es que algunas veces el segundo estampado sigue sin contener los datos de XML y UUID. 79 | 80 | 81 | ### Servicios afectados 82 | 83 | - `stamp`: al menos reporta que el CFDI ya fue timbrado. 84 | - `stamped`: incluso dice que el CFDI no ha sido timbrado con anterioridad. 85 | 86 | El servicio `quick_stamp` se salva de esta cuestión porque establece que en caso de haber un estampado previo 87 | entonces se devolverá un código `307` y ya. No se espera que devuelva el contenido del estampado previo. 88 | 89 | 90 | ## Reporte 91 | 92 | 2019-03-31 16:10 93 | 94 | ## Actualización 2019-04-01.1 95 | 96 | Han modificado su documentación -*¡zorro astuto!*- y este es el comportamiento esperado: 97 | 98 | > Al llamar a stamp dos veces con el mismo precfdi, la segunda vez puede regresar 99 | > *o puede no regresar* los datos del cfdi firmado previamente. 100 | 101 | Por lo que, al menos para el método `stamp`, que no regrese el contenido `xml` o `uuid` es considerado dentro de lo esperado. 102 | 103 | Para el método `stamped` es otra historia, porque el error devuelto por este método es una incidencia 104 | `603: El CFDI no contiene un timbre previo`. 105 | 106 | ## Actualización 2019-04-05.1 107 | 108 | Me responden en el ticket que han modificado el servicio `stamped`. 109 | 110 | No debería regresar nunca un error `603: El CFDI no contiene un timbre previo`. 111 | **Siempre debe regresar los datos del timbrado**. 112 | -------------------------------------------------------------------------------- /src/Definitions/CancelAnswer.php: -------------------------------------------------------------------------------- 1 | value()); 18 | } 19 | } 20 | -------------------------------------------------------------------------------- /src/Definitions/Environment.php: -------------------------------------------------------------------------------- 1 | 'https://demo-facturacion.finkok.com', 26 | 'production' => 'https://facturacion.finkok.com', 27 | ]; 28 | } 29 | } 30 | -------------------------------------------------------------------------------- /src/Definitions/EnvironmentManifest.php: -------------------------------------------------------------------------------- 1 | 'https://manifiesto.cfdiquadrum.com.mx:8008/', 26 | 'production' => 'https://manifiesto.cfdiquadrum.com.mx/', 27 | ]; 28 | } 29 | } 30 | -------------------------------------------------------------------------------- /src/Definitions/ReceiptType.php: -------------------------------------------------------------------------------- 1 | 'I', 23 | 'cancellation' => 'C', 24 | ]; 25 | } 26 | } 27 | -------------------------------------------------------------------------------- /src/Definitions/RfcRole.php: -------------------------------------------------------------------------------- 1 | '/servicios/soap/stamp.wsdl', 34 | 'utilities' => '/servicios/soap/utilities.wsdl', 35 | 'cancel' => '/servicios/soap/cancel.wsdl', 36 | 'manifest' => '/servicios/soap/firmar.wsdl', 37 | 'registration' => '/servicios/soap/registration.wsdl', 38 | 'retentions' => '/servicios/soap/retentions.wsdl', 39 | ]; 40 | } 41 | } 42 | -------------------------------------------------------------------------------- /src/Definitions/SignedDocumentFormat.php: -------------------------------------------------------------------------------- 1 | 'XML', 25 | 'pdf' => 'PDF', 26 | ]; 27 | } 28 | } 29 | -------------------------------------------------------------------------------- /src/Exceptions/FinkokException.php: -------------------------------------------------------------------------------- 1 | [Stamping\StampService::class, Stamping\StampingCommand::class], 47 | 'quickstamp' => [Stamping\QuickStampService::class, Stamping\StampingCommand::class], 48 | 'stamped' => [Stamping\StampedService::class, Stamping\StampingCommand::class], 49 | 'stampQueryPending' => [ 50 | Stamping\QueryPendingService::class, 51 | Stamping\QueryPendingCommand::class, 52 | 'queryPending', // override method name on service 53 | ], 54 | 'cancelSignature' => [Cancel\CancelSignatureService::class, Cancel\CancelSignatureCommand::class], 55 | 'getPendingToCancel' => [Cancel\GetPendingService::class, Cancel\GetPendingCommand::class, 'obtainPending'], 56 | 'getCancelReceipt' => [Cancel\GetReceiptService::class, Cancel\GetReceiptResult::class, 'download'], 57 | 'getSatStatus' => [Cancel\GetSatStatusService::class, Cancel\GetSatStatusCommand::class, 'query'], 58 | 'getRelatedSignature' => [ 59 | Cancel\GetRelatedSignatureService::class, 60 | Cancel\GetRelatedSignatureCommand::class, 61 | ], 62 | 'acceptRejectSignature' => [ 63 | Cancel\AcceptRejectSignatureService::class, 64 | Cancel\AcceptRejectSignatureCommand::class, 65 | ], 66 | 'datetime' => [Utilities\DatetimeService::class, Utilities\DatetimeCommand::class], 67 | 'downloadXml' => [Utilities\DownloadXmlService::class, Utilities\DownloadXmlCommand::class], 68 | 'reportCredit' => [Utilities\ReportCreditService::class, Utilities\ReportCreditCommand::class], 69 | 'reportTotal' => [Utilities\ReportTotalService::class, Utilities\ReportTotalCommand::class], 70 | 'reportUuid' => [Utilities\ReportUuidService::class, Utilities\ReportUuidCommand::class], 71 | 'getContracts' => [Manifest\GetContractsService::class, Manifest\GetContractsCommand::class, 'obtainContracts'], 72 | 'signContracts' => [ 73 | Manifest\SignContractsService::class, 74 | Manifest\SignContractsCommand::class, 75 | 'sendSignedContracts', 76 | ], 77 | 'getSignedContracts' => [ 78 | Manifest\GetSignedContractsService::class, 79 | Manifest\GetSignedContractsCommand::class, 80 | ], 81 | 'registrationAdd' => [Registration\AddService::class, Registration\AddCommand::class, 'add'], 82 | 'registrationAssign' => [Registration\AssignService::class, Registration\AssignCommand::class, 'assign'], 83 | 'registrationSwitch' => [Registration\SwitchService::class, Registration\SwitchCommand::class, 'switch'], 84 | 'registrationEdit' => [Registration\EditService::class, Registration\EditCommand::class, 'edit'], 85 | 'registrationObtain' => [Registration\ObtainService::class, Registration\ObtainCommand::class, 'obtain'], 86 | 'registrationCustomers' => [ 87 | Registration\ObtainCustomersService::class, 88 | Registration\ObtainCustomersCommand::class, 89 | 'obtainPage', 90 | ], 91 | ]; 92 | 93 | /** @var FinkokSettings */ 94 | private $settings; 95 | 96 | public function __construct(FinkokSettings $factory) 97 | { 98 | $this->settings = $factory; 99 | } 100 | 101 | public function settings(): FinkokSettings 102 | { 103 | return $this->settings; 104 | } 105 | 106 | /** 107 | * @param string $name 108 | * @param array $arguments 109 | * @return mixed 110 | */ 111 | public function __call(string $name, array $arguments) 112 | { 113 | if (array_key_exists($name, static::SERVICES_MAP)) { 114 | $command = $this->checkCommand($name, $arguments[0] ?? null); 115 | $service = $this->createService($name); 116 | return $this->executeService($name, $service, $command); 117 | } 118 | throw new BadMethodCallException(sprintf('Helper %s is not registered', $name)); 119 | } 120 | 121 | /** 122 | * @param string $method 123 | * @param mixed $command 124 | * @return object|null 125 | */ 126 | protected function checkCommand(string $method, $command): ?object 127 | { 128 | $expected = static::SERVICES_MAP[$method][1]; 129 | if ('' === $expected) { 130 | return null; 131 | } 132 | if (! is_object($command) || ! is_a($command, $expected)) { 133 | $type = (is_object($command)) ? get_class($command) : gettype($command); 134 | throw new InvalidArgumentException( 135 | sprintf('Call %s::%s expect %s but received %s', static::class, $method, $expected, $type) 136 | ); 137 | } 138 | return $command; 139 | } 140 | 141 | /** 142 | * @param string $method 143 | * @return object 144 | */ 145 | protected function createService(string $method): object 146 | { 147 | $serviceClass = static::SERVICES_MAP[$method][0]; 148 | return new $serviceClass($this->settings); 149 | } 150 | 151 | /** 152 | * @param string $method 153 | * @param object $service 154 | * @param object|null $command 155 | * @return mixed 156 | */ 157 | protected function executeService(string $method, object $service, ?object $command) 158 | { 159 | $method = static::SERVICES_MAP[$method][2] ?? $method; 160 | if (! is_callable([$service, $method])) { 161 | throw new BadMethodCallException( 162 | sprintf('The service %s does not have a method %s', get_class($service), $method) 163 | ); 164 | } 165 | return $service->{$method}($command); 166 | } 167 | } 168 | -------------------------------------------------------------------------------- /src/FinkokEnvironment.php: -------------------------------------------------------------------------------- 1 | environment = $environment; 15 | } 16 | 17 | public static function makeDevelopment(): self 18 | { 19 | return new self(Definitions\Environment::development()); 20 | } 21 | 22 | public static function makeProduction(): self 23 | { 24 | return new self(Definitions\Environment::production()); 25 | } 26 | 27 | public function isDevelopment(): bool 28 | { 29 | return $this->environment->isDevelopment(); 30 | } 31 | 32 | public function isProduction(): bool 33 | { 34 | return $this->environment->isProduction(); 35 | } 36 | 37 | public function server(): string 38 | { 39 | return $this->environment->value(); 40 | } 41 | 42 | public function endpoint(Definitions\Services $service): string 43 | { 44 | $environment = $this->environment; 45 | if ($service->isManifest()) { 46 | $environment = (new Definitions\EnvironmentManifest($environment->index())); 47 | } 48 | 49 | return rtrim($environment->value(), '/') . '/' . ltrim($service->value(), '/'); 50 | } 51 | } 52 | -------------------------------------------------------------------------------- /src/FinkokSettings.php: -------------------------------------------------------------------------------- 1 | username = $username; 36 | $this->password = $password; 37 | $this->environment = $environment ?? FinkokEnvironment::makeDevelopment(); 38 | $this->soapFactory = new SoapFactory(); 39 | } 40 | 41 | public function changeSoapFactory(SoapFactory $soapFactory): void 42 | { 43 | $this->soapFactory = $soapFactory; 44 | } 45 | 46 | public function username(): string 47 | { 48 | return $this->username; 49 | } 50 | 51 | public function password(): string 52 | { 53 | return $this->password; 54 | } 55 | 56 | public function environment(): FinkokEnvironment 57 | { 58 | return $this->environment; 59 | } 60 | 61 | public function soapFactory(): SoapFactory 62 | { 63 | return $this->soapFactory; 64 | } 65 | 66 | /** 67 | * This method created a configured SoapCaller with wsdlLocation and default options 68 | * 69 | * @param Services $service 70 | * @param string $usernameKey defaults to username, if empty then it will be ommited 71 | * @param string $passwordKey defaults to password, if empty then it will be ommited 72 | * @return SoapCaller 73 | */ 74 | public function createCallerForService( 75 | Services $service, 76 | string $usernameKey = 'username', 77 | string $passwordKey = 'password' 78 | ): SoapCaller { 79 | $wsdlLocation = $this->environment()->endpoint($service); 80 | $credentials = array_merge( 81 | ('' !== $usernameKey) ? [$usernameKey => $this->username()] : [], 82 | ('' !== $passwordKey) ? [$passwordKey => $this->password()] : [] 83 | ); 84 | return $this->soapFactory()->createSoapCaller($wsdlLocation, $credentials); 85 | } 86 | } 87 | -------------------------------------------------------------------------------- /src/Helpers/AcceptRejectSigner.php: -------------------------------------------------------------------------------- 1 | uuid = $uuid; 44 | $this->answer = $answer; 45 | $this->dateTime = $dateTime ?? new DateTimeImmutable(); 46 | $this->pacRfc = $pacRfc ?: static::DEFAULT_PACRFC; 47 | } 48 | 49 | public function uuid(): string 50 | { 51 | return $this->uuid; 52 | } 53 | 54 | public function answer(): CancelAnswer 55 | { 56 | return $this->answer; 57 | } 58 | 59 | public function pacRfc(): string 60 | { 61 | return $this->pacRfc; 62 | } 63 | 64 | public function dateTime(): DateTimeImmutable 65 | { 66 | return $this->dateTime; 67 | } 68 | 69 | public function sign(Credential $credential): string 70 | { 71 | $helper = new XmlCancelacionHelper(XmlCancelacionCredentials::createWithPhpCfdiCredential($credential)); 72 | return $helper->signCancellationAnswer($this->uuid(), $this->answer(), $this->pacRfc(), $this->dateTime()); 73 | } 74 | } 75 | -------------------------------------------------------------------------------- /src/Helpers/CancelSigner.php: -------------------------------------------------------------------------------- 1 | documents = $documents; 30 | $this->dateTime = $dateTime ?? new DateTimeImmutable(); 31 | } 32 | 33 | /** @return CancelDocuments */ 34 | public function documents(): CancelDocuments 35 | { 36 | return $this->documents; 37 | } 38 | 39 | public function dateTime(): DateTimeImmutable 40 | { 41 | return $this->dateTime; 42 | } 43 | 44 | public function sign(Credential $credential): string 45 | { 46 | $helper = new XmlCancelacionHelper(XmlCancelacionCredentials::createWithPhpCfdiCredential($credential)); 47 | return $helper->signCancellationUuids($this->documents(), $this->dateTime()); 48 | } 49 | 50 | public function signRetention(Credential $credential): string 51 | { 52 | $helper = new XmlCancelacionHelper(XmlCancelacionCredentials::createWithPhpCfdiCredential($credential)); 53 | return $helper->signRetentionCancellationUuids($this->documents(), $this->dateTime()); 54 | } 55 | } 56 | -------------------------------------------------------------------------------- /src/Helpers/DocumentSigner.php: -------------------------------------------------------------------------------- 1 | rfc = $rfc; 28 | $this->date = $date; 29 | $this->content = $content; 30 | } 31 | 32 | public function rfc(): string 33 | { 34 | return $this->rfc; 35 | } 36 | 37 | public function date(): DateTimeImmutable 38 | { 39 | return $this->date; 40 | } 41 | 42 | public function content(): string 43 | { 44 | return $this->content; 45 | } 46 | 47 | public function sign(string $certificateFile, string $privateKeyFile, string $passPhrase): string 48 | { 49 | $credential = Credential::openFiles($certificateFile, $privateKeyFile, $passPhrase); 50 | return $this->signUsingCredential($credential); 51 | } 52 | 53 | public function signUsingCredential(Credential $credential): string 54 | { 55 | $document = $this->createDocumentToSign(); 56 | $this->signDocumentUsingCredential($document, $credential); 57 | return strval($document->saveXML()); 58 | } 59 | 60 | public function createDocumentToSign(): DOMDocument 61 | { 62 | $document = new DOMDocument('1.0', 'UTF-8'); 63 | $root = $document->createElement('documento'); 64 | $document->appendChild($root); 65 | $contract = $document->createElement('contrato', $this->content()); 66 | $contract->setAttribute('rfc', $this->rfc()); 67 | $contract->setAttribute('fecha', $this->date()->format('Y-m-d\TH:i:s')); 68 | $root->appendChild($contract); 69 | return $document; 70 | } 71 | 72 | public function signDocumentUsingCredential(DOMDocument $document, Credential $credential): void 73 | { 74 | $root = $document->documentElement; 75 | if (null === $root) { 76 | throw new LogicException('The DOM Document does not contains a root element'); 77 | } 78 | $objDSig = new XMLSecurityDSig(); 79 | $objDSig->setCanonicalMethod(XMLSecurityDSig::EXC_C14N); 80 | $objDSig->addReference( 81 | $document, 82 | XMLSecurityDSig::SHA1, 83 | ['http://www.w3.org/2000/09/xmldsig#enveloped-signature'], 84 | ['force_uri' => true] 85 | ); 86 | 87 | $objKey = new XMLSecurityKey(XMLSecurityKey::RSA_SHA1, ['type' => 'private']); 88 | $objKey->passphrase = $credential->privateKey()->passPhrase(); // set passphrase before loading key 89 | $objKey->loadKey($credential->privateKey()->pem(), false, false); 90 | 91 | $objDSig->sign($objKey); 92 | $objDSig->add509Cert($credential->certificate()->pem(), true, false); 93 | 94 | $objDSig->appendSignature($root); 95 | } 96 | } 97 | -------------------------------------------------------------------------------- /src/Helpers/FileLogger.php: -------------------------------------------------------------------------------- 1 | outputFile = $outputFile; 18 | } 19 | 20 | /** 21 | * @inheritDoc 22 | * @param string|\Stringable $message 23 | * @param mixed[] $context 24 | */ 25 | public function log($level, $message, array $context = []): void 26 | { 27 | file_put_contents($this->outputFile, $message . PHP_EOL, FILE_APPEND); 28 | } 29 | } 30 | -------------------------------------------------------------------------------- /src/Helpers/GetRelatedSigner.php: -------------------------------------------------------------------------------- 1 | uuid = $uuid; 35 | $this->role = $role ?? RfcRole::issuer(); 36 | $this->pacRfc = $pacRfc ?: static::DEFAULT_PACRFC; 37 | } 38 | 39 | public function uuid(): string 40 | { 41 | return $this->uuid; 42 | } 43 | 44 | public function role(): RfcRole 45 | { 46 | return $this->role; 47 | } 48 | 49 | public function pacRfc(): string 50 | { 51 | return $this->pacRfc; 52 | } 53 | 54 | public function sign(Credential $credential): string 55 | { 56 | $helper = new XmlCancelacionHelper(XmlCancelacionCredentials::createWithPhpCfdiCredential($credential)); 57 | return $helper->signObtainRelated($this->uuid(), $this->role(), $this->pacRfc()); 58 | } 59 | } 60 | -------------------------------------------------------------------------------- /src/Helpers/GetSatStatusExtractor.php: -------------------------------------------------------------------------------- 1 | $expressionData 29 | */ 30 | public function __construct(array $expressionData) 31 | { 32 | $this->expressionData = [ 33 | 're' => strval($expressionData['re'] ?? ''), 34 | 'rr' => strval($expressionData['rr'] ?? ''), 35 | 'tt' => strval($expressionData['tt'] ?? ''), 36 | 'id' => strval($expressionData['id'] ?? ''), 37 | ]; 38 | } 39 | 40 | public static function fromXmlDocument(DOMDocument $document): self 41 | { 42 | $discoverer = new DiscoverExtractor( 43 | new Comprobante40(), 44 | new Comprobante33(), 45 | new Comprobante32(), 46 | ); 47 | try { 48 | $values = $discoverer->obtain($document); 49 | } catch (UnmatchedDocumentException $exception) { 50 | $message = 'Unable to obtain the expression values, document must be valid a CFDI version 4.0, 3.3 or 3.2'; 51 | throw new RuntimeException($message, 0, $exception); 52 | } 53 | return new self($values); 54 | } 55 | 56 | public static function fromXmlString(string $xmlCfdi): self 57 | { 58 | $document = new DOMDocument(); 59 | $document->loadXML($xmlCfdi); 60 | return static::fromXmlDocument($document); 61 | } 62 | 63 | public function buildCommand(): GetSatStatusCommand 64 | { 65 | $expData = $this->expressionData; 66 | return new GetSatStatusCommand($expData['re'], $expData['rr'], $expData['id'], $expData['tt']); 67 | } 68 | } 69 | -------------------------------------------------------------------------------- /src/Helpers/JsonDecoderLogger.php: -------------------------------------------------------------------------------- 1 | logger = $logger; 38 | } 39 | 40 | /** 41 | * Define si se utilizará la función \json_validate en caso de estar disponible. 42 | * 43 | * @param bool|null $value El nuevo estado, si se establece NULL entonces solo devuelve el espado previo. 44 | * @return bool El estado previo 45 | */ 46 | public function setUseJsonValidateIfAvailable(bool $value = null): bool 47 | { 48 | $previous = $this->useJsonValidateIfAvailable; 49 | if (null !== $value) { 50 | $this->useJsonValidateIfAvailable = $value; 51 | } 52 | return $previous; 53 | } 54 | 55 | /** 56 | * Define si también se mandará el mensaje JSON al Logger. 57 | * 58 | * @param bool|null $value El nuevo estado, si se establece NULL entonces solo devuelve el espado previo. 59 | * @return bool El estado previo 60 | */ 61 | public function setAlsoLogJsonMessage(bool $value = null): bool 62 | { 63 | $previous = $this->alsoLogJsonMessage; 64 | if (null !== $value) { 65 | $this->alsoLogJsonMessage = $value; 66 | } 67 | return $previous; 68 | } 69 | 70 | public function lastMessageWasJsonValid(): bool 71 | { 72 | return $this->lastMessageWasJsonValid; 73 | } 74 | 75 | /** 76 | * @inheritDoc 77 | * @param string|\Stringable $message 78 | * @param mixed[] $context 79 | */ 80 | public function log($level, $message, array $context = []): void 81 | { 82 | $this->logger->log($level, $this->jsonDecode($message), $context); 83 | if ($this->lastMessageWasJsonValid && $this->alsoLogJsonMessage) { 84 | $this->logger->log($level, $message, $context); 85 | } 86 | } 87 | 88 | /** @param string|\Stringable $string */ 89 | private function jsonDecode($string): string 90 | { 91 | $this->lastMessageWasJsonValid = false; 92 | $string = strval($string); 93 | 94 | // json_validate and json_decode 95 | if ($this->useJsonValidateIfAvailable && function_exists('\json_validate')) { 96 | if (\json_validate($string)) { 97 | $this->lastMessageWasJsonValid = true; 98 | return $this->varDump(json_decode($string)); 99 | } 100 | 101 | return $string; 102 | } 103 | 104 | // json_decode only 105 | $decoded = json_decode($string); 106 | if (JSON_ERROR_NONE === json_last_error()) { 107 | $this->lastMessageWasJsonValid = true; 108 | return $this->varDump($decoded); 109 | } 110 | 111 | return $string; 112 | } 113 | 114 | /** @param mixed $var */ 115 | private function varDump($var): string 116 | { 117 | return print_r($var, true); 118 | } 119 | } 120 | -------------------------------------------------------------------------------- /src/Services/AbstractCollection.php: -------------------------------------------------------------------------------- 1 | 16 | */ 17 | abstract class AbstractCollection implements Countable, IteratorAggregate 18 | { 19 | /** 20 | * @param stdClass $content 21 | * @return TItem 22 | */ 23 | abstract protected function createItemFromStdClass(stdClass $content): object; 24 | 25 | /** @var ArrayObject */ 26 | protected $collection; 27 | 28 | /** @param array $collection */ 29 | public function __construct(array $collection) 30 | { 31 | $this->collection = new ArrayObject(); 32 | foreach ($collection as $content) { 33 | $this->collection->append($this->createItemFromStdClass($content)); 34 | } 35 | } 36 | 37 | /** 38 | * @param int $index 39 | * @return TItem 40 | */ 41 | public function get(int $index): object 42 | { 43 | if (! isset($this->collection[$index])) { 44 | return $this->createItemFromStdClass((object) []); 45 | } 46 | return $this->collection[$index]; 47 | } 48 | 49 | public function count(): int 50 | { 51 | return $this->collection->count(); 52 | } 53 | 54 | /** @return TItem */ 55 | public function first(): object 56 | { 57 | return $this->get(0); 58 | } 59 | 60 | /** @return ArrayIterator */ 61 | public function getIterator(): ArrayIterator 62 | { 63 | return $this->collection->getIterator(); 64 | } 65 | } 66 | -------------------------------------------------------------------------------- /src/Services/AbstractResult.php: -------------------------------------------------------------------------------- 1 | data = $data; 21 | $root = $this->findInDescendent($data, ...$meanLocation); 22 | if (! $root instanceof stdClass) { 23 | throw new InvalidArgumentException( 24 | sprintf('Unable to find mean object at /%s', implode('/', $meanLocation)) 25 | ); 26 | } 27 | $this->root = $root; 28 | } 29 | 30 | public function rawData(): stdClass 31 | { 32 | return clone $this->data; 33 | } 34 | 35 | /** 36 | * @param stdClass|array|mixed $haystack 37 | * @param string ...$location 38 | * @return mixed 39 | */ 40 | protected function findInDescendent($haystack, string ...$location) 41 | { 42 | if (0 === count($location)) { 43 | return $haystack; 44 | } 45 | $search = array_shift($location); 46 | if (is_array($haystack)) { 47 | return (isset($haystack[$search])) ? $this->findInDescendent($haystack[$search], ...$location) : null; 48 | } 49 | if ($haystack instanceof stdClass) { 50 | return (isset($haystack->{$search})) ? $this->findInDescendent($haystack->{$search}, ...$location) : null; 51 | } 52 | throw new InvalidArgumentException('Cannot find descendent on non-array non-object haystack'); 53 | } 54 | 55 | protected function get(string $keyword): string 56 | { 57 | return strval($this->root->{$keyword} ?? ''); 58 | } 59 | } 60 | -------------------------------------------------------------------------------- /src/Services/Cancel/AcceptRejectSignatureCommand.php: -------------------------------------------------------------------------------- 1 | xml = $xml; 15 | } 16 | 17 | public function xml(): string 18 | { 19 | return $this->xml; 20 | } 21 | } 22 | -------------------------------------------------------------------------------- /src/Services/Cancel/AcceptRejectSignatureResult.php: -------------------------------------------------------------------------------- 1 | findInDescendent($data, $container, 'aceptacion'); 23 | $rechazo = $this->findInDescendent($data, $container, 'rechazo'); 24 | $this->uuids = new AcceptRejectUuidList( 25 | array_merge( 26 | is_array($aceptacion) ? $aceptacion : [], 27 | is_array($rechazo) ? $rechazo : [] 28 | ) 29 | ); 30 | $this->error = $this->get('error'); 31 | } 32 | 33 | public function uuids(): AcceptRejectUuidList 34 | { 35 | return $this->uuids; 36 | } 37 | 38 | public function error(): string 39 | { 40 | return $this->error; 41 | } 42 | } 43 | -------------------------------------------------------------------------------- /src/Services/Cancel/AcceptRejectSignatureService.php: -------------------------------------------------------------------------------- 1 | settings = $settings; 18 | } 19 | 20 | public function settings(): FinkokSettings 21 | { 22 | return $this->settings; 23 | } 24 | 25 | public function acceptRejectSignature(AcceptRejectSignatureCommand $command): AcceptRejectSignatureResult 26 | { 27 | $soapCaller = $this->settings()->createCallerForService(Services::cancel()); 28 | $rawResponse = $soapCaller->call('accept_reject_signature', [ 29 | 'xml' => $command->xml(), 30 | ]); 31 | return new AcceptRejectSignatureResult($rawResponse); 32 | } 33 | } 34 | -------------------------------------------------------------------------------- /src/Services/Cancel/AcceptRejectUuidItem.php: -------------------------------------------------------------------------------- 1 | uuid = $uuid; 23 | $this->status = $status; 24 | $this->answer = $answer; 25 | } 26 | 27 | public function uuid(): string 28 | { 29 | return $this->uuid; 30 | } 31 | 32 | public function status(): AcceptRejectUuidStatus 33 | { 34 | return $this->status; 35 | } 36 | 37 | public function answer(): CancelAnswer 38 | { 39 | return $this->answer; 40 | } 41 | } 42 | -------------------------------------------------------------------------------- /src/Services/Cancel/AcceptRejectUuidList.php: -------------------------------------------------------------------------------- 1 | 18 | */ 19 | class AcceptRejectUuidList extends AbstractCollection 20 | { 21 | public function findByUuidOrFail(string $uuid): AcceptRejectUuidItem 22 | { 23 | $found = $this->findByUuid($uuid); 24 | if (null === $found) { 25 | throw new OutOfRangeException(sprintf('UUID %s not found on result', $uuid)); 26 | } 27 | return $found; 28 | } 29 | 30 | public function findByUuid(string $uuid): ?AcceptRejectUuidItem 31 | { 32 | foreach ($this->getIterator() as $item) { 33 | if (0 === strcasecmp($item->uuid(), $uuid)) { 34 | return $item; 35 | } 36 | } 37 | return null; 38 | } 39 | 40 | protected function createItemFromStdClass(stdClass $content): object 41 | { 42 | if (isset($content->{'Acepta'})) { 43 | $source = $content->{'Acepta'}; 44 | $answer = CancelAnswer::accept(); 45 | } elseif (isset($content->{'Rechaza'})) { 46 | $source = $content->{'Rechaza'}; 47 | $answer = CancelAnswer::reject(); 48 | } else { 49 | $source = (object)[]; 50 | $answer = CancelAnswer::accept(); 51 | } 52 | return new AcceptRejectUuidItem( 53 | strval($source->uuid ?? ''), 54 | new AcceptRejectUuidStatus($source->status ?? '0'), 55 | $answer 56 | ); 57 | } 58 | } 59 | -------------------------------------------------------------------------------- /src/Services/Cancel/AcceptRejectUuidStatus.php: -------------------------------------------------------------------------------- 1 | 11 | */ 12 | final class AcceptRejectUuidStatus extends MicroCatalog 13 | { 14 | public static function getEntriesArray(): array 15 | { 16 | return [ 17 | '1000' => 'Se recibió la respuesta de la petición de forma exitosa', 18 | '1001' => 'No existen peticiones de cancelación en espera de respuesta para el UUID', 19 | '1002' => 'Ya se recibió una respuesta para la petición de cancelación del UUID', 20 | '1003' => 'El sello no corresponde al RFC receptor', 21 | '1004' => 'Existen más de una petición de cancelación para el mismo UUID', 22 | '1005' => 'El UUID es nulo o no posee el formato correcto', 23 | '1006' => 'Se rebasó el número máximo de solicitudes permitidas', 24 | ]; 25 | } 26 | 27 | public function getEntryValueOnUndefined(): string 28 | { 29 | return 'Respuesta del SAT desconocida'; 30 | } 31 | 32 | public function getCode(): string 33 | { 34 | return $this->getEntryId(); 35 | } 36 | 37 | public function getMessage(): string 38 | { 39 | return strval($this->getEntryValue()); 40 | } 41 | 42 | public function isSuccess(): bool 43 | { 44 | return ('1000' === $this->getCode()); 45 | } 46 | } 47 | -------------------------------------------------------------------------------- /src/Services/Cancel/CancelSignatureCommand.php: -------------------------------------------------------------------------------- 1 | xml = $xml; 26 | $this->storePending = $storePending ?? CancelStorePending::no(); 27 | } 28 | 29 | public function xml(): string 30 | { 31 | return $this->xml; 32 | } 33 | 34 | public function storePending(): CancelStorePending 35 | { 36 | return $this->storePending; 37 | } 38 | } 39 | -------------------------------------------------------------------------------- /src/Services/Cancel/CancelSignatureResult.php: -------------------------------------------------------------------------------- 1 | findInDescendent($data, 'cancel_signatureResult', 'Folios', 'Folio'); 19 | $this->documents = new CancelledDocuments(is_array($documents) ? $documents : []); 20 | } 21 | 22 | public function documents(): CancelledDocuments 23 | { 24 | return $this->documents; 25 | } 26 | 27 | public function voucher(): string 28 | { 29 | return $this->get('Acuse'); 30 | } 31 | 32 | public function date(): string 33 | { 34 | return $this->get('Fecha'); 35 | } 36 | 37 | public function rfc(): string 38 | { 39 | return $this->get('RfcEmisor'); 40 | } 41 | 42 | public function statusCode(): string 43 | { 44 | return $this->get('CodEstatus'); 45 | } 46 | } 47 | -------------------------------------------------------------------------------- /src/Services/Cancel/CancelSignatureService.php: -------------------------------------------------------------------------------- 1 | settings = $settings; 18 | } 19 | 20 | public function settings(): FinkokSettings 21 | { 22 | return $this->settings; 23 | } 24 | 25 | public function cancelSignature(CancelSignatureCommand $command): CancelSignatureResult 26 | { 27 | $soapCaller = $this->settings()->createCallerForService(Services::cancel()); 28 | $rawResponse = $soapCaller->call('cancel_signature', [ 29 | 'xml' => $command->xml(), 30 | 'store_pending' => $command->storePending()->asBool(), 31 | ]); 32 | return new CancelSignatureResult($rawResponse); 33 | } 34 | } 35 | -------------------------------------------------------------------------------- /src/Services/Cancel/CancelledDocument.php: -------------------------------------------------------------------------------- 1 | data = $raw; 17 | } 18 | 19 | private function get(string $keyword): string 20 | { 21 | return strval($this->data->{$keyword} ?? ''); 22 | } 23 | 24 | public function uuid(): string 25 | { 26 | return $this->get('UUID'); 27 | } 28 | 29 | public function documentStatus(): string 30 | { 31 | return $this->get('EstatusUUID'); 32 | } 33 | 34 | public function cancellationStatus(): string 35 | { 36 | return $this->get('EstatusCancelacion'); 37 | } 38 | 39 | /** @return array */ 40 | public function values(): array 41 | { 42 | return (array) $this->data; 43 | } 44 | } 45 | -------------------------------------------------------------------------------- /src/Services/Cancel/CancelledDocuments.php: -------------------------------------------------------------------------------- 1 | 16 | */ 17 | class CancelledDocuments extends AbstractCollection 18 | { 19 | protected function createItemFromStdClass(stdClass $content): object 20 | { 21 | return new CancelledDocument($content); 22 | } 23 | 24 | public function find(string $uuid): ?CancelledDocument 25 | { 26 | foreach ($this->getIterator() as $document) { 27 | if ($uuid === $document->uuid()) { 28 | return $document; 29 | } 30 | } 31 | return null; 32 | } 33 | } 34 | -------------------------------------------------------------------------------- /src/Services/Cancel/GetPendingCommand.php: -------------------------------------------------------------------------------- 1 | rfc = $rfc; 15 | } 16 | 17 | public function rfc(): string 18 | { 19 | return $this->rfc; 20 | } 21 | } 22 | -------------------------------------------------------------------------------- /src/Services/Cancel/GetPendingResult.php: -------------------------------------------------------------------------------- 1 | findInDescendent($data, 'get_pendingResult', 'uuids', 'string') ?? []; 19 | if (! is_array($items)) { 20 | $items = []; 21 | } 22 | $this->uuids = $items; 23 | } 24 | 25 | /** @return string[] */ 26 | public function uuids(): array 27 | { 28 | return $this->uuids; 29 | } 30 | 31 | public function error(): string 32 | { 33 | return $this->get('error'); 34 | } 35 | } 36 | -------------------------------------------------------------------------------- /src/Services/Cancel/GetPendingService.php: -------------------------------------------------------------------------------- 1 | settings = $settings; 18 | } 19 | 20 | public function settings(): FinkokSettings 21 | { 22 | return $this->settings; 23 | } 24 | 25 | public function obtainPending(GetPendingCommand $command): GetPendingResult 26 | { 27 | $soapCaller = $this->settings()->createCallerForService(Services::cancel()); 28 | $rawResponse = $soapCaller->call('get_pending', [ 29 | 'rtaxpayer_id' => $command->rfc(), 30 | ]); 31 | return new GetPendingResult($rawResponse); 32 | } 33 | } 34 | -------------------------------------------------------------------------------- /src/Services/Cancel/GetReceiptCommand.php: -------------------------------------------------------------------------------- 1 | rfc = $rfc; 23 | $this->uuid = $uuid; 24 | $this->type = $type; 25 | } 26 | 27 | public function uuid(): string 28 | { 29 | return $this->uuid; 30 | } 31 | 32 | public function rfc(): string 33 | { 34 | return $this->rfc; 35 | } 36 | 37 | public function type(): ReceiptType 38 | { 39 | return $this->type; 40 | } 41 | } 42 | -------------------------------------------------------------------------------- /src/Services/Cancel/GetReceiptResult.php: -------------------------------------------------------------------------------- 1 | get('success')); 20 | } 21 | 22 | public function uuid(): string 23 | { 24 | return $this->get('uuid'); 25 | } 26 | 27 | public function receipt(): string 28 | { 29 | return $this->get('receipt'); 30 | } 31 | 32 | public function rfc(): string 33 | { 34 | return $this->get('taxpayer_id'); 35 | } 36 | 37 | public function error(): string 38 | { 39 | return $this->get('error'); 40 | } 41 | 42 | public function date(): string 43 | { 44 | return $this->get('date'); 45 | } 46 | } 47 | -------------------------------------------------------------------------------- /src/Services/Cancel/GetReceiptService.php: -------------------------------------------------------------------------------- 1 | settings = $settings; 18 | } 19 | 20 | public function settings(): FinkokSettings 21 | { 22 | return $this->settings; 23 | } 24 | 25 | public function download(GetReceiptCommand $command): GetReceiptResult 26 | { 27 | $soapCaller = $this->settings()->createCallerForService(Services::cancel()); 28 | $rawResponse = $soapCaller->call('get_receipt', [ 29 | 'taxpayer_id' => $command->rfc(), 30 | 'uuid' => $command->uuid(), 31 | 'type' => $command->type()->value(), 32 | ]); 33 | return new GetReceiptResult($rawResponse); 34 | } 35 | } 36 | -------------------------------------------------------------------------------- /src/Services/Cancel/GetRelatedSignatureCommand.php: -------------------------------------------------------------------------------- 1 | xml = $xml; 15 | } 16 | 17 | public function xml(): string 18 | { 19 | return $this->xml; 20 | } 21 | } 22 | -------------------------------------------------------------------------------- /src/Services/Cancel/GetRelatedSignatureResult.php: -------------------------------------------------------------------------------- 1 | findInDescendent($data, $container, 'Padres', 'Padre'); 26 | $this->parents = new RelatedItems(is_array($parents) ? $parents : []); 27 | $children = $this->findInDescendent($data, $container, 'Hijos', 'Hijo'); 28 | $this->children = new RelatedItems(is_array($children) ? $children : []); 29 | $this->error = $this->get('error'); 30 | } 31 | 32 | public function parents(): RelatedItems 33 | { 34 | return $this->parents; 35 | } 36 | 37 | public function children(): RelatedItems 38 | { 39 | return $this->children; 40 | } 41 | 42 | public function error(): string 43 | { 44 | return $this->error; 45 | } 46 | } 47 | -------------------------------------------------------------------------------- /src/Services/Cancel/GetRelatedSignatureService.php: -------------------------------------------------------------------------------- 1 | settings = $settings; 18 | } 19 | 20 | public function settings(): FinkokSettings 21 | { 22 | return $this->settings; 23 | } 24 | 25 | public function getRelatedSignature(GetRelatedSignatureCommand $command): GetRelatedSignatureResult 26 | { 27 | $soapCaller = $this->settings()->createCallerForService(Services::cancel()); 28 | $rawResponse = $soapCaller->call('get_related_signature', [ 29 | 'xml' => $command->xml(), 30 | ]); 31 | return new GetRelatedSignatureResult($rawResponse); 32 | } 33 | } 34 | -------------------------------------------------------------------------------- /src/Services/Cancel/GetSatStatusCommand.php: -------------------------------------------------------------------------------- 1 | rfcIssuer = $rfcIssuer; 24 | $this->rfcRecipient = $rfcRecipient; 25 | $this->uuid = $uuid; 26 | $this->total = $total; 27 | } 28 | 29 | public function rfcIssuer(): string 30 | { 31 | return $this->rfcIssuer; 32 | } 33 | 34 | public function rfcRecipient(): string 35 | { 36 | return $this->rfcRecipient; 37 | } 38 | 39 | public function uuid(): string 40 | { 41 | return $this->uuid; 42 | } 43 | 44 | public function total(): string 45 | { 46 | return $this->total; 47 | } 48 | } 49 | -------------------------------------------------------------------------------- /src/Services/Cancel/GetSatStatusResult.php: -------------------------------------------------------------------------------- 1 | get('CodigoEstatus'); 20 | } 21 | 22 | public function cfdi(): string 23 | { 24 | return $this->get('Estado'); 25 | } 26 | 27 | public function cancellable(): string 28 | { 29 | return $this->get('EsCancelable'); 30 | } 31 | 32 | public function cancellation(): string 33 | { 34 | return $this->get('EstatusCancelacion'); 35 | } 36 | } 37 | -------------------------------------------------------------------------------- /src/Services/Cancel/GetSatStatusService.php: -------------------------------------------------------------------------------- 1 | settings = $settings; 18 | } 19 | 20 | public function settings(): FinkokSettings 21 | { 22 | return $this->settings; 23 | } 24 | 25 | public function query(GetSatStatusCommand $command): GetSatStatusResult 26 | { 27 | $soapCaller = $this->settings()->createCallerForService(Services::cancel()); 28 | $rawResponse = $soapCaller->call('get_sat_status', [ 29 | 'taxpayer_id' => $command->rfcIssuer(), 30 | 'rtaxpayer_id' => $command->rfcRecipient(), 31 | 'uuid' => $command->uuid(), 32 | 'total' => $command->total(), 33 | ]); 34 | return new GetSatStatusResult($rawResponse); 35 | } 36 | 37 | public function queryUntilFoundOrTime(GetSatStatusCommand $command, int $waitSeconds = 120): GetSatStatusResult 38 | { 39 | $runUntilTime = time() + $waitSeconds; 40 | do { 41 | $result = $this->query($command); 42 | if ('No Encontrado' === $result->cfdi() && time() <= $runUntilTime) { 43 | usleep(200000); 44 | continue; 45 | } 46 | break; 47 | } while (true); 48 | return $result; 49 | } 50 | } 51 | -------------------------------------------------------------------------------- /src/Services/Cancel/RelatedItem.php: -------------------------------------------------------------------------------- 1 | uuid = $uuid; 21 | $this->rfcEmitter = $rfcEmitter; 22 | $this->rfcReceiver = $rfcReceiver; 23 | } 24 | 25 | public function uuid(): string 26 | { 27 | return $this->uuid; 28 | } 29 | 30 | public function rfcEmitter(): string 31 | { 32 | return $this->rfcEmitter; 33 | } 34 | 35 | public function rfcReceiver(): string 36 | { 37 | return $this->rfcReceiver; 38 | } 39 | } 40 | -------------------------------------------------------------------------------- /src/Services/Cancel/RelatedItems.php: -------------------------------------------------------------------------------- 1 | 16 | */ 17 | class RelatedItems extends AbstractCollection 18 | { 19 | protected function createItemFromStdClass(stdClass $content): object 20 | { 21 | return new RelatedItem( 22 | strval($content->{'uuid'} ?? ''), 23 | strval($content->{'emisor'} ?? ''), 24 | strval($content->{'receptor'} ?? '') 25 | ); 26 | } 27 | } 28 | -------------------------------------------------------------------------------- /src/Services/Manifest/GetContractsCommand.php: -------------------------------------------------------------------------------- 1 | rfc = $rfc; 27 | $this->name = $name; 28 | $this->address = $address; 29 | $this->email = $email; 30 | $this->snid = $snid; 31 | } 32 | 33 | public function rfc(): string 34 | { 35 | return $this->rfc; 36 | } 37 | 38 | public function name(): string 39 | { 40 | return $this->name; 41 | } 42 | 43 | public function address(): string 44 | { 45 | return $this->address; 46 | } 47 | 48 | public function email(): string 49 | { 50 | return $this->email; 51 | } 52 | 53 | public function snid(): string 54 | { 55 | return $this->snid; 56 | } 57 | } 58 | -------------------------------------------------------------------------------- /src/Services/Manifest/GetContractsResult.php: -------------------------------------------------------------------------------- 1 | (object) [ 21 | 'success' => $success, 22 | 'contract' => base64_encode($contract), 23 | 'privacy' => base64_encode($privacy), 24 | 'error' => $error, 25 | ], 26 | ]); 27 | } 28 | 29 | public function success(): bool 30 | { 31 | return boolval($this->get('success')); 32 | } 33 | 34 | public function contract(): string 35 | { 36 | return base64_decode($this->get('contract'), true) ?: ''; 37 | } 38 | 39 | public function privacy(): string 40 | { 41 | return base64_decode($this->get('privacy'), true) ?: ''; 42 | } 43 | 44 | public function error(): string 45 | { 46 | return $this->get('error'); 47 | } 48 | } 49 | -------------------------------------------------------------------------------- /src/Services/Manifest/GetContractsService.php: -------------------------------------------------------------------------------- 1 | settings = $settings; 18 | } 19 | 20 | public function settings(): FinkokSettings 21 | { 22 | return $this->settings; 23 | } 24 | 25 | public function obtainContracts(GetContractsCommand $command): GetContractsResult 26 | { 27 | // this empty string are for ommiting sending username and password 28 | $soapCaller = $this->settings()->createCallerForService(Services::manifest(), '', ''); 29 | $rawResponse = $soapCaller->call('get_contracts_snid', [ 30 | 'snid' => $command->snid(), 31 | 'taxpayer_id' => $command->rfc(), 32 | 'name' => $command->name(), 33 | 'address' => $command->address(), 34 | 'email' => $command->email(), 35 | ]); 36 | return new GetContractsResult($rawResponse); 37 | } 38 | } 39 | -------------------------------------------------------------------------------- /src/Services/Manifest/GetSignedContractsCommand.php: -------------------------------------------------------------------------------- 1 | snid = $snid; 23 | $this->rfc = $rfc; 24 | $this->format = $format; 25 | } 26 | 27 | public function snid(): string 28 | { 29 | return $this->snid; 30 | } 31 | 32 | public function rfc(): string 33 | { 34 | return $this->rfc; 35 | } 36 | 37 | public function format(): SignedDocumentFormat 38 | { 39 | return $this->format; 40 | } 41 | } 42 | -------------------------------------------------------------------------------- /src/Services/Manifest/GetSignedContractsResult.php: -------------------------------------------------------------------------------- 1 | success = boolval($this->get('success')); 28 | $this->contract = $this->get('contract'); 29 | $this->privacy = $this->get('privacy'); 30 | $this->error = $this->get('error'); 31 | if ($isBase64) { 32 | $this->contract = base64_decode($this->contract) ?: ''; 33 | $this->privacy = base64_decode($this->privacy) ?: ''; 34 | } 35 | } 36 | 37 | public function success(): bool 38 | { 39 | return $this->success; 40 | } 41 | 42 | public function contract(): string 43 | { 44 | return $this->contract; 45 | } 46 | 47 | public function privacy(): string 48 | { 49 | return $this->privacy; 50 | } 51 | 52 | public function error(): string 53 | { 54 | return $this->error; 55 | } 56 | } 57 | -------------------------------------------------------------------------------- /src/Services/Manifest/GetSignedContractsService.php: -------------------------------------------------------------------------------- 1 | settings = $settings; 18 | } 19 | 20 | public function settings(): FinkokSettings 21 | { 22 | return $this->settings; 23 | } 24 | 25 | public function getSignedContracts(GetSignedContractsCommand $command): GetSignedContractsResult 26 | { 27 | // this empty string are for ommiting sending username and password 28 | $soapCaller = $this->settings()->createCallerForService(Services::manifest(), '', ''); 29 | $rawResponse = $soapCaller->call('get_documents', [ 30 | 'snid' => $command->snid(), 31 | 'taxpayer_id' => $command->rfc(), 32 | 'type' => $command->format()->value(), 33 | ]); 34 | return new GetSignedContractsResult($rawResponse, $command->format()->isPdf()); 35 | } 36 | } 37 | -------------------------------------------------------------------------------- /src/Services/Manifest/SignContractsCommand.php: -------------------------------------------------------------------------------- 1 | snid = $snid; 21 | $this->privacy = $privacy; 22 | $this->contract = $contract; 23 | } 24 | 25 | public function snid(): string 26 | { 27 | return $this->snid; 28 | } 29 | 30 | public function privacy(): string 31 | { 32 | return $this->privacy; 33 | } 34 | 35 | public function contract(): string 36 | { 37 | return $this->contract; 38 | } 39 | } 40 | -------------------------------------------------------------------------------- /src/Services/Manifest/SignContractsResult.php: -------------------------------------------------------------------------------- 1 | (object) [ 21 | 'success' => $success, 22 | 'message' => $message, 23 | ], 24 | ]); 25 | } 26 | 27 | public function success(): bool 28 | { 29 | return boolval($this->get('success')); 30 | } 31 | 32 | public function message(): string 33 | { 34 | return $this->get('message'); 35 | } 36 | } 37 | -------------------------------------------------------------------------------- /src/Services/Manifest/SignContractsService.php: -------------------------------------------------------------------------------- 1 | settings = $settings; 18 | } 19 | 20 | public function settings(): FinkokSettings 21 | { 22 | return $this->settings; 23 | } 24 | 25 | public function sendSignedContracts(SignContractsCommand $command): SignContractsResult 26 | { 27 | // this empty string are for ommiting sending username and password 28 | $soapCaller = $this->settings()->createCallerForService(Services::manifest(), '', ''); 29 | $rawResponse = $soapCaller->call('sign_contract', [ 30 | 'snid' => $command->snid(), 31 | 'privacy_xml' => $command->privacy(), 32 | 'contract_xml' => $command->contract(), 33 | ]); 34 | return new SignContractsResult($rawResponse); 35 | } 36 | } 37 | -------------------------------------------------------------------------------- /src/Services/Registration/AddCommand.php: -------------------------------------------------------------------------------- 1 | rfc = $rfc; 32 | $this->type = $type ?? CustomerType::ondemand(); 33 | $this->certificate = $certificate; 34 | $this->privateKey = $privateKey; 35 | $this->passPhrase = $passPhrase; 36 | } 37 | 38 | public function rfc(): string 39 | { 40 | return $this->rfc; 41 | } 42 | 43 | public function type(): CustomerType 44 | { 45 | return $this->type; 46 | } 47 | 48 | public function certificate(): string 49 | { 50 | return $this->certificate; 51 | } 52 | 53 | public function privateKey(): string 54 | { 55 | return $this->privateKey; 56 | } 57 | 58 | public function passPhrase(): string 59 | { 60 | return $this->passPhrase; 61 | } 62 | } 63 | -------------------------------------------------------------------------------- /src/Services/Registration/AddResult.php: -------------------------------------------------------------------------------- 1 | get('success')); 20 | } 21 | 22 | public function message(): string 23 | { 24 | return $this->get('message'); 25 | } 26 | } 27 | -------------------------------------------------------------------------------- /src/Services/Registration/AddService.php: -------------------------------------------------------------------------------- 1 | settings = $settings; 18 | } 19 | 20 | public function settings(): FinkokSettings 21 | { 22 | return $this->settings; 23 | } 24 | 25 | public function add(AddCommand $command): AddResult 26 | { 27 | $soapCaller = $this->settings()->createCallerForService( 28 | Services::registration(), 29 | 'reseller_username', 30 | 'reseller_password' 31 | ); 32 | $rawResponse = $soapCaller->call('add', array_filter([ 33 | 'taxpayer_id' => $command->rfc(), 34 | 'type_user' => $command->type()->value(), 35 | 'cer' => $command->certificate(), 36 | 'key' => $command->privateKey(), 37 | 'passphrase' => $command->passPhrase(), 38 | ])); 39 | return new AddResult($rawResponse); 40 | } 41 | } 42 | -------------------------------------------------------------------------------- /src/Services/Registration/AssignCommand.php: -------------------------------------------------------------------------------- 1 | rfc = $rfc; 18 | $this->credit = $credit; 19 | } 20 | 21 | public function rfc(): string 22 | { 23 | return $this->rfc; 24 | } 25 | 26 | public function credit(): int 27 | { 28 | return $this->credit; 29 | } 30 | } 31 | -------------------------------------------------------------------------------- /src/Services/Registration/AssignResult.php: -------------------------------------------------------------------------------- 1 | get('success')); 20 | } 21 | 22 | public function credit(): int 23 | { 24 | return intval($this->get('credit')); 25 | } 26 | 27 | public function message(): string 28 | { 29 | return $this->get('message'); 30 | } 31 | } 32 | -------------------------------------------------------------------------------- /src/Services/Registration/AssignService.php: -------------------------------------------------------------------------------- 1 | settings = $settings; 18 | } 19 | 20 | public function settings(): FinkokSettings 21 | { 22 | return $this->settings; 23 | } 24 | 25 | public function assign(AssignCommand $command): AssignResult 26 | { 27 | $soapCaller = $this->settings()->createCallerForService(Services::registration()); 28 | $rawResponse = $soapCaller->call('assign', [ 29 | 'taxpayer_id' => $command->rfc(), 30 | 'credit' => $command->credit(), 31 | ]); 32 | return new AssignResult($rawResponse); 33 | } 34 | } 35 | -------------------------------------------------------------------------------- /src/Services/Registration/Customer.php: -------------------------------------------------------------------------------- 1 | data = $raw; 23 | $rawStatus = $this->get('status'); 24 | if (in_array($rawStatus, CustomerStatus::toArray())) { 25 | $this->status = new CustomerStatus($rawStatus); 26 | } else { 27 | $this->status = CustomerStatus::suspended(); 28 | } 29 | $this->type = (-1 === $this->credit()) ? CustomerType::ondemand() : CustomerType::prepaid(); 30 | } 31 | 32 | private function get(string $keyword): string 33 | { 34 | return strval($this->data->{$keyword} ?? ''); 35 | } 36 | 37 | public function status(): CustomerStatus 38 | { 39 | return $this->status; 40 | } 41 | 42 | public function counter(): int 43 | { 44 | return intval($this->get('counter')); 45 | } 46 | 47 | public function rfc(): string 48 | { 49 | return $this->get('taxpayer_id'); 50 | } 51 | 52 | public function credit(): int 53 | { 54 | return intval($this->get('credit')); 55 | } 56 | 57 | public function customerType(): CustomerType 58 | { 59 | return $this->type; 60 | } 61 | 62 | /** @return array */ 63 | public function values(): array 64 | { 65 | return (array) $this->data; 66 | } 67 | } 68 | -------------------------------------------------------------------------------- /src/Services/Registration/CustomerStatus.php: -------------------------------------------------------------------------------- 1 | 'A', 26 | 'suspended' => 'S', 27 | ]; 28 | } 29 | } 30 | -------------------------------------------------------------------------------- /src/Services/Registration/CustomerType.php: -------------------------------------------------------------------------------- 1 | 'O', 26 | 'prepaid' => 'P', 27 | ]; 28 | } 29 | } 30 | -------------------------------------------------------------------------------- /src/Services/Registration/Customers.php: -------------------------------------------------------------------------------- 1 | 17 | */ 18 | class Customers extends AbstractCollection 19 | { 20 | protected function createItemFromStdClass(stdClass $content): object 21 | { 22 | return new Customer($content); 23 | } 24 | 25 | public function getByRfc(string $rfc): Customer 26 | { 27 | $customer = $this->findByRfc($rfc); 28 | if (null === $customer) { 29 | throw new LogicException(sprintf('There is no customer with RFC %s', $rfc)); 30 | } 31 | return $customer; 32 | } 33 | 34 | public function findByRfc(string $rfc): ?Customer 35 | { 36 | foreach ($this->getIterator() as $customer) { 37 | if ($rfc === $customer->rfc()) { 38 | return $customer; 39 | } 40 | } 41 | return null; 42 | } 43 | 44 | public function merge(self $customers): self 45 | { 46 | $clone = clone $this; 47 | foreach ($customers as $customer) { 48 | $clone->collection->append($customer); 49 | } 50 | return $clone; 51 | } 52 | } 53 | -------------------------------------------------------------------------------- /src/Services/Registration/EditCommand.php: -------------------------------------------------------------------------------- 1 | rfc = $rfc; 32 | $this->status = $status; 33 | $this->certificate = $certificate; 34 | $this->privateKey = $privateKey; 35 | $this->passPhrase = $passPhrase; 36 | } 37 | 38 | public function rfc(): string 39 | { 40 | return $this->rfc; 41 | } 42 | 43 | public function status(): CustomerStatus 44 | { 45 | return $this->status; 46 | } 47 | 48 | public function certificate(): string 49 | { 50 | return $this->certificate; 51 | } 52 | 53 | public function privateKey(): string 54 | { 55 | return $this->privateKey; 56 | } 57 | 58 | public function passPhrase(): string 59 | { 60 | return $this->passPhrase; 61 | } 62 | } 63 | -------------------------------------------------------------------------------- /src/Services/Registration/EditResult.php: -------------------------------------------------------------------------------- 1 | get('success')); 20 | } 21 | 22 | public function message(): string 23 | { 24 | return $this->get('message'); 25 | } 26 | } 27 | -------------------------------------------------------------------------------- /src/Services/Registration/EditService.php: -------------------------------------------------------------------------------- 1 | settings = $settings; 18 | } 19 | 20 | public function settings(): FinkokSettings 21 | { 22 | return $this->settings; 23 | } 24 | 25 | public function edit(EditCommand $command): EditResult 26 | { 27 | $soapCaller = $this->settings()->createCallerForService( 28 | Services::registration(), 29 | 'reseller_username', 30 | 'reseller_password' 31 | ); 32 | $rawResponse = $soapCaller->call('edit', array_filter([ 33 | 'taxpayer_id' => $command->rfc(), 34 | 'status' => $command->status()->value(), 35 | 'cer' => $command->certificate(), 36 | 'key' => $command->privateKey(), 37 | 'passphrase' => $command->passPhrase(), 38 | ])); 39 | return new EditResult($rawResponse); 40 | } 41 | } 42 | -------------------------------------------------------------------------------- /src/Services/Registration/ObtainCommand.php: -------------------------------------------------------------------------------- 1 | rfc = $rfc; 21 | } 22 | 23 | public function rfc(): string 24 | { 25 | return $this->rfc; 26 | } 27 | } 28 | -------------------------------------------------------------------------------- /src/Services/Registration/ObtainCustomersCommand.php: -------------------------------------------------------------------------------- 1 | page = $page; 15 | } 16 | 17 | public function page(): int 18 | { 19 | return $this->page; 20 | } 21 | } 22 | -------------------------------------------------------------------------------- /src/Services/Registration/ObtainCustomersResult.php: -------------------------------------------------------------------------------- 1 | findInDescendent($data, 'customersResult', 'users', 'ResellerUser'); 19 | $this->customers = new Customers(is_array($customers) ? $customers : []); 20 | } 21 | 22 | public function message(): string 23 | { 24 | return $this->get('message'); 25 | } 26 | 27 | public function customers(): Customers 28 | { 29 | return $this->customers; 30 | } 31 | 32 | public function pagesInformation(): PageInformation 33 | { 34 | $message = $this->message(); 35 | return PageInformation::fromMessage($message); 36 | } 37 | } 38 | -------------------------------------------------------------------------------- /src/Services/Registration/ObtainCustomersService.php: -------------------------------------------------------------------------------- 1 | settings = $settings; 18 | } 19 | 20 | public function settings(): FinkokSettings 21 | { 22 | return $this->settings; 23 | } 24 | 25 | public function obtainAll(): Customers 26 | { 27 | $page = 0; 28 | $result = new Customers([]); 29 | 30 | do { 31 | $page = $page + 1; 32 | $command = new ObtainCustomersCommand($page); 33 | $current = $this->obtainPage($command); 34 | $result = $result->merge($current->customers()); 35 | } while ($current->pagesInformation()->hasMorePages()); 36 | 37 | return $result; 38 | } 39 | 40 | public function obtainPage(ObtainCustomersCommand $command): ObtainCustomersResult 41 | { 42 | $soapCaller = $this->settings()->createCallerForService( 43 | Services::registration(), 44 | 'username', 45 | 'password' 46 | ); 47 | $rawResponse = $soapCaller->call('customers', [ 48 | 'page' => $command->page(), 49 | ]); 50 | return new ObtainCustomersResult($rawResponse); 51 | } 52 | } 53 | -------------------------------------------------------------------------------- /src/Services/Registration/ObtainResult.php: -------------------------------------------------------------------------------- 1 | findInDescendent($data, 'getResult', 'users', 'ResellerUser'); 19 | $this->customers = new Customers(is_array($customers) ? $customers : []); 20 | } 21 | 22 | public function message(): string 23 | { 24 | return $this->get('message'); 25 | } 26 | 27 | public function customers(): Customers 28 | { 29 | return $this->customers; 30 | } 31 | } 32 | -------------------------------------------------------------------------------- /src/Services/Registration/ObtainService.php: -------------------------------------------------------------------------------- 1 | settings = $settings; 18 | } 19 | 20 | public function settings(): FinkokSettings 21 | { 22 | return $this->settings; 23 | } 24 | 25 | public function obtain(ObtainCommand $command): ObtainResult 26 | { 27 | $soapCaller = $this->settings()->createCallerForService( 28 | Services::registration(), 29 | 'reseller_username', 30 | 'reseller_password' 31 | ); 32 | $rawResponse = $soapCaller->call('get', [ 33 | 'taxpayer_id' => $command->rfc(), 34 | ]); 35 | return new ObtainResult($rawResponse); 36 | } 37 | } 38 | -------------------------------------------------------------------------------- /src/Services/Registration/PageInformation.php: -------------------------------------------------------------------------------- 1 | firstRecord = $firstRecord; 36 | $this->lastRecord = $lastRecord; 37 | $this->totalRecords = $totalRecords; 38 | $this->currentPage = $currentPage; 39 | $this->totalPages = $totalPages; 40 | $this->pageLength = $pageLength; 41 | } 42 | 43 | public static function empty(): self 44 | { 45 | return new self(1, 50, 0, 1, 1, 50); 46 | } 47 | 48 | public static function fromMessage(string $message): self 49 | { 50 | $found = preg_match('/^Showing (?\d+) to (?\d+) of (?\d+) entries$/', $message, $matches); 51 | if (1 !== $found) { 52 | return self::empty(); 53 | } 54 | $first = intval($matches['first']); 55 | $last = intval($matches['last']); 56 | $records = intval($matches['records']); 57 | return self::fromValues($first, $last, $records); 58 | } 59 | 60 | public static function fromValues(int $first, int $last, int $records): self 61 | { 62 | $pageLength = $last - $first + 1; 63 | if (0 === $pageLength) { 64 | return new self($first, $last, $records, 1, 1, $pageLength); 65 | } 66 | $pages = intval(ceil($records / $pageLength)); 67 | $page = intval(round($last / $pageLength, 0)); 68 | 69 | return new self($first, $last, $records, $page, $pages, $pageLength); 70 | } 71 | 72 | public function firstRecord(): int 73 | { 74 | return $this->firstRecord; 75 | } 76 | 77 | public function lastRecord(): int 78 | { 79 | return $this->lastRecord; 80 | } 81 | 82 | public function totalRecords(): int 83 | { 84 | return $this->totalRecords; 85 | } 86 | 87 | public function currentPage(): int 88 | { 89 | return $this->currentPage; 90 | } 91 | 92 | public function totalPages(): int 93 | { 94 | return $this->totalPages; 95 | } 96 | 97 | public function pageLength(): int 98 | { 99 | return $this->pageLength; 100 | } 101 | 102 | public function hasMorePages(): bool 103 | { 104 | return $this->currentPage < $this->totalPages; 105 | } 106 | } 107 | -------------------------------------------------------------------------------- /src/Services/Registration/SwitchCommand.php: -------------------------------------------------------------------------------- 1 | rfc = $rfc; 18 | $this->customerType = $customerType; 19 | } 20 | 21 | public function rfc(): string 22 | { 23 | return $this->rfc; 24 | } 25 | 26 | public function customerType(): CustomerType 27 | { 28 | return $this->customerType; 29 | } 30 | } 31 | -------------------------------------------------------------------------------- /src/Services/Registration/SwitchResult.php: -------------------------------------------------------------------------------- 1 | get('success')); 20 | } 21 | 22 | public function message(): string 23 | { 24 | return $this->get('message'); 25 | } 26 | } 27 | -------------------------------------------------------------------------------- /src/Services/Registration/SwitchService.php: -------------------------------------------------------------------------------- 1 | settings = $settings; 18 | } 19 | 20 | public function settings(): FinkokSettings 21 | { 22 | return $this->settings; 23 | } 24 | 25 | public function switch(SwitchCommand $command): SwitchResult 26 | { 27 | $soapCaller = $this->settings()->createCallerForService(Services::registration()); 28 | $rawResponse = $soapCaller->call('switch', [ 29 | 'taxpayer_id' => $command->rfc(), 30 | 'type_user' => $command->customerType()->value(), 31 | ]); 32 | return new SwitchResult($rawResponse); 33 | } 34 | } 35 | -------------------------------------------------------------------------------- /src/Services/Retentions/CancelSignatureCommand.php: -------------------------------------------------------------------------------- 1 | get('SeguimientoCancelacion'); 16 | } 17 | } 18 | -------------------------------------------------------------------------------- /src/Services/Retentions/CancelSignatureService.php: -------------------------------------------------------------------------------- 1 | settings = $settings; 18 | } 19 | 20 | public function settings(): FinkokSettings 21 | { 22 | return $this->settings; 23 | } 24 | 25 | public function cancelSignature(CancelSignatureCommand $command): CancelSignatureResult 26 | { 27 | $soapCaller = $this->settings()->createCallerForService(Services::retentions()); 28 | $rawResponse = $soapCaller->call('cancel_signature', [ 29 | 'xml' => $command->xml(), 30 | 'store_pending' => $command->storePending()->asBool(), 31 | ]); 32 | return new CancelSignatureResult($rawResponse); 33 | } 34 | } 35 | -------------------------------------------------------------------------------- /src/Services/Retentions/StampCommand.php: -------------------------------------------------------------------------------- 1 | settings = $settings; 18 | } 19 | 20 | public function settings(): FinkokSettings 21 | { 22 | return $this->settings; 23 | } 24 | 25 | public function stamp(StampCommand $command): StampResult 26 | { 27 | $soapCaller = $this->settings()->createCallerForService(Services::retentions()); 28 | $rawResponse = $soapCaller->call('stamp', [ 29 | 'xml' => $command->xml(), 30 | ]); 31 | return new StampResult($rawResponse); 32 | } 33 | } 34 | -------------------------------------------------------------------------------- /src/Services/Retentions/StampedCommand.php: -------------------------------------------------------------------------------- 1 | settings = $settings; 18 | } 19 | 20 | public function settings(): FinkokSettings 21 | { 22 | return $this->settings; 23 | } 24 | 25 | public function stamped(StampedCommand $command): StampedResult 26 | { 27 | $soapCaller = $this->settings()->createCallerForService(Services::retentions()); 28 | $rawResponse = $soapCaller->call('stamped', [ 29 | 'xml' => $command->xml(), 30 | ]); 31 | return new StampedResult($rawResponse); 32 | } 33 | } 34 | -------------------------------------------------------------------------------- /src/Services/Stamping/QueryPendingCommand.php: -------------------------------------------------------------------------------- 1 | uuid = $uuid; 15 | } 16 | 17 | public function uuid(): string 18 | { 19 | return $this->uuid; 20 | } 21 | } 22 | -------------------------------------------------------------------------------- /src/Services/Stamping/QueryPendingResult.php: -------------------------------------------------------------------------------- 1 | get('status'); 20 | } 21 | 22 | public function xml(): string 23 | { 24 | return $this->get('xml'); 25 | } 26 | 27 | public function uuid(): string 28 | { 29 | return $this->get('uuid'); 30 | } 31 | 32 | public function uuidStatus(): string 33 | { 34 | return $this->get('uuid_status'); 35 | } 36 | 37 | public function nextAttempt(): string 38 | { 39 | return $this->get('next_attempt'); 40 | } 41 | 42 | public function attempts(): string 43 | { 44 | return $this->get('attempts'); 45 | } 46 | 47 | public function error(): string 48 | { 49 | return $this->get('error'); 50 | } 51 | 52 | public function date(): string 53 | { 54 | return $this->get('date'); 55 | } 56 | } 57 | -------------------------------------------------------------------------------- /src/Services/Stamping/QueryPendingService.php: -------------------------------------------------------------------------------- 1 | settings = $settings; 18 | } 19 | 20 | public function settings(): FinkokSettings 21 | { 22 | return $this->settings; 23 | } 24 | 25 | public function queryPending(QueryPendingCommand $command): QueryPendingResult 26 | { 27 | $soapCaller = $this->settings()->createCallerForService(Services::stamping()); 28 | $rawResponse = $soapCaller->call('query_pending', [ 29 | 'uuid' => $command->uuid(), 30 | ]); 31 | return new QueryPendingResult($rawResponse); 32 | } 33 | } 34 | -------------------------------------------------------------------------------- /src/Services/Stamping/QuickStampService.php: -------------------------------------------------------------------------------- 1 | settings = $settings; 18 | } 19 | 20 | public function settings(): FinkokSettings 21 | { 22 | return $this->settings; 23 | } 24 | 25 | public function quickstamp(StampingCommand $command): StampingResult 26 | { 27 | $soapCaller = $this->settings()->createCallerForService(Services::stamping()); 28 | $rawResponse = $soapCaller->call('quick_stamp', [ 29 | 'xml' => $command->xml(), 30 | ]); 31 | return new StampingResult('quick_stampResult', $rawResponse); 32 | } 33 | } 34 | -------------------------------------------------------------------------------- /src/Services/Stamping/StampService.php: -------------------------------------------------------------------------------- 1 | settings = $settings; 18 | } 19 | 20 | public function settings(): FinkokSettings 21 | { 22 | return $this->settings; 23 | } 24 | 25 | public function stamp(StampingCommand $command): StampingResult 26 | { 27 | $soapCaller = $this->settings()->createCallerForService(Services::stamping()); 28 | // Finkok, repeat to fix bad webservice behavior of remote stamp method 29 | // This will not be fixed according to Finkok 30 | do { 31 | $rawResponse = $soapCaller->call('stamp', [ 32 | 'xml' => $command->xml(), 33 | ]); 34 | $result = new StampingResult('stampResult', $rawResponse); 35 | if (null !== $result->alerts()->findByErrorCode('307') && '' === $result->uuid()) { 36 | usleep(200000); // 0.2 seconds 37 | continue; 38 | } 39 | break; 40 | } while (true); 41 | return $result; 42 | } 43 | } 44 | -------------------------------------------------------------------------------- /src/Services/Stamping/StampedService.php: -------------------------------------------------------------------------------- 1 | settings = $settings; 18 | } 19 | 20 | public function settings(): FinkokSettings 21 | { 22 | return $this->settings; 23 | } 24 | 25 | public function stamped(StampingCommand $command): StampingResult 26 | { 27 | $soapCaller = $this->settings()->createCallerForService(Services::stamping()); 28 | $rawResponse = $soapCaller->call('stamped', [ 29 | 'xml' => $command->xml(), 30 | ]); 31 | return new StampingResult('stampedResult', $rawResponse); 32 | } 33 | } 34 | -------------------------------------------------------------------------------- /src/Services/Stamping/StampingAlert.php: -------------------------------------------------------------------------------- 1 | data = $raw; 17 | } 18 | 19 | private function get(string $keyword): string 20 | { 21 | return strval($this->data->{$keyword} ?? ''); 22 | } 23 | 24 | public function id(): string 25 | { 26 | return $this->get('IdIncidencia'); 27 | } 28 | 29 | public function uuid(): string 30 | { 31 | return $this->get('Uuid'); 32 | } 33 | 34 | public function errorCode(): string 35 | { 36 | return $this->get('CodigoError'); 37 | } 38 | 39 | public function workProcessId(): string 40 | { 41 | return $this->get('WorkProcessId'); 42 | } 43 | 44 | public function message(): string 45 | { 46 | return $this->get('MensajeIncidencia'); 47 | } 48 | 49 | public function extraInfo(): string 50 | { 51 | return $this->get('ExtraInfo'); 52 | } 53 | 54 | public function rfc(): string 55 | { 56 | return $this->get('RfcEmisor'); 57 | } 58 | 59 | public function certificatePac(): string 60 | { 61 | return $this->get('NoCertificadoPac'); 62 | } 63 | 64 | public function date(): string 65 | { 66 | return $this->get('FechaRegistro'); 67 | } 68 | 69 | /** @return array */ 70 | public function values(): array 71 | { 72 | return (array) $this->data; 73 | } 74 | } 75 | -------------------------------------------------------------------------------- /src/Services/Stamping/StampingAlerts.php: -------------------------------------------------------------------------------- 1 | 16 | */ 17 | class StampingAlerts extends AbstractCollection 18 | { 19 | protected function createItemFromStdClass(stdClass $content): object 20 | { 21 | return new StampingAlert($content); 22 | } 23 | 24 | public function findByErrorCode(string $errorCode): ?StampingAlert 25 | { 26 | foreach ($this->getIterator() as $alert) { 27 | if ($errorCode === $alert->errorCode()) { 28 | return $alert; 29 | } 30 | } 31 | return null; 32 | } 33 | } 34 | -------------------------------------------------------------------------------- /src/Services/Stamping/StampingCommand.php: -------------------------------------------------------------------------------- 1 | xml = $xml; 15 | } 16 | 17 | public function xml(): string 18 | { 19 | return $this->xml; 20 | } 21 | } 22 | -------------------------------------------------------------------------------- /src/Services/Stamping/StampingResult.php: -------------------------------------------------------------------------------- 1 | findInDescendent($data, $container, 'Incidencias', 'Incidencia'); 19 | $this->alerts = new StampingAlerts(is_array($alerts) ? $alerts : []); 20 | } 21 | 22 | public function xml(): string 23 | { 24 | return $this->get('xml'); 25 | } 26 | 27 | public function uuid(): string 28 | { 29 | return $this->get('UUID'); 30 | } 31 | 32 | public function faultString(): string 33 | { 34 | return $this->get('faultstring'); 35 | } 36 | 37 | public function faultCode(): string 38 | { 39 | return $this->get('faultcode'); 40 | } 41 | 42 | public function date(): string 43 | { 44 | return $this->get('Fecha'); 45 | } 46 | 47 | public function statusCode(): string 48 | { 49 | return $this->get('CodEstatus'); 50 | } 51 | 52 | public function seal(): string 53 | { 54 | return $this->get('SatSeal'); 55 | } 56 | 57 | public function certificateSat(): string 58 | { 59 | return $this->get('NoCertificadoSAT'); 60 | } 61 | 62 | public function alerts(): StampingAlerts 63 | { 64 | return $this->alerts; 65 | } 66 | 67 | public function hasAlerts(): bool 68 | { 69 | return ($this->alerts->count() > 0); 70 | } 71 | } 72 | -------------------------------------------------------------------------------- /src/Services/Utilities/DatetimeCommand.php: -------------------------------------------------------------------------------- 1 | postalCode = $postalCode; 15 | } 16 | 17 | public function postalCode(): string 18 | { 19 | return $this->postalCode; 20 | } 21 | } 22 | -------------------------------------------------------------------------------- /src/Services/Utilities/DatetimeResult.php: -------------------------------------------------------------------------------- 1 | get('datetime'); 20 | } 21 | 22 | public function error(): string 23 | { 24 | return $this->get('error'); 25 | } 26 | } 27 | -------------------------------------------------------------------------------- /src/Services/Utilities/DatetimeService.php: -------------------------------------------------------------------------------- 1 | settings = $settings; 18 | } 19 | 20 | public function settings(): FinkokSettings 21 | { 22 | return $this->settings; 23 | } 24 | 25 | public function datetime(DatetimeCommand $command): DatetimeResult 26 | { 27 | $soapCaller = $this->settings()->createCallerForService(Services::utilities()); 28 | $rawResponse = $soapCaller->call('datetime', array_filter([ 29 | 'zipcode' => $command->postalCode(), 30 | ])); 31 | return new DatetimeResult($rawResponse); 32 | } 33 | } 34 | -------------------------------------------------------------------------------- /src/Services/Utilities/DownloadXmlCommand.php: -------------------------------------------------------------------------------- 1 | uuid = $uuid; 21 | $this->rfc = $rfc; 22 | $this->type = $type; 23 | } 24 | 25 | public function uuid(): string 26 | { 27 | return $this->uuid; 28 | } 29 | 30 | public function rfc(): string 31 | { 32 | return $this->rfc; 33 | } 34 | 35 | public function type(): string 36 | { 37 | return $this->type; 38 | } 39 | } 40 | -------------------------------------------------------------------------------- /src/Services/Utilities/DownloadXmlResult.php: -------------------------------------------------------------------------------- 1 | get('xml'); 20 | } 21 | 22 | public function error(): string 23 | { 24 | return $this->get('error'); 25 | } 26 | } 27 | -------------------------------------------------------------------------------- /src/Services/Utilities/DownloadXmlService.php: -------------------------------------------------------------------------------- 1 | settings = $settings; 18 | } 19 | 20 | public function settings(): FinkokSettings 21 | { 22 | return $this->settings; 23 | } 24 | 25 | public function downloadXml(DownloadXmlCommand $command): DownloadXmlResult 26 | { 27 | $soapCaller = $this->settings()->createCallerForService(Services::utilities()); 28 | do { 29 | $rawResponse = $soapCaller->call('get_xml', [ 30 | 'uuid' => $command->uuid(), 31 | 'taxpayer_id' => $command->rfc(), 32 | 'invoice_type' => $command->type(), 33 | ]); 34 | $result = new DownloadXmlResult($rawResponse); 35 | // Finkok sometimes returns the path to the file instead of content (Ticket #18950) 36 | if ('.xml' === substr($result->xml(), -4)) { 37 | usleep(200000); // 0.2 seconds 38 | continue; 39 | } 40 | break; 41 | } while (true); 42 | return $result; 43 | } 44 | } 45 | -------------------------------------------------------------------------------- /src/Services/Utilities/ReportCreditCommand.php: -------------------------------------------------------------------------------- 1 | rfc = $rfc; 15 | } 16 | 17 | public function rfc(): string 18 | { 19 | return $this->rfc; 20 | } 21 | } 22 | -------------------------------------------------------------------------------- /src/Services/Utilities/ReportCreditResult.php: -------------------------------------------------------------------------------- 1 | */ 13 | private $items; 14 | 15 | public function __construct(stdClass $data) 16 | { 17 | parent::__construct($data, 'report_creditResult'); 18 | $this->items = []; 19 | 20 | $items = $this->findInDescendent($data, 'report_creditResult', 'result', 'ReportTotalCredit'); 21 | if (! is_array($items)) { 22 | $items = []; 23 | } 24 | foreach ($items as $item) { 25 | $this->items[] = [ 26 | 'credit' => strval($item->credit), 27 | 'date' => strval($item->date), 28 | ]; 29 | } 30 | } 31 | 32 | /** @return array */ 33 | public function items(): array 34 | { 35 | return $this->items; 36 | } 37 | 38 | public function error(): string 39 | { 40 | return $this->get('error'); 41 | } 42 | } 43 | -------------------------------------------------------------------------------- /src/Services/Utilities/ReportCreditService.php: -------------------------------------------------------------------------------- 1 | settings = $settings; 18 | } 19 | 20 | public function settings(): FinkokSettings 21 | { 22 | return $this->settings; 23 | } 24 | 25 | public function reportCredit(ReportCreditCommand $command): ReportCreditResult 26 | { 27 | $soapCaller = $this->settings()->createCallerForService(Services::utilities()); 28 | $rawResponse = $soapCaller->call('report_credit', [ 29 | 'taxpayer_id' => $command->rfc(), 30 | ]); 31 | return new ReportCreditResult($rawResponse); 32 | } 33 | } 34 | -------------------------------------------------------------------------------- /src/Services/Utilities/ReportTotalCommand.php: -------------------------------------------------------------------------------- 1 | today(); 47 | $currentYear = intval($today->format('Y')); 48 | $currentPeriod = sprintf('%04d-%02d', $currentYear, intval($today->format('m'))); 49 | 50 | $this->rfc = $rfc; 51 | $this->type = $type; 52 | $this->startYear = $startYear; 53 | $this->startMonth = $startMonth; 54 | $this->endYear = $endYear; 55 | $this->endMonth = $endMonth; 56 | $this->startPeriod = sprintf('%04d-%02d', $this->startYear, $this->startMonth); 57 | $this->endPeriod = sprintf('%04d-%02d', $this->endYear, $this->endMonth); 58 | 59 | if ($startYear < 2000 || $startYear > $currentYear) { 60 | throw new LogicException(sprintf('Start year %d is not between 2000 and %d', $startYear, $currentYear)); 61 | } 62 | if ($endYear < 2000 || $endYear > $currentYear) { 63 | throw new LogicException(sprintf('End year %d is not between 2000 and %d', $endYear, $currentYear)); 64 | } 65 | if ($startMonth < 1 || $startMonth > 12) { 66 | throw new LogicException(sprintf('Start month %d is not between 1 and 12', $startMonth)); 67 | } 68 | if ($endMonth < 1 || $endMonth > 12) { 69 | throw new LogicException(sprintf('End month %d is not between 1 and 12', $endMonth)); 70 | } 71 | 72 | if ($this->startPeriod > $this->endPeriod) { 73 | throw new LogicException( 74 | sprintf('Start period %s cannot be greater than end period %s', $this->startPeriod, $this->endPeriod) 75 | ); 76 | } 77 | 78 | if ($this->startPeriod < $this->endPeriod && $this->endPeriod >= $currentPeriod) { 79 | throw new LogicException('Cannot combine multiple past months with current/future months'); 80 | } 81 | } 82 | 83 | public function rfc(): string 84 | { 85 | return $this->rfc; 86 | } 87 | 88 | public function type(): string 89 | { 90 | return $this->type; 91 | } 92 | 93 | public function startPeriod(): string 94 | { 95 | return $this->startPeriod; 96 | } 97 | 98 | public function endPeriod(): string 99 | { 100 | return $this->endPeriod; 101 | } 102 | 103 | public function startString(): string 104 | { 105 | return sprintf('%04d-%02d-01T00:00:00', $this->startYear, $this->startMonth); 106 | } 107 | 108 | public function endString(): string 109 | { 110 | $date = new DateTimeImmutable(sprintf('%04d-%02d-01', $this->endYear, $this->endMonth)); 111 | $today = $this->today(); 112 | if ($this->endPeriod() === $today->format('Y-m')) { 113 | $date = $today; 114 | } else { 115 | $date = $date->modify('+ 1 month'); 116 | } 117 | return sprintf('%sT00:00:00', $date->format('Y-m-d')); 118 | } 119 | 120 | protected function today(): DateTimeImmutable 121 | { 122 | return new DateTimeImmutable('today'); 123 | } 124 | } 125 | -------------------------------------------------------------------------------- /src/Services/Utilities/ReportTotalResult.php: -------------------------------------------------------------------------------- 1 | findInDescendent($data, 'report_totalResult', 'result', 'ReportTotal') ?? []; 26 | $result = $items[0] ?? (object) []; 27 | $this->rfc = $result->taxpayer_id ?? ''; 28 | $this->total = strval($result->total ?? ''); 29 | $this->error = $this->get('error'); 30 | } 31 | 32 | public function rfc(): string 33 | { 34 | return $this->rfc; 35 | } 36 | 37 | public function total(): string 38 | { 39 | return $this->total; 40 | } 41 | 42 | public function error(): string 43 | { 44 | return $this->error; 45 | } 46 | } 47 | -------------------------------------------------------------------------------- /src/Services/Utilities/ReportTotalService.php: -------------------------------------------------------------------------------- 1 | settings = $settings; 18 | } 19 | 20 | public function settings(): FinkokSettings 21 | { 22 | return $this->settings; 23 | } 24 | 25 | public function reportTotal(ReportTotalCommand $command): ReportTotalResult 26 | { 27 | $soapCaller = $this->settings()->createCallerForService(Services::utilities()); 28 | $rawResponse = $soapCaller->call('report_total', [ 29 | 'taxpayer_id' => $command->rfc(), 30 | 'invoice_type' => $command->type(), 31 | 'date_from' => $command->startString(), 32 | 'date_to' => $command->endString(), 33 | ]); 34 | return new ReportTotalResult($rawResponse); 35 | } 36 | } 37 | -------------------------------------------------------------------------------- /src/Services/Utilities/ReportUuidCommand.php: -------------------------------------------------------------------------------- 1 | $until) { 27 | throw new LogicException('Since date is greater than until date'); 28 | } 29 | $this->rfc = $rfc; 30 | $this->type = $type; 31 | $this->since = $since; 32 | $this->until = $until; 33 | } 34 | 35 | public function rfc(): string 36 | { 37 | return $this->rfc; 38 | } 39 | 40 | public function type(): string 41 | { 42 | return $this->type; 43 | } 44 | 45 | public function since(): DateTimeImmutable 46 | { 47 | return $this->since; 48 | } 49 | 50 | public function until(): DateTimeImmutable 51 | { 52 | return $this->until; 53 | } 54 | 55 | public function sinceString(): string 56 | { 57 | return $this->since->format('Y-m-d\TH:i:s'); 58 | } 59 | 60 | public function untilString(): string 61 | { 62 | return $this->until->format('Y-m-d\TH:i:s'); 63 | } 64 | } 65 | -------------------------------------------------------------------------------- /src/Services/Utilities/ReportUuidResult.php: -------------------------------------------------------------------------------- 1 | */ 13 | private $items; 14 | 15 | public function __construct(stdClass $data) 16 | { 17 | parent::__construct($data, 'report_uuidResult'); 18 | $this->items = []; 19 | 20 | $items = $this->findInDescendent($data, 'report_uuidResult', 'invoices', 'ReportUUID'); 21 | if (! is_array($items)) { 22 | $items = []; 23 | } 24 | foreach ($items as $item) { 25 | $this->items[] = [ 26 | 'date' => strval($item->date), 27 | 'uuid' => strval($item->uuid), 28 | ]; 29 | } 30 | } 31 | 32 | /** 33 | * The returned array contains an array with keys date (string) and uuid (string) 34 | * 35 | * @return array 36 | */ 37 | public function items(): array 38 | { 39 | return $this->items; 40 | } 41 | 42 | public function error(): string 43 | { 44 | return $this->get('error'); 45 | } 46 | } 47 | -------------------------------------------------------------------------------- /src/Services/Utilities/ReportUuidService.php: -------------------------------------------------------------------------------- 1 | settings = $settings; 18 | } 19 | 20 | public function settings(): FinkokSettings 21 | { 22 | return $this->settings; 23 | } 24 | 25 | public function reportUuid(ReportUuidCommand $command): ReportUuidResult 26 | { 27 | $soapCaller = $this->settings()->createCallerForService(Services::utilities()); 28 | $rawResponse = $soapCaller->call('report_uuid', [ 29 | 'taxpayer_id' => $command->rfc(), 30 | 'invoice_type' => $command->type(), 31 | 'date_from' => $command->sinceString(), 32 | 'date_to' => $command->untilString(), 33 | ]); 34 | return new ReportUuidResult($rawResponse); 35 | } 36 | } 37 | -------------------------------------------------------------------------------- /src/SoapCaller.php: -------------------------------------------------------------------------------- 1 | */ 22 | private $extraParameters; 23 | 24 | /** @var LoggerInterface */ 25 | private $logger; 26 | 27 | /** 28 | * @param SoapClient $soapClient 29 | * @param array $extraParameters 30 | */ 31 | public function __construct(SoapClient $soapClient, array $extraParameters = []) 32 | { 33 | $this->soapClient = $soapClient; 34 | $this->extraParameters = $extraParameters; 35 | $this->logger = new NullLogger(); 36 | } 37 | 38 | private function soapClient(): SoapClient 39 | { 40 | return $this->soapClient; 41 | } 42 | 43 | /** @return array */ 44 | public function extraParameters(): array 45 | { 46 | return $this->extraParameters; 47 | } 48 | 49 | /** 50 | * @param string $methodName 51 | * @param array $parameters 52 | * @return stdClass 53 | */ 54 | public function call(string $methodName, array $parameters): stdClass 55 | { 56 | $finalParameters = $this->finalParameters($parameters); 57 | $soap = $this->soapClient(); 58 | try { 59 | $result = $soap->__soapCall($methodName, [$finalParameters]); 60 | $this->logger->debug(strval(json_encode([ 61 | $methodName => $this->extractSoapClientTrace($soap), 62 | ], JSON_PRETTY_PRINT | JSON_UNESCAPED_SLASHES))); 63 | /** @var stdClass $result */ 64 | return $result; 65 | } catch (Throwable $exception) { 66 | $this->logger->error(strval(json_encode( 67 | array_merge( 68 | ['method' => $methodName, 'parameters' => $finalParameters], 69 | $this->extractSoapClientTrace($soap), 70 | ['exception' => ($exception instanceof JsonSerializable) ? $exception : print_r($exception, true)] 71 | ), 72 | JSON_PRETTY_PRINT | JSON_UNESCAPED_SLASHES 73 | ))); 74 | throw new RuntimeException(sprintf('Fail soap call to %s', $methodName), 0, $exception); 75 | } 76 | } 77 | 78 | /** 79 | * @param SoapClient $soapClient 80 | * @return array> 81 | * @noinspection PhpUsageOfSilenceOperatorInspection 82 | */ 83 | protected function extractSoapClientTrace(SoapClient $soapClient): array 84 | { 85 | return [ 86 | 'request' => [ 87 | 'headers' => (string) @$soapClient->__getLastRequestHeaders(), 88 | 'body' => (string) @$soapClient->__getLastRequest(), 89 | ], 90 | 'response' => [ 91 | 'headers' => (string) @$soapClient->__getLastResponseHeaders(), 92 | 'body' => (string) @$soapClient->__getLastResponse(), 93 | ], 94 | ]; 95 | } 96 | 97 | /** 98 | * @param array $parameters 99 | * @return array 100 | */ 101 | public function finalParameters(array $parameters): array 102 | { 103 | return array_merge($parameters, $this->extraParameters()); 104 | } 105 | 106 | public function setLogger(LoggerInterface $logger): void 107 | { 108 | $this->logger = $logger; 109 | } 110 | } 111 | -------------------------------------------------------------------------------- /src/SoapFactory.php: -------------------------------------------------------------------------------- 1 | logger = $logger ?? new NullLogger(); 20 | } 21 | 22 | public function createSoapClient(string $wsdlLocation): SoapClient 23 | { 24 | return new SoapClient($wsdlLocation, [ 25 | 'features' => SOAP_SINGLE_ELEMENT_ARRAYS, 26 | 'cache_wsdl' => WSDL_CACHE_MEMORY, 27 | 'exceptions' => true, 28 | 'trace' => true, 29 | ]); 30 | } 31 | 32 | /** 33 | * @param string $wsdlLocation 34 | * @param array $defaultOptions 35 | * @return SoapCaller 36 | */ 37 | public function createSoapCaller(string $wsdlLocation, array $defaultOptions): SoapCaller 38 | { 39 | $caller = new SoapCaller($this->createSoapClient($wsdlLocation), $defaultOptions); 40 | $caller->setLogger($this->logger); 41 | return $caller; 42 | } 43 | 44 | public function setLogger(LoggerInterface $logger): void 45 | { 46 | $this->logger = $logger; 47 | } 48 | } 49 | --------------------------------------------------------------------------------