├── .editorconfig ├── .npmrc ├── .prettierignore ├── CHANGELOG.md ├── CONTRIBUTING.md ├── LICENSE ├── README.md ├── docs ├── .nojekyll ├── assets │ ├── hierarchy.js │ ├── highlight.css │ ├── icons.js │ ├── icons.svg │ ├── main.js │ ├── navigation.js │ ├── search.js │ ├── style.css │ └── typedoc-github-style.css ├── classes │ ├── internal_helpers.Helpers.html │ ├── internal_interacts_xml_trait.InteractsXmlTrait.html │ ├── internal_service_consumer.ServiceConsumer.html │ ├── internal_soap_fault_info_extractor.SoapFaultInfoExtractor.html │ ├── package_reader_cfdi_package_reader.CfdiPackageReader.html │ ├── package_reader_exceptions_create_temporary_file_zip_exception.CreateTemporaryZipFileException.html │ ├── package_reader_exceptions_open_zip_file_exception.OpenZipFileException.html │ ├── package_reader_exceptions_package_reader_exception.PackageReaderException.html │ ├── package_reader_internal_csv_reader.CsvReader.html │ ├── package_reader_internal_file_filters_cfdi_file_filter.CfdiFileFilter.html │ ├── package_reader_internal_file_filters_metadata_file_filter.MetadataFileFilter.html │ ├── package_reader_internal_file_filters_null_file_filter.NullFileFilter.html │ ├── package_reader_internal_file_filters_third_parties_file_filter.ThirdPartiesFileFilter.html │ ├── package_reader_internal_filtered_package_reader.FilteredPackageReader.html │ ├── package_reader_internal_metadata_content.MetadataContent.html │ ├── package_reader_internal_metadata_preprocessor.MetadataPreprocessor.html │ ├── package_reader_internal_third_parties_extractor.ThirdPartiesExtractor.html │ ├── package_reader_internal_third_parties_records.ThirdPartiesRecords.html │ ├── package_reader_metadata_item.MetadataItem.html │ ├── package_reader_metadata_package_reader.MetadataPackageReader.html │ ├── request_builder_fiel_request_builder_fiel.Fiel.html │ ├── request_builder_fiel_request_builder_fiel_request_builder.FielRequestBuilder.html │ ├── request_builder_request_builder_exception.RequestBuilderException.html │ ├── service.Service.html │ ├── services_authenticate_authenticate_translator.AuthenticateTranslator.html │ ├── services_download_download_result.DownloadResult.html │ ├── services_download_download_translator.DownloadTranslator.html │ ├── services_query_query_parameters.QueryParameters.html │ ├── services_query_query_result.QueryResult.html │ ├── services_query_query_translator.QueryTranslator.html │ ├── services_query_query_validator.QueryValidator.html │ ├── services_verify_verify_result.VerifyResult.html │ ├── services_verify_verify_translator.VerifyTranslator.html │ ├── shared_abstract_rfc_filter.AbstractRfcFilter.html │ ├── shared_code_request.CodeRequest.html │ ├── shared_complemento_cfdi.ComplementoCfdi.html │ ├── shared_complemento_retenciones.ComplementoRetenciones.html │ ├── shared_complemento_undefined.ComplementoUndefined.html │ ├── shared_date_time.DateTime.html │ ├── shared_date_time_period.DateTimePeriod.html │ ├── shared_document_status.DocumentStatus.html │ ├── shared_document_type.DocumentType.html │ ├── shared_download_type.DownloadType.html │ ├── shared_enum_base_enum.BaseEnum.html │ ├── shared_request_type.RequestType.html │ ├── shared_rfc_match.RfcMatch.html │ ├── shared_rfc_matches.RfcMatches.html │ ├── shared_rfc_on_behalf.RfcOnBehalf.html │ ├── shared_service_endpoints.ServiceEndpoints.html │ ├── shared_service_type.ServiceType.html │ ├── shared_status_code.StatusCode.html │ ├── shared_status_request.StatusRequest.html │ ├── shared_token.Token.html │ ├── shared_uuid.Uuid.html │ ├── web_client_crequest.CRequest.html │ ├── web_client_cresponse.CResponse.html │ ├── web_client_exceptions_http_client_error.HttpClientError.html │ ├── web_client_exceptions_http_server_error.HttpServerError.html │ ├── web_client_exceptions_http_timeout_error.HttpTimeoutError.html │ ├── web_client_exceptions_soap_fault_error.SoapFaultError.html │ ├── web_client_exceptions_web_client_exception.WebClientException.html │ ├── web_client_https_web_client.HttpsWebClient.html │ └── web_client_soap_fault_info.SoapFaultInfo.html ├── hierarchy.html ├── index.html ├── modules.html ├── modules │ ├── internal_helpers.html │ ├── internal_interacts_xml_trait.html │ ├── internal_service_consumer.html │ ├── internal_soap_fault_info_extractor.html │ ├── package_reader_cfdi_package_reader.html │ ├── package_reader_exceptions_create_temporary_file_zip_exception.html │ ├── package_reader_exceptions_open_zip_file_exception.html │ ├── package_reader_exceptions_package_reader_exception.html │ ├── package_reader_internal_csv_reader.html │ ├── package_reader_internal_file_filters_cfdi_file_filter.html │ ├── package_reader_internal_file_filters_file_filter_interface.html │ ├── package_reader_internal_file_filters_metadata_file_filter.html │ ├── package_reader_internal_file_filters_null_file_filter.html │ ├── package_reader_internal_file_filters_third_parties_file_filter.html │ ├── package_reader_internal_filtered_package_reader.html │ ├── package_reader_internal_metadata_content.html │ ├── package_reader_internal_metadata_preprocessor.html │ ├── package_reader_internal_third_parties_extractor.html │ ├── package_reader_internal_third_parties_records.html │ ├── package_reader_metadata_item.html │ ├── package_reader_metadata_item_interface.html │ ├── package_reader_metadata_package_reader.html │ ├── package_reader_package_reader_interface.html │ ├── request_builder_fiel_request_builder_fiel.html │ ├── request_builder_fiel_request_builder_fiel_request_builder.html │ ├── request_builder_request_builder_exception.html │ ├── request_builder_request_builder_interface.html │ ├── service.html │ ├── services_authenticate_authenticate_translator.html │ ├── services_download_download_result.html │ ├── services_download_download_translator.html │ ├── services_query_query_parameters.html │ ├── services_query_query_result.html │ ├── services_query_query_translator.html │ ├── services_query_query_validator.html │ ├── services_verify_verify_result.html │ ├── services_verify_verify_translator.html │ ├── shared_abstract_rfc_filter.html │ ├── shared_code_request.html │ ├── shared_complemento_cfdi.html │ ├── shared_complemento_interface.html │ ├── shared_complemento_retenciones.html │ ├── shared_complemento_undefined.html │ ├── shared_date_time.html │ ├── shared_date_time_period.html │ ├── shared_document_status.html │ ├── shared_document_type.html │ ├── shared_download_type.html │ ├── shared_enum_base_enum.html │ ├── shared_request_type.html │ ├── shared_rfc_match.html │ ├── shared_rfc_matches.html │ ├── shared_rfc_on_behalf.html │ ├── shared_service_endpoints.html │ ├── shared_service_type.html │ ├── shared_status_code.html │ ├── shared_status_request.html │ ├── shared_token.html │ ├── shared_uuid.html │ ├── types.html │ ├── web_client_crequest.html │ ├── web_client_cresponse.html │ ├── web_client_exceptions_http_client_error.html │ ├── web_client_exceptions_http_server_error.html │ ├── web_client_exceptions_http_timeout_error.html │ ├── web_client_exceptions_soap_fault_error.html │ ├── web_client_exceptions_web_client_exception.html │ ├── web_client_https_web_client.html │ ├── web_client_soap_fault_info.html │ └── web_client_web_client_interface.html ├── types │ ├── package_reader_internal_csv_reader.ReadLineInterface.html │ ├── package_reader_internal_file_filters_file_filter_interface.FileFilterInterface.html │ ├── package_reader_internal_third_parties_extractor.ThirdPartiesInterface.html │ ├── package_reader_metadata_item_interface.MetadataItemInterface.html │ ├── package_reader_package_reader_interface.PackageReaderInterface.html │ ├── request_builder_request_builder_interface.RequestBuilderInterface.html │ ├── shared_code_request.CodeRequestTypes.html │ ├── shared_complemento_cfdi.ComplementoCfdiTypes.html │ ├── shared_complemento_interface.ComplementoInterface.html │ ├── shared_complemento_retenciones.ComplementoRetencionesTypes.html │ ├── shared_complemento_undefined.ComplementoUndefinedTypes.html │ ├── shared_document_status.DocumentStatusTypes.html │ ├── shared_document_type.DocumentTypeTypes.html │ ├── shared_download_type.DownloadTypeTypes.html │ ├── shared_request_type.RequestTypeTypes.html │ ├── shared_service_type.ServiceTypeValues.html │ ├── shared_status_request.StatusRequestTypes.html │ ├── types.Constructor.html │ └── web_client_web_client_interface.WebClientInterface.html └── variables │ ├── shared_document_status.DocumentStatusEnum.html │ ├── shared_document_type.DocumentTypeEnum.html │ ├── shared_download_type.DownloadTypeEnum.html │ ├── shared_request_type.RequestTypeEnum.html │ └── shared_service_type.ServiceTypeEnum.html ├── eslint.config.js ├── index.ts ├── package.json ├── src ├── internal │ ├── helpers.ts │ ├── interacts_xml_trait.ts │ ├── service_consumer.ts │ └── soap_fault_info_extractor.ts ├── package_reader │ ├── cfdi_package_reader.ts │ ├── exceptions │ │ ├── create_temporary_file_zip_exception.ts │ │ ├── open_zip_file_exception.ts │ │ └── package_reader_exception.ts │ ├── internal │ │ ├── csv_reader.ts │ │ ├── file_filters │ │ │ ├── cfdi_file_filter.ts │ │ │ ├── file_filter_interface.ts │ │ │ ├── metadata_file_filter.ts │ │ │ ├── null_file_filter.ts │ │ │ └── third_parties_file_filter.ts │ │ ├── filtered_package_reader.ts │ │ ├── metadata_content.ts │ │ ├── metadata_preprocessor.ts │ │ ├── third_parties_extractor.ts │ │ └── third_parties_records.ts │ ├── metadata_item.ts │ ├── metadata_item_interface.ts │ ├── metadata_package_reader.ts │ └── package_reader_interface.ts ├── request_builder │ ├── fiel_request_builder │ │ ├── fiel.ts │ │ └── fiel_request_builder.ts │ ├── request_builder_exception.ts │ └── request_builder_interface.ts ├── service.ts ├── services │ ├── authenticate │ │ └── authenticate_translator.ts │ ├── download │ │ ├── download_result.ts │ │ └── download_translator.ts │ ├── query │ │ ├── query_parameters.ts │ │ ├── query_result.ts │ │ ├── query_translator.ts │ │ └── query_validator.ts │ └── verify │ │ ├── verify_result.ts │ │ └── verify_translator.ts ├── shared │ ├── abstract_rfc_filter.ts │ ├── code_request.ts │ ├── complemento_cfdi.ts │ ├── complemento_interface.ts │ ├── complemento_retenciones.ts │ ├── complemento_undefined.ts │ ├── date_time.ts │ ├── date_time_period.ts │ ├── document_status.ts │ ├── document_type.ts │ ├── download_type.ts │ ├── enum │ │ └── base_enum.ts │ ├── request_type.ts │ ├── rfc_match.ts │ ├── rfc_matches.ts │ ├── rfc_on_behalf.ts │ ├── service_endpoints.ts │ ├── service_type.ts │ ├── status_code.ts │ ├── status_request.ts │ ├── token.ts │ └── uuid.ts ├── types.ts └── web_client │ ├── crequest.ts │ ├── cresponse.ts │ ├── exceptions │ ├── http_client_error.ts │ ├── http_server_error.ts │ ├── http_timeout_error.ts │ ├── soap_fault_error.ts │ └── web_client_exception.ts │ ├── https_web_client.ts │ ├── soap_fault_info.ts │ └── web_client_interface.ts ├── tsconfig.json ├── typedoc.json └── vitest.config.ts /.editorconfig: -------------------------------------------------------------------------------- 1 | # http://editorconfig.org 2 | 3 | [*] 4 | indent_style = space 5 | indent_size = 2 6 | end_of_line = lf 7 | charset = utf-8 8 | trim_trailing_whitespace = true 9 | insert_final_newline = true 10 | 11 | [*.json] 12 | insert_final_newline = unset 13 | 14 | [**.min.js] 15 | indent_style = unset 16 | insert_final_newline = unset 17 | 18 | [MakeFile] 19 | indent_style = space 20 | 21 | [*.md] 22 | trim_trailing_whitespace = false 23 | -------------------------------------------------------------------------------- /.npmrc: -------------------------------------------------------------------------------- 1 | package-lock=false -------------------------------------------------------------------------------- /.prettierignore: -------------------------------------------------------------------------------- 1 | examples 2 | build 3 | dist 4 | docs 5 | coverage 6 | *.html 7 | tests/_files 8 | .pnpm-store 9 | CHANGELOG.md 10 | -------------------------------------------------------------------------------- /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 | - [nodeCfdi][] — 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 | Cuando se reporte un _Bug_, por favor incluye la mayor información posible para reproducir el problema, preferentemente 25 | con ejemplos de código o cualquier otra información técnica que nos pueda ayudar a identificar el caso. 26 | 27 | **Recuerda no incluir contraseñas, información personal o confidencial.** 28 | 29 | ## Corrección de Bugs 30 | 31 | Apreciamos mucho los _Pull Request_ para corregir Bugs. 32 | 33 | Si encuentras un reporte de Bug y te gustaría solucionarlo siéntete libre de hacerlo. 34 | Sigue las directrices de "Agregar nuevas funcionalidades" a continuación. 35 | 36 | ## Agregar nuevas funcionalidades 37 | 38 | Si tienes una idea para una nueva funcionalidad revisa primero que existan discusiones o _Pull Requests_ 39 | en donde ya se esté trabajando en la funcionalidad. 40 | 41 | Antes de trabajar en la nueva característica, utiliza los "Canales de comunicación" mencionados 42 | anteriormente para platicar acerca de tu idea. Si dialogas tus ideas con la comunidad y los 43 | mantenedores del proyecto, podrás ahorrar mucho esfuerzo de desarrollo y prevenir que tu 44 | _Pull Request_ sea rechazado. No nos gusta rechazar contribuciones, pero algunas características 45 | o la forma de desarrollarlas puede que no estén alineadas con el proyecto. 46 | 47 | Considera las siguientes directrices: 48 | 49 | - Usa una rama única que se desprenda de la rama principal. 50 | - No mezcles dos diferentes funcionalidades en una misma rama o _Pull Request_. 51 | - Describe claramente y en detalle los cambios que hiciste. 52 | - **Escribe pruebas** para la funcionalidad que deseas agregar. 53 | - Asegúrate que **las pruebas pasan** antes de enviar tu contribución. 54 | Usamos integración continua donde se hace esta verificación, pero es mucho mejor si lo pruebas localmente. 55 | - Intenta enviar una historia coherente, entenderemos cómo cambia el código si los _commits_ tienen significado. 56 | - La documentación es parte del proyecto. 57 | - Realiza los cambios en los archivos de ayuda para que reflejen los cambios en el código. 58 | 59 | [nodeCfdi]: https://github.com/nodecfdi/ 60 | [project]: https://github.com/nodecfdi/sat-ws-descarga-masiva 61 | [contributors]: https://github.com/nodecfdi/sat-ws-descarga-masiva/graphs/contributors 62 | [coc]: https://github.com/nodecfdi/sat-ws-descarga-masiva/blob/main/CODE_OF_CONDUCT.md 63 | [issues]: https://github.com/nodecfdi/sat-ws-descarga-masiva/issues 64 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | The MIT License (MIT) 2 | 3 | Copyright (c) 2022 - 2025 - NodeCfdi and OcelotlStudio 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 NON INFRINGEMENT. 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 | -------------------------------------------------------------------------------- /docs/.nojekyll: -------------------------------------------------------------------------------- 1 | TypeDoc added this file to prevent GitHub Pages from using Jekyll. You can turn off this behavior by setting the `githubPages` option to false. -------------------------------------------------------------------------------- /docs/assets/hierarchy.js: -------------------------------------------------------------------------------- 1 | window.hierarchyData = "eJytl11zmzgUhv+LrlXWwgaD79o0me3FTned7Me0k9HI4rDWVAgqiTbZjv/7jsDUwqGx2OyNP7De9318dCTEN6Tr2hq0+ZjnMSarnGCS4DheYJKvyD1GGkoJ3IpaGbT5hkjiXhWrAG3QO2VBM27NX5W800xYhNEnoQq0IXGGUasl2iAumTFgfhJusGKSikFFHypJrdNFT5yiva0kwr0abZA1xStn/aq/gBHfC1loUGjzMVvhdElwmqc4S5Y4T+L7A0bZyiO9rVlzw1pp36myvn6wLqvWQbimZg0tnZYKVdYUBnU0bXqB/IARSXIP7UoDs3AHVVNrph8/iOZGSLh+4NC4qj/H2DD+if0NVAMrQFMYNIbyzpTawZWWQgL9RzSnQdGF4ID/kfnN8L4B9f/A1w2ojrWDPgFPJVymjOOFR/lrn7rtQl/I+aNfoumQOS1NkhyTLHF9nC6Jx/+6tXtQVnA3dZopI9mFRjagvwgOhjJPOv5ivxtF0/6Xq5zmqUf5tv6qZM2KmYTFUXb64JE99bxMlSVLj+q3FvTjTKTPTnN89WDOrC6T5EnskfwBWpRzUb50ouHNgzl3C6DJfZrXO9PtXduS3whp4XmcPdNQUHbUUF1yt04t6OiJz7yOXy4wSdYr1/NkkfqL9qquGgkVKFtflYUIwOMnBeVlIaIzi4DNLU5+gLAFC4qLWoGZSaJPymjaMIBrOdoPPJvfVQGlUFDMpGoHXTRlFkC0Wi5Ga5+3zuLWMtuGVKg4CqjpFNHYICQ/SSfy7x4bmJNuHxuIfHFI8npy1wtNHja5PvkkDknO/S54wwxcq7YKSAXVVnTHDHSfokE5a6ku0gV26wO7ZsRu/rGbBOzqgUlClpikcdot5IT4O/AWPrdgQqdG96P7+njSkBPWqCW3Jf+FWb4PySw5rdzYaBCFpK1X47T36g3smSwDA2tFd934yNMG5Kax3363/Y0isLjH20pfXE8acuwbFfdna5srKUDZa62fv5V9hR3l3VD/GLW3tvl+2VlEZ5azejMn5L4/m67PIN2fBP0SSNM5eJCeZUjdRo8kTn8nKqjbFxXO9hYelG8aQJUTMvWg9B+ZvKelnmhsGMKz8nn+hN2xE0LO6tNMU1ejp8az2ixze16WrLGb1fvD4fAv/eVMFA==" -------------------------------------------------------------------------------- /docs/assets/highlight.css: -------------------------------------------------------------------------------- 1 | :root { 2 | --light-hl-0: #795E26; 3 | --dark-hl-0: #DCDCAA; 4 | --light-hl-1: #000000; 5 | --dark-hl-1: #D4D4D4; 6 | --light-hl-2: #A31515; 7 | --dark-hl-2: #CE9178; 8 | --light-hl-3: #0000FF; 9 | --dark-hl-3: #569CD6; 10 | --light-hl-4: #AF00DB; 11 | --dark-hl-4: #C586C0; 12 | --light-hl-5: #001080; 13 | --dark-hl-5: #9CDCFE; 14 | --light-hl-6: #008000; 15 | --dark-hl-6: #6A9955; 16 | --light-hl-7: #0070C1; 17 | --dark-hl-7: #4FC1FF; 18 | --light-hl-8: #000000FF; 19 | --dark-hl-8: #D4D4D4; 20 | --light-hl-9: #098658; 21 | --dark-hl-9: #B5CEA8; 22 | --light-hl-10: #267F99; 23 | --dark-hl-10: #4EC9B0; 24 | --light-code-background: #FFFFFF; 25 | --dark-code-background: #1E1E1E; 26 | } 27 | 28 | @media (prefers-color-scheme: light) { :root { 29 | --hl-0: var(--light-hl-0); 30 | --hl-1: var(--light-hl-1); 31 | --hl-2: var(--light-hl-2); 32 | --hl-3: var(--light-hl-3); 33 | --hl-4: var(--light-hl-4); 34 | --hl-5: var(--light-hl-5); 35 | --hl-6: var(--light-hl-6); 36 | --hl-7: var(--light-hl-7); 37 | --hl-8: var(--light-hl-8); 38 | --hl-9: var(--light-hl-9); 39 | --hl-10: var(--light-hl-10); 40 | --code-background: var(--light-code-background); 41 | } } 42 | 43 | @media (prefers-color-scheme: dark) { :root { 44 | --hl-0: var(--dark-hl-0); 45 | --hl-1: var(--dark-hl-1); 46 | --hl-2: var(--dark-hl-2); 47 | --hl-3: var(--dark-hl-3); 48 | --hl-4: var(--dark-hl-4); 49 | --hl-5: var(--dark-hl-5); 50 | --hl-6: var(--dark-hl-6); 51 | --hl-7: var(--dark-hl-7); 52 | --hl-8: var(--dark-hl-8); 53 | --hl-9: var(--dark-hl-9); 54 | --hl-10: var(--dark-hl-10); 55 | --code-background: var(--dark-code-background); 56 | } } 57 | 58 | :root[data-theme='light'] { 59 | --hl-0: var(--light-hl-0); 60 | --hl-1: var(--light-hl-1); 61 | --hl-2: var(--light-hl-2); 62 | --hl-3: var(--light-hl-3); 63 | --hl-4: var(--light-hl-4); 64 | --hl-5: var(--light-hl-5); 65 | --hl-6: var(--light-hl-6); 66 | --hl-7: var(--light-hl-7); 67 | --hl-8: var(--light-hl-8); 68 | --hl-9: var(--light-hl-9); 69 | --hl-10: var(--light-hl-10); 70 | --code-background: var(--light-code-background); 71 | } 72 | 73 | :root[data-theme='dark'] { 74 | --hl-0: var(--dark-hl-0); 75 | --hl-1: var(--dark-hl-1); 76 | --hl-2: var(--dark-hl-2); 77 | --hl-3: var(--dark-hl-3); 78 | --hl-4: var(--dark-hl-4); 79 | --hl-5: var(--dark-hl-5); 80 | --hl-6: var(--dark-hl-6); 81 | --hl-7: var(--dark-hl-7); 82 | --hl-8: var(--dark-hl-8); 83 | --hl-9: var(--dark-hl-9); 84 | --hl-10: var(--dark-hl-10); 85 | --code-background: var(--dark-code-background); 86 | } 87 | 88 | .hl-0 { color: var(--hl-0); } 89 | .hl-1 { color: var(--hl-1); } 90 | .hl-2 { color: var(--hl-2); } 91 | .hl-3 { color: var(--hl-3); } 92 | .hl-4 { color: var(--hl-4); } 93 | .hl-5 { color: var(--hl-5); } 94 | .hl-6 { color: var(--hl-6); } 95 | .hl-7 { color: var(--hl-7); } 96 | .hl-8 { color: var(--hl-8); } 97 | .hl-9 { color: var(--hl-9); } 98 | .hl-10 { color: var(--hl-10); } 99 | pre, code { background: var(--code-background); } 100 | -------------------------------------------------------------------------------- /docs/assets/navigation.js: -------------------------------------------------------------------------------- 1 | window.navigationData = "eJytW11v3DYQ/C/3HDSNi6Kt35qrjRhoGte+pEWDgKAlXo+IjlIkynVa5L8Xkk4SP3aXSzuPtnZmdocriqR07//bWPVgN+cbbaxqjaw2zzbFQVdlq8zm/P1y+aCqRrXd5tmmkfawOd8c67KvVPd8xolTxDcHexxIPmpTbs7PYLZXIVtRya6D2F5BrC/Ofvzy4cszP3dZ2E48HCthW6ktkSgQzUr6asb9eax2vkaUPqQR4emiOtXe60KJojZdf1QtUVEYyirndgJtI/qomIg+gCbqqGUj9rKvrNBmXwv1YAcXarIgDMOrrJbN5QC+Mvv6IpaLC0TlYCqoYLfkRhYf5d9KtEqWo7NQlsW+1CKKDBzxrwsAw3Jkuy/19QS7CZRmMxhKEQs98OqhUI3VtekwB1olrRJWHZu6le1nsdeVEv/qRizQpCOriGDQ8cwaeXYzzV+6udSVuohTQqzLTCkhR3tcN8qMfCPxo2xDKFhWvWmUeYo/mDbESxuBaeQ4gV1hWeHdGI8yA5WHqVOTUOKBXnT33FlnmSdXDO9G6u6Zsw2ksKDjQtcih4hftVHjs3UvC7VK2c8NTyjiCIr79qcfXnx/5nk7tuteV3ZaxKDTuxPHd9klFyENe7of7p3LQDnlPq3ss9K3o4Ob6P2xybIA5GL5sGab3R+MDAD2dOcclZWltPIrtAZExbLl9Qn45BYBM4jZ6VYxfVV9BTdCGpYTv/VV9WQXImWfla7eHnRbika2VqvuK9iA8rH82A3o6wn8ZF/wVGCZ1ANtAqsyd9HsZgcRcKeSEZu3ik5Kg7R0xyw3XVEbqwyw08WSCJFZk8U2VEuVHKkFTMwqm1Y1bV2oroN2jElxF55V7zWoyy7a04U4c6YFYsuMJYIQZM8CxPY5VxqkpRZ3LiD7Ac7JIuO57dO1qqjbEjgN42VzgmcPxk0omzcUsyxAmZp7l77WVh2TZXvRWXfdlUePlOfTu1DmnDLgMpalCC67MG4PY3ogV8aSM/O5ieDyZtGsxyWmCLJlnQfwBxsD5h8CcIcbVYTZsAF3q2/Vp151Vtz1usJPIPdaVYIfGlsXYAVEOP6TucRyNeYW4WtcRkLxrhQs+LFVhf9kV3kz4V6GGWTXHGUQs6dm9pCWODkLQ1Eoywg/TeLEjC+LUNJNEdIRU0Uqk7zJws8WnS34ogghZ8I4veKJSz5dyHmjFI/gTHILkSHvvbBzNdnbgzJWF9Kq5+4fw1s201USXCbPpMJDIHBWsT872B2gHNTOVIZZacPK+h9T1bJEDJsvi1Z1fQVsF5cMl8gAwvLjlxPmJlCJfEBVfAZezbxBjyUzB3tOjTXQpFrMlJqgP/Wq/YyM7XhtWN/Lo7Lg6/klqyk0BLCq/30AXQMiUemISEBAD+2ETjarK5XRpmMqyR4FyB0gJ39WX7oymR05psNqR0QkIODUdC8rXWaUtMTzK3oXS5AFrRI+PHVT3atW77G7arqYbsFTnBfOKvXdiEh2IcjvYulBO+FYnehLZfbilBKrGVGdkAMeQG+dcJCtwh568q4bD3xEuy/QA/WJQQCxvBXACXezL7BzckIhQtODWdSlmpd+aCVuEO9FYV2qm5A0SN4jdQDUyZ0TthsWseGaNkE9YtIHG0V9bCp1VMbW49tKwhc/kOnNAtp65JE/AXkApH3yQmmvaJlHeEbsdADRvM2Nkx26syFFIIK88lpllSl0bRS0Loq1nfjcEm8gKaJPXCmYhtk1DoLbPGntR7RSb0q112acj9NOL9G5Pr+NZQiXVxmIgunwEs/1l1ZleluO+0J9xG/NJYK3gRn2kx5d4NtKN4cmNmBzvGhUq2t83MPArHSvA24s6Znbx6W2kEU/jI3orLQ9PkUEccz94oS5DajD/ANqH0Y1qB9JtSatQbQjJnZheucVzb1stbwj7IrBvtx3Z/CoDEWkx2SIyhqRnUeLjcdI60I4YzHEsUYiYs8ahSGYOwaREsP/+cyC9t+JyjtBof13aV0I7f8aR/uPsTP9XwFJ/zGlpP/K9Mfnd7JTQnkawQAMF8USxhqBl7IL8g7cDzjneN7pOdkublDO0TjZLB6pA6A/F13CqE7BqFl94sQn2gSTSTbJsI08SlsccMfnCJ7d++K1Txd6vdDNoYmumOOJpbcTk5UkscR2KdfwdKq1EXfqIKs9mewSxU33jXkZsAL5rqwOgvfLIGXKptbG4h5HkTnvcS5i+iD/mD6E8gohJw83KCd9cvLwSB0ANXk4Ye9k1aOzB8Y9gVLThwNITB+YTnL6mFZm4+kL7voawzN9jN96lKHnDuUanuiQCZM6+PLDMhJOHX4FxB6I7BU3kHrUUALMvaKtPyrgbf1JYLzK+9LM5wmcmHh2MVs4ZH2v8d3gcJGVzFuPJchlZHkbUcXH/NZ3fk7GAsZiZw+ms23vfwA5DeBE4gSkR+ofdSeKSk+f0SK/iEMafYWKIuuEF+1wiHGLtrZ38NOqrqlNB8wePukUxc0z5IQTPXEu8XSqyd8gHqxtZnbVttBLEkff+bVWBGSV+craZjuCLnwxoFhSLCCiXRjhw+Ni+Fomu0gXyC7ydgTlF+mJBUSMIofDoLp/zFB6SHaZuwmVX6cvF1IlHorrz5Wz6gxxvOfk/AvorBojKZ+Grg+i5NYI/ZdV5x/q7nQ/4V+iZUjGdKln1dAWLhlZcRjMbthuSYwsL+L3sez+HH5OTxYSxOa15JXHDpQRsntIdhMS78mcKAiQ13noi7KUSsyAf/P34X+C6KhC" -------------------------------------------------------------------------------- /docs/modules/internal_helpers.html: -------------------------------------------------------------------------------- 1 | internal/helpers | @nodecfdi/sat-ws-descarga-masiva
@nodecfdi/sat-ws-descarga-masiva
    Preparing search index...

    Module internal/helpers

    Classes

    Helpers
    2 | -------------------------------------------------------------------------------- /docs/modules/service.html: -------------------------------------------------------------------------------- 1 | service | @nodecfdi/sat-ws-descarga-masiva
    @nodecfdi/sat-ws-descarga-masiva
      Preparing search index...

      Module service

      Classes

      Service
      2 | -------------------------------------------------------------------------------- /docs/modules/shared_token.html: -------------------------------------------------------------------------------- 1 | shared/token | @nodecfdi/sat-ws-descarga-masiva
      @nodecfdi/sat-ws-descarga-masiva
        Preparing search index...

        Module shared/token

        Classes

        Token
        2 | -------------------------------------------------------------------------------- /docs/modules/shared_uuid.html: -------------------------------------------------------------------------------- 1 | shared/uuid | @nodecfdi/sat-ws-descarga-masiva
        @nodecfdi/sat-ws-descarga-masiva
          Preparing search index...

          Module shared/uuid

          Classes

          Uuid
          2 | -------------------------------------------------------------------------------- /docs/types/shared_code_request.CodeRequestTypes.html: -------------------------------------------------------------------------------- 1 | CodeRequestTypes | @nodecfdi/sat-ws-descarga-masiva
          @nodecfdi/sat-ws-descarga-masiva
            Preparing search index...

            Type Alias CodeRequestTypes

            CodeRequestTypes:
                | "Accepted"
                | "Exhausted"
                | "MaximumLimitReaded"
                | "EmptyResult"
                | "Duplicated"
            2 | -------------------------------------------------------------------------------- /docs/types/shared_complemento_undefined.ComplementoUndefinedTypes.html: -------------------------------------------------------------------------------- 1 | ComplementoUndefinedTypes | @nodecfdi/sat-ws-descarga-masiva
            @nodecfdi/sat-ws-descarga-masiva
              Preparing search index...

              Type Alias ComplementoUndefinedTypes

              ComplementoUndefinedTypes: "undefined"
              2 | -------------------------------------------------------------------------------- /docs/types/shared_document_status.DocumentStatusTypes.html: -------------------------------------------------------------------------------- 1 | DocumentStatusTypes | @nodecfdi/sat-ws-descarga-masiva
              @nodecfdi/sat-ws-descarga-masiva
                Preparing search index...

                Type Alias DocumentStatusTypes

                DocumentStatusTypes: "undefined" | "active" | "cancelled"
                2 | -------------------------------------------------------------------------------- /docs/types/shared_document_type.DocumentTypeTypes.html: -------------------------------------------------------------------------------- 1 | DocumentTypeTypes | @nodecfdi/sat-ws-descarga-masiva
                @nodecfdi/sat-ws-descarga-masiva
                  Preparing search index...

                  Type Alias DocumentTypeTypes

                  DocumentTypeTypes:
                      | "undefined"
                      | "ingreso"
                      | "egreso"
                      | "traslado"
                      | "nomina"
                      | "pago"
                  2 | -------------------------------------------------------------------------------- /docs/types/shared_download_type.DownloadTypeTypes.html: -------------------------------------------------------------------------------- 1 | DownloadTypeTypes | @nodecfdi/sat-ws-descarga-masiva
                  @nodecfdi/sat-ws-descarga-masiva
                    Preparing search index...

                    Type Alias DownloadTypeTypes

                    DownloadTypeTypes: "issued" | "received"
                    2 | -------------------------------------------------------------------------------- /docs/types/shared_request_type.RequestTypeTypes.html: -------------------------------------------------------------------------------- 1 | RequestTypeTypes | @nodecfdi/sat-ws-descarga-masiva
                    @nodecfdi/sat-ws-descarga-masiva
                      Preparing search index...

                      Type Alias RequestTypeTypes

                      RequestTypeTypes: "xml" | "metadata"
                      2 | -------------------------------------------------------------------------------- /docs/types/shared_service_type.ServiceTypeValues.html: -------------------------------------------------------------------------------- 1 | ServiceTypeValues | @nodecfdi/sat-ws-descarga-masiva
                      @nodecfdi/sat-ws-descarga-masiva
                        Preparing search index...

                        Type Alias ServiceTypeValues

                        ServiceTypeValues: "cfdi" | "retenciones"
                        2 | -------------------------------------------------------------------------------- /docs/types/shared_status_request.StatusRequestTypes.html: -------------------------------------------------------------------------------- 1 | StatusRequestTypes | @nodecfdi/sat-ws-descarga-masiva
                        @nodecfdi/sat-ws-descarga-masiva
                          Preparing search index...

                          Type Alias StatusRequestTypes

                          StatusRequestTypes:
                              | "Accepted"
                              | "InProgress"
                              | "Finished"
                              | "Failure"
                              | "Rejected"
                              | "Expired"
                          2 | -------------------------------------------------------------------------------- /docs/types/types.Constructor.html: -------------------------------------------------------------------------------- 1 | Constructor | @nodecfdi/sat-ws-descarga-masiva
                          @nodecfdi/sat-ws-descarga-masiva
                            Preparing search index...

                            Type Alias Constructor<T>

                            Constructor: new (...args: T[]) => T

                            Type Parameters

                            • T = any
                            2 | -------------------------------------------------------------------------------- /docs/variables/shared_download_type.DownloadTypeEnum.html: -------------------------------------------------------------------------------- 1 | DownloadTypeEnum | @nodecfdi/sat-ws-descarga-masiva
                            @nodecfdi/sat-ws-descarga-masiva
                              Preparing search index...

                              Variable DownloadTypeEnumConst

                              DownloadTypeEnum: { issued: "RfcEmisor"; received: "RfcReceptor" } = ...

                              Type declaration

                              • Readonlyissued: "RfcEmisor"
                              • Readonlyreceived: "RfcReceptor"
                              2 | -------------------------------------------------------------------------------- /docs/variables/shared_request_type.RequestTypeEnum.html: -------------------------------------------------------------------------------- 1 | RequestTypeEnum | @nodecfdi/sat-ws-descarga-masiva
                              @nodecfdi/sat-ws-descarga-masiva
                                Preparing search index...

                                Variable RequestTypeEnumConst

                                RequestTypeEnum: { metadata: string; xml: string } = ...

                                Type declaration

                                • metadata: string
                                • xml: string
                                2 | -------------------------------------------------------------------------------- /docs/variables/shared_service_type.ServiceTypeEnum.html: -------------------------------------------------------------------------------- 1 | ServiceTypeEnum | @nodecfdi/sat-ws-descarga-masiva
                                @nodecfdi/sat-ws-descarga-masiva
                                  Preparing search index...

                                  Variable ServiceTypeEnumConst

                                  ServiceTypeEnum: { cfdi: "cfdi"; retenciones: "retenciones" } = ...

                                  Type declaration

                                  • Readonlycfdi: "cfdi"
                                  • Readonlyretenciones: "retenciones"
                                  2 | -------------------------------------------------------------------------------- /eslint.config.js: -------------------------------------------------------------------------------- 1 | // @ts-check 2 | import nodecfdiConfig from '@nodecfdi/eslint-config'; 3 | 4 | const { defineConfig } = nodecfdiConfig(import.meta.dirname, { vitest: true, sonarjs: true }); 5 | 6 | export default defineConfig(); 7 | -------------------------------------------------------------------------------- /index.ts: -------------------------------------------------------------------------------- 1 | export * from './src/internal/helpers.js'; 2 | export * from './src/internal/interacts_xml_trait.js'; 3 | export * from './src/internal/service_consumer.js'; 4 | export * from './src/internal/soap_fault_info_extractor.js'; 5 | export * from './src/package_reader/cfdi_package_reader.js'; 6 | export * from './src/package_reader/exceptions/create_temporary_file_zip_exception.js'; 7 | export * from './src/package_reader/exceptions/open_zip_file_exception.js'; 8 | export * from './src/package_reader/exceptions/package_reader_exception.js'; 9 | export * from './src/package_reader/internal/csv_reader.js'; 10 | export * from './src/package_reader/internal/file_filters/cfdi_file_filter.js'; 11 | export type * from './src/package_reader/internal/file_filters/file_filter_interface.js'; 12 | export * from './src/package_reader/internal/file_filters/metadata_file_filter.js'; 13 | export * from './src/package_reader/internal/file_filters/null_file_filter.js'; 14 | export * from './src/package_reader/internal/file_filters/third_parties_file_filter.js'; 15 | export * from './src/package_reader/internal/filtered_package_reader.js'; 16 | export * from './src/package_reader/internal/metadata_content.js'; 17 | export * from './src/package_reader/internal/metadata_preprocessor.js'; 18 | export * from './src/package_reader/internal/third_parties_extractor.js'; 19 | export * from './src/package_reader/internal/third_parties_records.js'; 20 | export * from './src/package_reader/metadata_item.js'; 21 | export type * from './src/package_reader/metadata_item_interface.js'; 22 | export * from './src/package_reader/metadata_package_reader.js'; 23 | export type * from './src/package_reader/package_reader_interface.js'; 24 | export * from './src/request_builder/fiel_request_builder/fiel.js'; 25 | export * from './src/request_builder/fiel_request_builder/fiel_request_builder.js'; 26 | export * from './src/request_builder/request_builder_exception.js'; 27 | export type * from './src/request_builder/request_builder_interface.js'; 28 | export * from './src/service.js'; 29 | export * from './src/services/authenticate/authenticate_translator.js'; 30 | export * from './src/services/download/download_result.js'; 31 | export * from './src/services/download/download_translator.js'; 32 | export * from './src/services/query/query_parameters.js'; 33 | export * from './src/services/query/query_result.js'; 34 | export * from './src/services/query/query_translator.js'; 35 | export * from './src/services/query/query_validator.js'; 36 | export * from './src/services/verify/verify_result.js'; 37 | export * from './src/services/verify/verify_translator.js'; 38 | export * from './src/shared/abstract_rfc_filter.js'; 39 | export * from './src/shared/code_request.js'; 40 | export * from './src/shared/complemento_cfdi.js'; 41 | export type * from './src/shared/complemento_interface.js'; 42 | export * from './src/shared/complemento_retenciones.js'; 43 | export * from './src/shared/complemento_undefined.js'; 44 | export * from './src/shared/date_time.js'; 45 | export * from './src/shared/date_time_period.js'; 46 | export * from './src/shared/document_status.js'; 47 | export * from './src/shared/document_type.js'; 48 | export * from './src/shared/download_type.js'; 49 | export * from './src/shared/enum/base_enum.js'; 50 | export * from './src/shared/request_type.js'; 51 | export * from './src/shared/rfc_match.js'; 52 | export * from './src/shared/rfc_matches.js'; 53 | export * from './src/shared/rfc_on_behalf.js'; 54 | export * from './src/shared/service_endpoints.js'; 55 | export * from './src/shared/service_type.js'; 56 | export * from './src/shared/status_code.js'; 57 | export * from './src/shared/status_request.js'; 58 | export * from './src/shared/token.js'; 59 | export * from './src/shared/uuid.js'; 60 | export * from './src/web_client/crequest.js'; 61 | export * from './src/web_client/cresponse.js'; 62 | export * from './src/web_client/exceptions/http_client_error.js'; 63 | export * from './src/web_client/exceptions/http_server_error.js'; 64 | export * from './src/web_client/exceptions/soap_fault_error.js'; 65 | export * from './src/web_client/exceptions/web_client_exception.js'; 66 | export * from './src/web_client/https_web_client.js'; 67 | export * from './src/web_client/soap_fault_info.js'; 68 | export type * from './src/web_client/web_client_interface.js'; 69 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "@nodecfdi/sat-ws-descarga-masiva", 3 | "description": "Librería para usar el servicio web del SAT de Descarga Masiva", 4 | "version": "2.0.0", 5 | "type": "module", 6 | "module": "./build/index.js", 7 | "types": "./build/index.d.ts", 8 | "files": [ 9 | "build" 10 | ], 11 | "exports": { 12 | ".": "./build/index.js", 13 | "./types": "./build/src/types.js" 14 | }, 15 | "imports": { 16 | "#src/*": "./src/*.js", 17 | "#tests/*": "./tests/*.js" 18 | }, 19 | "scripts": { 20 | "prepare": "is-in-ci || husky", 21 | "typecheck": "tsc --noEmit", 22 | "lint": "eslint . --fix", 23 | "lint:check": "eslint .", 24 | "format": "prettier --write .", 25 | "format:check": "prettier --check .", 26 | "test": "vitest", 27 | "test:run": "vitest run", 28 | "test:coverage": "vitest run --coverage", 29 | "tool:code": "pnpm run lint:check && pnpm run format:check && pnpm run typecheck", 30 | "tool:build": "pnpm run tool:code && pnpm run test:run", 31 | "clean": "del-cli build", 32 | "gen:docs": "typedoc --options typedoc.json", 33 | "changelog": "auto-changelog -p && git add CHANGELOG.md", 34 | "prebuild": "pnpm run lint:check && pnpm run typecheck", 35 | "build": "pnpm run clean && tsup", 36 | "postbuild": "pnpm run gen:docs && git add docs/*", 37 | "release": "np", 38 | "version": "pnpm run build && pnpm run changelog" 39 | }, 40 | "devDependencies": { 41 | "@commitlint/cli": "^19.8.1", 42 | "@commitlint/config-conventional": "^19.8.1", 43 | "@nodecfdi/eslint-config": "^3.3.0", 44 | "@nodecfdi/prettier-config": "^1.1.1", 45 | "@nodecfdi/tsconfig": "^1.5.0", 46 | "@peculiar/webcrypto": "^1.5.0", 47 | "@types/jsrsasign": "^10.5.14", 48 | "@types/luxon": "^3.6.2", 49 | "@types/node": "^22.15.29", 50 | "@vitest/coverage-istanbul": "^3.1.4", 51 | "auto-changelog": "^2.5.0", 52 | "del-cli": "^6.0.0", 53 | "eslint": "^9.27.0", 54 | "husky": "^9.0.11", 55 | "is-in-ci": "^1.0.0", 56 | "jest-xml-matcher": "^1.2.0", 57 | "np": "^10.2.0", 58 | "prettier": "^3.3.0", 59 | "tsup": "^8.0.2", 60 | "typedoc": "^0.28.5", 61 | "typedoc-github-theme": "^0.3.0", 62 | "typescript": "^5.8.3", 63 | "vitest": "^3.1.4", 64 | "vitest-mock-extended": "^3.1.0", 65 | "xadesjs": "^2.4.4", 66 | "xpath": "^0.0.34" 67 | }, 68 | "dependencies": { 69 | "@nodecfdi/credentials": "^3.2.0", 70 | "@nodecfdi/rfc": "^2.0.3", 71 | "jszip": "^3.10.1" 72 | }, 73 | "peerDependencies": { 74 | "@nodecfdi/cfdi-core": "^1.0.0", 75 | "luxon": "^3.6.1" 76 | }, 77 | "author": "Misael Limon ", 78 | "license": "MIT", 79 | "homepage": "https://github.com/nodecfdi/sat-ws-descarga-masiva", 80 | "repository": { 81 | "type": "git", 82 | "url": "git+https://github.com/nodecfdi/sat-ws-descarga-masiva" 83 | }, 84 | "bugs": { 85 | "url": "https://github.com/nodecfdi/sat-ws-descarga-masiva/issues" 86 | }, 87 | "keywords": [ 88 | "sat", 89 | "cfdi", 90 | "download", 91 | "descarga", 92 | "webservice" 93 | ], 94 | "engines": { 95 | "node": ">=18" 96 | }, 97 | "commitlint": { 98 | "extends": [ 99 | "@commitlint/config-conventional" 100 | ] 101 | }, 102 | "prettier": "@nodecfdi/prettier-config", 103 | "packageManager": "pnpm@10.11.0", 104 | "pnpm": { 105 | "onlyBuiltDependencies": [ 106 | "unrs-resolver" 107 | ], 108 | "ignoredBuiltDependencies": [ 109 | "esbuild" 110 | ] 111 | }, 112 | "publishConfig": { 113 | "access": "public", 114 | "tag": "latest" 115 | }, 116 | "auto-changelog": { 117 | "template": "keepachangelog", 118 | "hideCredit": true 119 | }, 120 | "np": { 121 | "message": "chore(release): :tada: %s", 122 | "tag": "latest", 123 | "branch": "main", 124 | "testScript": "test:run" 125 | }, 126 | "tsup": { 127 | "entry": [ 128 | "./index.ts", 129 | "./src/types.ts" 130 | ], 131 | "outDir": "./build", 132 | "clean": false, 133 | "format": "esm", 134 | "dts": true, 135 | "target": "esnext", 136 | "platform": "node" 137 | } 138 | } 139 | -------------------------------------------------------------------------------- /src/internal/helpers.ts: -------------------------------------------------------------------------------- 1 | /** 2 | * Helper functions used by the library. 3 | */ 4 | export class Helpers { 5 | public static nospaces(input: string): string { 6 | return input 7 | .replaceAll(/^\s*/gm, '') // A: remove horizontal spaces at beginning 8 | .replaceAll(/\s*\n/g, '') // B: remove horizontal spaces + optional CR + LF 9 | .replaceAll('?><', '?>\n<'); // C: xml definition on its own line 10 | } 11 | 12 | public static cleanPemContents(pemContents: string): string { 13 | const filteredLines = pemContents 14 | .split('\n') 15 | .filter((line: string): boolean => !line.startsWith('-----')); 16 | 17 | return filteredLines.map((line) => line.trim()).join(''); 18 | } 19 | 20 | public static htmlspecialchars(stringToReplace: string): string { 21 | return stringToReplace 22 | .replaceAll('&', '&') 23 | .replaceAll('<', '<') 24 | .replaceAll('>', '>') 25 | .replaceAll('"', '"') 26 | .replaceAll("'", '''); 27 | } 28 | } 29 | -------------------------------------------------------------------------------- /src/internal/interacts_xml_trait.ts: -------------------------------------------------------------------------------- 1 | import { type Document, type Element, getParser } from '@nodecfdi/cfdi-core'; 2 | /** 3 | * Contain functions to interact with XML contents and XML DOM 4 | * 5 | * This class is internal, do not use it outside this project 6 | */ 7 | export class InteractsXmlTrait { 8 | public readXmlDocument(source: string): Document { 9 | if (source === '') { 10 | throw new Error('Cannot load an xml with empty content'); 11 | } 12 | 13 | return getParser().parseFromString(source, 'text/xml'); 14 | } 15 | 16 | public readXmlElement(source: string): Element { 17 | const document = this.readXmlDocument(source); 18 | const element = document.documentElement!; 19 | 20 | return element; 21 | } 22 | 23 | public findElement(element: Element, ...names: string[]): Element | undefined { 24 | const first = names.shift(); 25 | const current = first ? first.toLowerCase() : ''; 26 | 27 | const children = element.childNodes; 28 | 29 | let index = 0; 30 | for (index; index < children.length; index += 1) { 31 | const child = children[index]; 32 | if (child.nodeType === child.ELEMENT_NODE) { 33 | const localName = (child as Element).localName!.toLowerCase(); 34 | if (localName === current) { 35 | return names.length > 0 36 | ? this.findElement(child as Element, ...names) 37 | : (child as Element); 38 | } 39 | } 40 | } 41 | 42 | return undefined; 43 | } 44 | 45 | public findContent(element: Element, ...names: string[]): string { 46 | const found = this.findElement(element, ...names); 47 | if (!found) { 48 | return ''; 49 | } 50 | 51 | return this.extractElementContent(found); 52 | } 53 | 54 | public extractElementContent(element: Element): string { 55 | const buffer: string[] = []; 56 | const children = element.childNodes; 57 | let index = 0; 58 | for (index; index < children.length; index += 1) { 59 | const child = children[index]; 60 | // of type Node.TEXT_NODE 61 | if (child.nodeType === 3) { 62 | const c = child; 63 | if (c.textContent !== null) { 64 | buffer.push(c.textContent); 65 | } 66 | } 67 | } 68 | 69 | return buffer.join(''); 70 | } 71 | 72 | public findElements(element: Element, ...names: string[]): Element[] { 73 | const last = names.pop(); 74 | const current = last ? last.toLowerCase() : ''; 75 | const temporaryElement = this.findElement(element, ...names); 76 | if (!temporaryElement) { 77 | return []; 78 | } 79 | 80 | const tempElement = temporaryElement; 81 | 82 | const found: Element[] = []; 83 | const children = tempElement.childNodes; 84 | let index = 0; 85 | for (index; index < children.length; index += 1) { 86 | const child = children[index]; 87 | if (child.nodeType === child.ELEMENT_NODE) { 88 | const localName = (child as Element).localName!.toLowerCase(); 89 | if (localName === current) { 90 | found.push(child as Element); 91 | } 92 | } 93 | } 94 | 95 | return found; 96 | } 97 | 98 | public findContents(element: Element, ...names: string[]): string[] { 99 | return this.findElements(element, ...names).map((elementItem) => 100 | this.extractElementContent(elementItem), 101 | ); 102 | } 103 | 104 | /** 105 | * Find the element determined by the chain of children and return the attributes as an 106 | * array using the attribute name as array key and attribute value as entry value. 107 | */ 108 | public findAtrributes(element: Element, ...search: string[]): Record { 109 | const found = this.findElement(element, ...search); 110 | if (!found) { 111 | return {}; 112 | } 113 | 114 | const attributes = new Map(); 115 | const elementAttributes = found.attributes; 116 | let index = 0; 117 | for (index; index < elementAttributes.length; index += 1) { 118 | attributes.set( 119 | elementAttributes[index].localName!.toLowerCase(), 120 | elementAttributes[index].value, 121 | ); 122 | } 123 | 124 | return Object.fromEntries(attributes) as Record; 125 | } 126 | } 127 | -------------------------------------------------------------------------------- /src/internal/service_consumer.ts: -------------------------------------------------------------------------------- 1 | import { type Token } from '#src/shared/token'; 2 | import { CRequest } from '#src/web_client/crequest'; 3 | import { type CResponse } from '#src/web_client/cresponse'; 4 | import { HttpClientError } from '#src/web_client/exceptions/http_client_error'; 5 | import { HttpServerError } from '#src/web_client/exceptions/http_server_error'; 6 | import { HttpTimeoutError } from '#src/web_client/exceptions/http_timeout_error'; 7 | import { SoapFaultError } from '#src/web_client/exceptions/soap_fault_error'; 8 | import { type WebClientException } from '#src/web_client/exceptions/web_client_exception'; 9 | import { type WebClientInterface } from '#src/web_client/web_client_interface'; 10 | import { SoapFaultInfoExtractor } from './soap_fault_info_extractor.js'; 11 | 12 | export class ServiceConsumer { 13 | public static async consume( 14 | webClient: WebClientInterface, 15 | soapAction: string, 16 | uri: string, 17 | body: string, 18 | token?: Token, 19 | ): Promise { 20 | return new ServiceConsumer().execute(webClient, soapAction, uri, body, token); 21 | } 22 | 23 | public async execute( 24 | webClient: WebClientInterface, 25 | soapAction: string, 26 | uri: string, 27 | body: string, 28 | token?: Token, 29 | ): Promise { 30 | const headers = this.createHeaders(soapAction, token); 31 | const request = this.createRequest(uri, body, headers); 32 | let exception: WebClientException | undefined; 33 | let response: CResponse; 34 | try { 35 | response = await this.runRequest(webClient, request); 36 | } catch (error) { 37 | const webError = error as WebClientException; 38 | exception = webError; 39 | response = webError.getResponse(); 40 | } 41 | 42 | this.checkErrors(request, response, exception); 43 | 44 | return response.getBody(); 45 | } 46 | 47 | public createRequest(uri: string, body: string, headers: Record): CRequest { 48 | return new CRequest('POST', uri, body, headers); 49 | } 50 | 51 | public createHeaders(soapAction: string, token?: Token): Record { 52 | const headers = new Map(); 53 | headers.set('SOAPAction', soapAction); 54 | if (token) { 55 | headers.set('Authorization', `WRAP access_token="${token.getValue()}"`); 56 | } 57 | 58 | return Object.fromEntries(headers) as Record; 59 | } 60 | 61 | public async runRequest(webClient: WebClientInterface, request: CRequest): Promise { 62 | webClient.fireRequest(request); 63 | let response: CResponse; 64 | try { 65 | response = await webClient.call(request); 66 | } catch (error) { 67 | const webError = error as WebClientException; 68 | webClient.fireResponse(webError.getResponse()); 69 | throw webError; 70 | } 71 | 72 | webClient.fireResponse(response); 73 | 74 | return response; 75 | } 76 | 77 | public checkErrors(request: CRequest, response: CResponse, exception?: Error): void { 78 | if (response.statusCodeIsTimeoutError()) { 79 | const message = `Unexpected timeout error: ${response.getBody()}`; 80 | throw new HttpTimeoutError(message, request, response, exception); 81 | } 82 | 83 | const fault = SoapFaultInfoExtractor.extract(response.getBody()); 84 | // evaluate SoapFaultInfo 85 | if (fault) { 86 | throw new SoapFaultError(request, response, fault, exception); 87 | } 88 | 89 | if (response.statusCodeIsClientError()) { 90 | const message = `Unexpected client error status code ${response.getStatusCode()}`; 91 | throw new HttpClientError(message, request, response, exception); 92 | } 93 | 94 | if (response.statusCodeIsServerError()) { 95 | const message = `Unexpected server error status code ${response.getStatusCode()}`; 96 | throw new HttpServerError(message, request, response, exception); 97 | } 98 | 99 | if (response.isEmpty()) { 100 | throw new HttpServerError( 101 | 'Unexpected empty response from server', 102 | request, 103 | response, 104 | exception, 105 | ); 106 | } 107 | } 108 | } 109 | -------------------------------------------------------------------------------- /src/internal/soap_fault_info_extractor.ts: -------------------------------------------------------------------------------- 1 | import { type Element } from '@nodecfdi/cfdi-core'; 2 | import { SoapFaultInfo } from '#src/web_client/soap_fault_info'; 3 | import { InteractsXmlTrait } from './interacts_xml_trait.js'; 4 | 5 | export class SoapFaultInfoExtractor extends InteractsXmlTrait { 6 | public static extract(source: string): SoapFaultInfo | undefined { 7 | return new SoapFaultInfoExtractor().obtainFault(source); 8 | } 9 | 10 | public obtainFault(source: string): SoapFaultInfo | undefined { 11 | let env: Element; 12 | try { 13 | env = this.readXmlElement(source); 14 | } catch { 15 | return; 16 | } 17 | const code = (this.findElement(env, 'body', 'fault', 'faultcode')?.textContent ?? '').trim(); 18 | const message = ( 19 | this.findElement(env, 'body', 'fault', 'faultstring')?.textContent ?? '' 20 | ).trim(); 21 | if (code === '' && message === '') { 22 | return; 23 | } 24 | 25 | return new SoapFaultInfo(code, message); 26 | } 27 | } 28 | -------------------------------------------------------------------------------- /src/package_reader/cfdi_package_reader.ts: -------------------------------------------------------------------------------- 1 | import { CfdiFileFilter } from './internal/file_filters/cfdi_file_filter.js'; 2 | import { FilteredPackageReader } from './internal/filtered_package_reader.js'; 3 | import { type PackageReaderInterface } from './package_reader_interface.js'; 4 | 5 | export class CfdiPackageReader implements PackageReaderInterface { 6 | public constructor(private readonly _packageReader: PackageReaderInterface) {} 7 | 8 | public static async createFromFile(filename: string): Promise { 9 | const packageReader = await FilteredPackageReader.createFromFile(filename); 10 | packageReader.setFilter(new CfdiFileFilter()); 11 | 12 | return new CfdiPackageReader(packageReader); 13 | } 14 | 15 | public static async createFromContents(contents: string): Promise { 16 | const packageReader = await FilteredPackageReader.createFromContents(contents); 17 | packageReader.setFilter(new CfdiFileFilter()); 18 | // delete temporary file 19 | await packageReader.destruct(); 20 | 21 | return new CfdiPackageReader(packageReader); 22 | } 23 | 24 | public async *cfdis(): AsyncGenerator> { 25 | for await (const content of this._packageReader.fileContents()) { 26 | let data = ''; 27 | for (const item of content) { 28 | data = item[1]; 29 | } 30 | 31 | yield new Map().set(CfdiFileFilter.obtainUuidFromXmlCfdi(data), data); 32 | } 33 | } 34 | 35 | public getFilename(): string { 36 | return this._packageReader.getFilename(); 37 | } 38 | 39 | public async count(): Promise { 40 | let count = 0; 41 | 42 | for await (const [,] of this.fileContents()) { 43 | count += 1; 44 | } 45 | 46 | return count; 47 | } 48 | 49 | public async *fileContents(): AsyncGenerator> { 50 | yield* this._packageReader.fileContents(); 51 | } 52 | 53 | public async jsonSerialize(): Promise<{ 54 | source: string; 55 | files: Record; 56 | cfdis: Record; 57 | }> { 58 | const filtered = await (this._packageReader as FilteredPackageReader).jsonSerialize(); 59 | let cfdis: Record = {}; 60 | for await (const item of this.cfdis()) { 61 | for (const [key, value] of item) { 62 | cfdis = { ...cfdis, [key]: value }; 63 | } 64 | } 65 | 66 | return { 67 | source: filtered.source, 68 | files: filtered.files, 69 | cfdis, 70 | }; 71 | } 72 | 73 | public async cfdisToArray(): Promise<{ uuid: string; content: string }[]> { 74 | const cfdis: { uuid: string; content: string }[] = []; 75 | for await (const item of this.cfdis()) { 76 | for (const [uuid, content] of item) { 77 | cfdis.push({ uuid, content }); 78 | } 79 | } 80 | 81 | return cfdis; 82 | } 83 | 84 | public async fileContentsToArray(): Promise<{ name: string; content: string }[]> { 85 | const contents: { name: string; content: string }[] = []; 86 | for await (const item of this.fileContents()) { 87 | for (const [name, content] of item) { 88 | contents.push({ name, content }); 89 | } 90 | } 91 | 92 | return contents; 93 | } 94 | } 95 | -------------------------------------------------------------------------------- /src/package_reader/exceptions/create_temporary_file_zip_exception.ts: -------------------------------------------------------------------------------- 1 | import { PackageReaderException } from './package_reader_exception.js'; 2 | 3 | export class CreateTemporaryZipFileException extends PackageReaderException { 4 | private readonly _previous?: Error; 5 | 6 | public constructor(message: string, previous?: Error) { 7 | super(message); 8 | this._previous = previous; 9 | } 10 | 11 | public static create(message: string, previous?: Error): CreateTemporaryZipFileException { 12 | const messageToSend = 13 | previous && previous.message !== '' ? `${message} : ${previous.message}` : message; 14 | 15 | return new CreateTemporaryZipFileException(messageToSend, previous); 16 | } 17 | 18 | public getPrevious(): Error | undefined { 19 | return this._previous; 20 | } 21 | } 22 | -------------------------------------------------------------------------------- /src/package_reader/exceptions/open_zip_file_exception.ts: -------------------------------------------------------------------------------- 1 | import { PackageReaderException } from './package_reader_exception.js'; 2 | 3 | export class OpenZipFileException extends PackageReaderException { 4 | private readonly _filename: string; 5 | 6 | private readonly _previous?: Error; 7 | 8 | private readonly _code: number; 9 | 10 | public constructor(message: string, code: number, filename: string, previous?: Error) { 11 | super(message); 12 | this._filename = filename; 13 | this._previous = previous; 14 | this._code = code; 15 | } 16 | 17 | public static create(filename: string, code: number, previous?: Error): OpenZipFileException { 18 | const messageToSend = 19 | previous && previous.message !== '' 20 | ? `Unable to open Zip file ${filename}. previous ${previous.message}` 21 | : `Unable to open Zip file ${filename}`; 22 | 23 | return new OpenZipFileException(messageToSend, code, filename, previous); 24 | } 25 | 26 | public getFileName(): string { 27 | return this._filename; 28 | } 29 | 30 | public getCode(): number { 31 | return this._code; 32 | } 33 | 34 | public getPrevious(): Error | undefined { 35 | return this._previous; 36 | } 37 | } 38 | -------------------------------------------------------------------------------- /src/package_reader/exceptions/package_reader_exception.ts: -------------------------------------------------------------------------------- 1 | export abstract class PackageReaderException extends Error {} 2 | -------------------------------------------------------------------------------- /src/package_reader/internal/csv_reader.ts: -------------------------------------------------------------------------------- 1 | import { randomUUID } from 'node:crypto'; 2 | import { createReadStream, realpathSync, writeFileSync } from 'node:fs'; 3 | import os from 'node:os'; 4 | import path from 'node:path'; 5 | import * as readline from 'node:readline'; 6 | 7 | export type ReadLineInterface = { 8 | [Symbol.asyncIterator](): AsyncIterableIterator; 9 | }; 10 | 11 | /** 12 | * Helper to iterate inside a CSV file 13 | * The file must have on the first line the headers. 14 | * The file uses "~" as separator and "|" as text delimiter. 15 | */ 16 | export class CsvReader { 17 | public constructor(private readonly _iterator: ReadLineInterface) {} 18 | 19 | public static createIteratorFromContents(contents: string): ReadLineInterface { 20 | const tmpdir = realpathSync(os.tmpdir()); 21 | const filePath = path.join(tmpdir, `${randomUUID()}.csv`); 22 | writeFileSync(filePath, contents); 23 | 24 | const iterator = readline.createInterface({ 25 | input: createReadStream(filePath), 26 | crlfDelay: Number.POSITIVE_INFINITY, 27 | }); 28 | 29 | return iterator; 30 | } 31 | 32 | public static createFromContents(contents: string): CsvReader { 33 | return new CsvReader(CsvReader.createIteratorFromContents(contents)); 34 | } 35 | 36 | public async *records(): AsyncGenerator> { 37 | const headers: string[] = []; 38 | for await (const line of this._iterator) { 39 | const clean = line.split(/[|~]/).map((item) => item.trim()); 40 | 41 | if (clean.length === 0 || JSON.stringify(clean) === '[""]') { 42 | continue; 43 | } 44 | 45 | if (headers.length === 0) { 46 | headers.push(...clean); 47 | continue; 48 | } 49 | 50 | yield this.combine(headers, clean); 51 | } 52 | } 53 | 54 | /** 55 | * Like array.concat but complement missing values or missing keys (#extra-01, #extra-02, etc...) 56 | */ 57 | public combine(keys: string[], values: string[]): Record { 58 | const countValues = values.length; 59 | const countKeys = keys.length; 60 | let newValues = values; 61 | if (countKeys > countValues) { 62 | const emptyArray: string[] = Array.from({ length: countKeys - countValues }); 63 | newValues = [...values, ...emptyArray.fill('')]; 64 | } 65 | 66 | if (countValues > countKeys) { 67 | for (let i = 1; i <= countValues - countKeys; i += 1) { 68 | const string_ = i.toString().padStart(2, '0'); 69 | keys.push(`#extra-${string_}`); 70 | } 71 | } 72 | 73 | const map = new Map(); 74 | for (const [index, value] of newValues.entries()) { 75 | map.set(keys[index], value); 76 | } 77 | 78 | return Object.fromEntries(map) as Record; 79 | } 80 | } 81 | -------------------------------------------------------------------------------- /src/package_reader/internal/file_filters/cfdi_file_filter.ts: -------------------------------------------------------------------------------- 1 | import { type FileFilterInterface } from './file_filter_interface.js'; 2 | 3 | export class CfdiFileFilter implements FileFilterInterface { 4 | public static obtainUuidFromXmlCfdi(xmlContent: string): string { 5 | const pattern = /:Complemento.*?:TimbreFiscalDigital.*?UUID="(?[\dA-Za-z-]{36})"/s; 6 | const found = pattern.exec(xmlContent); 7 | if (found?.groups?.uuid) { 8 | return found.groups.uuid.toLowerCase(); 9 | } 10 | 11 | return ''; 12 | } 13 | 14 | public filterFilename(filename: string): boolean { 15 | return /^[^/\\]+\.xml/i.test(filename); 16 | } 17 | 18 | public filterContents(contents: string): boolean { 19 | return CfdiFileFilter.obtainUuidFromXmlCfdi(contents) !== ''; 20 | } 21 | } 22 | -------------------------------------------------------------------------------- /src/package_reader/internal/file_filters/file_filter_interface.ts: -------------------------------------------------------------------------------- 1 | /** 2 | * Filter by filename or content contract 3 | */ 4 | export type FileFilterInterface = { 5 | /** 6 | * Filter the file name 7 | */ 8 | filterFilename(filename: string): boolean; 9 | 10 | /** 11 | * Filter the contents 12 | * 13 | * @param contents - Content 14 | * @returns boolean 15 | */ 16 | filterContents(contents: string): boolean; 17 | }; 18 | -------------------------------------------------------------------------------- /src/package_reader/internal/file_filters/metadata_file_filter.ts: -------------------------------------------------------------------------------- 1 | import { type FileFilterInterface } from './file_filter_interface.js'; 2 | /** 3 | * Implementation to filter a Metadata Package file contents 4 | * 5 | */ 6 | export class MetadataFileFilter implements FileFilterInterface { 7 | public filterFilename(filename: string): boolean { 8 | return /^[^/\\]+\.txt/i.test(filename); 9 | } 10 | 11 | public filterContents(contents: string): boolean { 12 | return contents.startsWith('Uuid~RfcEmisor~'); 13 | } 14 | } 15 | -------------------------------------------------------------------------------- /src/package_reader/internal/file_filters/null_file_filter.ts: -------------------------------------------------------------------------------- 1 | import { type FileFilterInterface } from './file_filter_interface.js'; 2 | /** 3 | * NullObject patern, it does not filter any file contents 4 | */ 5 | export class NullFileFilter implements FileFilterInterface { 6 | public filterFilename(_filename: string): boolean { 7 | return true; 8 | } 9 | 10 | public filterContents(_contents: string): boolean { 11 | return true; 12 | } 13 | } 14 | -------------------------------------------------------------------------------- /src/package_reader/internal/file_filters/third_parties_file_filter.ts: -------------------------------------------------------------------------------- 1 | import { type FileFilterInterface } from './file_filter_interface.js'; 2 | /** 3 | * Implementation to filter a Metadata Package file contents 4 | */ 5 | export class ThirdPartiesFileFilter implements FileFilterInterface { 6 | public filterFilename(filename: string): boolean { 7 | return /^[^/\\]+_tercero\.txt/i.test(filename); 8 | } 9 | 10 | public filterContents(contents: string): boolean { 11 | return contents.startsWith('Uuid~RfcACuentaTerceros~NombreACuentaTerceros'); 12 | } 13 | } 14 | -------------------------------------------------------------------------------- /src/package_reader/internal/filtered_package_reader.ts: -------------------------------------------------------------------------------- 1 | import { randomUUID } from 'node:crypto'; 2 | import { readFile, realpath, unlink, writeFile } from 'node:fs/promises'; 3 | import os from 'node:os'; 4 | import path from 'node:path'; 5 | import JSZip from 'jszip'; 6 | import { CreateTemporaryZipFileException } from '../exceptions/create_temporary_file_zip_exception.js'; 7 | import { OpenZipFileException } from '../exceptions/open_zip_file_exception.js'; 8 | import { type PackageReaderInterface } from '../package_reader_interface.js'; 9 | import { type FileFilterInterface } from './file_filters/file_filter_interface.js'; 10 | import { NullFileFilter } from './file_filters/null_file_filter.js'; 11 | 12 | export class FilteredPackageReader implements PackageReaderInterface { 13 | private readonly _filename: string; 14 | 15 | private readonly _archive: JSZip; 16 | 17 | private _removeOnDestruct = false; 18 | 19 | private _filter!: FileFilterInterface; 20 | 21 | public constructor(filename: string, archive: JSZip) { 22 | this._filename = filename; 23 | this._archive = archive; 24 | } 25 | 26 | public static async createFromFile(filename: string): Promise { 27 | let archive: JSZip; 28 | let data: Buffer; 29 | try { 30 | // if is directory fails in windows, linux and mac, not fails in BSD 31 | data = await readFile(filename); 32 | } catch { 33 | throw OpenZipFileException.create(filename, -1); 34 | } 35 | 36 | try { 37 | // eslint-disable-next-line sonarjs/no-unsafe-unzip 38 | archive = await JSZip.loadAsync(data); 39 | } catch { 40 | throw OpenZipFileException.create(filename, -1); 41 | } 42 | 43 | return new FilteredPackageReader(filename, archive); 44 | } 45 | 46 | public static async createFromContents(contents: string): Promise { 47 | const tmpdir = await realpath(os.tmpdir()); 48 | const tmpfile = path.join(tmpdir, `${randomUUID()}.zip`); 49 | // create temp file 50 | try { 51 | await writeFile(tmpfile, ''); 52 | } catch (error) { 53 | throw CreateTemporaryZipFileException.create( 54 | 'Cannot create a temporary file', 55 | error as Error, 56 | ); 57 | } 58 | 59 | // write contents 60 | try { 61 | await writeFile(tmpfile, contents, { encoding: 'binary' }); 62 | } catch (error) { 63 | throw CreateTemporaryZipFileException.create( 64 | 'Cannot store contents on temporary file', 65 | error as Error, 66 | ); 67 | } 68 | 69 | let cpackage: FilteredPackageReader; 70 | // build object 71 | try { 72 | cpackage = await FilteredPackageReader.createFromFile(tmpfile); 73 | } catch (error) { 74 | await unlink(tmpfile); 75 | throw error; 76 | } 77 | 78 | cpackage._removeOnDestruct = true; 79 | 80 | return cpackage; 81 | } 82 | 83 | public async destruct(): Promise { 84 | if (this._removeOnDestruct) { 85 | await unlink(this._filename); 86 | } 87 | } 88 | 89 | public async *fileContents(): AsyncGenerator> { 90 | const archive = this.getArchive(); 91 | const filter = this.getFilter(); 92 | const entries = Object.keys(archive.files).map((name) => archive.files[name].name); 93 | let contents: string | undefined; 94 | 95 | for (const entry of entries) { 96 | if (!filter.filterFilename(entry)) { 97 | continue; 98 | } 99 | 100 | contents = await archive.file(entry)?.async('text'); 101 | if (contents === undefined || !filter.filterContents(contents)) { 102 | continue; 103 | } 104 | 105 | yield new Map().set(entry, contents || ''); 106 | } 107 | } 108 | 109 | public async count(): Promise { 110 | let count = 0; 111 | for await (const [,] of this.fileContents()) { 112 | count += 1; 113 | } 114 | 115 | return count; 116 | } 117 | 118 | public getFilename(): string { 119 | return this._filename; 120 | } 121 | 122 | public getArchive(): JSZip { 123 | return this._archive; 124 | } 125 | 126 | public getFilter(): FileFilterInterface { 127 | return this._filter; 128 | } 129 | 130 | public setFilter(filter?: FileFilterInterface): void { 131 | this._filter = filter ?? new NullFileFilter(); 132 | } 133 | 134 | public changeFilter(filter: FileFilterInterface): FileFilterInterface { 135 | const previous = this.getFilter(); 136 | this.setFilter(filter); 137 | 138 | return previous; 139 | } 140 | 141 | public async jsonSerialize(): Promise<{ 142 | source: string; 143 | files: Record; 144 | }> { 145 | let files: Record = {}; 146 | for await (const item of this.fileContents()) { 147 | for (const [key, value] of item) { 148 | files = { ...files, [key]: value }; 149 | } 150 | } 151 | 152 | return { 153 | source: this.getFilename(), 154 | files, 155 | }; 156 | } 157 | } 158 | -------------------------------------------------------------------------------- /src/package_reader/internal/metadata_content.ts: -------------------------------------------------------------------------------- 1 | import { MetadataItem } from '../metadata_item.js'; 2 | import { CsvReader } from './csv_reader.js'; 3 | import { MetadataPreprocessor } from './metadata_preprocessor.js'; 4 | import { ThirdPartiesRecords } from './third_parties_records.js'; 5 | 6 | export class MetadataContent { 7 | /** 8 | * The iterator will be used in a foreach loop to create MetadataItems 9 | * The first iteration must contain an array of header names that will be renames to lower case first letter 10 | * The next iterations must contain an array with data 11 | */ 12 | public constructor( 13 | private readonly _csvReader: CsvReader, 14 | private readonly _thirdParties: ThirdPartiesRecords, 15 | ) {} 16 | 17 | /** 18 | * This method fix the content and create a SplTempFileObject to store the information 19 | */ 20 | public static createFromContents( 21 | contents: string, 22 | thirdParties?: ThirdPartiesRecords, 23 | ): MetadataContent { 24 | const defaultThirdParties = thirdParties ?? ThirdPartiesRecords.createEmpty(); 25 | // fix known errors on metadata text file 26 | const preprocessor = new MetadataPreprocessor(contents); 27 | preprocessor.fix(); 28 | 29 | const csvReader = CsvReader.createFromContents(preprocessor.getContents()); 30 | 31 | return new MetadataContent(csvReader, defaultThirdParties); 32 | } 33 | 34 | public async *eachItem(): AsyncGenerator { 35 | for await (const data of this._csvReader.records()) { 36 | yield new MetadataItem( 37 | this.changeArrayKeysFirstLetterLoweCase(this._thirdParties.addToData(data)), 38 | ); 39 | } 40 | } 41 | 42 | private changeArrayKeysFirstLetterLoweCase(data: Record): Record { 43 | for (const [key, value] of Object.entries(data)) { 44 | const newKey = key.charAt(0).toLowerCase() + key.slice(1); 45 | data[newKey] = value; 46 | if (key !== newKey) { 47 | // eslint-disable-next-line @typescript-eslint/no-dynamic-delete 48 | delete data[key]; 49 | } 50 | } 51 | 52 | return data; 53 | } 54 | } 55 | -------------------------------------------------------------------------------- /src/package_reader/internal/metadata_preprocessor.ts: -------------------------------------------------------------------------------- 1 | /** 2 | * This preprocesor fixes metadata issues: 3 | * - SAT CSV EOL is and might contain inside a field 4 | * 5 | * @see MetadataContent 6 | */ 7 | export class MetadataPreprocessor { 8 | /** The data to process */ 9 | private _contents: string; 10 | 11 | public constructor(contents: string) { 12 | this._contents = contents; 13 | } 14 | 15 | private readonly CONTROL_CR = '\r'; 16 | 17 | private readonly CONTROL_LF = '\n'; 18 | 19 | private readonly CONTROL_CRLF = '\r\n'; 20 | 21 | public getContents(): string { 22 | return this._contents; 23 | } 24 | 25 | public fix(): void { 26 | this.fixEolCrLf(); 27 | } 28 | 29 | public fixEolCrLf(): void { 30 | // check if EOL is 31 | const firstLineFeedPosition = this._contents.indexOf(this.CONTROL_LF); 32 | let eolIsCrLf: boolean; 33 | if (firstLineFeedPosition === -1) { 34 | eolIsCrLf = false; 35 | } else { 36 | eolIsCrLf = 37 | firstLineFeedPosition > 0 38 | ? this._contents.slice(firstLineFeedPosition - 1, firstLineFeedPosition) === 39 | this.CONTROL_CR 40 | : !this._contents.includes(this.CONTROL_CR); 41 | } 42 | 43 | // exit early if nothing to do 44 | if (!eolIsCrLf) { 45 | return; 46 | } 47 | 48 | const lines = this._contents.split(this.CONTROL_CRLF); 49 | this._contents = lines 50 | .map((line) => line.replaceAll(new RegExp(/\n/, 'g'), '')) 51 | .join(this.CONTROL_LF); 52 | } 53 | } 54 | -------------------------------------------------------------------------------- /src/package_reader/internal/third_parties_extractor.ts: -------------------------------------------------------------------------------- 1 | import { type PackageReaderInterface } from '../package_reader_interface.js'; 2 | import { CsvReader } from './csv_reader.js'; 3 | import { ThirdPartiesFileFilter } from './file_filters/third_parties_file_filter.js'; 4 | import { FilteredPackageReader } from './filtered_package_reader.js'; 5 | 6 | export type ThirdPartiesInterface = { 7 | RfcACuentaTerceros: string; 8 | NombreACuentaTerceros: string; 9 | }; 10 | 11 | /** 12 | * Class to extract the data from a "third parties" file. 13 | */ 14 | export class ThirdPartiesExtractor { 15 | public constructor(private readonly _csvReader: CsvReader) {} 16 | 17 | public static async createFromPackageReader( 18 | packageReader: PackageReaderInterface, 19 | ): Promise { 20 | if (!(packageReader instanceof FilteredPackageReader)) { 21 | throw new TypeError('PackageReader parameter must be a FilteredPackageReader'); 22 | } 23 | 24 | const previousFilter = packageReader.changeFilter(new ThirdPartiesFileFilter()); 25 | let contents = ''; 26 | // eslint-disable-next-line no-unreachable-loop 27 | for await (const fileContents of packageReader.fileContents()) { 28 | for (const item of fileContents) { 29 | contents = item[1]; 30 | } 31 | 32 | break; 33 | } 34 | 35 | packageReader.setFilter(previousFilter); 36 | 37 | return new ThirdPartiesExtractor(CsvReader.createFromContents(contents)); 38 | } 39 | 40 | public async *eachRecord(): AsyncGenerator> { 41 | let uuid: string; 42 | for await (const data of this._csvReader.records()) { 43 | uuid = data.Uuid.toUpperCase(); 44 | if (uuid === '') { 45 | continue; 46 | } 47 | 48 | const value = { 49 | RfcACuentaTerceros: data.RfcACuentaTerceros, 50 | NombreACuentaTerceros: data.NombreACuentaTerceros, 51 | }; 52 | yield new Map().set(uuid, value); 53 | } 54 | } 55 | } 56 | -------------------------------------------------------------------------------- /src/package_reader/internal/third_parties_records.ts: -------------------------------------------------------------------------------- 1 | import { type PackageReaderInterface } from '#src/package_reader/package_reader_interface'; 2 | import { ThirdPartiesExtractor, type ThirdPartiesInterface } from './third_parties_extractor.js'; 3 | 4 | export class ThirdPartiesRecords { 5 | public constructor( 6 | private readonly _records: Record, 7 | ) {} 8 | 9 | public static createEmpty(): ThirdPartiesRecords { 10 | return new ThirdPartiesRecords({}); 11 | } 12 | 13 | public static async createFromPackageReader( 14 | packageReader: PackageReaderInterface, 15 | ): Promise { 16 | const thirdPartiesBuilder = await ThirdPartiesExtractor.createFromPackageReader(packageReader); 17 | const records: Record = {}; 18 | for await (const iterator of thirdPartiesBuilder.eachRecord()) { 19 | for (const [key, value] of iterator) { 20 | records[ThirdPartiesRecords.formatUuid(key)] = value; 21 | } 22 | } 23 | 24 | return new ThirdPartiesRecords(records); 25 | } 26 | 27 | private static formatUuid(uuid: string): string { 28 | return uuid.toLowerCase(); 29 | } 30 | 31 | public addToData(data: Record): Record { 32 | const uuid = data.Uuid ?? ''; 33 | const values = this.getDataFromUuid(uuid); 34 | 35 | return { ...data, ...values }; 36 | } 37 | 38 | public getDataFromUuid(uuid: string): ThirdPartiesInterface { 39 | const defaultValue = { 40 | RfcACuentaTerceros: '', 41 | NombreACuentaTerceros: '', 42 | }; 43 | 44 | return this._records[ThirdPartiesRecords.formatUuid(uuid)] ?? defaultValue; 45 | } 46 | } 47 | -------------------------------------------------------------------------------- /src/package_reader/metadata_item.ts: -------------------------------------------------------------------------------- 1 | import { type MetadataItemInterface } from './metadata_item_interface.js'; 2 | 3 | /** 4 | * Metadata DTO object 5 | * 6 | * This collection of magic properties is reported as of 2019-08-01, if it changes use all()/get() methods 7 | * 8 | * - property-read string uuid 9 | * - property-read string rfcEmisor 10 | * - property-read string nombreEmisor 11 | * - property-read string rfcReceptor 12 | * - property-read string nombreReceptor 13 | * - property-read string rfcPac 14 | * - property-read string fechaEmision 15 | * - property-read string fechaCertificacionSat 16 | * - property-read string monto 17 | * - property-read string efectoComprobante 18 | * - property-read string estatus 19 | * - property-read string fechaCancelacion 20 | * - property-read string rfcACuentaTerceros 21 | * - property-read string nombreACuentaTerceros 22 | */ 23 | export class MetadataItem { 24 | private readonly _data: MetadataItemInterface[]; 25 | 26 | public constructor(data: Record) { 27 | this._data = Object.entries(data).map(([key, value]) => ({ 28 | key, 29 | value, 30 | })); 31 | } 32 | 33 | public get(key: string): string { 34 | return this._data.find((item) => item.key === key)?.value ?? ''; 35 | } 36 | 37 | /** 38 | * 39 | * returns all keys and values in a record form. 40 | */ 41 | public all(): Record { 42 | return Object.fromEntries(this._data.map((current) => [current.key, current.value])); 43 | } 44 | 45 | public [Symbol.iterator](): IterableIterator { 46 | return this._data[Symbol.iterator](); 47 | } 48 | } 49 | -------------------------------------------------------------------------------- /src/package_reader/metadata_item_interface.ts: -------------------------------------------------------------------------------- 1 | export type MetadataItemInterface = { 2 | key: string; 3 | value: string; 4 | }; 5 | -------------------------------------------------------------------------------- /src/package_reader/metadata_package_reader.ts: -------------------------------------------------------------------------------- 1 | import { MetadataFileFilter } from './internal/file_filters/metadata_file_filter.js'; 2 | import { FilteredPackageReader } from './internal/filtered_package_reader.js'; 3 | import { MetadataContent } from './internal/metadata_content.js'; 4 | import { ThirdPartiesRecords } from './internal/third_parties_records.js'; 5 | import { type MetadataItem } from './metadata_item.js'; 6 | import { type PackageReaderInterface } from './package_reader_interface.js'; 7 | 8 | export class MetadataPackageReader implements PackageReaderInterface { 9 | public constructor( 10 | private readonly _packageReader: PackageReaderInterface, 11 | private _thirdParties?: ThirdPartiesRecords, 12 | ) {} 13 | 14 | public static async createFromFile(fileName: string): Promise { 15 | const packageReader = await FilteredPackageReader.createFromFile(fileName); 16 | packageReader.setFilter(new MetadataFileFilter()); 17 | 18 | const thirdParties = await ThirdPartiesRecords.createFromPackageReader(packageReader); 19 | 20 | return new MetadataPackageReader(packageReader, thirdParties); 21 | } 22 | 23 | public static async createFromContents(contents: string): Promise { 24 | const packageReader = await FilteredPackageReader.createFromContents(contents); 25 | packageReader.setFilter(new MetadataFileFilter()); 26 | // delete temporary file 27 | await packageReader.destruct(); 28 | 29 | const thirdParties = await ThirdPartiesRecords.createFromPackageReader(packageReader); 30 | 31 | return new MetadataPackageReader(packageReader, thirdParties); 32 | } 33 | 34 | public async getThirdParties(): Promise { 35 | this._thirdParties = 36 | this._thirdParties ?? 37 | (await ThirdPartiesRecords.createFromPackageReader(this._packageReader)); 38 | 39 | return this._thirdParties; 40 | } 41 | 42 | public async *metadata(): AsyncGenerator { 43 | let reader: MetadataContent; 44 | for await (const content of this._packageReader.fileContents()) { 45 | const parties = await this.getThirdParties(); 46 | for (const [, value] of content) { 47 | reader = MetadataContent.createFromContents(value, parties); 48 | for await (const item of reader.eachItem()) { 49 | yield item; 50 | } 51 | } 52 | } 53 | } 54 | 55 | public getFilename(): string { 56 | return this._packageReader.getFilename(); 57 | } 58 | 59 | public async count(): Promise { 60 | let count = 0; 61 | for await (const [,] of this.fileContents()) { 62 | count += 1; 63 | } 64 | 65 | return count; 66 | } 67 | 68 | public async *fileContents(): AsyncGenerator> { 69 | yield* this._packageReader.fileContents(); 70 | } 71 | 72 | public async jsonSerialize(): Promise<{ 73 | source: string; 74 | files: Record; 75 | metadata: Record>; 76 | }> { 77 | const filtered = await (this._packageReader as FilteredPackageReader).jsonSerialize(); 78 | 79 | let metadata: Record> = {}; 80 | 81 | for await (const iterator of this.metadata()) { 82 | metadata = { ...metadata, [iterator.get('uuid')]: iterator.all() }; 83 | } 84 | 85 | return { 86 | source: filtered.source, 87 | files: filtered.files, 88 | metadata, 89 | }; 90 | } 91 | 92 | public async metadataToArray(): Promise { 93 | const content = []; 94 | for await (const iterator of this.metadata()) { 95 | content.push(iterator); 96 | } 97 | 98 | return content; 99 | } 100 | } 101 | -------------------------------------------------------------------------------- /src/package_reader/package_reader_interface.ts: -------------------------------------------------------------------------------- 1 | /** 2 | * Expected behavior of a PackageReader contract 3 | */ 4 | export type PackageReaderInterface = { 5 | /** 6 | * Traverse each file inside the package, with the filename as key and file content as value 7 | */ 8 | fileContents(): AsyncGenerator>; 9 | /** 10 | * Return the number of elements on the package 11 | */ 12 | count(): Promise; 13 | /** 14 | * Retrieve the currently open file name 15 | */ 16 | getFilename(): string; 17 | }; 18 | -------------------------------------------------------------------------------- /src/request_builder/fiel_request_builder/fiel.ts: -------------------------------------------------------------------------------- 1 | import { Credential } from '@nodecfdi/credentials/node'; 2 | 3 | /** 4 | * Defines a eFirma/FIEL/FEA 5 | * This object is based on nodecfdi/credentials Credential object 6 | * 7 | * @see Credential 8 | */ 9 | export class Fiel { 10 | public constructor(private readonly _credential: Credential) {} 11 | 12 | /** 13 | * Create a Fiel based on certificate and private key contents 14 | */ 15 | public static create( 16 | certificateContents: string, 17 | privateKeyContents: string, 18 | passPhrase: string, 19 | ): Fiel { 20 | const credential = Credential.create(certificateContents, privateKeyContents, passPhrase); 21 | 22 | return new Fiel(credential); 23 | } 24 | 25 | public sign(toSign: string, algorithm: 'md5' | 'sha1' | 'sha256' | 'sha384' | 'sha512'): string { 26 | return this._credential.sign(toSign, algorithm); 27 | } 28 | 29 | public isValid(): boolean { 30 | if (!this._credential.certificate().satType().isFiel()) { 31 | return false; 32 | } 33 | 34 | return this._credential.certificate().validOn(); 35 | } 36 | 37 | public getCertificatePemContents(): string { 38 | return this._credential.certificate().pem(); 39 | } 40 | 41 | public getRfc(): string { 42 | return this._credential.rfc(); 43 | } 44 | 45 | public getCertificateSerial(): string { 46 | return this._credential.certificate().serialNumber().decimal(); 47 | } 48 | 49 | /** missing function this.credential.certificate().issuerAsRfc4514() */ 50 | public getCertificateIssuerName(): string { 51 | return this._credential.certificate().issuerAsRfc4514(); 52 | } 53 | } 54 | -------------------------------------------------------------------------------- /src/request_builder/request_builder_exception.ts: -------------------------------------------------------------------------------- 1 | export abstract class RequestBuilderException extends Error {} 2 | -------------------------------------------------------------------------------- /src/request_builder/request_builder_interface.ts: -------------------------------------------------------------------------------- 1 | import { type QueryParameters } from '#src/services/query/query_parameters'; 2 | import { type DateTime } from '#src/shared/date_time'; 3 | /** 4 | * The implementors must create the request signed ready to send to the SAT Web Service Descarga Masiva 5 | * The information about owner like RFC, certificate, private key, etc. are outside the scope of this interface 6 | */ 7 | export type RequestBuilderInterface = { 8 | /** 9 | * Creates an authorization signed xml message 10 | * 11 | * @param created - must use SAT format 'Y-m-dTH:i:s.000T' 12 | * @param expires - must use SAT format 'Y-m-dTH:i:s.000T' 13 | * @param securityTokenId - if empty, the authentication method will create one by its own 14 | */ 15 | authorization(created: DateTime, expires: DateTime, securityTokenId: string): string; 16 | 17 | /** 18 | * Creates a query signed xml message 19 | * 20 | * @throws RequestBuilderException 21 | */ 22 | query(queryParameters: QueryParameters): string; 23 | 24 | /** 25 | * Creates a verify signed xml message 26 | * 27 | * @throws RequestBuilderException 28 | */ 29 | verify(requestId: string): string; 30 | 31 | /** 32 | * Creates a download signed xml message 33 | * 34 | * @throws RequestBuilderException 35 | */ 36 | download(packageId: string): string; 37 | }; 38 | -------------------------------------------------------------------------------- /src/service.ts: -------------------------------------------------------------------------------- 1 | import { ServiceConsumer } from './internal/service_consumer.js'; 2 | import { type RequestBuilderInterface } from './request_builder/request_builder_interface.js'; 3 | import { AuthenticateTranslator } from './services/authenticate/authenticate_translator.js'; 4 | import { type DownloadResult } from './services/download/download_result.js'; 5 | import { DownloadTranslator } from './services/download/download_translator.js'; 6 | import { type QueryParameters } from './services/query/query_parameters.js'; 7 | import { type QueryResult } from './services/query/query_result.js'; 8 | import { QueryTranslator } from './services/query/query_translator.js'; 9 | import { type VerifyResult } from './services/verify/verify_result.js'; 10 | import { VerifyTranslator } from './services/verify/verify_translator.js'; 11 | import { ServiceEndpoints } from './shared/service_endpoints.js'; 12 | import { Token } from './shared/token.js'; 13 | import { type WebClientInterface } from './web_client/web_client_interface.js'; 14 | 15 | export class Service { 16 | public readonly endpoints: ServiceEndpoints; 17 | 18 | private _currentToken: Token; 19 | 20 | /** 21 | * Client constructor of "servicio de consulta y recuperación de comprobantes" 22 | * 23 | * @param endpoints - endpoints If undefined uses CFDI endpoints 24 | */ 25 | public constructor( 26 | private readonly _requestBuilder: RequestBuilderInterface, 27 | private readonly _webClient: WebClientInterface, 28 | _currentToken: Token | null = null, 29 | endpoints: ServiceEndpoints | null = null, 30 | ) { 31 | this._currentToken = _currentToken ?? Token.empty(); 32 | this.endpoints = endpoints ?? ServiceEndpoints.cfdi(); 33 | } 34 | 35 | /** 36 | * This method will reuse the current token, 37 | * it will create a new one if there is none or the current token is no longer valid 38 | */ 39 | public async obtainCurrentToken(): Promise { 40 | if (!this._currentToken.isValid()) { 41 | this._currentToken = await this.authenticate(); 42 | } 43 | 44 | return this._currentToken; 45 | } 46 | 47 | public getToken(): Token { 48 | return this._currentToken; 49 | } 50 | 51 | public setToken(token: Token): void { 52 | this._currentToken = token; 53 | } 54 | 55 | /** 56 | * Perform authentication and return a Token, the token might be invalid 57 | */ 58 | public async authenticate(): Promise { 59 | const authenticateTranslator = new AuthenticateTranslator(); 60 | const soapBody = authenticateTranslator.createSoapRequest(this._requestBuilder); 61 | const responseBody = await this.consume( 62 | 'http://DescargaMasivaTerceros.gob.mx/IAutenticacion/Autentica', 63 | this.endpoints.getAuthenticate(), 64 | soapBody, 65 | ); 66 | 67 | return authenticateTranslator.createTokenFromSoapResponse(responseBody); 68 | } 69 | 70 | /** 71 | * Consume the "SolicitaDescarga" web service 72 | */ 73 | public async query(parameters: QueryParameters): Promise { 74 | let defaultParameters = parameters; 75 | 76 | if (!this.endpoints.getServiceType().equalTo(defaultParameters.getServiceType())) { 77 | defaultParameters = defaultParameters.withServiceType(this.endpoints.getServiceType()); 78 | } 79 | 80 | const queryTranslator = new QueryTranslator(); 81 | const soapBody = queryTranslator.createSoapRequest(this._requestBuilder, defaultParameters); 82 | const soapAction = this.resolveSoapAction(defaultParameters); 83 | 84 | const currentToken = await this.obtainCurrentToken(); 85 | const responseBody = await this.consume( 86 | `http://DescargaMasivaTerceros.sat.gob.mx/ISolicitaDescargaService/${soapAction}`, 87 | this.endpoints.getQuery(), 88 | soapBody, 89 | currentToken, 90 | ); 91 | 92 | return queryTranslator.createQueryResultFromSoapResponse(responseBody); 93 | } 94 | 95 | private resolveSoapAction(parameters: QueryParameters): string { 96 | if (!parameters.getUuid().isEmpty()) { 97 | return 'SolicitaDescargaFolio'; 98 | } 99 | 100 | return parameters.getDownloadType().isTypeOf('received') 101 | ? 'SolicitaDescargaRecibidos' 102 | : 'SolicitaDescargaEmitidos'; 103 | } 104 | 105 | /** 106 | * Consume the "VerificaSolicitudDescarga" web service 107 | */ 108 | public async verify(requestId: string): Promise { 109 | const verifyTranslator = new VerifyTranslator(); 110 | const soapBody = verifyTranslator.createSoapRequest(this._requestBuilder, requestId); 111 | const currentToken = await this.obtainCurrentToken(); 112 | const responseBody = await this.consume( 113 | 'http://DescargaMasivaTerceros.sat.gob.mx/IVerificaSolicitudDescargaService/VerificaSolicitudDescarga', 114 | this.endpoints.getVerify(), 115 | soapBody, 116 | currentToken, 117 | ); 118 | 119 | return verifyTranslator.createVerifyResultFromSoapResponse(responseBody); 120 | } 121 | 122 | public async download(packageId: string): Promise { 123 | const downloadTranslator = new DownloadTranslator(); 124 | const soapBody = downloadTranslator.createSoapRequest(this._requestBuilder, packageId); 125 | const currentToken = await this.obtainCurrentToken(); 126 | const responseBody = await this.consume( 127 | 'http://DescargaMasivaTerceros.sat.gob.mx/IDescargaMasivaTercerosService/Descargar', 128 | this.endpoints.getDownload(), 129 | soapBody, 130 | currentToken, 131 | ); 132 | 133 | return downloadTranslator.createDownloadResultFromSoapResponse(responseBody); 134 | } 135 | 136 | private async consume( 137 | soapAction: string, 138 | uri: string, 139 | body: string, 140 | token?: Token, 141 | ): Promise { 142 | return ServiceConsumer.consume(this._webClient, soapAction, uri, body, token); 143 | } 144 | } 145 | -------------------------------------------------------------------------------- /src/services/authenticate/authenticate_translator.ts: -------------------------------------------------------------------------------- 1 | import { InteractsXmlTrait } from '#src/internal/interacts_xml_trait'; 2 | import { type RequestBuilderInterface } from '#src/request_builder/request_builder_interface'; 3 | import { DateTime } from '#src/shared/date_time'; 4 | import { Token } from '#src/shared/token'; 5 | 6 | export class AuthenticateTranslator extends InteractsXmlTrait { 7 | public createTokenFromSoapResponse(content: string): Token { 8 | const env = this.readXmlElement(content); 9 | let timeContent = this.findContent(env, 'header', 'security', 'timestamp', 'created'); 10 | const created = DateTime.create(timeContent === '' ? 0 : timeContent); 11 | timeContent = this.findContent(env, 'header', 'security', 'timestamp', 'expires'); 12 | const expires = DateTime.create(timeContent === '' ? 0 : timeContent); 13 | const value = this.findContent(env, 'body', 'autenticaResponse', 'autenticaResult'); 14 | 15 | return new Token(created, expires, value); 16 | } 17 | 18 | public createSoapRequest(requestBuilder: RequestBuilderInterface): string { 19 | const since = DateTime.now(); 20 | const until = since.modify({ minutes: 5 }); 21 | 22 | return this.createSoapRequestWithData(requestBuilder, since, until); 23 | } 24 | 25 | public createSoapRequestWithData( 26 | requestBuilder: RequestBuilderInterface, 27 | since: DateTime, 28 | until: DateTime, 29 | securityToken = '', 30 | ): string { 31 | return requestBuilder.authorization(since, until, securityToken); 32 | } 33 | } 34 | -------------------------------------------------------------------------------- /src/services/download/download_result.ts: -------------------------------------------------------------------------------- 1 | import { type StatusCode } from '#src/shared/status_code'; 2 | 3 | export class DownloadResult { 4 | private readonly _packageSize: number; 5 | 6 | public constructor( 7 | private readonly _status: StatusCode, 8 | private readonly _packageContent: string, 9 | ) { 10 | this._packageSize = _packageContent.length; 11 | } 12 | 13 | /** 14 | * Status of the download call 15 | */ 16 | public getStatus(): StatusCode { 17 | return this._status; 18 | } 19 | 20 | /** 21 | * If available, contains the package contents 22 | */ 23 | public getPackageContent(): string { 24 | return this._packageContent; 25 | } 26 | 27 | /** 28 | * If available, contains the package contents length in bytesF 29 | */ 30 | public getPackageSize(): number { 31 | return this._packageSize; 32 | } 33 | 34 | public toJSON(): { status: StatusCode; length: number } { 35 | return { 36 | status: this._status, 37 | length: this._packageSize, 38 | }; 39 | } 40 | } 41 | -------------------------------------------------------------------------------- /src/services/download/download_translator.ts: -------------------------------------------------------------------------------- 1 | import { InteractsXmlTrait } from '#src/internal/interacts_xml_trait'; 2 | import { type RequestBuilderInterface } from '#src/request_builder/request_builder_interface'; 3 | import { StatusCode } from '#src/shared/status_code'; 4 | import { DownloadResult } from './download_result.js'; 5 | 6 | export class DownloadTranslator extends InteractsXmlTrait { 7 | public createDownloadResultFromSoapResponse(content: string): DownloadResult { 8 | const env = this.readXmlElement(content); 9 | const values = this.findAtrributes(env, 'header', 'respuesta'); 10 | 11 | const status = new StatusCode(Number(values.codestatus), values.mensaje); 12 | const cpackage = this.findContent( 13 | env, 14 | 'body', 15 | 'RespuestaDescargaMasivaTercerosSalida', 16 | 'Paquete', 17 | ); 18 | 19 | return new DownloadResult(status, Buffer.from(cpackage).toString()); 20 | } 21 | 22 | public createSoapRequest(requestBuilder: RequestBuilderInterface, packageId: string): string { 23 | return requestBuilder.download(packageId); 24 | } 25 | } 26 | -------------------------------------------------------------------------------- /src/services/query/query_result.ts: -------------------------------------------------------------------------------- 1 | import { type StatusCode } from '#src/shared/status_code'; 2 | 3 | export class QueryResult { 4 | private readonly _status: StatusCode; 5 | 6 | private readonly _requestId: string; 7 | 8 | public constructor(statusCode: StatusCode, requestId: string) { 9 | this._status = statusCode; 10 | this._requestId = requestId; 11 | } 12 | 13 | /** 14 | * Status of the verification call 15 | */ 16 | public getStatus(): StatusCode { 17 | return this._status; 18 | } 19 | 20 | /** 21 | * If accepted, contains the request identification required for verification 22 | */ 23 | public getRequestId(): string { 24 | return this._requestId; 25 | } 26 | 27 | public toJSON(): { status: StatusCode; requestId: string } { 28 | return { 29 | status: this._status, 30 | requestId: this._requestId, 31 | }; 32 | } 33 | } 34 | -------------------------------------------------------------------------------- /src/services/query/query_translator.ts: -------------------------------------------------------------------------------- 1 | import { type Element } from '@nodecfdi/cfdi-core'; 2 | import { InteractsXmlTrait } from '#src/internal/interacts_xml_trait'; 3 | import { type RequestBuilderInterface } from '#src/request_builder/request_builder_interface'; 4 | import { StatusCode } from '#src/shared/status_code'; 5 | import { type QueryParameters } from './query_parameters.js'; 6 | import { QueryResult } from './query_result.js'; 7 | 8 | export class QueryTranslator extends InteractsXmlTrait { 9 | private resolveResponsePath(envelope: Element): string[] { 10 | if (this.findElement(envelope, 'body', 'solicitaDescargaEmitidosResponse')) { 11 | return ['body', 'solicitaDescargaEmitidosResponse', 'solicitaDescargaEmitidosResult']; 12 | } 13 | if (this.findElement(envelope, 'body', 'solicitaDescargaRecibidosResponse')) { 14 | return ['body', 'solicitaDescargaRecibidosResponse', 'solicitaDescargaRecibidosResult']; 15 | } 16 | if (this.findElement(envelope, 'body', 'SolicitaDescargaFolioResponse')) { 17 | return ['body', 'SolicitaDescargaFolioResponse', 'SolicitaDescargaFolioResult']; 18 | } 19 | 20 | return []; 21 | } 22 | 23 | public createQueryResultFromSoapResponse(content: string): QueryResult { 24 | const env = this.readXmlElement(content); 25 | const path = this.resolveResponsePath(env); 26 | 27 | const values = this.findAtrributes(env, ...path); 28 | const status = new StatusCode(Number(values.codestatus), values.mensaje); 29 | const requestId = values.idsolicitud; 30 | 31 | return new QueryResult(status, requestId); 32 | } 33 | 34 | public createSoapRequest( 35 | requestBuilder: RequestBuilderInterface, 36 | parameters: QueryParameters, 37 | ): string { 38 | return requestBuilder.query(parameters); 39 | } 40 | } 41 | -------------------------------------------------------------------------------- /src/services/query/query_validator.ts: -------------------------------------------------------------------------------- 1 | import { DateTime as LDateTime } from 'luxon'; 2 | import { type QueryParameters } from '#src/services/query/query_parameters'; 3 | import { ComplementoCfdi } from '#src/shared/complemento_cfdi'; 4 | import { ComplementoRetenciones } from '#src/shared/complemento_retenciones'; 5 | import { DateTime } from '#src/shared/date_time'; 6 | 7 | export class QueryValidator { 8 | public validate(query: QueryParameters): string[] { 9 | if (!query.getUuid().isEmpty()) { 10 | return this.validateFolio(query); 11 | } 12 | 13 | return this.validateQuery(query); 14 | } 15 | 16 | private validateFolio(query: QueryParameters): string[] { 17 | const errors: string[] = []; 18 | if (!query.getRfcMatches().isEmpty()) { 19 | errors.push('En una consulta por UUID no se debe usar el filtro de RFC.'); 20 | } 21 | if (!query.getComplement().isTypeOf('undefined')) { 22 | errors.push('En una consulta por UUID no se debe usar el filtro de complemento.'); 23 | } 24 | if (!query.getDocumentStatus().isTypeOf('undefined')) { 25 | errors.push('En una consulta por UUID no se debe usar el filtro de estado de documento.'); 26 | } 27 | if (!query.getDocumentType().isTypeOf('undefined')) { 28 | errors.push('En una consulta por UUID no se debe usar el filtro de tipo de documento.'); 29 | } 30 | 31 | return errors; 32 | } 33 | 34 | private validateQuery(query: QueryParameters): string[] { 35 | const errors: string[] = []; 36 | const start = query.getPeriod().getStart(); 37 | const end = query.getPeriod().getEnd(); 38 | const format = 'yyyy-MM-dd HH:mm:ss'; 39 | 40 | if (start.compareTo(end) >= 0) { 41 | errors.push( 42 | `La fecha de inicio (${start.format(format)}) no puede ser mayor o igual a la fecha final (${end.format(format)}) del periodo de consulta.`, 43 | ); 44 | } 45 | 46 | const minimalDate = new DateTime( 47 | LDateTime.now().minus({ years: 6 }).startOf('day').toUnixInteger(), 48 | ); 49 | if (query.getPeriod().getStart().compareTo(minimalDate) < 0) { 50 | errors.push( 51 | `La fecha de inicio (${query.getPeriod().getStart().format(format)}) no puede ser menor a hoy menos 6 años atrás (${minimalDate.format(format)}).`, 52 | ); 53 | } 54 | 55 | if ( 56 | query.getDownloadType().isTypeOf('received') && 57 | query.getRequestType().isTypeOf('xml') && 58 | !query.getDocumentStatus().isTypeOf('active') 59 | ) { 60 | errors.push( 61 | `No es posible hacer una consulta de XML Recibidos que contenga Cancelados. Solicitado: ${query.getDocumentStatus().getQueryAttributeValue()}.`, 62 | ); 63 | } 64 | 65 | if (query.getDownloadType().isTypeOf('received') && query.getRfcMatches().count() > 1) { 66 | errors.push('No es posible hacer una consulta de Recibidos con más de 1 RFC emisor.'); 67 | } 68 | 69 | if (query.getDownloadType().isTypeOf('issued') && query.getRfcMatches().count() > 5) { 70 | errors.push('No es posible hacer una consulta de Recibidos con más de 5 RFC receptores.'); 71 | } 72 | 73 | if ( 74 | query.getServiceType().isTypeOf('cfdi') && 75 | !query.getComplement().isTypeOf('undefined') && 76 | !(query.getComplement() instanceof ComplementoCfdi) 77 | ) { 78 | errors.push( 79 | `El complemento de CFDI definido no es un complemento registrado de este tipo ${query.getComplement().label()}.`, 80 | ); 81 | } 82 | 83 | if ( 84 | query.getServiceType().isTypeOf('retenciones') && 85 | !query.getComplement().isTypeOf('undefined') && 86 | !(query.getComplement() instanceof ComplementoRetenciones) 87 | ) { 88 | errors.push( 89 | `El complemento de Retenciones definido no es un complemento registrado de este tipo ${query.getComplement().label()}.`, 90 | ); 91 | } 92 | 93 | return errors; 94 | } 95 | } 96 | -------------------------------------------------------------------------------- /src/services/verify/verify_result.ts: -------------------------------------------------------------------------------- 1 | import { type CodeRequest } from '#src/shared/code_request'; 2 | import { type StatusCode } from '#src/shared/status_code'; 3 | import { type StatusRequest } from '#src/shared/status_request'; 4 | 5 | export class VerifyResult { 6 | private readonly _status: StatusCode; 7 | 8 | private readonly _statusRequest: StatusRequest; 9 | 10 | private readonly _codeRequest: CodeRequest; 11 | 12 | private readonly _numberCfdis: number; 13 | 14 | private readonly _packagesIds: string[]; 15 | 16 | public constructor( 17 | statusCode: StatusCode, 18 | statusRequest: StatusRequest, 19 | codeRequest: CodeRequest, 20 | numberCfdis: number, 21 | ...packageIds: string[] 22 | ) { 23 | this._status = statusCode; 24 | this._statusRequest = statusRequest; 25 | this._codeRequest = codeRequest; 26 | this._numberCfdis = numberCfdis; 27 | this._packagesIds = packageIds; 28 | } 29 | 30 | /** 31 | * Status of the verification call 32 | */ 33 | public getStatus(): StatusCode { 34 | return this._status; 35 | } 36 | 37 | /** 38 | * Status of the query 39 | */ 40 | public getStatusRequest(): StatusRequest { 41 | return this._statusRequest; 42 | } 43 | 44 | /** 45 | * Code related to the status of the query 46 | */ 47 | public getCodeRequest(): CodeRequest { 48 | return this._codeRequest; 49 | } 50 | 51 | /** 52 | * Number of CFDI given by the query 53 | */ 54 | public getNumberCfdis(): number { 55 | return this._numberCfdis; 56 | } 57 | 58 | /** 59 | * An array containing the package identifications, required to perform the download process 60 | */ 61 | public getPackageIds(): string[] { 62 | return this._packagesIds; 63 | } 64 | 65 | public countPackages(): number { 66 | return this._packagesIds.length; 67 | } 68 | 69 | public toJSON(): { 70 | status: { code: number; message: string }; 71 | codeRequest: { value: number | undefined; message: string }; 72 | statusRequest: { value: number | undefined; message: string }; 73 | numberCfdis: number; 74 | packagesIds: string[]; 75 | } { 76 | return { 77 | status: this._status.toJSON(), 78 | codeRequest: this._codeRequest.toJSON(), 79 | statusRequest: this._statusRequest.toJSON(), 80 | numberCfdis: this._numberCfdis, 81 | packagesIds: this._packagesIds, 82 | }; 83 | } 84 | } 85 | -------------------------------------------------------------------------------- /src/services/verify/verify_translator.ts: -------------------------------------------------------------------------------- 1 | import { InteractsXmlTrait } from '#src/internal/interacts_xml_trait'; 2 | import { type RequestBuilderInterface } from '#src/request_builder/request_builder_interface'; 3 | import { CodeRequest } from '#src/shared/code_request'; 4 | import { StatusCode } from '#src/shared/status_code'; 5 | import { StatusRequest } from '#src/shared/status_request'; 6 | import { VerifyResult } from './verify_result.js'; 7 | 8 | export class VerifyTranslator extends InteractsXmlTrait { 9 | public createVerifyResultFromSoapResponse(content: string): VerifyResult { 10 | const env = this.readXmlElement(content); 11 | 12 | const values = this.findAtrributes( 13 | env, 14 | 'body', 15 | 'VerificaSolicitudDescargaResponse', 16 | 'VerificaSolicitudDescargaResult', 17 | ); 18 | const status = new StatusCode(Number(values.codestatus), values.mensaje); 19 | const statusRequest = new StatusRequest(Number(values.estadosolicitud)); 20 | 21 | const codeRequest = new CodeRequest(Number(values.codigoestadosolicitud)); 22 | const numberCfdis = Number(values.numerocfdis); 23 | const packages = this.findContents( 24 | env, 25 | 'body', 26 | 'VerificaSolicitudDescargaResponse', 27 | 'VerificaSolicitudDescargaResult', 28 | 'IdsPaquetes', 29 | ); 30 | 31 | return new VerifyResult(status, statusRequest, codeRequest, numberCfdis, ...packages); 32 | } 33 | 34 | public createSoapRequest(requestBuilder: RequestBuilderInterface, requestId: string): string { 35 | return requestBuilder.verify(requestId); 36 | } 37 | } 38 | -------------------------------------------------------------------------------- /src/shared/abstract_rfc_filter.ts: -------------------------------------------------------------------------------- 1 | import { Rfc } from '@nodecfdi/rfc'; 2 | 3 | export class AbstractRfcFilter { 4 | protected constructor(private readonly _value?: Rfc) {} 5 | 6 | public static create(value: string): AbstractRfcFilter { 7 | try { 8 | return new AbstractRfcFilter(Rfc.parse(value)); 9 | } catch { 10 | throw new Error('RFC is invalid'); 11 | } 12 | } 13 | 14 | public static empty(): AbstractRfcFilter { 15 | return new AbstractRfcFilter(); 16 | } 17 | 18 | public static check(value: string): boolean { 19 | try { 20 | AbstractRfcFilter.create(value); 21 | 22 | return true; 23 | } catch { 24 | return false; 25 | } 26 | } 27 | 28 | public isEmpty(): boolean { 29 | return this._value === undefined; 30 | } 31 | 32 | public getValue(): string { 33 | if (this._value === undefined) { 34 | return ''; 35 | } 36 | 37 | return this._value.getRfc(); 38 | } 39 | 40 | public toJSON(): string | undefined { 41 | return this._value?.toJSON(); 42 | } 43 | } 44 | -------------------------------------------------------------------------------- /src/shared/code_request.ts: -------------------------------------------------------------------------------- 1 | export type CodeRequestTypes = 2 | | 'Accepted' 3 | | 'Exhausted' 4 | | 'MaximumLimitReaded' 5 | | 'EmptyResult' 6 | | 'Duplicated'; 7 | 8 | export class CodeRequest { 9 | protected static readonly VALUES = [ 10 | { 11 | code: 5000, 12 | name: 'Accepted', 13 | message: 'Solicitud recibida con éxito', 14 | }, 15 | { 16 | code: 5002, 17 | name: 'Exhausted', 18 | message: 19 | 'Se agotó las solicitudes de por vida: Máximo para solicitudes con los mismos parámetros', 20 | }, 21 | { 22 | code: 5003, 23 | name: 'MaximumLimitReaded', 24 | message: 'Tope máximo: Indica que se está superando el tope máximo de CFDI o Metadata', 25 | }, 26 | { 27 | code: 5004, 28 | name: 'EmptyResult', 29 | message: 30 | 'No se encontró la información: Indica que no generó paquetes por falta de información.', 31 | }, 32 | { 33 | code: 5005, 34 | name: 'Duplicated', 35 | message: 'Solicitud duplicada: Si existe una solicitud vigente con los mismos parámetros', 36 | }, 37 | ]; 38 | 39 | private readonly value!: { code?: number; name: string; message: string }; 40 | 41 | /** 42 | * 43 | * @param index - if assign by Values.code 44 | */ 45 | public constructor(index: number) { 46 | const value = CodeRequest.VALUES.find((element) => index === element.code); 47 | if (!value) { 48 | this.value = this.getEntryValueOnUndefined(); 49 | 50 | return; 51 | } 52 | this.value = value; 53 | } 54 | 55 | public static getEntries(): { 56 | code: number; 57 | name: string; 58 | message: string; 59 | }[] { 60 | return CodeRequest.VALUES; 61 | } 62 | 63 | public getEntryValueOnUndefined(): { 64 | code?: number; 65 | name: string; 66 | message: string; 67 | } { 68 | return { name: 'Unknown', message: 'Desconocida' }; 69 | } 70 | 71 | public getEtryValueOnUndefined(): { name: string; message: string } { 72 | return { name: 'Unknown', message: 'Desconocida' }; 73 | } 74 | 75 | public getEntryId(): string { 76 | return this.value.name; 77 | } 78 | 79 | public getMessage(): string { 80 | return this.value.message; 81 | } 82 | 83 | public getValue(): number | undefined { 84 | return this.value.code; 85 | } 86 | 87 | public isTypeOf(type: CodeRequestTypes): boolean { 88 | return this.getEntryId() === type; 89 | } 90 | 91 | public toJSON(): { value: number | undefined; message: string } { 92 | return { 93 | value: this.value.code, 94 | message: this.value.message, 95 | }; 96 | } 97 | } 98 | -------------------------------------------------------------------------------- /src/shared/complemento_interface.ts: -------------------------------------------------------------------------------- 1 | export type ComplementoInterface = { 2 | label(): string; 3 | 4 | value(): string; 5 | 6 | toJSON(): string; 7 | 8 | isTypeOf(type: T): boolean; 9 | }; 10 | -------------------------------------------------------------------------------- /src/shared/complemento_retenciones.ts: -------------------------------------------------------------------------------- 1 | import { type ComplementoInterface } from './complemento_interface.js'; 2 | import { BaseEnum } from './enum/base_enum.js'; 3 | 4 | export type ComplementoRetencionesTypes = 5 | | 'undefined' 6 | | 'arrendamientoEnFideicomiso' 7 | | 'dividendos' 8 | | 'enajenacionAcciones' 9 | | 'fideicomisoNoEmpresarial' 10 | | 'intereses' 11 | | 'interesesHipotecarios' 12 | | 'operacionesConDerivados' 13 | | 'pagosAExtranjeros' 14 | | 'planesRetiro10' 15 | | 'planesRetiro11' 16 | | 'premios' 17 | | 'sectorFinanciero' 18 | | 'serviciosPlataformasTecnologicas'; 19 | 20 | export class ComplementoRetenciones 21 | extends BaseEnum 22 | implements ComplementoInterface 23 | { 24 | public readonly Map = { 25 | undefined: { 26 | satCode: '', 27 | label: 'Sin complemento definido', 28 | }, 29 | arrendamientoEnFideicomiso: { 30 | satCode: 'arrendamientoenfideicomiso', 31 | label: 'Arrendamiento en fideicomiso', 32 | }, 33 | dividendos: { 34 | satCode: 'dividendos', 35 | label: 'Dividendos', 36 | }, 37 | enajenacionAcciones: { 38 | satCode: 'enajenaciondeacciones', 39 | label: 'Enajenación de acciones', 40 | }, 41 | fideicomisoNoEmpresarial: { 42 | satCode: 'fideicomisonoempresarial', 43 | label: 'Fideicomiso no empresarial', 44 | }, 45 | intereses: { 46 | satCode: 'intereses', 47 | label: 'Intereses', 48 | }, 49 | interesesHipotecarios: { 50 | satCode: 'intereseshipotecarios', 51 | label: 'Intereses hipotecarios', 52 | }, 53 | operacionesConDerivados: { 54 | satCode: 'operacionesconderivados', 55 | label: 'Operaciones con derivados', 56 | }, 57 | pagosAExtranjeros: { 58 | satCode: 'pagosaextranjeros', 59 | label: 'Pagos a extranjeros', 60 | }, 61 | planesRetiro10: { 62 | satCode: 'planesderetiro', 63 | label: 'Planes de retiro 1.0', 64 | }, 65 | planesRetiro11: { 66 | satCode: 'planesderetiro11', 67 | label: 'Planes de retiro 1.1', 68 | }, 69 | premios: { 70 | satCode: 'premios', 71 | label: 'Premios', 72 | }, 73 | sectorFinanciero: { 74 | satCode: 'sectorfinanciero', 75 | label: 'Sector Financiero', 76 | }, 77 | serviciosPlataformasTecnologicas: { 78 | satCode: 'serviciosplataformastecnologicas10', 79 | label: 'Servicios Plataformas Tecnológicas', 80 | }, 81 | }; 82 | 83 | public static create(id: ComplementoRetencionesTypes): ComplementoRetenciones { 84 | return new ComplementoRetenciones(id); 85 | } 86 | 87 | public static undefined(): ComplementoRetenciones { 88 | return new ComplementoRetenciones('undefined'); 89 | } 90 | 91 | public label(): string { 92 | return this.Map[this._id].label; 93 | } 94 | 95 | public value(): string { 96 | return this.Map[this._id].satCode; 97 | } 98 | 99 | public override toJSON(): string { 100 | return this.value(); 101 | } 102 | } 103 | -------------------------------------------------------------------------------- /src/shared/complemento_undefined.ts: -------------------------------------------------------------------------------- 1 | import { type ComplementoInterface } from './complemento_interface.js'; 2 | import { BaseEnum } from './enum/base_enum.js'; 3 | 4 | export type ComplementoUndefinedTypes = 'undefined'; 5 | 6 | export class ComplementoUndefined 7 | extends BaseEnum 8 | implements ComplementoInterface 9 | { 10 | public readonly Map = { 11 | undefined: { 12 | satCode: '', 13 | label: 'Sin complemento definido', 14 | }, 15 | }; 16 | 17 | public static create( 18 | id: ComplementoUndefinedTypes, 19 | ): ComplementoInterface { 20 | return new ComplementoUndefined(id); 21 | } 22 | 23 | public static undefined(): ComplementoInterface { 24 | return new ComplementoUndefined('undefined'); 25 | } 26 | 27 | public label(): string { 28 | return this.Map[this._id].label; 29 | } 30 | 31 | public value(): string { 32 | return this.Map[this._id].satCode; 33 | } 34 | 35 | public override toJSON(): string { 36 | return this.value(); 37 | } 38 | } 39 | -------------------------------------------------------------------------------- /src/shared/date_time.ts: -------------------------------------------------------------------------------- 1 | import { DateTime as DateTimeImmutable, type DurationLike } from 'luxon'; 2 | /** 3 | * Defines a date and time 4 | */ 5 | export class DateTime { 6 | private _value: DateTimeImmutable; 7 | 8 | private readonly _defaultTimeZone: string; 9 | 10 | /** 11 | * DateTime constructor. 12 | * 13 | * If value is an integer is used as a timestamp, if is a string is evaluated 14 | * as an argument for DateTimeImmutable and if it is DateTimeImmutable is used as is. 15 | * 16 | * @throws Error if unable to create a DateTime 17 | */ 18 | public constructor(value?: number | string | DateTimeImmutable, defaultTimeZone?: string) { 19 | let newValue = value ?? 'now'; 20 | 21 | const originalValue = newValue; 22 | this._defaultTimeZone = defaultTimeZone ?? DateTimeImmutable.now().zone.name; 23 | if (typeof newValue === 'number') { 24 | this._value = DateTimeImmutable.fromSeconds(newValue, { 25 | zone: this._defaultTimeZone, 26 | }); 27 | if (!this._value.isValid) { 28 | throw new Error(`Unable to create a Datetime("${originalValue as string}")`); 29 | } 30 | 31 | return; 32 | } 33 | 34 | if (typeof newValue === 'string') { 35 | newValue = this.castStringToDateTimeImmutable(newValue, originalValue as string); 36 | } 37 | 38 | if (!(newValue instanceof DateTimeImmutable) || !newValue.isValid) { 39 | throw new Error('Unable to create a Datetime'); 40 | } 41 | 42 | this._value = newValue; 43 | } 44 | 45 | /** 46 | * Create a DateTime instance 47 | * 48 | * If value is an integer is used as a timestamp, if is a string is evaluated 49 | * as an argument for DateTimeImmutable and if it is DateTimeImmutable is used as is. 50 | */ 51 | public static create( 52 | value?: number | string | DateTimeImmutable, 53 | defaultTimeZone?: string, 54 | ): DateTime { 55 | return new DateTime(value, defaultTimeZone); 56 | } 57 | 58 | public static now(): DateTime { 59 | return new DateTime(); 60 | } 61 | 62 | public formatSat(): string { 63 | return this.formatTimeZone('UTC'); 64 | } 65 | 66 | public format(format: string, timezone = ''): string { 67 | let clonedTimeZone = timezone; 68 | if (clonedTimeZone === '') { 69 | clonedTimeZone = this._defaultTimeZone; 70 | } 71 | 72 | this._value = this._value.setZone(clonedTimeZone); 73 | 74 | return this._value.toFormat(format); 75 | } 76 | 77 | public formateDefaultTimeZone(): string { 78 | return this.formatTimeZone(this._defaultTimeZone); 79 | } 80 | 81 | public formatTimeZone(timezone: string): string { 82 | return this._value.setZone(timezone).toISO() ?? ''; 83 | } 84 | 85 | /** 86 | * add or sub in given DurationLike 87 | * 88 | */ 89 | public modify(time: DurationLike): DateTime { 90 | const temporary = this._value; 91 | 92 | return new DateTime(temporary.plus(time)); 93 | } 94 | 95 | public compareTo(otherDate: DateTime): number { 96 | return this.formatSat().toString().localeCompare(otherDate.formatSat().toString()); 97 | } 98 | 99 | public equalsTo(expectedExpires: DateTime): boolean { 100 | return this.formatSat() === expectedExpires.formatSat(); 101 | } 102 | 103 | public toJSON(): number { 104 | return this._value.toSeconds(); 105 | } 106 | 107 | private castStringToDateTimeImmutable(value: string, originalValue: string): DateTimeImmutable { 108 | if (value === 'now') { 109 | return DateTimeImmutable.fromISO(DateTimeImmutable.now().toISO(), { 110 | zone: this._defaultTimeZone, 111 | }); 112 | } 113 | 114 | const temporary = DateTimeImmutable.fromSQL(value, { 115 | zone: this._defaultTimeZone, 116 | }); 117 | const newValue = temporary.isValid ? temporary : DateTimeImmutable.fromISO(value); 118 | 119 | if (!newValue.isValid) { 120 | throw new Error(`Unable to create a Datetime("${originalValue}")`); 121 | } 122 | 123 | return newValue; 124 | } 125 | } 126 | -------------------------------------------------------------------------------- /src/shared/date_time_period.ts: -------------------------------------------------------------------------------- 1 | import { DateTime } from './date_time.js'; 2 | /** 3 | * Defines a period of time by start of period and end of period values 4 | */ 5 | export class DateTimePeriod { 6 | private readonly _start: DateTime; 7 | 8 | private readonly _end: DateTime; 9 | 10 | public constructor(start: DateTime, end: DateTime) { 11 | if (end.compareTo(start) < 0) { 12 | throw new Error('The final date must be greater than the initial date'); 13 | } 14 | 15 | this._start = start; 16 | this._end = end; 17 | } 18 | 19 | public static create(start: DateTime, end: DateTime): DateTimePeriod { 20 | return new DateTimePeriod(start, end); 21 | } 22 | 23 | public static createFromValues(start: string, end: string): DateTimePeriod { 24 | return new DateTimePeriod(new DateTime(start), new DateTime(end)); 25 | } 26 | 27 | public getStart(): DateTime { 28 | return this._start; 29 | } 30 | 31 | public getEnd(): DateTime { 32 | return this._end; 33 | } 34 | 35 | public toJSON(): { start: number; end: number } { 36 | return { 37 | start: this._start.toJSON(), 38 | end: this._end.toJSON(), 39 | }; 40 | } 41 | } 42 | -------------------------------------------------------------------------------- /src/shared/document_status.ts: -------------------------------------------------------------------------------- 1 | import { BaseEnum } from './enum/base_enum.js'; 2 | 3 | export type DocumentStatusTypes = 'undefined' | 'active' | 'cancelled'; 4 | 5 | export const DocumentStatusEnum = { 6 | undefined: '', 7 | active: '1', 8 | cancelled: '0', 9 | } as const; 10 | 11 | export class DocumentStatus extends BaseEnum { 12 | public value(): string { 13 | return DocumentStatusEnum[this._id]; 14 | } 15 | 16 | public getQueryAttributeValue(): string { 17 | if (this.isTypeOf('undefined')) { 18 | return 'Todos'; 19 | } 20 | if (this.isTypeOf('active')) { 21 | return 'Vigente'; 22 | } 23 | if (this.isTypeOf('cancelled')) { 24 | return 'Cancelado'; 25 | } 26 | throw new Error('Impossible case'); 27 | } 28 | 29 | public override toJSON(): string { 30 | return DocumentStatusEnum[this._id]; 31 | } 32 | } 33 | -------------------------------------------------------------------------------- /src/shared/document_type.ts: -------------------------------------------------------------------------------- 1 | import { BaseEnum } from './enum/base_enum.js'; 2 | 3 | export type DocumentTypeTypes = 'undefined' | 'ingreso' | 'egreso' | 'traslado' | 'nomina' | 'pago'; 4 | 5 | export const DocumentTypeEnum = { 6 | undefined: '', 7 | ingreso: 'I', 8 | egreso: 'E', 9 | traslado: 'T', 10 | nomina: 'N', 11 | pago: 'P', 12 | } as const; 13 | 14 | export class DocumentType extends BaseEnum { 15 | public value(): string { 16 | return DocumentTypeEnum[this._id]; 17 | } 18 | 19 | public override toJSON(): string { 20 | return DocumentTypeEnum[this._id]; 21 | } 22 | } 23 | -------------------------------------------------------------------------------- /src/shared/download_type.ts: -------------------------------------------------------------------------------- 1 | import { BaseEnum } from './enum/base_enum.js'; 2 | 3 | export type DownloadTypeTypes = 'issued' | 'received'; 4 | 5 | export const DownloadTypeEnum = { 6 | issued: 'RfcEmisor', 7 | received: 'RfcReceptor', 8 | } as const; 9 | 10 | export class DownloadType extends BaseEnum { 11 | public value(): string { 12 | return DownloadTypeEnum[this._id]; 13 | } 14 | 15 | public override toJSON(): string { 16 | return DownloadTypeEnum[this._id]; 17 | } 18 | } 19 | -------------------------------------------------------------------------------- /src/shared/enum/base_enum.ts: -------------------------------------------------------------------------------- 1 | export abstract class BaseEnum { 2 | public constructor(public readonly _id: T) {} 3 | 4 | public index(): string { 5 | return this._id; 6 | } 7 | 8 | public isTypeOf(type: T): boolean { 9 | return this._id === type; 10 | } 11 | 12 | public toJSON(): string { 13 | return this._id; 14 | } 15 | 16 | public abstract value(): string; 17 | } 18 | -------------------------------------------------------------------------------- /src/shared/request_type.ts: -------------------------------------------------------------------------------- 1 | import { BaseEnum } from './enum/base_enum.js'; 2 | 3 | export type RequestTypeTypes = 'xml' | 'metadata'; 4 | 5 | export const RequestTypeEnum = { 6 | xml: 'xml', 7 | metadata: 'metadata', 8 | }; 9 | 10 | export class RequestType extends BaseEnum { 11 | public getQueryAttributeValue(): string { 12 | return this.isTypeOf('xml') ? 'CFDI' : 'Metadata'; 13 | } 14 | 15 | public value(): string { 16 | return RequestTypeEnum[this._id]; 17 | } 18 | } 19 | -------------------------------------------------------------------------------- /src/shared/rfc_match.ts: -------------------------------------------------------------------------------- 1 | import { AbstractRfcFilter } from './abstract_rfc_filter.js'; 2 | 3 | export class RfcMatch extends AbstractRfcFilter {} 4 | -------------------------------------------------------------------------------- /src/shared/rfc_matches.ts: -------------------------------------------------------------------------------- 1 | import { RfcMatch } from './rfc_match.js'; 2 | 3 | export class RfcMatches { 4 | private readonly _items: RfcMatch[]; 5 | 6 | private readonly _count: number; 7 | 8 | public constructor(...items: RfcMatch[]) { 9 | this._items = items; 10 | this._count = items.length; 11 | } 12 | 13 | public static create(...items: RfcMatch[]): RfcMatches { 14 | const map = new Map(); 15 | const values: RfcMatch[] = []; 16 | for (const item of items) { 17 | const key = item.getValue(); 18 | if (!item.isEmpty() && !map.get(key)) { 19 | map.set(item.getValue(), item); 20 | values.push(item); 21 | } 22 | } 23 | 24 | return new RfcMatches(...values); 25 | } 26 | 27 | public static createFromValues(...values: string[]): RfcMatches { 28 | const valuesRfc = values.map((value) => 29 | value === '' ? RfcMatch.empty() : RfcMatch.create(value), 30 | ); 31 | 32 | return RfcMatches.create(...valuesRfc); 33 | } 34 | 35 | public isEmpty(): boolean { 36 | return this._count === 0; 37 | } 38 | 39 | public getFirst(): RfcMatch { 40 | return this._items[0] ?? RfcMatch.empty(); 41 | } 42 | 43 | public count(): number { 44 | return this._count; 45 | } 46 | 47 | public [Symbol.iterator](): Iterable { 48 | return this._items; 49 | } 50 | 51 | public itemsToArray(): RfcMatch[] { 52 | const values: RfcMatch[] = []; 53 | for (const iterator of this._items) { 54 | values.push(iterator); 55 | } 56 | 57 | return values; 58 | } 59 | 60 | public toJSON(): RfcMatch[] { 61 | return this._items; 62 | } 63 | } 64 | -------------------------------------------------------------------------------- /src/shared/rfc_on_behalf.ts: -------------------------------------------------------------------------------- 1 | import { AbstractRfcFilter } from './abstract_rfc_filter.js'; 2 | 3 | export class RfcOnBehalf extends AbstractRfcFilter {} 4 | -------------------------------------------------------------------------------- /src/shared/service_endpoints.ts: -------------------------------------------------------------------------------- 1 | import { ServiceType } from './service_type.js'; 2 | 3 | /** 4 | * This class contains the end points to consume the service 5 | * Use ServiceEndpoints.cfdi() for "CFDI regulares" 6 | * Use ServiceEndpoints.retenciones() for "CFDI de retenciones e información de pagos" 7 | * 8 | * @see ServiceEndpoints.cfdi() 9 | * @see ServiceEndpoints.retenciones() 10 | */ 11 | export class ServiceEndpoints { 12 | public constructor( 13 | private readonly _authenticate: string, 14 | private readonly _query: string, 15 | private readonly _verify: string, 16 | private readonly _download: string, 17 | private readonly _serviceType: ServiceType, 18 | ) {} 19 | 20 | /** 21 | * Create an object with known endpoints for "CFDI regulares" 22 | */ 23 | public static cfdi(): ServiceEndpoints { 24 | return new ServiceEndpoints( 25 | 'https://cfdidescargamasivasolicitud.clouda.sat.gob.mx/Autenticacion/Autenticacion.svc', 26 | 'https://cfdidescargamasivasolicitud.clouda.sat.gob.mx/SolicitaDescargaService.svc', 27 | 'https://cfdidescargamasivasolicitud.clouda.sat.gob.mx/VerificaSolicitudDescargaService.svc', 28 | 'https://cfdidescargamasiva.clouda.sat.gob.mx/DescargaMasivaService.svc', 29 | new ServiceType('cfdi'), 30 | ); 31 | } 32 | 33 | public static retenciones(): ServiceEndpoints { 34 | return new ServiceEndpoints( 35 | 'https://retendescargamasivasolicitud.clouda.sat.gob.mx/Autenticacion/Autenticacion.svc', 36 | 'https://retendescargamasivasolicitud.clouda.sat.gob.mx/SolicitaDescargaService.svc', 37 | 'https://retendescargamasivasolicitud.clouda.sat.gob.mx/VerificaSolicitudDescargaService.svc', 38 | 'https://retendescargamasiva.clouda.sat.gob.mx/DescargaMasivaService.svc', 39 | new ServiceType('retenciones'), 40 | ); 41 | } 42 | 43 | public getAuthenticate(): string { 44 | return this._authenticate; 45 | } 46 | 47 | public getQuery(): string { 48 | return this._query; 49 | } 50 | 51 | public getVerify(): string { 52 | return this._verify; 53 | } 54 | 55 | public getDownload(): string { 56 | return this._download; 57 | } 58 | 59 | public getServiceType(): ServiceType { 60 | return this._serviceType; 61 | } 62 | } 63 | -------------------------------------------------------------------------------- /src/shared/service_type.ts: -------------------------------------------------------------------------------- 1 | import { BaseEnum } from './enum/base_enum.js'; 2 | 3 | export type ServiceTypeValues = 'cfdi' | 'retenciones'; 4 | 5 | export const ServiceTypeEnum = { 6 | cfdi: 'cfdi', 7 | retenciones: 'retenciones', 8 | } as const; 9 | 10 | export class ServiceType extends BaseEnum { 11 | public equalTo(serviceType: ServiceType): boolean { 12 | return this._id === serviceType._id; 13 | } 14 | 15 | public value(): string { 16 | return ServiceTypeEnum[this._id]; 17 | } 18 | } 19 | -------------------------------------------------------------------------------- /src/shared/status_code.ts: -------------------------------------------------------------------------------- 1 | export class StatusCode { 2 | public constructor( 3 | private readonly _code: number, 4 | private readonly _message: string, 5 | ) {} 6 | 7 | /** 8 | * Contains the value of "CodEstatus" 9 | */ 10 | public getCode(): number { 11 | return this._code; 12 | } 13 | 14 | /** 15 | * Contains the value of "Mensaje" 16 | */ 17 | public getMessage(): string { 18 | return this._message; 19 | } 20 | 21 | /** 22 | * Return true when "CodEstatus" is success 23 | * The only success code is "5000: Solicitud recibida con éxito" 24 | */ 25 | public isAccepted(): boolean { 26 | return this._code === 5000; 27 | } 28 | 29 | public toJSON(): { code: number; message: string } { 30 | return { 31 | code: this._code, 32 | message: this._message, 33 | }; 34 | } 35 | } 36 | -------------------------------------------------------------------------------- /src/shared/status_request.ts: -------------------------------------------------------------------------------- 1 | export type StatusRequestTypes = 2 | | 'Accepted' 3 | | 'InProgress' 4 | | 'Finished' 5 | | 'Failure' 6 | | 'Rejected' 7 | | 'Expired'; 8 | 9 | export class StatusRequest { 10 | protected static readonly VALUES = [ 11 | { code: 1, name: 'Accepted', message: 'Aceptada' }, 12 | { code: 2, name: 'InProgress', message: 'En proceso' }, 13 | { code: 3, name: 'Finished', message: 'Terminada' }, 14 | { code: 4, name: 'Failure', message: 'Error' }, 15 | { code: 5, name: 'Rejected', message: 'Rechazada' }, 16 | { code: 6, name: 'Expired', message: 'Vencida' }, 17 | ]; 18 | 19 | private readonly value!: { code?: number; name: string; message: string }; 20 | 21 | /** 22 | * 23 | * @param index - if number is send assign value by array index of VALUES, values from 0 to 5 if string is send find value by Values.name 24 | */ 25 | public constructor(index: number | string) { 26 | if (typeof index === 'number') { 27 | const value = StatusRequest.VALUES.find((element) => index === element.code); 28 | if (!value) { 29 | this.value = this.getEntryValueOnUndefined(); 30 | 31 | return; 32 | } 33 | 34 | this.value = value; 35 | } 36 | 37 | if (typeof index === 'string') { 38 | const value = StatusRequest.VALUES.find((element) => index === element.name); 39 | if (!value) { 40 | this.value = this.getEntryValueOnUndefined(); 41 | 42 | return; 43 | } 44 | 45 | this.value = value; 46 | } 47 | } 48 | 49 | public static getEntriesArray(): { name: string; message: string }[] { 50 | return StatusRequest.VALUES; 51 | } 52 | 53 | public getEntryValueOnUndefined(): { 54 | code?: number; 55 | name: string; 56 | message: string; 57 | } { 58 | return { name: 'Unknown', message: 'Desconocida' }; 59 | } 60 | 61 | public getEntryId(): string { 62 | return this.value.name; 63 | } 64 | 65 | public getValue(): number | undefined { 66 | return this.value.code; 67 | } 68 | 69 | public isTypeOf(type: StatusRequestTypes): boolean { 70 | return this.getEntryId() === type; 71 | } 72 | 73 | public toJSON(): { value: number | undefined; message: string } { 74 | return { 75 | value: this.value.code, 76 | message: this.value.message, 77 | }; 78 | } 79 | } 80 | -------------------------------------------------------------------------------- /src/shared/token.ts: -------------------------------------------------------------------------------- 1 | import { DateTime } from './date_time.js'; 2 | 3 | export class Token { 4 | private readonly _created: DateTime; 5 | 6 | public constructor( 7 | created: DateTime, 8 | private readonly _expires: DateTime, 9 | private readonly _value: string, 10 | ) { 11 | if (_expires.compareTo(created) < 0) { 12 | throw new Error('Cannot create a token with expiration lower than creation'); 13 | } 14 | 15 | this._created = created; 16 | } 17 | 18 | public static empty(): Token { 19 | return new Token(DateTime.create(0), DateTime.create(0), ''); 20 | } 21 | 22 | public getCreated(): DateTime { 23 | return this._created; 24 | } 25 | 26 | public getExpires(): DateTime { 27 | return this._expires; 28 | } 29 | 30 | public getValue(): string { 31 | return this._value; 32 | } 33 | 34 | /** 35 | * A token is empty if does not contains an internal value 36 | */ 37 | public isValueEmpty(): boolean { 38 | return this._value === ''; 39 | } 40 | 41 | /** 42 | * A token is expired if the expiration date is greater or equal to current time 43 | */ 44 | public isExpired(): boolean { 45 | return this._expires.compareTo(DateTime.now()) < 0; 46 | } 47 | 48 | /** 49 | * A token is valid if contains a value and is not expired 50 | */ 51 | public isValid(): boolean { 52 | return !(this.isValueEmpty() || this.isExpired()); 53 | } 54 | 55 | public toJSON(): { created: DateTime; expires: DateTime; value: string } { 56 | return { 57 | created: this._created, 58 | expires: this._expires, 59 | value: this._value, 60 | }; 61 | } 62 | } 63 | -------------------------------------------------------------------------------- /src/shared/uuid.ts: -------------------------------------------------------------------------------- 1 | export class Uuid { 2 | public constructor(private readonly _value: string) {} 3 | 4 | public static create(value: string): Uuid { 5 | const newValue = value.toLowerCase(); 6 | if (!/^[\da-f]{8}(?:-[\da-f]{4}){3}-[\da-f]{12}$/.test(newValue)) { 7 | throw new Error('UUID does not have the correct format'); 8 | } 9 | 10 | return new Uuid(newValue); 11 | } 12 | 13 | public static empty(): Uuid { 14 | return new Uuid(''); 15 | } 16 | 17 | public static check(value: string): boolean { 18 | try { 19 | Uuid.create(value); 20 | 21 | return true; 22 | } catch { 23 | return false; 24 | } 25 | } 26 | 27 | public isEmpty(): boolean { 28 | return this._value === ''; 29 | } 30 | 31 | public getValue(): string { 32 | return this._value; 33 | } 34 | 35 | public toJSON(): string { 36 | return this._value; 37 | } 38 | } 39 | -------------------------------------------------------------------------------- /src/types.ts: -------------------------------------------------------------------------------- 1 | // eslint-disable-next-line @typescript-eslint/no-explicit-any 2 | export type Constructor = new (...args: T[]) => T; 3 | -------------------------------------------------------------------------------- /src/web_client/crequest.ts: -------------------------------------------------------------------------------- 1 | export class CRequest { 2 | private readonly _method: string; 3 | 4 | private readonly _uri: string; 5 | 6 | private readonly _body: string; 7 | 8 | private readonly _headers: Record; 9 | 10 | private readonly _timeout?: number; 11 | 12 | public constructor( 13 | method: string, 14 | uri: string, 15 | body: string, 16 | headers: Record, 17 | timeout?: number, 18 | ) { 19 | this._method = method; 20 | this._uri = uri; 21 | this._body = body; 22 | const map = new Map([...Object.entries(this.defaultHeaders()), ...Object.entries(headers)]); 23 | this._headers = Object.fromEntries(map); 24 | this._timeout = timeout; 25 | } 26 | 27 | public getMethod(): string { 28 | return this._method; 29 | } 30 | 31 | public getUri(): string { 32 | return this._uri; 33 | } 34 | 35 | public getBody(): string { 36 | return this._body; 37 | } 38 | 39 | public getHeaders(): Record { 40 | return this._headers; 41 | } 42 | 43 | public getTimeout(): number | undefined { 44 | return this._timeout; 45 | } 46 | 47 | public defaultHeaders(): { 48 | 'Content-type': string; 49 | 'Accept': string; 50 | 'Cache-Control': string; 51 | } { 52 | return { 53 | 'Content-type': 'text/xml; charset="utf-8"', 54 | 'Accept': 'text/xml', 55 | 'Cache-Control': 'no-cache', 56 | }; 57 | } 58 | 59 | public toJSON(): { 60 | method: string; 61 | uri: string; 62 | body: string; 63 | headers: Record; 64 | timeout?: number; 65 | } { 66 | return { 67 | method: this._method, 68 | uri: this._uri, 69 | body: this._body, 70 | headers: this._headers, 71 | timeout: this._timeout, 72 | }; 73 | } 74 | } 75 | -------------------------------------------------------------------------------- /src/web_client/cresponse.ts: -------------------------------------------------------------------------------- 1 | export class CResponse { 2 | private readonly _statusCode: number; 3 | 4 | private readonly _body: string; 5 | 6 | private readonly _headers: Record; 7 | 8 | private static readonly TIMEOUT_CODE = 1; 9 | 10 | public constructor(statuscode: number, body: string, headers: Record = {}) { 11 | this._statusCode = statuscode; 12 | this._body = body; 13 | this._headers = headers; 14 | } 15 | 16 | public static timeout(milliseconds?: number): CResponse { 17 | return new CResponse( 18 | CResponse.TIMEOUT_CODE, 19 | `Timeout after ${milliseconds ?? 'unknown'} ms`, 20 | {}, 21 | ); 22 | } 23 | 24 | public getStatusCode(): number { 25 | return this._statusCode; 26 | } 27 | 28 | public getBody(): string { 29 | return this._body; 30 | } 31 | 32 | public getHeaders(): Record { 33 | return this._headers; 34 | } 35 | 36 | public isEmpty(): boolean { 37 | return this._body === ''; 38 | } 39 | 40 | public statusCodeIsTimeoutError(): boolean { 41 | return this._statusCode === CResponse.TIMEOUT_CODE; 42 | } 43 | 44 | public statusCodeIsClientError(): boolean { 45 | return this._statusCode < 500 && this._statusCode >= 400; 46 | } 47 | 48 | public statusCodeIsServerError(): boolean { 49 | return this._statusCode < 600 && this._statusCode >= 500; 50 | } 51 | 52 | public toJSON(): { 53 | statusCode: number; 54 | body: string; 55 | headers: Record; 56 | } { 57 | return { 58 | statusCode: this._statusCode, 59 | body: this._body, 60 | headers: this._headers, 61 | }; 62 | } 63 | } 64 | -------------------------------------------------------------------------------- /src/web_client/exceptions/http_client_error.ts: -------------------------------------------------------------------------------- 1 | import { WebClientException } from './web_client_exception.js'; 2 | 3 | export class HttpClientError extends WebClientException {} 4 | -------------------------------------------------------------------------------- /src/web_client/exceptions/http_server_error.ts: -------------------------------------------------------------------------------- 1 | import { WebClientException } from './web_client_exception.js'; 2 | 3 | export class HttpServerError extends WebClientException {} 4 | -------------------------------------------------------------------------------- /src/web_client/exceptions/http_timeout_error.ts: -------------------------------------------------------------------------------- 1 | import { WebClientException } from './web_client_exception.js'; 2 | 3 | export class HttpTimeoutError extends WebClientException {} 4 | -------------------------------------------------------------------------------- /src/web_client/exceptions/soap_fault_error.ts: -------------------------------------------------------------------------------- 1 | import { type CRequest } from '../crequest.js'; 2 | import { type CResponse } from '../cresponse.js'; 3 | import { type SoapFaultInfo } from '../soap_fault_info.js'; 4 | import { HttpClientError } from './http_client_error.js'; 5 | 6 | export class SoapFaultError extends HttpClientError { 7 | private readonly _fault: SoapFaultInfo; 8 | 9 | public constructor( 10 | request: CRequest, 11 | response: CResponse, 12 | fault: SoapFaultInfo, 13 | previous?: Error, 14 | ) { 15 | const message = `Fault: ${fault.getCode()} - ${fault.getMessage()}`; 16 | super(message, request, response, previous); 17 | this._fault = fault; 18 | } 19 | 20 | public getFault(): SoapFaultInfo { 21 | return this._fault; 22 | } 23 | } 24 | -------------------------------------------------------------------------------- /src/web_client/exceptions/web_client_exception.ts: -------------------------------------------------------------------------------- 1 | import { type CRequest } from '../crequest.js'; 2 | import { type CResponse } from '../cresponse.js'; 3 | 4 | export class WebClientException extends Error { 5 | private readonly _request: CRequest; 6 | 7 | private readonly _response: CResponse; 8 | 9 | private readonly _previous?: Error; 10 | 11 | public constructor(message: string, request: CRequest, response: CResponse, previous?: Error) { 12 | super(message); 13 | this._request = request; 14 | this._response = response; 15 | this._previous = previous; 16 | } 17 | 18 | public getRequest(): CRequest { 19 | return this._request; 20 | } 21 | 22 | public getResponse(): CResponse { 23 | return this._response; 24 | } 25 | 26 | public getPrevious(): Error | undefined { 27 | return this._previous; 28 | } 29 | } 30 | -------------------------------------------------------------------------------- /src/web_client/https_web_client.ts: -------------------------------------------------------------------------------- 1 | import { type ClientRequest } from 'node:http'; 2 | import https from 'node:https'; 3 | import { type CRequest } from './crequest.js'; 4 | import { CResponse } from './cresponse.js'; 5 | import { WebClientException } from './exceptions/web_client_exception.js'; 6 | import { type WebClientInterface } from './web_client_interface.js'; 7 | 8 | export class HttpsWebClient implements WebClientInterface { 9 | private readonly _fireRequestClosure?: (request: CRequest) => void; 10 | 11 | private readonly _fireResponseClosure?: (response: CResponse) => void; 12 | 13 | private readonly _timeout?: number; 14 | 15 | public constructor( 16 | onFireRequest?: (request: CRequest) => void, 17 | onFireResponse?: (response: CResponse) => void, 18 | timeout?: number, 19 | ) { 20 | this._fireRequestClosure = onFireRequest; 21 | this._fireResponseClosure = onFireResponse; 22 | this._timeout = timeout; 23 | } 24 | 25 | public fireRequest(request: CRequest): void { 26 | if (this._fireRequestClosure) { 27 | this._fireRequestClosure(request); 28 | } 29 | } 30 | 31 | public fireResponse(response: CResponse): void { 32 | if (this._fireResponseClosure) { 33 | this._fireResponseClosure(response); 34 | } 35 | } 36 | 37 | public async call(request: CRequest): Promise { 38 | const options = { 39 | method: request.getMethod(), 40 | headers: request.getHeaders(), 41 | timeout: this._timeout ?? request.getTimeout() ?? undefined, 42 | }; 43 | 44 | return new Promise((resolve, reject) => { 45 | let clientRequest: ClientRequest; 46 | try { 47 | clientRequest = https.request(request.getUri(), options, (response) => { 48 | const code = response.statusCode ?? 0; 49 | const body: Uint8Array[] = []; 50 | response.on('data', (chunk: Uint8Array) => body.push(chunk)); 51 | response.on('end', () => { 52 | const responseString = Buffer.concat(body).toString(); 53 | resolve(new CResponse(code, responseString)); 54 | }); 55 | }); 56 | } catch (error) { 57 | const catchedError = error as Error; 58 | const errorResponse = new CResponse(0, catchedError.message, {}); 59 | throw new WebClientException(catchedError.message, request, errorResponse); 60 | } 61 | 62 | clientRequest.on('error', (error) => { 63 | const errorResponse = new CResponse(0, error.message, {}); 64 | reject(new WebClientException(error.message, request, errorResponse)); 65 | }); 66 | 67 | clientRequest.on('timeout', () => { 68 | clientRequest.destroy(); 69 | 70 | const rejectReason = 71 | this._timeout === undefined 72 | ? new Error('Request time out') 73 | : new WebClientException('Request time out', request, CResponse.timeout(this._timeout)); 74 | 75 | reject(rejectReason); 76 | }); 77 | 78 | clientRequest.write(request.getBody()); 79 | clientRequest.end(); 80 | }); 81 | } 82 | } 83 | -------------------------------------------------------------------------------- /src/web_client/soap_fault_info.ts: -------------------------------------------------------------------------------- 1 | export class SoapFaultInfo { 2 | private readonly _code: string; 3 | 4 | private readonly _message: string; 5 | 6 | public constructor(code: string, message: string) { 7 | this._code = code; 8 | this._message = message; 9 | } 10 | 11 | public getCode(): string { 12 | return this._code; 13 | } 14 | 15 | public getMessage(): string { 16 | return this._message; 17 | } 18 | 19 | public toJSON(): { code: string; message: string } { 20 | return { 21 | code: this._code, 22 | message: this._message, 23 | }; 24 | } 25 | } 26 | -------------------------------------------------------------------------------- /src/web_client/web_client_interface.ts: -------------------------------------------------------------------------------- 1 | import { type CRequest } from './crequest.js'; 2 | import { type CResponse } from './cresponse.js'; 3 | 4 | export type WebClientInterface = { 5 | /** 6 | * Make the Http call to the web service 7 | * This method should *not* call fireRequest/fireResponse 8 | * 9 | * @throws WebClientException when an error is found 10 | */ 11 | call(request: CRequest): Promise; 12 | 13 | /** 14 | * Method called before calling the web service 15 | */ 16 | fireRequest(request: CRequest): void; 17 | 18 | /** 19 | * Method called after calling the web service 20 | */ 21 | fireResponse(response: CResponse): void; 22 | }; 23 | -------------------------------------------------------------------------------- /tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "extends": "@nodecfdi/tsconfig/tsconfig.package.json", 3 | "compilerOptions": { 4 | "rootDir": "./", 5 | "outDir": "./build", 6 | "types": ["vitest/globals", "@types/node"] 7 | } 8 | } 9 | -------------------------------------------------------------------------------- /typedoc.json: -------------------------------------------------------------------------------- 1 | { 2 | "plugin": ["typedoc-github-theme"], 3 | "entryPoints": ["./src"], 4 | "entryPointStrategy": "expand", 5 | "out": "./docs" 6 | } 7 | -------------------------------------------------------------------------------- /vitest.config.ts: -------------------------------------------------------------------------------- 1 | import { defineConfig } from 'vitest/config'; 2 | 3 | export default defineConfig({ 4 | test: { 5 | globals: true, 6 | coverage: { 7 | all: true, 8 | provider: 'istanbul', 9 | reporter: ['text', 'lcov'], 10 | include: ['src/**/*.ts'], 11 | }, 12 | }, 13 | }); 14 | --------------------------------------------------------------------------------