From ad749f100923721f0f8841a2f22d6c55777b3f21 Mon Sep 17 00:00:00 2001 From: Jaummm777 Date: Fri, 10 Jul 2026 16:32:22 -0300 Subject: [PATCH] PRIMEIRO COMMIT --- Models/config.py | 18 + Models/empresa.py | 97 + Models/nfe.py | 115 + Models/nfse.py | 49 + Models/payload.py | 518 ++++ Models/servico.py | 150 ++ Models/tomador.py | 34 + README.md | 204 ++ ...s_Worker_Plugnotas.postman_collection.json | 649 +++++ ...as_Example-SandBox.postman_collection.json | 2243 +++++++++++++++++ backup/auxiliar.json | 8 + backup/cadastrar_empresa.json | 86 + backup/collectionsForEduardo.json | 125 + backup/json_insercao_empresa.json | 86 + backup/modelo_json_rpc.json | 29 + backup/modelo_system_config.json | 6 + backup/servicoPlugnotasCollections.json | 129 + backup/src/Models/config.py | 18 + backup/src/Models/empresa.py | 96 + backup/src/Models/nfe.py | 99 + backup/src/Models/nfse.py | 49 + backup/src/Models/payload.py | 518 ++++ backup/src/Models/servico.py | 150 ++ backup/src/Models/tomador.py | 34 + backup/src/NFSe/Servico/servico.py | 105 + backup/src/NFSe/Tomador/tomador.py | 91 + backup/src/NFSe/nfse.py | 167 ++ backup/src/NFe/registroNFe.py | 126 + backup/src/Relatorios/relatorios.py | 70 + .../certificado_digital.py | 165 ++ backup/src/empresas/empresa.py | 177 ++ backup/src/listenerEndpoint.py | 173 ++ backup/src/logs/integracao_01 | 24 + backup/src/logs/integracao_02 | 194 ++ backup/src/logs/integracao_06 | 56 + backup/src/logs/integracao_25 | 0 backup/src/logs/integracao_26 | 0 backup/src/logs/integracao_27 | 140 + backup/src/main.py | 686 +++++ backup/src/mainQUEUE.py | 660 +++++ backup/src/main_listener.py | 21 + backup/src/tests/__init__.py | 1 + backup/src/tests/conftest.py | 119 + backup/src/tests/debug_payload.py | 67 + backup/src/tests/debug_report_api.py | 33 + backup/src/tests/purge_queue.py | 46 + backup/src/tests/test.py | 134 + backup/src/tests/testArch.py | 58 + backup/src/tests/test_1000_empresas.py | 144 ++ backup/src/tests/test_1000_nfe.py | 219 ++ backup/src/tests/test_1000_nfse.py | 173 ++ backup/src/tests/test_api.py | 29 + backup/src/tests/test_callback.py | 96 + backup/src/tests/test_nfe.py | 179 ++ backup/src/tests/test_nfse.py | 146 ++ backup/src/tests/test_queue.py | 164 ++ backup/src/tests/test_servicos.py | 328 +++ backup/src/tests/test_simplified_payload.py | 80 + backup/src/tests/test_upload_certificado.py | 88 + backup/src/utils/Exchange2.py | 1041 ++++++++ backup/src/utils/api.py | 158 ++ backup/src/utils/banco.py | 241 ++ backup/src/utils/enviaListerner.py | 35 + backup/src/utils/exchangeModels.py | 127 + backup/src/utils/redis.py | 107 + backup/src/webhooks/webhooks.py | 64 + backup/test_payload.py | 52 + buildImages.sh | 86 + docker-compose.yml | 111 + endpoint_listener/Dockerfile | 28 + endpoint_listener/requirements.txt | 13 + endpoint_listener/src/Models/config.py | 18 + endpoint_listener/src/Models/empresa.py | 97 + endpoint_listener/src/Models/nfe.py | 115 + endpoint_listener/src/Models/nfse.py | 49 + endpoint_listener/src/Models/payload.py | 518 ++++ endpoint_listener/src/Models/servico.py | 150 ++ endpoint_listener/src/Models/tomador.py | 34 + endpoint_listener/src/NFSe/Servico/servico.py | 105 + endpoint_listener/src/NFSe/Tomador/tomador.py | 91 + endpoint_listener/src/NFSe/nfse.py | 167 ++ endpoint_listener/src/NFe/registroNFe.py | 126 + .../src/Relatorios/relatorios.py | 70 + .../certificado_digital.py | 165 ++ endpoint_listener/src/empresas/empresa.py | 184 ++ endpoint_listener/src/endpoint_listener.py | 21 + endpoint_listener/src/listenerEndpoint.py | 170 ++ endpoint_listener/src/logs/integracao_01 | 24 + endpoint_listener/src/logs/integracao_02 | 194 ++ endpoint_listener/src/logs/integracao_06 | 56 + endpoint_listener/src/logs/integracao_25 | 0 endpoint_listener/src/logs/integracao_26 | 0 endpoint_listener/src/logs/integracao_27 | 140 + endpoint_listener/src/tests/__init__.py | 1 + endpoint_listener/src/tests/conftest.py | 119 + endpoint_listener/src/tests/debug_payload.py | 67 + .../src/tests/debug_report_api.py | 33 + endpoint_listener/src/tests/purge_queue.py | 46 + endpoint_listener/src/tests/test.py | 134 + endpoint_listener/src/tests/testArch.py | 58 + .../src/tests/test_1000_empresas.py | 144 ++ endpoint_listener/src/tests/test_1000_nfe.py | 219 ++ endpoint_listener/src/tests/test_1000_nfse.py | 173 ++ endpoint_listener/src/tests/test_api.py | 29 + endpoint_listener/src/tests/test_callback.py | 96 + endpoint_listener/src/tests/test_nfe.py | 179 ++ endpoint_listener/src/tests/test_nfse.py | 146 ++ endpoint_listener/src/tests/test_queue.py | 164 ++ endpoint_listener/src/tests/test_servicos.py | 328 +++ .../src/tests/test_simplified_payload.py | 80 + .../src/tests/test_upload_certificado.py | 88 + endpoint_listener/src/utils/Exchange2.py | 1041 ++++++++ endpoint_listener/src/utils/api.py | 158 ++ endpoint_listener/src/utils/banco.py | 242 ++ endpoint_listener/src/utils/enviaListerner.py | 35 + endpoint_listener/src/utils/exchangeModels.py | 127 + endpoint_listener/src/utils/redis.py | 107 + endpoint_listener/src/webhooks/webhooks.py | 64 + logs/integracao_02 | 280 ++ logs/integracao_27 | 0 pytest.ini | 3 + tecnospeed_queue/Dockerfile | 28 + tecnospeed_queue/requirements.txt | 13 + tecnospeed_queue/src/Models/config.py | 18 + tecnospeed_queue/src/Models/empresa.py | 97 + tecnospeed_queue/src/Models/nfe.py | 115 + tecnospeed_queue/src/Models/nfse.py | 49 + tecnospeed_queue/src/Models/payload.py | 518 ++++ tecnospeed_queue/src/Models/servico.py | 150 ++ tecnospeed_queue/src/Models/tomador.py | 34 + tecnospeed_queue/src/NFSe/Servico/servico.py | 105 + tecnospeed_queue/src/NFSe/Tomador/tomador.py | 91 + tecnospeed_queue/src/NFSe/nfse.py | 167 ++ tecnospeed_queue/src/NFe/registroNFe.py | 126 + tecnospeed_queue/src/Relatorios/relatorios.py | 70 + .../certificado_digital.py | 194 ++ tecnospeed_queue/src/empresas/empresa.py | 184 ++ tecnospeed_queue/src/logs/integracao_01 | 24 + tecnospeed_queue/src/logs/integracao_02 | 194 ++ tecnospeed_queue/src/logs/integracao_06 | 56 + tecnospeed_queue/src/logs/integracao_25 | 0 tecnospeed_queue/src/logs/integracao_26 | 0 tecnospeed_queue/src/logs/integracao_27 | 140 + tecnospeed_queue/src/tecnospeed_queue.py | 685 +++++ tecnospeed_queue/src/tests/__init__.py | 1 + tecnospeed_queue/src/tests/conftest.py | 119 + tecnospeed_queue/src/tests/debug_payload.py | 67 + .../src/tests/debug_report_api.py | 33 + tecnospeed_queue/src/tests/purge_queue.py | 46 + tecnospeed_queue/src/tests/test.py | 134 + tecnospeed_queue/src/tests/testArch.py | 58 + .../src/tests/test_1000_empresas.py | 144 ++ tecnospeed_queue/src/tests/test_1000_nfe.py | 221 ++ tecnospeed_queue/src/tests/test_1000_nfse.py | 173 ++ tecnospeed_queue/src/tests/test_api.py | 29 + tecnospeed_queue/src/tests/test_callback.py | 96 + tecnospeed_queue/src/tests/test_nfe.py | 179 ++ tecnospeed_queue/src/tests/test_nfse.py | 146 ++ tecnospeed_queue/src/tests/test_queue.py | 164 ++ tecnospeed_queue/src/tests/test_servicos.py | 328 +++ .../src/tests/test_simplified_payload.py | 80 + .../src/tests/test_upload_certificado.py | 88 + tecnospeed_queue/src/utils/Exchange2.py | 1047 ++++++++ tecnospeed_queue/src/utils/api.py | 158 ++ tecnospeed_queue/src/utils/banco.py | 243 ++ tecnospeed_queue/src/utils/enviaListerner.py | 35 + tecnospeed_queue/src/utils/exchangeModels.py | 127 + tecnospeed_queue/src/utils/redis.py | 107 + tecnospeed_queue/src/webhooks/webhooks.py | 64 + tecnospeed_rpc/Dockerfile | 28 + tecnospeed_rpc/requirements.txt | 13 + tecnospeed_rpc/src/Models/config.py | 18 + tecnospeed_rpc/src/Models/empresa.py | 97 + tecnospeed_rpc/src/Models/nfe.py | 115 + tecnospeed_rpc/src/Models/nfse.py | 49 + tecnospeed_rpc/src/Models/payload.py | 518 ++++ tecnospeed_rpc/src/Models/servico.py | 150 ++ tecnospeed_rpc/src/Models/tomador.py | 34 + tecnospeed_rpc/src/NFSe/Servico/servico.py | 105 + tecnospeed_rpc/src/NFSe/Tomador/tomador.py | 91 + tecnospeed_rpc/src/NFSe/nfse.py | 167 ++ tecnospeed_rpc/src/NFe/registroNFe.py | 126 + tecnospeed_rpc/src/Relatorios/relatorios.py | 70 + .../certificado_digital.py | 205 ++ tecnospeed_rpc/src/empresas/empresa.py | 278 ++ tecnospeed_rpc/src/logs/integracao_01 | 24 + tecnospeed_rpc/src/logs/integracao_02 | 194 ++ tecnospeed_rpc/src/logs/integracao_06 | 56 + tecnospeed_rpc/src/logs/integracao_25 | 0 tecnospeed_rpc/src/logs/integracao_26 | 0 tecnospeed_rpc/src/logs/integracao_27 | 140 + tecnospeed_rpc/src/tecnospeed_rpc.py | 721 ++++++ tecnospeed_rpc/src/tests/__init__.py | 1 + tecnospeed_rpc/src/tests/conftest.py | 119 + tecnospeed_rpc/src/tests/debug_payload.py | 67 + tecnospeed_rpc/src/tests/debug_report_api.py | 33 + tecnospeed_rpc/src/tests/purge_queue.py | 46 + tecnospeed_rpc/src/tests/test.py | 134 + tecnospeed_rpc/src/tests/testArch.py | 58 + .../src/tests/test_1000_empresas.py | 144 ++ tecnospeed_rpc/src/tests/test_1000_nfe.py | 219 ++ tecnospeed_rpc/src/tests/test_1000_nfse.py | 173 ++ tecnospeed_rpc/src/tests/test_api.py | 29 + tecnospeed_rpc/src/tests/test_callback.py | 95 + tecnospeed_rpc/src/tests/test_nfe.py | 179 ++ tecnospeed_rpc/src/tests/test_nfse.py | 146 ++ tecnospeed_rpc/src/tests/test_queue.py | 164 ++ tecnospeed_rpc/src/tests/test_servicos.py | 328 +++ .../src/tests/test_simplified_payload.py | 80 + .../src/tests/test_upload_certificado.py | 88 + tecnospeed_rpc/src/utils/Exchange2.py | 1047 ++++++++ tecnospeed_rpc/src/utils/api.py | 158 ++ tecnospeed_rpc/src/utils/banco.py | 243 ++ tecnospeed_rpc/src/utils/enviaListerner.py | 35 + tecnospeed_rpc/src/utils/exchangeModels.py | 127 + tecnospeed_rpc/src/utils/redis.py | 107 + tecnospeed_rpc/src/webhooks/webhooks.py | 64 + utils/Exchange2.py | 1041 ++++++++ utils/api.py | 158 ++ utils/banco.py | 241 ++ utils/enviaListerner.py | 35 + utils/exchangeModels.py | 127 + utils/redis.py | 107 + 223 files changed, 34924 insertions(+) create mode 100644 Models/config.py create mode 100644 Models/empresa.py create mode 100644 Models/nfe.py create mode 100644 Models/nfse.py create mode 100644 Models/payload.py create mode 100644 Models/servico.py create mode 100644 Models/tomador.py create mode 100644 README.md create mode 100644 backup/Payloads_Worker_Plugnotas.postman_collection.json create mode 100644 backup/PlugNotas_Example-SandBox.postman_collection.json create mode 100644 backup/auxiliar.json create mode 100644 backup/cadastrar_empresa.json create mode 100644 backup/collectionsForEduardo.json create mode 100644 backup/json_insercao_empresa.json create mode 100644 backup/modelo_json_rpc.json create mode 100644 backup/modelo_system_config.json create mode 100644 backup/servicoPlugnotasCollections.json create mode 100644 backup/src/Models/config.py create mode 100644 backup/src/Models/empresa.py create mode 100644 backup/src/Models/nfe.py create mode 100644 backup/src/Models/nfse.py create mode 100644 backup/src/Models/payload.py create mode 100644 backup/src/Models/servico.py create mode 100644 backup/src/Models/tomador.py create mode 100644 backup/src/NFSe/Servico/servico.py create mode 100644 backup/src/NFSe/Tomador/tomador.py create mode 100644 backup/src/NFSe/nfse.py create mode 100644 backup/src/NFe/registroNFe.py create mode 100644 backup/src/Relatorios/relatorios.py create mode 100644 backup/src/certificado_digital/certificado_digital.py create mode 100644 backup/src/empresas/empresa.py create mode 100644 backup/src/listenerEndpoint.py create mode 100644 backup/src/logs/integracao_01 create mode 100644 backup/src/logs/integracao_02 create mode 100644 backup/src/logs/integracao_06 create mode 100644 backup/src/logs/integracao_25 create mode 100644 backup/src/logs/integracao_26 create mode 100644 backup/src/logs/integracao_27 create mode 100644 backup/src/main.py create mode 100644 backup/src/mainQUEUE.py create mode 100644 backup/src/main_listener.py create mode 100644 backup/src/tests/__init__.py create mode 100644 backup/src/tests/conftest.py create mode 100644 backup/src/tests/debug_payload.py create mode 100644 backup/src/tests/debug_report_api.py create mode 100644 backup/src/tests/purge_queue.py create mode 100644 backup/src/tests/test.py create mode 100644 backup/src/tests/testArch.py create mode 100644 backup/src/tests/test_1000_empresas.py create mode 100644 backup/src/tests/test_1000_nfe.py create mode 100644 backup/src/tests/test_1000_nfse.py create mode 100644 backup/src/tests/test_api.py create mode 100644 backup/src/tests/test_callback.py create mode 100644 backup/src/tests/test_nfe.py create mode 100644 backup/src/tests/test_nfse.py create mode 100644 backup/src/tests/test_queue.py create mode 100644 backup/src/tests/test_servicos.py create mode 100644 backup/src/tests/test_simplified_payload.py create mode 100644 backup/src/tests/test_upload_certificado.py create mode 100644 backup/src/utils/Exchange2.py create mode 100644 backup/src/utils/api.py create mode 100644 backup/src/utils/banco.py create mode 100644 backup/src/utils/enviaListerner.py create mode 100644 backup/src/utils/exchangeModels.py create mode 100644 backup/src/utils/redis.py create mode 100644 backup/src/webhooks/webhooks.py create mode 100644 backup/test_payload.py create mode 100644 buildImages.sh create mode 100644 docker-compose.yml create mode 100644 endpoint_listener/Dockerfile create mode 100644 endpoint_listener/requirements.txt create mode 100644 endpoint_listener/src/Models/config.py create mode 100644 endpoint_listener/src/Models/empresa.py create mode 100644 endpoint_listener/src/Models/nfe.py create mode 100644 endpoint_listener/src/Models/nfse.py create mode 100644 endpoint_listener/src/Models/payload.py create mode 100644 endpoint_listener/src/Models/servico.py create mode 100644 endpoint_listener/src/Models/tomador.py create mode 100644 endpoint_listener/src/NFSe/Servico/servico.py create mode 100644 endpoint_listener/src/NFSe/Tomador/tomador.py create mode 100644 endpoint_listener/src/NFSe/nfse.py create mode 100644 endpoint_listener/src/NFe/registroNFe.py create mode 100644 endpoint_listener/src/Relatorios/relatorios.py create mode 100644 endpoint_listener/src/certificado_digital/certificado_digital.py create mode 100644 endpoint_listener/src/empresas/empresa.py create mode 100644 endpoint_listener/src/endpoint_listener.py create mode 100644 endpoint_listener/src/listenerEndpoint.py create mode 100644 endpoint_listener/src/logs/integracao_01 create mode 100644 endpoint_listener/src/logs/integracao_02 create mode 100644 endpoint_listener/src/logs/integracao_06 create mode 100644 endpoint_listener/src/logs/integracao_25 create mode 100644 endpoint_listener/src/logs/integracao_26 create mode 100644 endpoint_listener/src/logs/integracao_27 create mode 100644 endpoint_listener/src/tests/__init__.py create mode 100644 endpoint_listener/src/tests/conftest.py create mode 100644 endpoint_listener/src/tests/debug_payload.py create mode 100644 endpoint_listener/src/tests/debug_report_api.py create mode 100644 endpoint_listener/src/tests/purge_queue.py create mode 100644 endpoint_listener/src/tests/test.py create mode 100644 endpoint_listener/src/tests/testArch.py create mode 100644 endpoint_listener/src/tests/test_1000_empresas.py create mode 100644 endpoint_listener/src/tests/test_1000_nfe.py create mode 100644 endpoint_listener/src/tests/test_1000_nfse.py create mode 100644 endpoint_listener/src/tests/test_api.py create mode 100644 endpoint_listener/src/tests/test_callback.py create mode 100644 endpoint_listener/src/tests/test_nfe.py create mode 100644 endpoint_listener/src/tests/test_nfse.py create mode 100644 endpoint_listener/src/tests/test_queue.py create mode 100644 endpoint_listener/src/tests/test_servicos.py create mode 100644 endpoint_listener/src/tests/test_simplified_payload.py create mode 100644 endpoint_listener/src/tests/test_upload_certificado.py create mode 100644 endpoint_listener/src/utils/Exchange2.py create mode 100644 endpoint_listener/src/utils/api.py create mode 100644 endpoint_listener/src/utils/banco.py create mode 100644 endpoint_listener/src/utils/enviaListerner.py create mode 100644 endpoint_listener/src/utils/exchangeModels.py create mode 100644 endpoint_listener/src/utils/redis.py create mode 100644 endpoint_listener/src/webhooks/webhooks.py create mode 100644 logs/integracao_02 create mode 100644 logs/integracao_27 create mode 100644 pytest.ini create mode 100644 tecnospeed_queue/Dockerfile create mode 100644 tecnospeed_queue/requirements.txt create mode 100644 tecnospeed_queue/src/Models/config.py create mode 100644 tecnospeed_queue/src/Models/empresa.py create mode 100644 tecnospeed_queue/src/Models/nfe.py create mode 100644 tecnospeed_queue/src/Models/nfse.py create mode 100644 tecnospeed_queue/src/Models/payload.py create mode 100644 tecnospeed_queue/src/Models/servico.py create mode 100644 tecnospeed_queue/src/Models/tomador.py create mode 100644 tecnospeed_queue/src/NFSe/Servico/servico.py create mode 100644 tecnospeed_queue/src/NFSe/Tomador/tomador.py create mode 100644 tecnospeed_queue/src/NFSe/nfse.py create mode 100644 tecnospeed_queue/src/NFe/registroNFe.py create mode 100644 tecnospeed_queue/src/Relatorios/relatorios.py create mode 100644 tecnospeed_queue/src/certificado_digital/certificado_digital.py create mode 100644 tecnospeed_queue/src/empresas/empresa.py create mode 100644 tecnospeed_queue/src/logs/integracao_01 create mode 100644 tecnospeed_queue/src/logs/integracao_02 create mode 100644 tecnospeed_queue/src/logs/integracao_06 create mode 100644 tecnospeed_queue/src/logs/integracao_25 create mode 100644 tecnospeed_queue/src/logs/integracao_26 create mode 100644 tecnospeed_queue/src/logs/integracao_27 create mode 100644 tecnospeed_queue/src/tecnospeed_queue.py create mode 100644 tecnospeed_queue/src/tests/__init__.py create mode 100644 tecnospeed_queue/src/tests/conftest.py create mode 100644 tecnospeed_queue/src/tests/debug_payload.py create mode 100644 tecnospeed_queue/src/tests/debug_report_api.py create mode 100644 tecnospeed_queue/src/tests/purge_queue.py create mode 100644 tecnospeed_queue/src/tests/test.py create mode 100644 tecnospeed_queue/src/tests/testArch.py create mode 100644 tecnospeed_queue/src/tests/test_1000_empresas.py create mode 100644 tecnospeed_queue/src/tests/test_1000_nfe.py create mode 100644 tecnospeed_queue/src/tests/test_1000_nfse.py create mode 100644 tecnospeed_queue/src/tests/test_api.py create mode 100644 tecnospeed_queue/src/tests/test_callback.py create mode 100644 tecnospeed_queue/src/tests/test_nfe.py create mode 100644 tecnospeed_queue/src/tests/test_nfse.py create mode 100644 tecnospeed_queue/src/tests/test_queue.py create mode 100644 tecnospeed_queue/src/tests/test_servicos.py create mode 100644 tecnospeed_queue/src/tests/test_simplified_payload.py create mode 100644 tecnospeed_queue/src/tests/test_upload_certificado.py create mode 100644 tecnospeed_queue/src/utils/Exchange2.py create mode 100644 tecnospeed_queue/src/utils/api.py create mode 100644 tecnospeed_queue/src/utils/banco.py create mode 100644 tecnospeed_queue/src/utils/enviaListerner.py create mode 100644 tecnospeed_queue/src/utils/exchangeModels.py create mode 100644 tecnospeed_queue/src/utils/redis.py create mode 100644 tecnospeed_queue/src/webhooks/webhooks.py create mode 100644 tecnospeed_rpc/Dockerfile create mode 100644 tecnospeed_rpc/requirements.txt create mode 100644 tecnospeed_rpc/src/Models/config.py create mode 100644 tecnospeed_rpc/src/Models/empresa.py create mode 100644 tecnospeed_rpc/src/Models/nfe.py create mode 100644 tecnospeed_rpc/src/Models/nfse.py create mode 100644 tecnospeed_rpc/src/Models/payload.py create mode 100644 tecnospeed_rpc/src/Models/servico.py create mode 100644 tecnospeed_rpc/src/Models/tomador.py create mode 100644 tecnospeed_rpc/src/NFSe/Servico/servico.py create mode 100644 tecnospeed_rpc/src/NFSe/Tomador/tomador.py create mode 100644 tecnospeed_rpc/src/NFSe/nfse.py create mode 100644 tecnospeed_rpc/src/NFe/registroNFe.py create mode 100644 tecnospeed_rpc/src/Relatorios/relatorios.py create mode 100644 tecnospeed_rpc/src/certificado_digital/certificado_digital.py create mode 100644 tecnospeed_rpc/src/empresas/empresa.py create mode 100644 tecnospeed_rpc/src/logs/integracao_01 create mode 100644 tecnospeed_rpc/src/logs/integracao_02 create mode 100644 tecnospeed_rpc/src/logs/integracao_06 create mode 100644 tecnospeed_rpc/src/logs/integracao_25 create mode 100644 tecnospeed_rpc/src/logs/integracao_26 create mode 100644 tecnospeed_rpc/src/logs/integracao_27 create mode 100644 tecnospeed_rpc/src/tecnospeed_rpc.py create mode 100644 tecnospeed_rpc/src/tests/__init__.py create mode 100644 tecnospeed_rpc/src/tests/conftest.py create mode 100644 tecnospeed_rpc/src/tests/debug_payload.py create mode 100644 tecnospeed_rpc/src/tests/debug_report_api.py create mode 100644 tecnospeed_rpc/src/tests/purge_queue.py create mode 100644 tecnospeed_rpc/src/tests/test.py create mode 100644 tecnospeed_rpc/src/tests/testArch.py create mode 100644 tecnospeed_rpc/src/tests/test_1000_empresas.py create mode 100644 tecnospeed_rpc/src/tests/test_1000_nfe.py create mode 100644 tecnospeed_rpc/src/tests/test_1000_nfse.py create mode 100644 tecnospeed_rpc/src/tests/test_api.py create mode 100644 tecnospeed_rpc/src/tests/test_callback.py create mode 100644 tecnospeed_rpc/src/tests/test_nfe.py create mode 100644 tecnospeed_rpc/src/tests/test_nfse.py create mode 100644 tecnospeed_rpc/src/tests/test_queue.py create mode 100644 tecnospeed_rpc/src/tests/test_servicos.py create mode 100644 tecnospeed_rpc/src/tests/test_simplified_payload.py create mode 100644 tecnospeed_rpc/src/tests/test_upload_certificado.py create mode 100644 tecnospeed_rpc/src/utils/Exchange2.py create mode 100644 tecnospeed_rpc/src/utils/api.py create mode 100644 tecnospeed_rpc/src/utils/banco.py create mode 100644 tecnospeed_rpc/src/utils/enviaListerner.py create mode 100644 tecnospeed_rpc/src/utils/exchangeModels.py create mode 100644 tecnospeed_rpc/src/utils/redis.py create mode 100644 tecnospeed_rpc/src/webhooks/webhooks.py create mode 100644 utils/Exchange2.py create mode 100644 utils/api.py create mode 100644 utils/banco.py create mode 100644 utils/enviaListerner.py create mode 100644 utils/exchangeModels.py create mode 100644 utils/redis.py diff --git a/Models/config.py b/Models/config.py new file mode 100644 index 0000000..76500ee --- /dev/null +++ b/Models/config.py @@ -0,0 +1,18 @@ +from pydantic import BaseModel + +# Dados de conexão ao banco de dados isolado do cliente e da API PlugNotas +class Config(BaseModel): + api_key: str | None = None + x_api_key: str | None = None + user_postgres: str + pass_postgres: str + host_postgres: str + port_postgres: str + database_postgres: str + +# Informações de mensageria para a troca de arquivos (AMQP) via subagent +class SystemConfig(BaseModel): + serviceid: str + servicekey: str + origin: str + amqps: str \ No newline at end of file diff --git a/Models/empresa.py b/Models/empresa.py new file mode 100644 index 0000000..0aad758 --- /dev/null +++ b/Models/empresa.py @@ -0,0 +1,97 @@ +from pydantic import BaseModel + + +################### +## POST /empresas # +################### +class Endereco(BaseModel): + codigoPais: str + descricaoPais: str + tipoLogradouro: str | None = None + logradouro: str + numero: str + complemento: str | None = None + tipoBairro: str | None = None + bairro: str + codigoCidade: str + descricaoCidade: str + estado: str + cep: str + +class Rps(BaseModel): + lote: int + numeracao: list + +class Email(BaseModel): + envio: bool + +class Config(BaseModel): + rps: Rps | None = None + email: Email | None = None + producao: bool | None = None + envioLimitado: bool | None = None + integracoes: list | None = None + prefeitura: dict | str | int | None = None + +class Nfse(BaseModel): + config: Config | None = None + ativo: bool = False + tipoContrato: int | None = 0 + +class Dfe(BaseModel): + primeiroNsu: bool + +class ConfigNfe(BaseModel): + dfe: Dfe | None = None + email: Email | None = None + integracoes: dict | None = None + producao: bool | None = None + impressaoFcp: bool | None = None + impressaoPartilha: bool | None = None + versaoManual: str | None = None + versaoEsquema: str | None = None + numeracao: list | None = None + calculoAutomaticoIbpt: bool | None = True + +class Nfe(BaseModel): + config: ConfigNfe | None = None + ativo: bool = False + tipoContrato: int | None = 0 + +class ConfigNfce(BaseModel): + email: Email | None = None + integracoes: dict | None = None + producao: bool | None = None + versaoManual: str | None = None + versaoEsquema: str | None = None + numeracao: list | None = None + +class Nfce(BaseModel): + config: ConfigNfce | None = None + ativo: bool = False + tipoContrato: int | None = 0 + + +# Entidade Empresa que cadastra a credencial emissora no sistema da PlugNotas +class Empresa(BaseModel): + endereco: Endereco + nfse: Nfse | None = None + nfe: Nfe | None = None + nfce: Nfce | None = None + incentivoFiscal: bool + incentivadorCultural: bool + cpfCnpj: str + inscricaoMunicipal: str + inscricaoEstadual: str | None = None + certificado: str + razaoSocial: str + nomeFantasia: str | None = None + simplesNacional: bool + regimeTributario: int + regimeTributarioEspecial: int + id: str | None = None + banco: str | None = None + email: str | None = None + id_empresa: int | None = None + + diff --git a/Models/nfe.py b/Models/nfe.py new file mode 100644 index 0000000..b6e186a --- /dev/null +++ b/Models/nfe.py @@ -0,0 +1,115 @@ +from pydantic import BaseModel +from typing import List, Optional, Literal + +EstadoEnum = Literal[ + "AC", "AL", "AM", "AP", "BA", "CE", "DF", "ES", "GO", "MA", "MG", "MS", "MT", + "PA", "PB", "PE", "PI", "PR", "RJ", "RN", "RO", "RR", "RS", "SC", "SE", "SP", "TO" +] + +TipoLogradouroEnum = Literal[ + "Alameda", "Avenida", "Chácara", "Colônia", "Condomínio", "Estância", "Estrada", + "Fazenda", "Praça", "Prolongamento", "Rodovia", "Rua", "Sítio", "Travessa", "Vicinal", "Eqnp" +] + +class Emitente(BaseModel): + cpfCnpj: str + inscricaoEstadual: Optional[str] = None + +class Endereco(BaseModel): + logradouro: str + numero: str + bairro: str + codigoCidade: str + estado: EstadoEnum + cep: str + tipoLogradouro: TipoLogradouroEnum + codigoPais: Optional[str] = "1058" + complemento: Optional[str] = None + descricaoCidade: Optional[str] = None + descricaoPais: Optional[str] = "Brasil" + +class Destinatario(BaseModel): + cpfCnpj: str + razaoSocial: str + nomeFantasia: Optional[str] = None + email: Optional[str] = None + inscricaoEstadual: Optional[str] = None + inscricaoMunicipal: Optional[str] = None + inscricaoSuframa: Optional[str] = None + orgaoPublico: Optional[bool] = False + endereco: Endereco + +class ValorTributavel(BaseModel): + comercial: float + tributavel: float + +class BaseCalculoIcms(BaseModel): + modalidadeDeterminacao: int + valor: float + +class Icms(BaseModel): + origem: str + cst: str + baseCalculo: BaseCalculoIcms + aliquota: float + valor: float + +class BaseCalculoPis(BaseModel): + valor: float + quantidade: int + +class Pis(BaseModel): + cst: str + baseCalculo: BaseCalculoPis + aliquota: float + valor: float + +class BaseCalculoCofins(BaseModel): + valor: float + +class Cofins(BaseModel): + cst: str + baseCalculo: BaseCalculoCofins + aliquota: float + valor: float + +class Tributos(BaseModel): + icms: Optional[Icms] = None + pis: Optional[Pis] = None + cofins: Optional[Cofins] = None + +class ItemNFe(BaseModel): + codigo: str + descricao: str + ncm: str + cest: Optional[str] = None + cfop: str + valorUnitario: ValorTributavel + valor: float + tributos: Tributos + +class Pagamento(BaseModel): + aVista: Optional[bool] = True + meio: Optional[str] = "01" + valor: float + +class Telefone(BaseModel): + ddd: str + numero: str + +class ResponsavelTecnico(BaseModel): + cpfCnpj: str + nome: str + email: str + telefone: Telefone + +class NFe(BaseModel): + idIntegracao: str + presencial: bool + consumidorFinal: bool + natureza: str + emitente: Emitente + destinatario: Destinatario + itens: List[ItemNFe] + pagamentos: List[Pagamento] + responsavelTecnico: Optional[ResponsavelTecnico] = None diff --git a/Models/nfse.py b/Models/nfse.py new file mode 100644 index 0000000..857ad97 --- /dev/null +++ b/Models/nfse.py @@ -0,0 +1,49 @@ +from pydantic import BaseModel +from typing import Optional + +class Prestador(BaseModel): + cpfCnpj: str + +class Endereco(BaseModel): + descricaoCidade: str + cep: str + tipoLogradouro: str + logradouro: str + tipoBairro: Optional[str] = None + codigoCidade: str + complemento: Optional[str] = None + estado: str + numero: str + bairro: str + +class Tomador(BaseModel): + cpfCnpj: str + razaoSocial: str + inscricaoMunicipal: Optional[str] = None + email: str + endereco: dict + +class ISS(BaseModel): + tipoTributacao: int + exigibilidade: int + aliquota: float + +class Valor(BaseModel): + servico: float + descontoCondicionado: Optional[float] = 0 + descontoIncondicionado: Optional[float] = 0 + +class Servico(BaseModel): + codigo: str + codigoTributacao: str + discriminacao: str + cnae: str + iss: ISS + valor: Valor + +# Estrutura padrão de envio de uma NFSe contendo Prestador, Tomador e Serviço prestado +class NFSe(BaseModel): + idIntegracao: str + prestador: Prestador + tomador: Tomador + servico: Servico diff --git a/Models/payload.py b/Models/payload.py new file mode 100644 index 0000000..841551b --- /dev/null +++ b/Models/payload.py @@ -0,0 +1,518 @@ +from enum import Enum +from pydantic import BaseModel, ConfigDict, Field + +from Models.config import Config +from Models.empresa import Empresa +from Models.nfe import NFe +from Models.nfse import NFSe +from Models.servico import Servico +from Models.tomador import Tomador + + +class Fila(str, Enum): + NFE = "nfe" + NFSE = "nfse" + CERTIFICADO_DIGITAL = "certificado_digital" + EMPRESA = "empresa" + SERVICO = "servico" + TOMADOR = "tomador" + RELATORIOS = "relatorios" + WEBHOOKS = "webhooks" + + +class NFeMetodo(str, Enum): + POST = "post" + GET = "get" + DOWNLOAD = "download" + DOWNLOAD_XML = "download_xml" + CANCELAR = "cancelar" + STATUS_CANCELAMENTO = "status_cancelamento" + CCE = "cce" + STATUS_CCE = "status_cce" + RESUMO = "resumo" + + +class NFSeMetodo(str, Enum): + POST = "post" + GET = "get" + DOWNLOAD = "download" + DOWNLOAD_XML = "download_xml" + CANCELAR = "cancelar" + STATUS_CANCELAMENTO = "status_cancelamento" + RESUMO = "resumo" + HISTORICO = "historico" + + +class CertificadoMetodo(str, Enum): + POST = "post" + GET = "get" + PUT = "put" + + +class EmpresaMetodo(str, Enum): + POST = "post" + GET = "get" + POST_FILE = "post_file" + PATCH = "patch" + + +class ServicoMetodo(str, Enum): + POST = "post" + GET = "get" + PUT = "put" + DELETE = "delete" + + +class TomadorMetodo(str, Enum): + POST = "post" + GET = "get" + PUT = "put" + DELETE = "delete" + + +class RelatorioMetodo(str, Enum): + NFE_EMITIDAS = "nfe_emitidas" + NFE_POR_CNPJ = "nfe_por_cnpj" + NFSE_EMITIDAS = "nfse_emitidas" + NFSE_POR_CNPJ = "nfse_por_cnpj" + + +class WebhookMetodo(str, Enum): + GET = "get" + POST = "post" + DELETE = "delete" + PUT = "put" + TEST = "test" + + +class BaseEnvelope(BaseModel): + cliente: str | None = None + config: Config + url_base: str | None = None + + +# --- Base Data Models --- + +class BaseDados(BaseModel): + model_config = ConfigDict(populate_by_name=True) + id_usuario: int | None = 1 + + +class NFePostDadosWrapped(BaseDados): + nfe: NFe + id_empresa: str | None = None + + +class NFePostDadosDirect(NFe): + id_usuario: int | None = 1 + id_empresa: str | None = None + + +class NFeIdDados(BaseDados): + id: str + + +class NFeCancelarDados(NFeIdDados): + justificativa: str | None = None + + +class NFeCCEDados(NFeIdDados): + correcao: str + + +class NFeResumoDados(BaseDados): + cnpj: str + id_integracao: str | None = None + id: str | None = None + + +class NFSePostDados(NFSe): + id_usuario: int | None = 1 + id_empresa: str | None = None + + +class NFSeIdDados(BaseDados): + id: str + + +class NFSeGetDados(BaseDados): + id: str | None = None + + +class NFSeResumoDados(NFSeIdDados): + cnpj: str + + +class NFSeHistoricoDados(NFSeIdDados): + cnpj: str | None = None + + +class CertificadoPostDados(BaseDados): + arquivo: str + senha: str + id_empresa: str | None = None + email: str | None = None + expiration_at: str | None = Field(None, alias="expirationAt") + data_expiracao: str | None = None + + +class CertificadoGetDados(BaseDados): + id: str | None = None + + +class CertificadoPutDados(CertificadoPostDados): + id: str + + +class EmpresaPostDados(Empresa): + id_usuario: int | None = 1 + + +class EmpresaGetDados(BaseDados): + cnpj: str | None = None + + +class EmpresaPostFileDados(BaseDados): + file: str + cnpj: str + + +class EmpresaPatchDados(BaseDados): + cnpj: str + certificado: str + + +class ServicoPostDados(Servico): + id_usuario: int | None = 1 + + +class ServicoGetDados(BaseDados): + id: str | None = None + + +class ServicoPutDados(Servico): + id: str + id_usuario: int | None = 1 + + +class ServicoDeleteDados(BaseDados): + id: str + + +class TomadorPostDados(Tomador): + id_usuario: int | None = 1 + + +class TomadorGetDados(BaseDados): + cpf_cnpj: str | None = None + + +class TomadorPutDados(Tomador): + cpf_cnpj: str + id_usuario: int | None = 1 + + +class TomadorDeleteDados(BaseDados): + cpf_cnpj: str + + +class RelatorioVazioDados(BaseDados): + pass + + +class RelatorioCnpjDados(BaseDados): + cnpj: str + + +class RelatorioPeriodoDados(BaseDados): + data_inicial: str | None = None + data_final: str | None = None + + +class RelatorioCnpjPeriodoDados(RelatorioPeriodoDados): + cnpj: str + + +class WebhookGetDeleteTestDados(BaseDados): + cnpj: str + + +class WebhookPostPutDados(WebhookGetDeleteTestDados): + url: str + + +# --- Envelopes --- + +class NFePostEnvelope(BaseEnvelope): + fila: Fila = Fila.NFE + metodo: NFeMetodo = NFeMetodo.POST + dados: NFePostDadosDirect | NFePostDadosWrapped + + +class NFeIdEnvelope(BaseEnvelope): + fila: Fila = Fila.NFE + metodo: NFeMetodo + dados: NFeIdDados + + +class NFeCancelarEnvelope(BaseEnvelope): + fila: Fila = Fila.NFE + metodo: NFeMetodo = NFeMetodo.CANCELAR + dados: NFeCancelarDados + + +class NFeCCEEnvelope(BaseEnvelope): + fila: Fila = Fila.NFE + metodo: NFeMetodo = NFeMetodo.CCE + dados: NFeCCEDados + + +class NFeResumoEnvelope(BaseEnvelope): + fila: Fila = Fila.NFE + metodo: NFeMetodo = NFeMetodo.RESUMO + dados: NFeResumoDados + + +class NFSePostEnvelope(BaseEnvelope): + fila: Fila = Fila.NFSE + metodo: NFSeMetodo = NFSeMetodo.POST + dados: NFSePostDados + + +class NFSeGetEnvelope(BaseEnvelope): + fila: Fila = Fila.NFSE + metodo: NFSeMetodo = NFSeMetodo.GET + dados: NFSeGetDados + + +class NFSeIdEnvelope(BaseEnvelope): + fila: Fila = Fila.NFSE + metodo: NFSeMetodo + dados: NFSeIdDados + + +class NFSeResumoEnvelope(BaseEnvelope): + fila: Fila = Fila.NFSE + metodo: NFSeMetodo = NFSeMetodo.RESUMO + dados: NFSeResumoDados + + +class NFSeHistoricoEnvelope(BaseEnvelope): + fila: Fila = Fila.NFSE + metodo: NFSeMetodo = NFSeMetodo.HISTORICO + dados: NFSeHistoricoDados + + +class CertificadoPostEnvelope(BaseEnvelope): + fila: Fila = Fila.CERTIFICADO_DIGITAL + metodo: CertificadoMetodo = CertificadoMetodo.POST + dados: CertificadoPostDados + + +class CertificadoGetEnvelope(BaseEnvelope): + fila: Fila = Fila.CERTIFICADO_DIGITAL + metodo: CertificadoMetodo = CertificadoMetodo.GET + dados: CertificadoGetDados + + +class CertificadoPutEnvelope(BaseEnvelope): + fila: Fila = Fila.CERTIFICADO_DIGITAL + metodo: CertificadoMetodo = CertificadoMetodo.PUT + dados: CertificadoPutDados + + +class EmpresaPostEnvelope(BaseEnvelope): + fila: Fila = Fila.EMPRESA + metodo: EmpresaMetodo = EmpresaMetodo.POST + dados: EmpresaPostDados + + +class EmpresaGetEnvelope(BaseEnvelope): + fila: Fila = Fila.EMPRESA + metodo: EmpresaMetodo = EmpresaMetodo.GET + dados: EmpresaGetDados + + +class EmpresaPostFileEnvelope(BaseEnvelope): + fila: Fila = Fila.EMPRESA + metodo: EmpresaMetodo = EmpresaMetodo.POST_FILE + dados: EmpresaPostFileDados + + +class EmpresaPatchEnvelope(BaseEnvelope): + fila: Fila = Fila.EMPRESA + metodo: EmpresaMetodo = EmpresaMetodo.PATCH + dados: EmpresaPatchDados + + +class ServicoPostEnvelope(BaseEnvelope): + fila: Fila = Fila.SERVICO + metodo: ServicoMetodo = ServicoMetodo.POST + dados: ServicoPostDados + + +class ServicoGetEnvelope(BaseEnvelope): + fila: Fila = Fila.SERVICO + metodo: ServicoMetodo = ServicoMetodo.GET + dados: ServicoGetDados + + +class ServicoPutEnvelope(BaseEnvelope): + fila: Fila = Fila.SERVICO + metodo: ServicoMetodo = ServicoMetodo.PUT + dados: ServicoPutDados + + +class ServicoDeleteEnvelope(BaseEnvelope): + fila: Fila = Fila.SERVICO + metodo: ServicoMetodo = ServicoMetodo.DELETE + dados: ServicoDeleteDados + + +class TomadorPostEnvelope(BaseEnvelope): + fila: Fila = Fila.TOMADOR + metodo: TomadorMetodo = TomadorMetodo.POST + dados: TomadorPostDados + + +class TomadorGetEnvelope(BaseEnvelope): + fila: Fila = Fila.TOMADOR + metodo: TomadorMetodo = TomadorMetodo.GET + dados: TomadorGetDados + + +class TomadorPutEnvelope(BaseEnvelope): + fila: Fila = Fila.TOMADOR + metodo: TomadorMetodo = TomadorMetodo.PUT + dados: TomadorPutDados + + +class TomadorDeleteEnvelope(BaseEnvelope): + fila: Fila = Fila.TOMADOR + metodo: TomadorMetodo = TomadorMetodo.DELETE + dados: TomadorDeleteDados + + +class RelatorioNFeEmitidasEnvelope(BaseEnvelope): + fila: Fila = Fila.RELATORIOS + metodo: RelatorioMetodo = RelatorioMetodo.NFE_EMITIDAS + dados: RelatorioVazioDados + + +class RelatorioNFeCnpjEnvelope(BaseEnvelope): + fila: Fila = Fila.RELATORIOS + metodo: RelatorioMetodo = RelatorioMetodo.NFE_POR_CNPJ + dados: RelatorioCnpjDados + + +class RelatorioNFSeEmitidasEnvelope(BaseEnvelope): + fila: Fila = Fila.RELATORIOS + metodo: RelatorioMetodo = RelatorioMetodo.NFSE_EMITIDAS + dados: RelatorioPeriodoDados + + +class RelatorioNFSeCnpjEnvelope(BaseEnvelope): + fila: Fila = Fila.RELATORIOS + metodo: RelatorioMetodo = RelatorioMetodo.NFSE_POR_CNPJ + dados: RelatorioCnpjPeriodoDados + + +class WebhookGetDeleteTestEnvelope(BaseEnvelope): + fila: Fila = Fila.WEBHOOKS + metodo: WebhookMetodo + dados: WebhookGetDeleteTestDados + + +class WebhookPostPutEnvelope(BaseEnvelope): + fila: Fila = Fila.WEBHOOKS + metodo: WebhookMetodo + dados: WebhookPostPutDados + + +# --- Generic Envelope for basic structural validation --- + +class GenericEnvelope(BaseEnvelope): + """Validates that minimum required fields exist before routing.""" + fila: Fila + metodo: str + dados: dict | None = None + + +# --- Payload Validation Router --- + +# Maps (fila, metodo) -> Envelope class +_ENVELOPE_MAP: dict[tuple[str, str], type[BaseEnvelope]] = { + # NFe + ("nfe", "post"): NFePostEnvelope, + ("nfe", "get"): NFeIdEnvelope, + ("nfe", "download"): NFeIdEnvelope, + ("nfe", "download_xml"): NFeIdEnvelope, + ("nfe", "cancelar"): NFeCancelarEnvelope, + ("nfe", "status_cancelamento"): NFeIdEnvelope, + ("nfe", "cce"): NFeCCEEnvelope, + ("nfe", "status_cce"): NFeIdEnvelope, + ("nfe", "resumo"): NFeResumoEnvelope, + # NFSe + ("nfse", "post"): NFSePostEnvelope, + ("nfse", "get"): NFSeGetEnvelope, + ("nfse", "download"): NFSeIdEnvelope, + ("nfse", "download_xml"): NFSeIdEnvelope, + ("nfse", "cancelar"): NFSeIdEnvelope, + ("nfse", "status_cancelamento"): NFSeIdEnvelope, + ("nfse", "resumo"): NFSeResumoEnvelope, + ("nfse", "historico"): NFSeHistoricoEnvelope, + # Certificado Digital + ("certificado_digital", "post"): CertificadoPostEnvelope, + ("certificado_digital", "get"): CertificadoGetEnvelope, + ("certificado_digital", "put"): CertificadoPutEnvelope, + # Empresa + ("empresa", "post"): EmpresaPostEnvelope, + ("empresa", "get"): EmpresaGetEnvelope, + ("empresa", "post_file"): EmpresaPostFileEnvelope, + ("empresa", "patch"): EmpresaPatchEnvelope, + # Serviço + ("servico", "post"): ServicoPostEnvelope, + ("servico", "get"): ServicoGetEnvelope, + ("servico", "put"): ServicoPutEnvelope, + ("servico", "delete"): ServicoDeleteEnvelope, + # Tomador + ("tomador", "post"): TomadorPostEnvelope, + ("tomador", "get"): TomadorGetEnvelope, + ("tomador", "put"): TomadorPutEnvelope, + ("tomador", "delete"): TomadorDeleteEnvelope, + # Relatórios + ("relatorios", "nfe_emitidas"): RelatorioNFeEmitidasEnvelope, + ("relatorios", "nfe_por_cnpj"): RelatorioNFeCnpjEnvelope, + ("relatorios", "nfse_emitidas"): RelatorioNFSeEmitidasEnvelope, + ("relatorios", "nfse_por_cnpj"): RelatorioNFSeCnpjEnvelope, + # Webhooks + ("webhooks", "get"): WebhookGetDeleteTestEnvelope, + ("webhooks", "post"): WebhookPostPutEnvelope, + ("webhooks", "delete"): WebhookGetDeleteTestEnvelope, + ("webhooks", "put"): WebhookPostPutEnvelope, + ("webhooks", "test"): WebhookGetDeleteTestEnvelope, +} + + +def validate_payload(payload_dict: dict) -> BaseEnvelope: + # Step 1: Basic structural validation + generic = GenericEnvelope(**payload_dict) + + fila = generic.fila.value + metodo = generic.metodo.lower() + + # Step 2: Find specific envelope + key = (fila, metodo) + envelope_cls = _ENVELOPE_MAP.get(key) + + if envelope_cls is None: + raise ValueError(f"Combinação fila/metodo desconhecida: {fila}/{metodo}") + + # Step 3: Normalize metodo in the dict to match enum lowercase values + payload_dict['metodo'] = metodo + + # Step 4: Full typed validation + return envelope_cls(**payload_dict) + + + diff --git a/Models/servico.py b/Models/servico.py new file mode 100644 index 0000000..afb717f --- /dev/null +++ b/Models/servico.py @@ -0,0 +1,150 @@ +from pydantic import BaseModel +from enum import IntEnum + +class TipoTributacao(IntEnum): + NAO_DEFINIDO = 0 + ISENTO_ISS = 1 + IMUNE = 2 + NAO_INCIDENCIA = 3 + NAO_TRIBUTAVEL = 4 + RETIDO = 5 + TRIBUTAVEL_MUNICIPIO = 6 + TRIBUTAVEL_FORA_MUNICIPIO = 7 + TRIBUTAVEL_MUNICIPIO_TOMADOR = 8 + +class TipoExigibilidade(IntEnum): + EXIGIVEL = 1 + NAO_INCIDENCIA = 2 + ISENCAO = 3 + EXPORTACAO = 4 + IMUNIDADE = 5 + SUSPENSO_ACAO_JUDICIAL = 6 + SUSPENSO_ACAO_ADMINISTRATIVA = 7 + +class ISS(BaseModel): + tipoTributacao: int #TipoTributacao + exigibilidade: int #TipoExigibilidade + retido: bool | None = None # Se o ISS foi retido na fonte + aliquota: float # Aliquota do ISS + valor: float # Valor do ISS + valorRetido: float # Valor do ISS retido + processoSuspensao: str # Numero processo de suspensão do ISS +############################################################################## +# tipoTributacao integer +# Default: 6 +# Enum: 0 1 2 3 4 5 6 7 8 +# Tipo de Tributação do Serviço, valores: +# 0 - Não definido +# 1 - Isento de ISS +# 2 - Imune +# 3 - Não Incidência no Município +# 4 - Não Tributável +# 5 - Retido +# 6 - Tributável Dentro do Município +# 7 - Tributável Fora do Município +# 8 - Tributável Dentro do Município pelo tomador +############################################################################## +# exigibilidade integer +# Default: 1 +# Enum: 1 2 3 4 5 6 7 +# Exigibilidade do ISS, valores: +# 1 - Exigível +# 2 - Não Incidência +# 3 - Isenção +# 4 - Exportação +# 5 - Imunidade +# 6 - Supenso por Ação Judicial +# 7 - Suspenso por Ação Administrativa +############################################################################## +# responsavelRetencao string +# Valores aceitos: +# 1 - Tomador +# 2 - Intermediário +############################################################################## + +class Obra(BaseModel): + art: str # Anotação de Responsabilidade Técnica (Codigo ART) + codigo: str # Código da obra + +class Valor(BaseModel): + servico: float # Valor bruto do serviço prestado. + baseCalculo: float # Valor da base de cálculo dos impostos. + deducoes: float # Valor deduções. + descontoCondicionado: float # Valor do desconto condicionado. + descontoIncondicionado: float # Valor do desconto incondicionado. + liquido: float # Valor liquido do serviço, caso você não informe esse campo, vamos calcular automáticamente. + unitario: float # Valor unitário do serviço. + valorAproximadoTributos: float # Valor aproximado dos tributos. + +class Deducao(BaseModel): + tipo: int + descricao: str # Descrição da dedução +############################################################################## +# tipo integer +# Tipo de dedução: +# 0 - Sem Deduções +# 1 - Materiais +# 2 - Subempreitada de Mão de Obra +# 3 - Serviços +# 4 - Produção Externa +# 5 - Alimentação e Bebidas +# 6 - Reembolso de Despesas +# 7 - Repasse Consorciado +# 8 - Repasse Plano de Saúde +# 99 - Outras Deduções +############################################################################## + +class RetencaoInterno(BaseModel): + aliquota: float # Aliquota da retenção + valor: float # Valor da retenção + +class Retencao(BaseModel): + pis: RetencaoInterno + cofins: RetencaoInterno + csll: RetencaoInterno + inss: RetencaoInterno + irrf: RetencaoInterno + outrasRetencoes: float # Valor de outras retenções + cpp: RetencaoInterno + +class Aliquota(BaseModel): + municipal: float # Aliquota municipal + estadual: float # Aliquota estadual + federal: float # Aliquota federal + +class Detalhado(BaseModel): + aliquota: Aliquota + +class Simplificado(BaseModel): + aliquota: float # Aliquota geral de impostos + +class IBPT(BaseModel): + simplificado: Simplificado + detalhado: Detalhado + +# Representa o detalhamento do serviço prestado na NFSe, impostos incidentes e deduções +class Servico(BaseModel): + codigo: str # Código de serviço, também conhecido como Código Item Lista Serviço, geralmente segue a LC116, podendo váriar de acordo com a prefeitura.Você pode encontrar esse dado no portal da prefeitura, em uma nota emitida, ou junto ao contador. + idIntegracao: str # Código EXTERNO de integração do serviço. Utilizado SOMENTE quando se cadastra previamente um serviço. + discriminacao: str # Descrição do serviço prestado + codigoTributacao: str # Código tributação no Município + cnae: str # O código CNAE ou, por extenso, Classificação Nacional de Atividades Econômicas, é uma forma de padronizar, em todo o território nacional, os códigos de atividades econômicas e os critérios de enquadramento usados pelos mais diversos órgãos da administração tributária do Brasil. + codigoCidadeIncidencia: str # Código IBGE da cidade de incidência do ISS. + descricaoCidadeIncidencia: str # Descrição da cidade de incidência do ISS. + unidade: str # Unidade do serviço prestado. + quantidade: int # Quantidade dos serviços prestados. + iss: ISS + obra: Obra + valor: Valor + deducao: Deducao + retencao: Retencao + tributavel: bool # Indica se o serviço está sujeito a tributação. + ibpt: IBPT + responsavelRetencao: str + +############################################################################## +# responsavelRetencao Valores aceitos: +# 1 - Tomador +# 2 - Intermediário +# Caso o responsável pela retenção seja o próprio prestador, não gerar o campo. +############################################################################## diff --git a/Models/tomador.py b/Models/tomador.py new file mode 100644 index 0000000..4e3cf5a --- /dev/null +++ b/Models/tomador.py @@ -0,0 +1,34 @@ +from pydantic import BaseModel + + +class Endereco(BaseModel): + tipoLogradouro: str + logradouro: str + numero: str + complemento: str + tipoBairro: str + bairro: str + codigoPais: str + descricaoPais: str + codigoCidade: str + descricaoCidade: str + estado: str + cep: str + +class Telefone(BaseModel): + ddd: str + numero: str + +# Representa o cliente (Tomador do Serviço) vinculado a uma nota fiscal de serviço +class Tomador(BaseModel): + cpfCnpj: str + inscricaoMunicipal: str + inscricaoEstadual: str + inscricaoSuframa: str + indicadorInscricaoEstadual: int + razaoSocial: str + nomeFantasia: str + endereco: Endereco + telefone: Telefone + email: str + orgaoPublico: bool diff --git a/README.md b/README.md new file mode 100644 index 0000000..e3428b2 --- /dev/null +++ b/README.md @@ -0,0 +1,204 @@ +# Serviço de Integração PlugNotas da Tecnospeed + +Serviço especializado e containerizado para gerenciar a emissão, controle e recebimento de informações sobre **Notas Fiscais (NFe e NFSe)** via API da PlugNotas (Tecnospeed). Com uma arquitetura orientada a eventos e filas (RabbitMQ), o sistema é altamente desacoplado, assíncrono e integrado a um banco de dados PostgreSQL para registro de todas as operações e relatórios de auditoria. + +## 🚀 Principais Tecnologias e Arquitetura + +- **Python & FastAPI**: Base de processamento, recebimento de Webhooks (via `main_listener.py`) e tipagem rigorosa utilizando Pydantic. +- **RabbitMQ (AMQP)**: Comunicação entre os módulos utilizando as ferramentas especializadas implementadas (como `QueueExchange2` e `RPCExchange2`), promovendo uma arquitetura event-driven livre de bloqueios síncronos. +- **PostgreSQL**: Utilizado com SQLAlchemy para logs, auditorias detalhadas das operações, cache das bases de Serviços, Tomadores, Empresas e relatórios operacionais. +- **Redis**: Sistema de gerenciamento e armazenamento de persistência de curadoria (como configurações dinâmicas de acesso e credenciais de forma ágil). +- **IC2 Logger**: Envio centralizado de todos os logs, fluxos de erros, métricas operacionais para um sistema `InfoConnector` usando RabbitMQ Topic. + +## 📦 Estrutura de Módulos (Serviços) + +O serviço central funciona consumindo a fila `tecnospeed` e encaminhando as requisições para os seus respectivos módulos especializados. + +### 1. NFe (Nota Fiscal Eletrônica de Produto) +Módulo que envia e orquestra operações envolvendo notas fiscais de venda (produtos): +- Envio de emissões de NFe em unitário ou em lote. +- Consulta de resumos processados. +- Cancelamentos suportados por justificativas. +- Envio de Cartas de Correção Eletrônica (CCe). +- Download de relatórios, XMLs originais e PDFs de Danfe. + +### 2. NFSe (Nota Fiscal de Serviços Eletrônica) +Voltado a comunicações com prefeituras para emissão de serviços, contém os fluxos de controle essenciais: +- Realiza emissão dos documentos pelo CNPJ do prestador. +- Controle e solicitação formal de cancelamento nas prefeituras. +- Verificação de status e download de histórico de RPS. +- **Tomadores**: Gerenciamento integrado dos clientes finais (Tomadores da NFSe). +- **Serviços**: Cadastro de descritivos padrão, itens e alíquotas com base na lei complementar municipal local (LC116). + +### 3. Empresas e Certificados Digitais +Serviço de onboarding das novas contas emissoras junto à PlugNotas: +- **Empresas**: Vinculação do CNPJ emissor (incluindo endereços, configurações fiscais iniciais e logotipos de impressão). +- **Certificados Digitais**: Responsável pelo cadastro do certificado físico formato `.pfx` com envios do tipo `multipart/form-data`, renovação de senhas e update recorrente dos certificados A1. + +### 4. Webhooks (Listeners Internos e Externos) +Gerenciador automatizado orientando o lado da PlugNotas para onde devolver as informações assincronamente: +- Criação, leitura, deleção e envio de testes para registro das URLs via API do Plugnotas. +- Um **Listener próprio do lado da integração** (`main_listener.py` + `listenerEndpoint.py`) absorve o payload recebido da PlugNotas e encaminha de volta ao fluxo RabbitMQ principal usando RPC — mantendo assim uma auditoria segura com log para todas as frentes (rejeições, aceites contábeis etc). + +### 5. Relatórios +Serviço voltado a buscas gerenciais dentro do portal da Tecnospeed para contabilidades e analistas de sistemas: +- Extratos iterados limitados de NFe e NFSe emitidas ou registradas num dado range de período. +- Recortes centralizados e consolidações por um CNPJ específico. + +## ⚙️ Como Funciona o Ciclo de Eventos (Workflows) + +1. **Ação Disparada**: O sistema que integra de fora (ERP etc) publica uma mensagem JSON na fila RabbitMQ com os comandos e a carga de dados de Pydantic Models associada, incluindo as configurações do banco. +2. **Workers de Fila (`mainQUEUE.py`)**: As mensagens são absorvidas, descriptografadas, instanciam as credenciais adequadas e ativam a Controller/Service (ex: EmpresaController, RegistroNFeService). +3. **Persistência no Postgre (`utils/banco.py`)**: A operação feita junto as rotas do portal salva um rastro de log com o `status` ("SUCESSO", "ERRO"). Instalações da NFe e NFSe e Certificados refletem diretamente nesse PostgreSQL. +4. **Listener Asincrono de Retorno (`listenerEndpoint.py`)**: A rota em FastAPI escuta atualizações atrasadas (ex: webservices estaduais demorados) e sinaliza ativamente ao logger e à base da empresa o desfecho da nota. + +## 🗂️ Estrutura de Diretórios + +``` +servico_plugnotas/ +├── src/ +│ ├── main.py # Worker principal (RPCExchange2) +│ ├── mainQUEUE.py # Worker alternativo (QueueExchange2 + IC2 Logger) +│ ├── main_listener.py # Entrypoint do container de Webhook Listener +│ ├── listenerEndpoint.py # Rotas FastAPI que recebem webhooks da PlugNotas +│ ├── test.py # Script de testes manuais +│ │ +│ ├── NFe/ +│ │ └── registroNFe.py # Service de emissão, cancelamento e download de NFe +│ ├── NFSe/ +│ │ ├── nfse.py # Service de emissão e controle de NFSe +│ │ ├── Servico/ +│ │ │ └── servico.py # Controller de cadastro de Serviços (LC116) +│ │ └── Tomador/ +│ │ └── tomador.py # Controller de cadastro de Tomadores +│ ├── empresas/ +│ │ └── empresa.py # Controller de cadastro de Empresas emissoras +│ ├── certificado_digital/ +│ │ └── certificado_digital.py # Service de upload e gestão de certificados .pfx +│ ├── webhooks/ +│ │ └── webhooks.py # CRUD de URLs de webhook por CNPJ +│ ├── Relatorios/ +│ │ └── relatorios.py # Consultas de relatórios em lote +│ │ +│ ├── Models/ # Modelos Pydantic que validam os payloads +│ │ ├── config.py # Config (banco) e SystemConfig (AMQP/Redis) +│ │ ├── empresa.py # Modelo de Empresa emissora +│ │ ├── nfe.py # Modelo de NFe (Emitente, Destinatário, Itens, Tributos) +│ │ ├── nfse.py # Modelo de NFSe (Prestador, Tomador, Serviço) +│ │ ├── servico.py # Modelo detalhado de Serviço (ISS, Retenções, IBPT) +│ │ └── tomador.py # Modelo de Tomador (cliente fiscal) +│ │ +│ └── utils/ # Classes auxiliares compartilhadas +│ ├── api.py # Wrapper HTTP (requests) para a API PlugNotas +│ ├── banco.py # ORM SQLAlchemy (tabelas + CRUD + logs) +│ ├── redis.py # Cliente Redis para cache de configurações +│ ├── Exchange2.py # QueueExchange2, RPCExchange2, LoggerExchange2 +│ ├── exchangeModels.py # Modelos Pydantic do InfoConnector (IC2) +│ └── Logger.py # LogService para logs via mensageria +│ +├── Dockerfile # Imagem base Python 3.12-slim +├── docker-compose.yml # Orquestração dos containers +├── buildImages.sh # Script de build com clone de dependências +├── requirements.txt # Dependências Python +├── modelo_system_config.json # Exemplo de configuração do Redis +└── modelo_json_rpc.json # Exemplo de payload RPC +``` + +## 🐳 Infraestrutura Docker + +O projeto é composto por **4 containers** orquestrados via `docker-compose.yml`: + +| Container | Imagem | Porta | Função | +|----------------------|--------------------|---------|------------------------------------------------------------| +| `redis` | `redis` | interna | Armazena `system_config` (credenciais AMQP, chaves) | +| `postgres` | `postgres:15` | interna | Banco de dados de auditoria e registros de notas | +| `tecnospeed` | build local | — | Worker que consome fila RabbitMQ e processa operações | +| `webhook-listener` | build local | `8000` | FastAPI que recebe webhooks da PlugNotas via HTTP | + +### Subindo o ambiente + +```bash +# Build e execução completa +docker compose up -d --build + +# Ou utilizando o script de build (clona dependências externas automaticamente) +bash buildImages.sh clone +``` + +### Variáveis de ambiente + +| Variável | Container | Descrição | Default | +|-----------------|--------------------|-----------------------------------------|-------------| +| `TZ` | todos | Timezone da aplicação | `America/Sao_Paulo` | +| `REDIS_HOST` | tecnospeed, webhook-listener | Host do Redis | `localhost` | +| `WEBHOOK_PORT` | webhook-listener | Porta do servidor FastAPI | `8000` | + +## ⚡ Configuração Inicial (Redis) + +Antes de subir os workers, é necessário inserir a chave `system_config` no Redis com as credenciais de acesso ao RabbitMQ: + +```json +{ + "serviceid": "01234567890123456789012345678901", + "servicekey": "ABCDEFGHIJKLMNOPQRSTUVWXYZ123456", + "origin": "nome-da-maquina", + "amqps": "amqps://usuario:senha@host.rmq.cloudamqp.com/vhost" +} +``` + +Esse JSON é lido automaticamente na inicialização dos workers (`main.py`, `mainQUEUE.py`, `listenerEndpoint.py`). + +## 📨 Exemplo de Payload RPC + +As mensagens que chegam pela fila RabbitMQ seguem o seguinte formato (já descriptografado): + +```json +{ + "fila": "nfse", + "cliente": "NomeCliente", + "metodo": "POST", + "dados": { ... }, + "config": { + "api_key": "sua-api-key-plugnotas", + "user_postgres": "postgres", + "pass_postgres": "postgres", + "host_postgres": "localhost", + "port_postgres": "5432", + "database_postgres": "tecnospeed" + } +} +``` + +- **`fila`**: Seleciona o módulo de destino (`nfe`, `nfse`, `empresa`, `certificado_digital`, `servico`, `tomador`, `webhooks`, `relatorios`, `webhook_callback`). +- **`metodo`**: Ação a executar (`post`, `get`, `put`, `delete`, `cancelar`, `download`, etc.). +- **`dados`**: Corpo da requisição conforme o Pydantic Model do módulo. +- **`config`**: Credenciais isoladas por cliente para a API e o banco PostgreSQL. + +## 📦 Dependências + +``` +sqlalchemy # ORM para PostgreSQL +pydantic # Validação de modelos de dados +requests # Requisições HTTP à API PlugNotas +redis # Cliente Redis +pytz # Timezone São Paulo +pika # Cliente AMQP (RabbitMQ) +cryptography # Criptografia AES-256 dos payloads +psycopg2-binary # Driver PostgreSQL +fastapi # Framework HTTP para o Webhook Listener +uvicorn[standard] # Servidor ASGI +``` + +## 🔖 Versionamento + +| Versão | Descrição | +|---------|--------------------------------------------------| +| `1.0.0` | Versão inicial do sistema | +| `1.0.1` | Implementação do ORM (SQLAlchemy) | +| `1.0.2` | Implementação de RPC via RabbitMQ | +| `1.0.3` | Configuração dinâmica via Redis | +| `2.0.0` | Suporte a NFe e refatoração da Main | +| `2.0.1` | Refatoração para RPCExchange2 / QueueExchange2 | + +--- +*Documento gerado com base no código operacional contido em `src/` do serviço (v2.0.1).* diff --git a/backup/Payloads_Worker_Plugnotas.postman_collection.json b/backup/Payloads_Worker_Plugnotas.postman_collection.json new file mode 100644 index 0000000..72a1c43 --- /dev/null +++ b/backup/Payloads_Worker_Plugnotas.postman_collection.json @@ -0,0 +1,649 @@ +{ + "info": { + "name": "Serviço TecnoSpeed - Payloads do Worker", + "description": "Exemplos completos de todos os payloads esperados pelo `main.py` do Worker no RabbitMQ. ATENÇÃO: Essas requisições são modelos do JSON interno. Seu back-end deve encapsula-las com `hmac` e enviá-las ao RabbitMQ.", + "schema": "https://schema.getpostman.com/json/collection/v2.1.0/collection.json" + }, + "item": [ + { + "name": "NFe (Nota Fiscal Eletrônica)", + "item": [ + { + "name": "1. Cadastrar NFe", + "request": { + "method": "POST", + "header": [ + { + "key": "Content-Type", + "value": "application/json" + } + ], + "body": { + "mode": "raw", + "raw": "{\n \"fila\": \"nfe\",\n \"cliente\": \"IdentificadorDoCliente\",\n \"metodo\": \"post\",\n \"url_base\": \"https://api.sandbox.plugnotas.com.br\",\n \"config\": {\n \"api_key\": \"SUA_API_KEY\",\n \"user_postgres\": \"postgres\",\n \"pass_postgres\": \"postgres\",\n \"host_postgres\": \"localhost\",\n \"port_postgres\": \"5432\",\n \"database_postgres\": \"seu_banco\"\n },\n \"dados\": {\n \"nfe\": {\n \"emitente\": {\n \"cpfCnpj\": \"12345678000123\"\n },\n \"destinatario\": {\n \"cpfCnpj\": \"98765432000198\",\n \"razaoSocial\": \"Cliente Destino LTDA\"\n },\n \"itens\": [\n {\n \"codigo\": \"123\",\n \"descricao\": \"Produto Teste\",\n \"valorUnitario\": 100.0,\n \"quantidade\": 1\n }\n ]\n }\n }\n}" + }, + "description": "Payload interno para a fila **nfe** usando o método **post**.\n\n", + "url": { + "raw": "amqp://seu-rabbitmq/fila/nfe/post", + "protocol": "amqp", + "host": [ + "seu-rabbitmq" + ], + "path": [ + "fila", + "nfe", + "post" + ] + } + }, + "response": [] + }, + { + "name": "2. Consultar Resumo NFe", + "request": { + "method": "POST", + "header": [ + { + "key": "Content-Type", + "value": "application/json" + } + ], + "body": { + "mode": "raw", + "raw": "{\n \"fila\": \"nfe\",\n \"cliente\": \"IdentificadorDoCliente\",\n \"metodo\": \"get\",\n \"url_base\": \"https://api.sandbox.plugnotas.com.br\",\n \"config\": {\n \"api_key\": \"SUA_API_KEY\",\n \"user_postgres\": \"postgres\",\n \"pass_postgres\": \"postgres\",\n \"host_postgres\": \"localhost\",\n \"port_postgres\": \"5432\",\n \"database_postgres\": \"seu_banco\"\n },\n \"dados\": {\n \"id\": \"ID_DA_NFE_AQUI\"\n }\n}" + }, + "description": "Payload interno para a fila **nfe** usando o método **get**.\n\n", + "url": { + "raw": "amqp://seu-rabbitmq/fila/nfe/get", + "protocol": "amqp", + "host": [ + "seu-rabbitmq" + ], + "path": [ + "fila", + "nfe", + "get" + ] + } + }, + "response": [] + }, + { + "name": "3. Baixar PDF NFe", + "request": { + "method": "POST", + "header": [ + { + "key": "Content-Type", + "value": "application/json" + } + ], + "body": { + "mode": "raw", + "raw": "{\n \"fila\": \"nfe\",\n \"cliente\": \"IdentificadorDoCliente\",\n \"metodo\": \"download\",\n \"url_base\": \"https://api.sandbox.plugnotas.com.br\",\n \"config\": {\n \"api_key\": \"SUA_API_KEY\",\n \"user_postgres\": \"postgres\",\n \"pass_postgres\": \"postgres\",\n \"host_postgres\": \"localhost\",\n \"port_postgres\": \"5432\",\n \"database_postgres\": \"seu_banco\"\n },\n \"dados\": {\n \"id\": \"ID_DA_NFE_AQUI\"\n }\n}" + }, + "description": "Payload interno para a fila **nfe** usando o método **download**.\n\n", + "url": { + "raw": "amqp://seu-rabbitmq/fila/nfe/download", + "protocol": "amqp", + "host": [ + "seu-rabbitmq" + ], + "path": [ + "fila", + "nfe", + "download" + ] + } + }, + "response": [] + }, + { + "name": "4. Baixar XML NFe", + "request": { + "method": "POST", + "header": [ + { + "key": "Content-Type", + "value": "application/json" + } + ], + "body": { + "mode": "raw", + "raw": "{\n \"fila\": \"nfe\",\n \"cliente\": \"IdentificadorDoCliente\",\n \"metodo\": \"download_xml\",\n \"url_base\": \"https://api.sandbox.plugnotas.com.br\",\n \"config\": {\n \"api_key\": \"SUA_API_KEY\",\n \"user_postgres\": \"postgres\",\n \"pass_postgres\": \"postgres\",\n \"host_postgres\": \"localhost\",\n \"port_postgres\": \"5432\",\n \"database_postgres\": \"seu_banco\"\n },\n \"dados\": {\n \"id\": \"ID_DA_NFE_AQUI\"\n }\n}" + }, + "description": "Payload interno para a fila **nfe** usando o método **download_xml**.\n\n", + "url": { + "raw": "amqp://seu-rabbitmq/fila/nfe/download_xml", + "protocol": "amqp", + "host": [ + "seu-rabbitmq" + ], + "path": [ + "fila", + "nfe", + "download_xml" + ] + } + }, + "response": [] + }, + { + "name": "5. Cancelar NFe", + "request": { + "method": "POST", + "header": [ + { + "key": "Content-Type", + "value": "application/json" + } + ], + "body": { + "mode": "raw", + "raw": "{\n \"fila\": \"nfe\",\n \"cliente\": \"IdentificadorDoCliente\",\n \"metodo\": \"cancelar\",\n \"url_base\": \"https://api.sandbox.plugnotas.com.br\",\n \"config\": {\n \"api_key\": \"SUA_API_KEY\",\n \"user_postgres\": \"postgres\",\n \"pass_postgres\": \"postgres\",\n \"host_postgres\": \"localhost\",\n \"port_postgres\": \"5432\",\n \"database_postgres\": \"seu_banco\"\n },\n \"dados\": {\n \"id\": \"ID_DA_NFE_AQUI\",\n \"justificativa\": \"Cancelamento a pedido do integrador\"\n }\n}" + }, + "description": "Payload interno para a fila **nfe** usando o método **cancelar**.\n\n", + "url": { + "raw": "amqp://seu-rabbitmq/fila/nfe/cancelar", + "protocol": "amqp", + "host": [ + "seu-rabbitmq" + ], + "path": [ + "fila", + "nfe", + "cancelar" + ] + } + }, + "response": [] + } + ] + }, + { + "name": "NFSe (Nota Fiscal de Serviço)", + "item": [ + { + "name": "1. Cadastrar NFSe", + "request": { + "method": "POST", + "header": [ + { + "key": "Content-Type", + "value": "application/json" + } + ], + "body": { + "mode": "raw", + "raw": "{\n \"fila\": \"nfse\",\n \"cliente\": \"IdentificadorDoCliente\",\n \"metodo\": \"post\",\n \"url_base\": \"https://api.sandbox.plugnotas.com.br\",\n \"config\": {\n \"api_key\": \"SUA_API_KEY\",\n \"user_postgres\": \"postgres\",\n \"pass_postgres\": \"postgres\",\n \"host_postgres\": \"localhost\",\n \"port_postgres\": \"5432\",\n \"database_postgres\": \"seu_banco\"\n },\n \"dados\": {\n \"idIntegracao\": \"INTEGRACAO-NFSE-101\",\n \"prestador\": {\n \"cpfCnpj\": \"12345678000123\"\n },\n \"tomador\": {\n \"cpfCnpj\": \"98765432000198\",\n \"razaoSocial\": \"Jo\\u00e3o Silva\",\n \"endereco\": {\n \"logradouro\": \"Rua Teste\",\n \"numero\": \"123\",\n \"codigoCidade\": \"4106902\"\n }\n },\n \"servico\": {\n \"codigo\": \"1.01\",\n \"valor\": 1500.5,\n \"discriminacao\": \"Servi\\u00e7os prestados de TI\"\n }\n }\n}" + }, + "description": "Payload interno para a fila **nfse** usando o método **post**.\n\n", + "url": { + "raw": "amqp://seu-rabbitmq/fila/nfse/post", + "protocol": "amqp", + "host": [ + "seu-rabbitmq" + ], + "path": [ + "fila", + "nfse", + "post" + ] + } + }, + "response": [] + }, + { + "name": "2. Consultar/Listar NFSe", + "request": { + "method": "POST", + "header": [ + { + "key": "Content-Type", + "value": "application/json" + } + ], + "body": { + "mode": "raw", + "raw": "{\n \"fila\": \"nfse\",\n \"cliente\": \"IdentificadorDoCliente\",\n \"metodo\": \"get\",\n \"url_base\": \"https://api.sandbox.plugnotas.com.br\",\n \"config\": {\n \"api_key\": \"SUA_API_KEY\",\n \"user_postgres\": \"postgres\",\n \"pass_postgres\": \"postgres\",\n \"host_postgres\": \"localhost\",\n \"port_postgres\": \"5432\",\n \"database_postgres\": \"seu_banco\"\n },\n \"dados\": {\n \"id\": \"ID_DA_NFSE_AQUI\"\n }\n}" + }, + "description": "Payload interno para a fila **nfse** usando o método **get**.\n\n", + "url": { + "raw": "amqp://seu-rabbitmq/fila/nfse/get", + "protocol": "amqp", + "host": [ + "seu-rabbitmq" + ], + "path": [ + "fila", + "nfse", + "get" + ] + } + }, + "response": [] + }, + { + "name": "3. Cancelar NFSe", + "request": { + "method": "POST", + "header": [ + { + "key": "Content-Type", + "value": "application/json" + } + ], + "body": { + "mode": "raw", + "raw": "{\n \"fila\": \"nfse\",\n \"cliente\": \"IdentificadorDoCliente\",\n \"metodo\": \"cancelar\",\n \"url_base\": \"https://api.sandbox.plugnotas.com.br\",\n \"config\": {\n \"api_key\": \"SUA_API_KEY\",\n \"user_postgres\": \"postgres\",\n \"pass_postgres\": \"postgres\",\n \"host_postgres\": \"localhost\",\n \"port_postgres\": \"5432\",\n \"database_postgres\": \"seu_banco\"\n },\n \"dados\": {\n \"id\": \"ID_DA_NFSE_AQUI\"\n }\n}" + }, + "description": "Payload interno para a fila **nfse** usando o método **cancelar**.\n\n", + "url": { + "raw": "amqp://seu-rabbitmq/fila/nfse/cancelar", + "protocol": "amqp", + "host": [ + "seu-rabbitmq" + ], + "path": [ + "fila", + "nfse", + "cancelar" + ] + } + }, + "response": [] + } + ] + }, + { + "name": "Empresa", + "item": [ + { + "name": "1. Cadastrar Empresa", + "request": { + "method": "POST", + "header": [ + { + "key": "Content-Type", + "value": "application/json" + } + ], + "body": { + "mode": "raw", + "raw": "{\n \"fila\": \"empresa\",\n \"cliente\": \"IdentificadorDoCliente\",\n \"metodo\": \"post\",\n \"url_base\": \"https://api.sandbox.plugnotas.com.br\",\n \"config\": {\n \"api_key\": \"SUA_API_KEY\",\n \"user_postgres\": \"postgres\",\n \"pass_postgres\": \"postgres\",\n \"host_postgres\": \"localhost\",\n \"port_postgres\": \"5432\",\n \"database_postgres\": \"seu_banco\"\n },\n \"dados\": {\n \"json\": {\n \"cpfCnpj\": \"12345678000123\",\n \"razaoSocial\": \"Nome da Sua Empresa LTDA\",\n \"nomeFantasia\": \"Nome Fantasia\",\n \"inscricaoMunicipal\": \"123456\",\n \"endereco\": {\n \"logradouro\": \"Rua Exemplo\",\n \"numero\": \"100\",\n \"codigoCidade\": \"4106902\",\n \"uf\": \"PR\",\n \"cep\": \"80000000\"\n }\n },\n \"info\": {\n \"ativo\": \"true\"\n }\n }\n}" + }, + "description": "Payload interno para a fila **empresa** usando o método **post**.\n\n", + "url": { + "raw": "amqp://seu-rabbitmq/fila/empresa/post", + "protocol": "amqp", + "host": [ + "seu-rabbitmq" + ], + "path": [ + "fila", + "empresa", + "post" + ] + } + }, + "response": [] + }, + { + "name": "2. Consultar Empresa", + "request": { + "method": "POST", + "header": [ + { + "key": "Content-Type", + "value": "application/json" + } + ], + "body": { + "mode": "raw", + "raw": "{\n \"fila\": \"empresa\",\n \"cliente\": \"IdentificadorDoCliente\",\n \"metodo\": \"get\",\n \"url_base\": \"https://api.sandbox.plugnotas.com.br\",\n \"config\": {\n \"api_key\": \"SUA_API_KEY\",\n \"user_postgres\": \"postgres\",\n \"pass_postgres\": \"postgres\",\n \"host_postgres\": \"localhost\",\n \"port_postgres\": \"5432\",\n \"database_postgres\": \"seu_banco\"\n },\n \"dados\": {\n \"cnpj\": \"12345678000123\"\n }\n}" + }, + "description": "Payload interno para a fila **empresa** usando o método **get**.\n\n", + "url": { + "raw": "amqp://seu-rabbitmq/fila/empresa/get", + "protocol": "amqp", + "host": [ + "seu-rabbitmq" + ], + "path": [ + "fila", + "empresa", + "get" + ] + } + }, + "response": [] + } + ] + }, + { + "name": "Certificado Digital", + "item": [ + { + "name": "1. Cadastrar Certificado", + "request": { + "method": "POST", + "header": [ + { + "key": "Content-Type", + "value": "application/json" + } + ], + "body": { + "mode": "raw", + "raw": "{\n \"fila\": \"certificado_digital\",\n \"cliente\": \"IdentificadorDoCliente\",\n \"metodo\": \"post\",\n \"url_base\": \"https://api.sandbox.plugnotas.com.br\",\n \"config\": {\n \"api_key\": \"SUA_API_KEY\",\n \"user_postgres\": \"postgres\",\n \"pass_postgres\": \"postgres\",\n \"host_postgres\": \"localhost\",\n \"port_postgres\": \"5432\",\n \"database_postgres\": \"seu_banco\"\n },\n \"dados\": {\n \"arquivo\": {\n \"filename\": \"certificado.pfx\",\n \"content\": \"BASE64_CONTEUDO_DO_ARQUIVO\"\n },\n \"senha\": \"SenhaDoCertificadoAqui\",\n \"data_expiracao\": \"2025-12-31\",\n \"id_empresa\": 105,\n \"id_usuario\": 10\n }\n}" + }, + "description": "Payload interno para a fila **certificado_digital** usando o método **post**.\n\n", + "url": { + "raw": "amqp://seu-rabbitmq/fila/certificado_digital/post", + "protocol": "amqp", + "host": [ + "seu-rabbitmq" + ], + "path": [ + "fila", + "certificado_digital", + "post" + ] + } + }, + "response": [] + }, + { + "name": "2. Consultar Certificado", + "request": { + "method": "POST", + "header": [ + { + "key": "Content-Type", + "value": "application/json" + } + ], + "body": { + "mode": "raw", + "raw": "{\n \"fila\": \"certificado_digital\",\n \"cliente\": \"IdentificadorDoCliente\",\n \"metodo\": \"get\",\n \"url_base\": \"https://api.sandbox.plugnotas.com.br\",\n \"config\": {\n \"api_key\": \"SUA_API_KEY\",\n \"user_postgres\": \"postgres\",\n \"pass_postgres\": \"postgres\",\n \"host_postgres\": \"localhost\",\n \"port_postgres\": \"5432\",\n \"database_postgres\": \"seu_banco\"\n },\n \"dados\": {\n \"id\": \"ID_DO_CERTIFICADO\"\n }\n}" + }, + "description": "Payload interno para a fila **certificado_digital** usando o método **get**.\n\n", + "url": { + "raw": "amqp://seu-rabbitmq/fila/certificado_digital/get", + "protocol": "amqp", + "host": [ + "seu-rabbitmq" + ], + "path": [ + "fila", + "certificado_digital", + "get" + ] + } + }, + "response": [] + }, + { + "name": "3. Atualizar Certificado", + "request": { + "method": "POST", + "header": [ + { + "key": "Content-Type", + "value": "application/json" + } + ], + "body": { + "mode": "raw", + "raw": "{\n \"fila\": \"certificado_digital\",\n \"cliente\": \"IdentificadorDoCliente\",\n \"metodo\": \"put\",\n \"url_base\": \"https://api.sandbox.plugnotas.com.br\",\n \"config\": {\n \"api_key\": \"SUA_API_KEY\",\n \"user_postgres\": \"postgres\",\n \"pass_postgres\": \"postgres\",\n \"host_postgres\": \"localhost\",\n \"port_postgres\": \"5432\",\n \"database_postgres\": \"seu_banco\"\n },\n \"dados\": {\n \"id\": \"ID_DO_CERTIFICADO\",\n \"arquivo\": {\n \"filename\": \"novo_certificado.pfx\",\n \"content\": \"NEW_BASE64_CONTEUDO_DO_ARQUIVO\"\n },\n \"senha\": \"NovaSenhaDoCertificadoAqui\",\n \"data_expiracao\": \"2026-12-31\",\n \"id_empresa\": 105,\n \"id_usuario\": 10\n }\n}" + }, + "description": "Payload interno para a fila **certificado_digital** usando o método **put**.\n\n", + "url": { + "raw": "amqp://seu-rabbitmq/fila/certificado_digital/put", + "protocol": "amqp", + "host": [ + "seu-rabbitmq" + ], + "path": [ + "fila", + "certificado_digital", + "put" + ] + } + }, + "response": [] + } + ] + }, + { + "name": "Serviços", + "item": [ + { + "name": "1. Cadastrar Serviço", + "request": { + "method": "POST", + "header": [ + { + "key": "Content-Type", + "value": "application/json" + } + ], + "body": { + "mode": "raw", + "raw": "{\n \"fila\": \"servico\",\n \"cliente\": \"IdentificadorDoCliente\",\n \"metodo\": \"post\",\n \"url_base\": \"https://api.sandbox.plugnotas.com.br\",\n \"config\": {\n \"api_key\": \"SUA_API_KEY\",\n \"user_postgres\": \"postgres\",\n \"pass_postgres\": \"postgres\",\n \"host_postgres\": \"localhost\",\n \"port_postgres\": \"5432\",\n \"database_postgres\": \"seu_banco\"\n },\n \"dados\": {\n \"json\": {\n \"codigo\": \"1.01\",\n \"idIntegracao\": \"SERV-01\",\n \"descricao\": \"An\\u00e1lise e desenvolvimento de sistemas\",\n \"aliquota\": 2.5\n },\n \"info\": {\n \"parametros_adicionais\": \"teste\"\n }\n }\n}" + }, + "description": "Payload interno para a fila **servico** usando o método **post**.\n\n", + "url": { + "raw": "amqp://seu-rabbitmq/fila/servico/post", + "protocol": "amqp", + "host": [ + "seu-rabbitmq" + ], + "path": [ + "fila", + "servico", + "post" + ] + } + }, + "response": [] + }, + { + "name": "2. Buscar Serviço", + "request": { + "method": "POST", + "header": [ + { + "key": "Content-Type", + "value": "application/json" + } + ], + "body": { + "mode": "raw", + "raw": "{\n \"fila\": \"servico\",\n \"cliente\": \"IdentificadorDoCliente\",\n \"metodo\": \"get\",\n \"url_base\": \"https://api.sandbox.plugnotas.com.br\",\n \"config\": {\n \"api_key\": \"SUA_API_KEY\",\n \"user_postgres\": \"postgres\",\n \"pass_postgres\": \"postgres\",\n \"host_postgres\": \"localhost\",\n \"port_postgres\": \"5432\",\n \"database_postgres\": \"seu_banco\"\n },\n \"dados\": {\n \"id\": \"ID_DO_SERVICO\"\n }\n}" + }, + "description": "Payload interno para a fila **servico** usando o método **get**.\n\n", + "url": { + "raw": "amqp://seu-rabbitmq/fila/servico/get", + "protocol": "amqp", + "host": [ + "seu-rabbitmq" + ], + "path": [ + "fila", + "servico", + "get" + ] + } + }, + "response": [] + } + ] + }, + { + "name": "Tomador", + "item": [ + { + "name": "1. Cadastrar Tomador", + "request": { + "method": "POST", + "header": [ + { + "key": "Content-Type", + "value": "application/json" + } + ], + "body": { + "mode": "raw", + "raw": "{\n \"fila\": \"tomador\",\n \"cliente\": \"IdentificadorDoCliente\",\n \"metodo\": \"post\",\n \"url_base\": \"https://api.sandbox.plugnotas.com.br\",\n \"config\": {\n \"api_key\": \"SUA_API_KEY\",\n \"user_postgres\": \"postgres\",\n \"pass_postgres\": \"postgres\",\n \"host_postgres\": \"localhost\",\n \"port_postgres\": \"5432\",\n \"database_postgres\": \"seu_banco\"\n },\n \"dados\": {\n \"cpf_cnpj\": \"00000000000\",\n \"razaoSocial\": \"Tomador Teste SA\",\n \"email\": \"contato@teste.com\",\n \"endereco\": {\n \"logradouro\": \"Rua teste\",\n \"codigoCidade\": \"1234567\"\n }\n }\n}" + }, + "description": "Payload interno para a fila **tomador** usando o método **post**.\n\n", + "url": { + "raw": "amqp://seu-rabbitmq/fila/tomador/post", + "protocol": "amqp", + "host": [ + "seu-rabbitmq" + ], + "path": [ + "fila", + "tomador", + "post" + ] + } + }, + "response": [] + }, + { + "name": "2. Buscar Tomador", + "request": { + "method": "POST", + "header": [ + { + "key": "Content-Type", + "value": "application/json" + } + ], + "body": { + "mode": "raw", + "raw": "{\n \"fila\": \"tomador\",\n \"cliente\": \"IdentificadorDoCliente\",\n \"metodo\": \"get\",\n \"url_base\": \"https://api.sandbox.plugnotas.com.br\",\n \"config\": {\n \"api_key\": \"SUA_API_KEY\",\n \"user_postgres\": \"postgres\",\n \"pass_postgres\": \"postgres\",\n \"host_postgres\": \"localhost\",\n \"port_postgres\": \"5432\",\n \"database_postgres\": \"seu_banco\"\n },\n \"dados\": {\n \"cpf_cnpj\": \"00000000000\"\n }\n}" + }, + "description": "Payload interno para a fila **tomador** usando o método **get**.\n\n", + "url": { + "raw": "amqp://seu-rabbitmq/fila/tomador/get", + "protocol": "amqp", + "host": [ + "seu-rabbitmq" + ], + "path": [ + "fila", + "tomador", + "get" + ] + } + }, + "response": [] + } + ] + }, + { + "name": "Relatórios", + "item": [ + { + "name": "1. NFSe Emitidas", + "request": { + "method": "POST", + "header": [ + { + "key": "Content-Type", + "value": "application/json" + } + ], + "body": { + "mode": "raw", + "raw": "{\n \"fila\": \"relatorios\",\n \"cliente\": \"IdentificadorDoCliente\",\n \"metodo\": \"nfse_emitidas\",\n \"url_base\": \"https://api.sandbox.plugnotas.com.br\",\n \"config\": {\n \"api_key\": \"SUA_API_KEY\",\n \"user_postgres\": \"postgres\",\n \"pass_postgres\": \"postgres\",\n \"host_postgres\": \"localhost\",\n \"port_postgres\": \"5432\",\n \"database_postgres\": \"seu_banco\"\n },\n \"dados\": {\n \"data_inicial\": \"01/01/2026\",\n \"data_final\": \"31/01/2026\"\n }\n}" + }, + "description": "Payload interno para a fila **relatorios** usando o método **nfse_emitidas**.\n\n", + "url": { + "raw": "amqp://seu-rabbitmq/fila/relatorios/nfse_emitidas", + "protocol": "amqp", + "host": [ + "seu-rabbitmq" + ], + "path": [ + "fila", + "relatorios", + "nfse_emitidas" + ] + } + }, + "response": [] + }, + { + "name": "2. NFSe por CNPJ", + "request": { + "method": "POST", + "header": [ + { + "key": "Content-Type", + "value": "application/json" + } + ], + "body": { + "mode": "raw", + "raw": "{\n \"fila\": \"relatorios\",\n \"cliente\": \"IdentificadorDoCliente\",\n \"metodo\": \"nfse_por_cnpj\",\n \"url_base\": \"https://api.sandbox.plugnotas.com.br\",\n \"config\": {\n \"api_key\": \"SUA_API_KEY\",\n \"user_postgres\": \"postgres\",\n \"pass_postgres\": \"postgres\",\n \"host_postgres\": \"localhost\",\n \"port_postgres\": \"5432\",\n \"database_postgres\": \"seu_banco\"\n },\n \"dados\": {\n \"cnpj\": \"12345678000123\",\n \"data_inicial\": \"01/01/2026\",\n \"data_final\": \"31/01/2026\"\n }\n}" + }, + "description": "Payload interno para a fila **relatorios** usando o método **nfse_por_cnpj**.\n\n", + "url": { + "raw": "amqp://seu-rabbitmq/fila/relatorios/nfse_por_cnpj", + "protocol": "amqp", + "host": [ + "seu-rabbitmq" + ], + "path": [ + "fila", + "relatorios", + "nfse_por_cnpj" + ] + } + }, + "response": [] + } + ] + }, + { + "name": "Webhook Callbacks (Uso Interno do FastAPI)", + "item": [ + { + "name": "1. Notificação Concluído", + "request": { + "method": "POST", + "header": [ + { + "key": "Content-Type", + "value": "application/json" + } + ], + "body": { + "mode": "raw", + "raw": "{\n \"fila\": \"webhook_callback\",\n \"cliente\": \"IdentificadorDoCliente\",\n \"metodo\": \"concluido\",\n \"url_base\": \"https://api.sandbox.plugnotas.com.br\",\n \"config\": {\n \"api_key\": \"SUA_API_KEY\",\n \"user_postgres\": \"postgres\",\n \"pass_postgres\": \"postgres\",\n \"host_postgres\": \"localhost\",\n \"port_postgres\": \"5432\",\n \"database_postgres\": \"seu_banco\"\n },\n \"dados\": {\n \"documento\": \"nfe\",\n \"status\": \"CONCLUIDO\",\n \"id_nota\": \"ID_DA_NFE\",\n \"id_integracao\": \"SEU_SISTEMA_123\",\n \"cnpj\": \"12345678000123\",\n \"payload_plugnotas\": {\n \"id\": \"ID_DA_NFE\",\n \"status\": \"CONCLUIDO\",\n \"mensagem\": \"Autorizado o uso da NFe\"\n }\n }\n}" + }, + "description": "Payload interno para a fila **webhook_callback** usando o método **concluido**.\n\n", + "url": { + "raw": "amqp://seu-rabbitmq/fila/webhook_callback/concluido", + "protocol": "amqp", + "host": [ + "seu-rabbitmq" + ], + "path": [ + "fila", + "webhook_callback", + "concluido" + ] + } + }, + "response": [] + } + ] + } + ] +} \ No newline at end of file diff --git a/backup/PlugNotas_Example-SandBox.postman_collection.json b/backup/PlugNotas_Example-SandBox.postman_collection.json new file mode 100644 index 0000000..c4bdba3 --- /dev/null +++ b/backup/PlugNotas_Example-SandBox.postman_collection.json @@ -0,0 +1,2243 @@ +{ + "info": { + "_postman_id": "10233609-b985-4597-9080-5bc2d3b8ef21", + "name": "PlugNotas_Example-SandBox", + "schema": "https://schema.getpostman.com/json/collection/v2.1.0/collection.json", + "_exporter_id": "35998214" + }, + "item": [ + { + "name": "Certificado", + "item": [ + { + "name": "Cadastrar Certificado", + "request": { + "method": "POST", + "header": [ + { + "key": "x-api-key", + "value": "2da392a6-79d2-4304-a8b7-959572c7e44d" + } + ], + "body": { + "mode": "formdata", + "formdata": [ + { + "key": "senha", + "value": "102030", + "type": "text" + }, + { + "key": "arquivo", + "type": "file", + "src": "/home/baltazar/Documentos/certificado/certificate.pfx" + } + ] + }, + "url": { + "raw": "https://api.sandbox.plugnotas.com.br/certificado", + "protocol": "https", + "host": [ + "api", + "sandbox", + "plugnotas", + "com", + "br" + ], + "path": [ + "certificado" + ] + } + }, + "response": [] + }, + { + "name": "Consultar Certificado", + "protocolProfileBehavior": { + "disableBodyPruning": true + }, + "request": { + "method": "GET", + "header": [ + { + "key": "x-api-key", + "value": "2da392a6-79d2-4304-a8b7-959572c7e44d", + "type": "text" + } + ], + "body": { + "mode": "formdata", + "formdata": [] + }, + "url": { + "raw": "https://api.sandbox.plugnotas.com.br/certificado", + "protocol": "https", + "host": [ + "api", + "sandbox", + "plugnotas", + "com", + "br" + ], + "path": [ + "certificado" + ] + } + }, + "response": [] + }, + { + "name": "Alterar Certificado", + "request": { + "method": "PUT", + "header": [ + { + "key": "x-api-key", + "value": "{{token}}", + "type": "text" + } + ], + "body": { + "mode": "formdata", + "formdata": [ + { + "key": "senha", + "value": "{{senha}}", + "type": "text" + }, + { + "key": "arquivo", + "type": "file", + "src": [] + } + ] + }, + "url": { + "raw": "https://api.sandbox.plugnotas.com.br/certificado/{idCertificadoASerSubstituido}", + "protocol": "https", + "host": [ + "api", + "sandbox", + "plugnotas", + "com", + "br" + ], + "path": [ + "certificado", + "{idCertificadoASerSubstituido}" + ] + } + }, + "response": [] + }, + { + "name": "Consultar Certificado Específico", + "protocolProfileBehavior": { + "disableBodyPruning": true + }, + "request": { + "method": "GET", + "header": [ + { + "key": "x-api-key", + "value": "2da392a6-79d2-4304-a8b7-959572c7e44d", + "type": "text" + } + ], + "body": { + "mode": "formdata", + "formdata": [] + }, + "url": { + "raw": "https://api.sandbox.plugnotas.com.br/certificado/67c0a3d6703b90ee3053d56f", + "protocol": "https", + "host": [ + "api", + "sandbox", + "plugnotas", + "com", + "br" + ], + "path": [ + "certificado", + "67c0a3d6703b90ee3053d56f" + ] + } + }, + "response": [] + } + ] + }, + { + "name": "Empresa", + "item": [ + { + "name": "Cadastro da Empresa", + "request": { + "method": "POST", + "header": [ + { + "key": "Content-Type", + "value": "application/json" + }, + { + "key": "x-api-key", + "value": "2da392a6-79d2-4304-a8b7-959572c7e44d" + } + ], + "body": { + "mode": "raw", + "raw": "{\r\n \"endereco\" : {\r\n \"codigoPais\": \"1058\",\r\n\"descricaoPais\": \"Brasil\",\r\n\"tipoLogradouro\": \"Rua\",\r\n\"logradouro\": \"T55\",\r\n\"numero\": \"882\",\r\n\"complemento\": \"Quadra99 Lote 11/14 Sala 1507\",\r\n\"tipoBairro\": \"Zona\",\r\n\"bairro\": \"Setor Bueno\",\r\n\"codigoCidade\": \"5208707\",\r\n\"descricaoCidade\": \"Goiania\",\r\n\"estado\": \"GO\",\r\n\"cep\": \"74215170\"\r\n },\r\n \"nfse\" : {\r\n \"config\" : {\r\n \"rps\" : {\r\n \"lote\" : 12,\r\n \"numeracao\" : [ \r\n {\r\n \"numero\" : 2,\r\n \"serie\" : \"CTBZ\",\r\n \"numeracaoAtual\" : 2\r\n }\r\n ]\r\n },\r\n \"email\" : {\r\n \"envio\" : false\r\n },\r\n \"producao\" : true,\r\n \"envioLimitado\" : false,\r\n \"integracoes\" : []\r\n },\r\n \"ativo\" : true,\r\n \"tipoContrato\" : 0\r\n },\r\n \"nfe\" : {\r\n \"config\" : {\r\n \"dfe\" : {\r\n \"primeiroNsu\" : true\r\n },\r\n \"email\" : {\r\n \"envio\" : false\r\n },\r\n \"integracoes\" : {\r\n \"plugStorage\" : {\r\n \"ativo\" : false\r\n }\r\n },\r\n \"producao\" : false,\r\n \"impressaoFcp\" : false,\r\n \"impressaoPartilha\" : true,\r\n \"versaoManual\" : \"6.0\",\r\n \"versaoEsquema\" : \"pl_009\",\r\n \"numeracao\" : []\r\n },\r\n \"ativo\" : false\r\n },\r\n \"nfce\" : {\r\n \"config\" : {\r\n \"email\" : {\r\n \"envio\" : false\r\n },\r\n \"integracoes\" : {\r\n \"plugStorage\" : {\r\n \"ativo\" : false\r\n }\r\n },\r\n \"producao\" : false,\r\n \"versaoManual\" : \"6.0\",\r\n \"versaoEsquema\" : \"pl_009\",\r\n \"numeracao\" : []\r\n },\r\n \"ativo\" : false\r\n },\r\n \"incentivoFiscal\" : false,\r\n \"incentivadorCultural\" : false,\r\n \"cpfCnpj\" : \"18212566000142\",\r\n \"inscricaoMunicipal\" : \"108532836\",\r\n \"certificado\" : \"668698592698d7831c758d52\",\r\n \"razaoSocial\" : \"Baltazar Barbosa de Oliveira Neto\",\r\n \"simplesNacional\" : true,\r\n \"regimeTributario\" : 1,\r\n \"regimeTributarioEspecial\" : 6\r\n}" + }, + "url": { + "raw": "https://api.sandbox.plugnotas.com.br/empresa", + "protocol": "https", + "host": [ + "api", + "sandbox", + "plugnotas", + "com", + "br" + ], + "path": [ + "empresa" + ] + } + }, + "response": [] + }, + { + "name": "Consultar Empresas", + "request": { + "method": "GET", + "header": [ + { + "key": "x-api-key", + "value": "2da392a6-79d2-4304-a8b7-959572c7e44d" + } + ], + "url": { + "raw": "https://api.sandbox.plugnotas.com.br/empresa", + "protocol": "https", + "host": [ + "api", + "sandbox", + "plugnotas", + "com", + "br" + ], + "path": [ + "empresa" + ] + } + }, + "response": [] + }, + { + "name": "Alterar Dados da Empresa", + "request": { + "method": "PATCH", + "header": [ + { + "key": "x-api-key", + "value": "{{token}}" + }, + { + "key": "Content-Type", + "value": "application/json" + } + ], + "body": { + "mode": "raw", + "raw": "{\n \"certificado\" : \"IdCertificado\"\n}" + }, + "url": { + "raw": "https://api.sandbox.plugnotas.com.br/empresa/{cnpj}", + "protocol": "https", + "host": [ + "api", + "sandbox", + "plugnotas", + "com", + "br" + ], + "path": [ + "empresa", + "{cnpj}" + ] + } + }, + "response": [] + }, + { + "name": "Consultar a empresa", + "request": { + "method": "GET", + "header": [ + { + "key": "x-api-key", + "value": "2da392a6-79d2-4304-a8b7-959572c7e44d", + "type": "text" + } + ], + "url": { + "raw": "https://api.sandbox.plugnotas.com.br/empresa/80972076000118", + "protocol": "https", + "host": [ + "api", + "sandbox", + "plugnotas", + "com", + "br" + ], + "path": [ + "empresa", + "80972076000118" + ] + } + }, + "response": [] + }, + { + "name": "Upload Logotipo", + "request": { + "method": "POST", + "header": [ + { + "key": "x-api-key", + "value": "2da392a6-79d2-4304-a8b7-959572c7e44d", + "type": "text" + } + ], + "body": { + "mode": "formdata", + "formdata": [ + { + "key": "arquivo", + "type": "file", + "src": "/C:/Users/BaltazarBarbosadeOli/Documents/Tecnospeed/empresas/src/LEGO_logo.png" + } + ] + }, + "url": { + "raw": "https://api.sandbox.plugnotas.com.br/empresa/80972076000118/logotipo", + "protocol": "https", + "host": [ + "api", + "sandbox", + "plugnotas", + "com", + "br" + ], + "path": [ + "empresa", + "80972076000118", + "logotipo" + ] + } + }, + "response": [] + }, + { + "name": "Download Logotipo", + "protocolProfileBehavior": { + "disableBodyPruning": true + }, + "request": { + "method": "GET", + "header": [ + { + "key": "x-api-key", + "value": "2da392a6-79d2-4304-a8b7-959572c7e44d", + "type": "text" + } + ], + "body": { + "mode": "formdata", + "formdata": [] + }, + "url": { + "raw": "https://api.sandbox.plugnotas.com.br/empresa/80972076000118/logotipo", + "protocol": "https", + "host": [ + "api", + "sandbox", + "plugnotas", + "com", + "br" + ], + "path": [ + "empresa", + "80972076000118", + "logotipo" + ] + } + }, + "response": [] + }, + { + "name": "Cadastrar Webhook", + "request": { + "method": "POST", + "header": [ + { + "key": "x-api-key", + "value": "{{token}}", + "type": "text" + }, + { + "key": "Content-Type", + "value": "application/json", + "type": "text" + } + ], + "body": { + "mode": "raw", + "raw": "{\r\n \"url\": \"https://api.minhaapi.com.br/v1/callback/tecnospeed\",\r\n \"method\": \"POST\",\r\n \"headers\": {\r\n \"Authorization\": \"Basic YWRtaW46MTIzbXV=\"\r\n }\r\n}" + }, + "url": { + "raw": "https://api.sandbox.plugnotas.com.br/empresa/{cnpj}/webhook", + "protocol": "https", + "host": [ + "api", + "sandbox", + "plugnotas", + "com", + "br" + ], + "path": [ + "empresa", + "{cnpj}", + "webhook" + ] + } + }, + "response": [] + }, + { + "name": "Consultar Webhook", + "request": { + "method": "GET", + "header": [ + { + "key": "x-api-key", + "value": "{{token}}", + "type": "text" + } + ], + "url": { + "raw": "https://api.sandbox.plugnotas.com.br/empresa/{cnpj}/webhook", + "protocol": "https", + "host": [ + "api", + "sandbox", + "plugnotas", + "com", + "br" + ], + "path": [ + "empresa", + "{cnpj}", + "webhook" + ] + } + }, + "response": [] + }, + { + "name": "Alterar Webhook", + "request": { + "method": "PUT", + "header": [ + { + "key": "x-api-key", + "value": "{{token}}", + "type": "text" + } + ], + "url": { + "raw": "https://api.sandbox.plugnotas.com.br/empresa/{cnpj}/webhook", + "protocol": "https", + "host": [ + "api", + "sandbox", + "plugnotas", + "com", + "br" + ], + "path": [ + "empresa", + "{cnpj}", + "webhook" + ] + } + }, + "response": [] + }, + { + "name": "Testar Webhook", + "request": { + "method": "POST", + "header": [ + { + "key": "x-api-key", + "value": "{{token}}", + "type": "text" + } + ], + "url": { + "raw": "https://api.sandbox.plugnotas.com.br/empresa/{cnpj}/webhook/verify", + "protocol": "https", + "host": [ + "api", + "sandbox", + "plugnotas", + "com", + "br" + ], + "path": [ + "empresa", + "{cnpj}", + "webhook", + "verify" + ] + } + }, + "response": [] + }, + { + "name": "Remover Webhook", + "request": { + "method": "DELETE", + "header": [ + { + "key": "x-api-key", + "value": "{{token}}", + "type": "text" + } + ], + "url": { + "raw": "https://api.sandbox.plugnotas.com.br/empresa/{cnpj}/webhook", + "protocol": "https", + "host": [ + "api", + "sandbox", + "plugnotas", + "com", + "br" + ], + "path": [ + "empresa", + "{cnpj}", + "webhook" + ] + } + }, + "response": [] + } + ], + "event": [ + { + "listen": "prerequest", + "script": { + "type": "text/javascript", + "exec": [ + "" + ] + } + }, + { + "listen": "test", + "script": { + "type": "text/javascript", + "exec": [ + "" + ] + } + } + ] + }, + { + "name": "Relatórios", + "item": [ + { + "name": "Relatório de registro para NFSe", + "request": { + "method": "GET", + "header": [ + { + "key": "x-api-key", + "value": "2da392a6-79d2-4304-a8b7-959572c7e44d", + "type": "text" + }, + { + "key": "Content-Type", + "value": "application/json", + "type": "text" + } + ], + "url": { + "raw": "https://api.sandbox.plugnotas.com.br/nfse/relatorio?from=2024-01-01&to=2024-01-31", + "protocol": "https", + "host": [ + "api", + "sandbox", + "plugnotas", + "com", + "br" + ], + "path": [ + "nfse", + "relatorio" + ], + "query": [ + { + "key": "from", + "value": "2024-01-01" + }, + { + "key": "to", + "value": "2024-01-31" + } + ] + } + }, + "response": [] + }, + { + "name": "Relatório de registro para NFe", + "request": { + "method": "GET", + "header": [ + { + "key": "Content-Type", + "value": "application/json", + "type": "text" + }, + { + "key": "x-api-key", + "value": "2da392a6-79d2-4304-a8b7-959572c7e44d", + "type": "text" + } + ], + "url": { + "raw": "https://api.sandbox.plugnotas.com.br/nfe/relatorio", + "protocol": "https", + "host": [ + "api", + "sandbox", + "plugnotas", + "com", + "br" + ], + "path": [ + "nfe", + "relatorio" + ] + } + }, + "response": [] + }, + { + "name": "Relatório de registro para NFe por CNPJ", + "request": { + "method": "GET", + "header": [ + { + "key": "Content-Type", + "value": "application/json", + "type": "text" + }, + { + "key": "x-api-key", + "value": "2da392a6-79d2-4304-a8b7-959572c7e44d", + "type": "text" + } + ], + "url": { + "raw": "https://api.sandbox.plugnotas.com.br/nfe/{cnpj}/relatorio", + "protocol": "https", + "host": [ + "api", + "sandbox", + "plugnotas", + "com", + "br" + ], + "path": [ + "nfe", + "{cnpj}", + "relatorio" + ] + } + }, + "response": [] + }, + { + "name": "Relatório de registro para NFSe por CNPJ", + "request": { + "method": "GET", + "header": [ + { + "key": "Content-Type", + "value": "application/json", + "type": "text" + }, + { + "key": "x-api-key", + "value": "2da392a6-79d2-4304-a8b7-959572c7e44d", + "type": "text" + } + ], + "url": { + "raw": "https://api.sandbox.plugnotas.com.br/nfse/09389243000138/relatorio?from=2024-01-01&to=2024-01-31", + "protocol": "https", + "host": [ + "api", + "sandbox", + "plugnotas", + "com", + "br" + ], + "path": [ + "nfse", + "09389243000138", + "relatorio" + ], + "query": [ + { + "key": "from", + "value": "2024-01-01" + }, + { + "key": "to", + "value": "2024-01-31" + } + ] + } + }, + "response": [] + } + ] + }, + { + "name": "Webhook", + "item": [ + { + "name": "Consultar Webhook", + "request": { + "method": "GET", + "header": [ + { + "key": "Content-Type", + "value": "application/json" + }, + { + "key": "x-api-key", + "value": "{{token}}" + } + ], + "url": { + "raw": "https://api.sandbox.plugnotas.com.br/empresa/{cnpj}/webhook", + "protocol": "https", + "host": [ + "api", + "sandbox", + "plugnotas", + "com", + "br" + ], + "path": [ + "empresa", + "{cnpj}", + "webhook" + ] + } + }, + "response": [] + }, + { + "name": "Alterar Webhook", + "request": { + "method": "PUT", + "header": [ + { + "key": "Content-Type", + "value": "application/json" + }, + { + "key": "x-api-key", + "value": "{{token}}" + } + ], + "body": { + "mode": "raw", + "raw": "{\r\n \"url\": \"http://webhook.site/bb1308fd-73a6-42fd-bb1e-1fdd08092174\",\r\n \"method\": \"POST\"\r\n}" + }, + "url": { + "raw": "https://api.sandbox.plugnotas.com.br/empresa/{cnpj}/webhook", + "protocol": "https", + "host": [ + "api", + "sandbox", + "plugnotas", + "com", + "br" + ], + "path": [ + "empresa", + "{cnpj}", + "webhook" + ] + } + }, + "response": [] + }, + { + "name": "Configurar Webhook", + "request": { + "method": "POST", + "header": [ + { + "key": "Content-Type", + "value": "application/json" + }, + { + "key": "x-api-key", + "value": "{{token}}" + } + ], + "body": { + "mode": "raw", + "raw": "{\r\n \"url\": \"https://api.minhaapi.com.br/v1/callback/tecnospeed\",\r\n \"method\": \"POST\"\r\n}" + }, + "url": { + "raw": "https://api.sandbox.plugnotas.com.br/empresa/{cnpj}/webhook", + "protocol": "https", + "host": [ + "api", + "sandbox", + "plugnotas", + "com", + "br" + ], + "path": [ + "empresa", + "{cnpj}", + "webhook" + ] + } + }, + "response": [] + }, + { + "name": "Remover Webhook ", + "request": { + "method": "DELETE", + "header": [ + { + "key": "x-api-key", + "value": "{{token}}" + } + ], + "url": { + "raw": "https://api.sandbox.plugnotas.com.br/empresa/{cnpj}/webhook", + "protocol": "https", + "host": [ + "api", + "sandbox", + "plugnotas", + "com", + "br" + ], + "path": [ + "empresa", + "{cnpj}", + "webhook" + ] + } + }, + "response": [] + }, + { + "name": "Testar Webhook", + "request": { + "method": "POST", + "header": [ + { + "key": "x-api-key", + "value": "{{token}}" + } + ], + "body": { + "mode": "formdata", + "formdata": [] + }, + "url": { + "raw": "https://api.sandbox.plugnotas.com.br/empresa/{cnpj}/webhook/verify", + "protocol": "https", + "host": [ + "api", + "sandbox", + "plugnotas", + "com", + "br" + ], + "path": [ + "empresa", + "{cnpj}", + "webhook", + "verify" + ] + } + }, + "response": [] + } + ] + }, + { + "name": "Auxiliares", + "item": [ + { + "name": "Consulta de Municípios Atendidos e Metadados", + "request": { + "method": "GET", + "header": [ + { + "key": "x-api-key", + "value": "{{token}}", + "type": "text" + } + ], + "url": { + "raw": "https://api.sandbox.plugnotas.com.br/nfse/cidades", + "protocol": "https", + "host": [ + "api", + "sandbox", + "plugnotas", + "com", + "br" + ], + "path": [ + "nfse", + "cidades" + ] + } + }, + "response": [] + }, + { + "name": "Consulta disponibilidade de Município e Metadados", + "request": { + "method": "GET", + "header": [ + { + "key": "x-api-key", + "value": "{{token}}", + "type": "text" + }, + { + "key": "Content-Type", + "value": "application/json", + "type": "text" + } + ], + "url": { + "raw": "https://api.sandbox.plugnotas.com.br/nfse/cidades/{codigoIbge}", + "protocol": "https", + "host": [ + "api", + "sandbox", + "plugnotas", + "com", + "br" + ], + "path": [ + "nfse", + "cidades", + "{codigoIbge}" + ] + } + }, + "response": [] + }, + { + "name": "Consulta de endereço pelo CEP", + "request": { + "method": "GET", + "header": [ + { + "key": "Content-Type", + "value": "application/ecmascript", + "type": "text" + }, + { + "key": "x-api-key", + "value": "{{token}}", + "type": "text" + } + ], + "url": { + "raw": "https://api.sandbox.plugnotas.com.br/cep/{cep}", + "protocol": "https", + "host": [ + "api", + "sandbox", + "plugnotas", + "com", + "br" + ], + "path": [ + "cep", + "{cep}" + ] + } + }, + "response": [] + }, + { + "name": "Consulta de dados do CNPJ", + "request": { + "method": "GET", + "header": [ + { + "key": "Content-Type", + "value": "application/json", + "type": "text" + }, + { + "key": "x-api-key", + "value": "2da392a6-79d2-4304-a8b7-959572c7e44d", + "type": "text" + } + ], + "url": { + "raw": "https://api.sandbox.plugnotas.com.br/cnpj/22127038000162", + "protocol": "https", + "host": [ + "api", + "sandbox", + "plugnotas", + "com", + "br" + ], + "path": [ + "cnpj", + "22127038000162" + ] + } + }, + "response": [] + } + ] + }, + { + "name": "NFSe", + "item": [ + { + "name": "Tomador", + "item": [ + { + "name": "Cadastrar Tomador", + "request": { + "method": "POST", + "header": [ + { + "key": "Content-Type", + "value": "application/json" + }, + { + "key": "x-api-key", + "value": "2da392a6-79d2-4304-a8b7-959572c7e44d" + } + ], + "body": { + "mode": "raw", + "raw": "{\r\n \"cpfCnpj\": \"37875748000148\",\r\n \"inscricaoMunicipal\": \"8214100099\",\r\n \"inscricaoEstadual\": \"1234567850\",\r\n \"inscricaoSuframa\": \"1234567850\",\r\n \"indicadorInscricaoEstadual\": 9,\r\n \"razaoSocial\": \"Tomador de teste LTDA\",\r\n \"nomeFantasia\": \"Tomador de teste\",\r\n \"endereco\": {\r\n \"tipoLogradouro\": \"Avenida\",\r\n \"logradouro\": \"Duque de Caxias\",\r\n \"numero\": \"882\",\r\n \"complemento\": \"17 andar\",\r\n \"tipoBairro\": \"Zona\",\r\n \"bairro\": \"Zona 01\",\r\n \"codigoPais\": \"1058\",\r\n \"descricaoPais\": \"Brasil\",\r\n \"codigoCidade\": \"4115200\",\r\n \"descricaoCidade\": \"Maringá\",\r\n \"estado\": \"PR\",\r\n \"cep\": \"87020025\"\r\n },\r\n \"telefone\": {\r\n \"ddd\": \"44\",\r\n \"numero\": \"30379500\"\r\n },\r\n \"email\": \"tomador@plugnotas.com.br\",\r\n \"orgaoPublico\": true\r\n}" + }, + "url": { + "raw": "https://api.sandbox.plugnotas.com.br/nfse/tomador", + "protocol": "https", + "host": [ + "api", + "sandbox", + "plugnotas", + "com", + "br" + ], + "path": [ + "nfse", + "tomador" + ] + } + }, + "response": [] + }, + { + "name": "Consulta de Tomador", + "request": { + "method": "GET", + "header": [ + { + "key": "Content-Type", + "value": "application/json" + }, + { + "key": "x-api-key", + "value": "2da392a6-79d2-4304-a8b7-959572c7e44d" + } + ], + "url": { + "raw": "https://api.sandbox.plugnotas.com.br/nfse/tomador/37875748000148", + "protocol": "https", + "host": [ + "api", + "sandbox", + "plugnotas", + "com", + "br" + ], + "path": [ + "nfse", + "tomador", + "37875748000148" + ] + } + }, + "response": [] + }, + { + "name": "Update de Tomador", + "request": { + "method": "PATCH", + "header": [ + { + "key": "Content-Type", + "value": "application/json", + "type": "text" + }, + { + "key": "x-api-key", + "value": "{{token}}", + "type": "text" + } + ], + "url": { + "raw": "https://api.sandbox.plugnotas.com.br/nfse/tomador/{cnpj}", + "protocol": "https", + "host": [ + "api", + "sandbox", + "plugnotas", + "com", + "br" + ], + "path": [ + "nfse", + "tomador", + "{cnpj}" + ] + } + }, + "response": [] + } + ] + }, + { + "name": "Serviço", + "item": [ + { + "name": "Cadastrar Serviço", + "request": { + "method": "POST", + "header": [ + { + "key": "Content-Type", + "value": "application/json" + }, + { + "key": "x-api-key", + "value": "2da392a6-79d2-4304-a8b7-959572c7e44d" + } + ], + "body": { + "mode": "raw", + "raw": "{\r\n \"codigo\": \"1.02\",\r\n \"idIntegracao\": \"A001XT\",\r\n \"discriminacao\": \"Descrição dos serviços prestados, utilize | para quebra de linha na impressão.\",\r\n \"codigoTributacao\": \"4115200\",\r\n \"cnae\": \"4751201\",\r\n \"codigoCidadeIncidencia\": \"4115200\",\r\n \"descricaoCidadeIncidencia\": \"MARINGA\",\r\n \"unidade\": \"UN\",\r\n \"quantidade\": 1,\r\n \"iss\": {\r\n \"tipoTributacao\": 0,\r\n \"exigibilidade\": 1,\r\n \"aliquota\": 3,\r\n \"valor\": 0,\r\n \"valorRetido\": 0,\r\n \"processoSuspensao\": \"70049875465\"\r\n },\r\n \"obra\": {\r\n \"art\": \"6270201\",\r\n \"codigo\": \"21\"\r\n },\r\n \"valor\": {\r\n \"servico\": 0.1,\r\n \"baseCalculo\": 0.1,\r\n \"deducoes\": 0,\r\n \"descontoCondicionado\": 0,\r\n \"descontoIncondicionado\": 0,\r\n \"liquido\": 0.1,\r\n \"unitario\": 0.1,\r\n \"valorAproximadoTributos\": 0\r\n },\r\n \"deducao\": {\r\n \"tipo\": 0,\r\n \"descricao\": \"Sem Deduções\"\r\n },\r\n \"retencao\": {\r\n \"pis\": {\r\n \"aliquota\": 0,\r\n \"valor\": 0\r\n },\r\n \"cofins\": {\r\n \"aliquota\": 0,\r\n \"valor\": 0\r\n },\r\n \"csll\": {\r\n \"aliquota\": 0,\r\n \"valor\": 0\r\n },\r\n \"inss\": {\r\n \"aliquota\": 0,\r\n \"valor\": 0\r\n },\r\n \"irrf\": {\r\n \"aliquota\": 0,\r\n \"valor\": 0\r\n },\r\n \"outrasRetencoes\": 0,\r\n \"cpp\": {\r\n \"aliquota\": 0,\r\n \"valor\": 0\r\n }\r\n },\r\n \"tributavel\": true,\r\n \"ibpt\": {\r\n \"simplificado\": {\r\n \"aliquota\": 0\r\n },\r\n \"detalhado\": {\r\n \"aliquota\": {\r\n \"municipal\": 0,\r\n \"estadual\": 0,\r\n \"federal\": 0\r\n }\r\n }\r\n },\r\n \"responsavelRetencao\": \"1\"\r\n}" + }, + "url": { + "raw": "https://api.sandbox.plugnotas.com.br/nfse/servico", + "protocol": "https", + "host": [ + "api", + "sandbox", + "plugnotas", + "com", + "br" + ], + "path": [ + "nfse", + "servico" + ] + } + }, + "response": [] + }, + { + "name": "Consultar Serviço", + "request": { + "method": "GET", + "header": [ + { + "key": "Content-Type", + "value": "application/json" + }, + { + "key": "x-api-key", + "value": "2da392a6-79d2-4304-a8b7-959572c7e44d" + } + ], + "url": { + "raw": "https://api.sandbox.plugnotas.com.br/nfse/servico/67c1aade2764db1f85076628", + "protocol": "https", + "host": [ + "api", + "sandbox", + "plugnotas", + "com", + "br" + ], + "path": [ + "nfse", + "servico", + "67c1aade2764db1f85076628" + ] + } + }, + "response": [] + }, + { + "name": "Update de Serviço", + "request": { + "method": "PATCH", + "header": [ + { + "key": "Content-Type", + "value": "application/json" + }, + { + "key": "x-api-key", + "value": "{{token}}" + } + ], + "body": { + "mode": "raw", + "raw": "{\r\n\t\"codigo\": \"0107\",\r\n\t\"descricaoLC116\": \"Programação\",\r\n\t\"discriminacao\": \"Programação de software\",\r\n\t\"cnae\": \"00000\",\r\n\t\"codigoTributacao\": \"4115200\",\r\n\t\"codigoCidadeIncidencia\": \"4115200\",\r\n\t\"descricaoCidadeIncidencia\": \"MARINGA\",\r\n\t\"iss\": {\r\n\t\t\"exigibilidade\": 1,\r\n\t\t\"retido\": false,\r\n\t\t\"aliquota\": 3,\r\n\t\t\"aliquotaRetido\": 2,\r\n\t\t\"tipoTributacao\": 7\r\n\t},\r\n\t\"retencao\": {\r\n\t\t\"pis\": {\r\n\t\t\t\"aliquota\": 0.65\r\n\t\t},\r\n\t\t\"cofins\": {\r\n\t\t\t\"aliquota\": 3\r\n\t\t},\r\n\t\t\"csll\": {\r\n\t\t\t\"aliquota\": 0\r\n\t\t}\r\n\t},\r\n\t\"valor\": {\r\n\t\t\"deducoes\": 0,\r\n\t\t\"baseCalculo\": 0.1,\r\n\t\t\"servico\": 45,\r\n\t\t\"descontoIncondicionado\": 0,\r\n\t\t\"descontoCondicionado\": 0,\r\n\t\t\"liquido\": 0.1\r\n\t}\r\n}" + }, + "url": { + "raw": "https://api.sandbox.plugnotas.com.br/nfse/servico/{idServico}", + "protocol": "https", + "host": [ + "api", + "sandbox", + "plugnotas", + "com", + "br" + ], + "path": [ + "nfse", + "servico", + "{idServico}" + ] + } + }, + "response": [] + } + ] + }, + { + "name": "Registrar NFSe", + "request": { + "method": "POST", + "header": [ + { + "key": "Content-Type", + "value": "application/json" + }, + { + "key": "x-api-key", + "value": "2da392a6-79d2-4304-a8b7-959572c7e44d" + } + ], + "body": { + "mode": "raw", + "raw": "[\n {\n \"idIntegracao\": \"XXXYY999\",\n \"prestador\": {\n \"cpfCnpj\": \"80972076000118\"\n },\n \"tomador\": {\n \"cpfCnpj\": \"82995888000104\",\n \"razaoSocial\": \"Empresa de Teste LTDA\",\n \"inscricaoMunicipal\": \"8214100099\",\n \"email\": \"teste@plugnotas.com.br\",\n \"endereco\": {\n \"descricaoCidade\": \"Maringa\",\n \"cep\": \"87020100\",\n \"tipoLogradouro\": \"Rua\",\n \"logradouro\": \"Barao do rio branco\",\n \"tipoBairro\": \"Centro\",\n \"codigoCidade\": \"4115200\",\n \"complemento\": \"sala 01\",\n \"estado\": \"PR\",\n \"numero\": \"1001\",\n \"bairro\": \"Centro\"\n }\n },\n \"servico\": [\n {\n \"codigo\": \"14.10\",\n \"codigoTributacao\": \"14.10\",\n \"discriminacao\": \"Descrição dos serviços prestados, utilize | para quebra de linha na impressão.\",\n \"cnae\": \"7490104\",\n \"iss\": {\n \"tipoTributacao\": 7,\n \"exigibilidade\": 1,\n \"aliquota\": 3\n },\n \"valor\": {\n \"servico\": 1,\n \"descontoCondicionado\": 0,\n \"descontoIncondicionado\": 0\n }\n }\n ]\n }\n]" + }, + "url": { + "raw": "https://api.sandbox.plugnotas.com.br/nfse", + "protocol": "https", + "host": [ + "api", + "sandbox", + "plugnotas", + "com", + "br" + ], + "path": [ + "nfse" + ] + } + }, + "response": [] + }, + { + "name": "Consultar NFSe", + "request": { + "method": "GET", + "header": [ + { + "key": "Content-Type", + "value": "application/json" + }, + { + "key": "x-api-key", + "value": "2da392a6-79d2-4304-a8b7-959572c7e44d" + } + ], + "url": { + "raw": "https://api.sandbox.plugnotas.com.br/nfse/6686a8bc31b7ba9012ae16e1", + "protocol": "https", + "host": [ + "api", + "sandbox", + "plugnotas", + "com", + "br" + ], + "path": [ + "nfse", + "6686a8bc31b7ba9012ae16e1" + ] + } + }, + "response": [] + }, + { + "name": "Download do PDF", + "request": { + "method": "GET", + "header": [ + { + "key": "Content-Type", + "value": "application/json" + }, + { + "key": "x-api-key", + "value": "2da392a6-79d2-4304-a8b7-959572c7e44d" + } + ], + "url": { + "raw": "https://api.sandbox.plugnotas.com.br/nfse/pdf/6686a8bc31b7ba9012ae16e1", + "protocol": "https", + "host": [ + "api", + "sandbox", + "plugnotas", + "com", + "br" + ], + "path": [ + "nfse", + "pdf", + "6686a8bc31b7ba9012ae16e1" + ] + } + }, + "response": [] + }, + { + "name": "Consultar XML", + "request": { + "method": "GET", + "header": [ + { + "key": "x-api-key", + "value": "{{token}}" + } + ], + "url": { + "raw": "https://api.sandbox.plugnotas.com.br/nfse/xml/{idNota}", + "protocol": "https", + "host": [ + "api", + "sandbox", + "plugnotas", + "com", + "br" + ], + "path": [ + "nfse", + "xml", + "{idNota}" + ] + } + }, + "response": [] + }, + { + "name": "Cancelar NFSe", + "request": { + "method": "POST", + "header": [ + { + "key": "Content-Type", + "value": "application/json" + }, + { + "key": "x-api-key", + "value": "{{token}}" + } + ], + "body": { + "mode": "raw", + "raw": "{\n\t\"codigo\" : \"C099\",\n\t\"message\" : \"É necessário o body ao cancelar uma NFSe, para incluir o código de cancelamento da cidade.\"\n}\n\n" + }, + "url": { + "raw": "https://api.sandbox.plugnotas.com.br/nfse/cancelar/{idNota}", + "protocol": "https", + "host": [ + "api", + "sandbox", + "plugnotas", + "com", + "br" + ], + "path": [ + "nfse", + "cancelar", + "{idNota}" + ] + } + }, + "response": [] + }, + { + "name": "Consultar o status de Cancelamento", + "request": { + "method": "GET", + "header": [ + { + "key": "x-api-key", + "value": "{{token}}" + }, + { + "key": "Content-Type", + "value": "application/json" + } + ], + "url": { + "raw": "https://api.sandbox.plugnotas.com.br/nfse/cancelar/status/{cancellationProtocol}", + "protocol": "https", + "host": [ + "api", + "sandbox", + "plugnotas", + "com", + "br" + ], + "path": [ + "nfse", + "cancelar", + "status", + "{cancellationProtocol}" + ] + } + }, + "response": [] + }, + { + "name": "Consultar os Dados de NFSe", + "request": { + "method": "GET", + "header": [ + { + "key": "Content-Type", + "value": "application/json" + }, + { + "key": "x-api-key", + "value": "{{token}}" + } + ], + "url": { + "raw": "https://api.sandbox.plugnotas.com.br/nfse/consultar/{idNotaOrProtocol}", + "protocol": "https", + "host": [ + "api", + "sandbox", + "plugnotas", + "com", + "br" + ], + "path": [ + "nfse", + "consultar", + "{idNotaOrProtocol}" + ] + } + }, + "response": [] + }, + { + "name": "Consulta resumida por CNPJ e Código", + "request": { + "method": "GET", + "header": [ + { + "key": "x-api", + "value": "{{token}}", + "type": "text" + } + ], + "url": { + "raw": "https://api.sandbox.plugnotas.com.br/nfse/consultar/{idIntegracao}/{cnpj}", + "protocol": "https", + "host": [ + "api", + "sandbox", + "plugnotas", + "com", + "br" + ], + "path": [ + "nfse", + "consultar", + "{idIntegracao}", + "{cnpj}" + ] + } + }, + "response": [] + } + ] + }, + { + "name": "NFe", + "item": [ + { + "name": "Registrar NFe", + "request": { + "method": "POST", + "header": [ + { + "key": "Content-Type", + "value": "application/json" + }, + { + "key": "x-api-key", + "value": "{{token}}" + } + ], + "body": { + "mode": "raw", + "raw": "[\n {\n \"idIntegracao\": \"XXXYY99999\",\n \"presencial\": true,\n \"consumidorFinal\": true,\n \"natureza\": \"OPERAÇÃO INTERNA\",\n \"emitente\": {\n \"cpfCnpj\": \"08187168000160\"\n },\n \"destinatario\": {\n \"cpfCnpj\": \"08114280956\",\n \"razaoSocial\": \"NF-E EMITIDA EM AMBIENTE DE HOMOLOGACAO - SEM VALOR FISCAL\",\n \"email\": \"contato@tecnospeed.com.br\",\n \"endereco\": {\n \"logradouro\": \"AVENIDA DUQUE DE CAXIAS\",\n \"numero\": \"882\",\n \"bairro\": \"CENTRO\",\n \"codigoCidade\": \"4115200\",\n \"descricaoCidade\": \"MARINGA\",\n \"estado\": \"PR\",\n \"cep\": \"87020025\"\n }\n },\n \"itens\": [\n {\n \"codigo\": \"1\",\n \"descricao\": \"NOTA FISCAL EMITIDA EM AMBIENTE DE HOMOLOGACAO - SEM VALOR FISCAL\",\n \"ncm\": \"06029090\",\n \"cest\": \"0123456\",\n \"cfop\": \"5101\",\n \"valorUnitario\": {\n \"comercial\": 4.6,\n \"tributavel\": 4.6\n },\n \"valor\": 4.6,\n \"tributos\": {\n \"icms\": {\n \"origem\": \"0\",\n \"cst\": \"00\",\n \"baseCalculo\": {\n \"modalidadeDeterminacao\": 0,\n \"valor\": 0\n },\n \"aliquota\": 0,\n \"valor\": 0\n },\n \"pis\": {\n \"cst\": \"99\",\n \"baseCalculo\": {\n \"valor\": 0,\n \"quantidade\": 0\n },\n \"aliquota\": 0,\n \"valor\": 0\n },\n \"cofins\": {\n \"cst\": \"07\",\n \"baseCalculo\": {\n \"valor\": 0\n },\n \"aliquota\": 0,\n \"valor\": 0\n }\n }\n }\n ],\n \"pagamentos\": [\n {\n \"aVista\": true,\n \"meio\": \"01\",\n \"valor\": 4.6\n }\n ],\n \"responsavelTecnico\": {\n \"cpfCnpj\": \"08187168000160\",\n \"nome\": \"Tecnospeed\",\n \"email\": \"contato@tecnospeed.com.br\",\n \"telefone\": {\n \"ddd\": \"44\",\n \"numero\": \"30379500\"\n }\n }\n }\n]" + }, + "url": { + "raw": "https://api.sandbox.plugnotas.com.br/nfe", + "protocol": "https", + "host": [ + "api", + "sandbox", + "plugnotas", + "com", + "br" + ], + "path": [ + "nfe" + ] + } + }, + "response": [] + }, + { + "name": "Cancelar NFe", + "request": { + "method": "POST", + "header": [ + { + "key": "x-api-key", + "value": "{{token}}", + "type": "text" + }, + { + "key": "Content-Type", + "value": "application/json", + "type": "text" + } + ], + "body": { + "mode": "raw", + "raw": "{\n \"justificativa\": \"Erro na emissão da NFe - Teste cancelamento\"\n}" + }, + "url": { + "raw": "https://api.sandbox.plugnotas.com.br/nfe/{idNota}/cancelamento", + "protocol": "https", + "host": [ + "api", + "sandbox", + "plugnotas", + "com", + "br" + ], + "path": [ + "nfe", + "{idNota}", + "cancelamento" + ] + } + }, + "response": [] + }, + { + "name": "Enviar CCe", + "request": { + "method": "POST", + "header": [ + { + "key": "Content-Type", + "value": "application/json" + }, + { + "key": "x-api-key", + "value": "{{token}}" + } + ], + "body": { + "mode": "raw", + "raw": "{\n\t\"correcao\": \"Onde consta Rua X lê-se Rua Y\"\n}" + }, + "url": { + "raw": "https://api.sandbox.plugnotas.com.br/nfe/{idNota}/cce", + "protocol": "https", + "host": [ + "api", + "sandbox", + "plugnotas", + "com", + "br" + ], + "path": [ + "nfe", + "{idNota}", + "cce" + ] + } + }, + "response": [] + }, + { + "name": "Consulta Resumida NFe", + "request": { + "method": "GET", + "header": [ + { + "key": "x-api-key", + "value": "{{token}}", + "type": "text" + } + ], + "url": { + "raw": "https://api.sandbox.plugnotas.com.br/nfe/{idNota}/resumo", + "protocol": "https", + "host": [ + "api", + "sandbox", + "plugnotas", + "com", + "br" + ], + "path": [ + "nfe", + "{idNota}", + "resumo" + ] + } + }, + "response": [] + }, + { + "name": "Consultar Status Cancelamento NFe", + "protocolProfileBehavior": { + "disableBodyPruning": true + }, + "request": { + "method": "GET", + "header": [ + { + "key": "x-api-key", + "value": "{{token}}", + "type": "text" + }, + { + "key": "Content-Type", + "value": "application/json", + "type": "text" + } + ], + "body": { + "mode": "raw", + "raw": "" + }, + "url": { + "raw": "https://api.sandbox.plugnotas.com.br/nfe/{idNota}/cancelamento/status", + "protocol": "https", + "host": [ + "api", + "sandbox", + "plugnotas", + "com", + "br" + ], + "path": [ + "nfe", + "{idNota}", + "cancelamento", + "status" + ] + } + }, + "response": [] + }, + { + "name": "Consultar Status CCe NFe", + "protocolProfileBehavior": { + "disableBodyPruning": true + }, + "request": { + "method": "GET", + "header": [ + { + "key": "x-api-key", + "value": "{{token}}", + "type": "text" + }, + { + "key": "Content-Type", + "value": "application/json", + "type": "text" + } + ], + "body": { + "mode": "raw", + "raw": "" + }, + "url": { + "raw": "https://api.sandbox.plugnotas.com.br/nfe/{idNota}/cce/status", + "protocol": "https", + "host": [ + "api", + "sandbox", + "plugnotas", + "com", + "br" + ], + "path": [ + "nfe", + "{idNota}", + "cce", + "status" + ] + } + }, + "response": [] + }, + { + "name": "Gerar PDF", + "request": { + "method": "GET", + "header": [ + { + "key": "x-api-key", + "value": "{{token}}", + "type": "text" + } + ], + "url": { + "raw": "https://api.sandbox.plugnotas.com.br/nfe/{idNota}/pdf", + "protocol": "https", + "host": [ + "api", + "sandbox", + "plugnotas", + "com", + "br" + ], + "path": [ + "nfe", + "{idNota}", + "pdf" + ] + } + }, + "response": [] + }, + { + "name": "Gerar XML", + "request": { + "method": "GET", + "header": [ + { + "key": "x-api-key", + "value": "{{token}}", + "type": "text" + } + ], + "url": { + "raw": "https://api.sandbox.plugnotas.com.br/nfe/{idNota}/xml", + "protocol": "https", + "host": [ + "api", + "sandbox", + "plugnotas", + "com", + "br" + ], + "path": [ + "nfe", + "{idNota}", + "xml" + ] + } + }, + "response": [] + }, + { + "name": "Consulta resumida por CNPJ e Código", + "request": { + "method": "GET", + "header": [ + { + "key": "x-api-key", + "value": "{{token}}", + "type": "text" + } + ], + "url": { + "raw": "https://api.sandbox.plugnotas.com.br/nfe/{cnpj}/{idIntegracao}/resumo", + "protocol": "https", + "host": [ + "api", + "sandbox", + "plugnotas", + "com", + "br" + ], + "path": [ + "nfe", + "{cnpj}", + "{idIntegracao}", + "resumo" + ] + } + }, + "response": [] + } + ] + }, + { + "name": "NFCe", + "item": [ + { + "name": "Registrar NFCe", + "protocolProfileBehavior": { + "disabledSystemHeaders": {} + }, + "request": { + "method": "POST", + "header": [ + { + "key": "Content-Type", + "value": "application/json", + "type": "text" + }, + { + "key": "x-api-key", + "value": "{{token}}", + "type": "text" + } + ], + "body": { + "mode": "raw", + "raw": "[\r\n {\r\n \"idIntegracao\": \"XXXYY999999\",\r\n \"natureza\": \"VENDA\",\r\n \"emitente\": {\r\n \"cpfCnpj\": \"08187168000160\"\r\n },\r\n \"itens\": [\r\n {\r\n \"codigo\": \"1\",\r\n \"descricao\": \"NOTA FISCAL EMITIDA EM AMBIENTE DE HOMOLOGACAO - SEM VALOR FISCAL\",\r\n \"ncm\": \"06029090\",\r\n \"cfop\": \"5101\",\r\n \"valorUnitario\": {\r\n \"comercial\": 4.6,\r\n \"tributavel\": 4.6\r\n },\r\n \"valor\": 4.6,\r\n \"tributos\": {\r\n \"icms\": {},\r\n \"pis\": {},\r\n \"cofins\": {}\r\n }\r\n }\r\n ],\r\n \"pagamentos\": [\r\n {\r\n \"aVista\": true,\r\n \"meio\": \"01\",\r\n \"valor\": 4.6\r\n }\r\n ],\r\n \"responsavelTecnico\": {\r\n \"cpfCnpj\": \"08187168000160\",\r\n \"nome\": \"Tecnospeed\",\r\n \"email\": \"contato@tecnospeed.com.br\",\r\n \"telefone\": {\r\n \"ddd\": \"44\",\r\n \"numero\": \"30379500\"\r\n }\r\n }\r\n }\r\n]", + "options": { + "raw": { + "language": "json" + } + } + }, + "url": { + "raw": "https://api.sandbox.plugnotas.com.br/nfce", + "protocol": "https", + "host": [ + "api", + "sandbox", + "plugnotas", + "com", + "br" + ], + "path": [ + "nfce" + ] + } + }, + "response": [] + }, + { + "name": "Cancelar NFCe", + "request": { + "method": "POST", + "header": [ + { + "key": "Content-Type", + "value": "application/json", + "type": "text" + }, + { + "key": "x-api-key", + "value": "{{token}}", + "type": "text" + } + ], + "body": { + "mode": "raw", + "raw": "{\r\n \"justificativa\": \"Erro na emissão da Nota Fiscal eletrônica.\"\r\n}" + }, + "url": { + "raw": "https://api.sandbox.plugnotas.com.br/nfce/{idNota}/cancelamento", + "protocol": "https", + "host": [ + "api", + "sandbox", + "plugnotas", + "com", + "br" + ], + "path": [ + "nfce", + "{idNota}", + "cancelamento" + ] + } + }, + "response": [] + }, + { + "name": "Consultar Status Cancelamento NFCe", + "request": { + "method": "GET", + "header": [ + { + "key": "x-api-key", + "value": "{{token}}", + "type": "text" + } + ], + "url": { + "raw": "https://api.sandbox.plugnotas.com.br/nfce/{idNota}/cancelamento/status", + "protocol": "https", + "host": [ + "api", + "sandbox", + "plugnotas", + "com", + "br" + ], + "path": [ + "nfce", + "{idNota}", + "cancelamento", + "status" + ] + } + }, + "response": [] + }, + { + "name": "Consulta Resumida NFCe", + "request": { + "method": "GET", + "header": [ + { + "key": "x-api-key", + "value": "{{token}}", + "type": "text" + } + ], + "url": { + "raw": "https://api.sandbox.plugnotas.com.br/nfce/{idNota}/resumo", + "protocol": "https", + "host": [ + "api", + "sandbox", + "plugnotas", + "com", + "br" + ], + "path": [ + "nfce", + "{idNota}", + "resumo" + ] + } + }, + "response": [] + }, + { + "name": "Consulta resumida por CNPJ e Código", + "request": { + "method": "GET", + "header": [ + { + "key": "x-api-key", + "value": "{{token}}", + "type": "text" + } + ], + "url": { + "raw": "https://api.sandbox.plugnotas.com.br/nfce/{cnpj}/{idIntegracao}/resumo", + "protocol": "https", + "host": [ + "api", + "sandbox", + "plugnotas", + "com", + "br" + ], + "path": [ + "nfce", + "{cnpj}", + "{idIntegracao}", + "resumo" + ] + } + }, + "response": [] + }, + { + "name": "Gerar PDF", + "request": { + "method": "GET", + "header": [ + { + "key": "x-api-key", + "value": "{{token}}", + "type": "text" + }, + { + "key": "Content-Type", + "value": "application/pdf", + "type": "text" + } + ], + "url": { + "raw": "https://api.sandbox.plugnotas.com.br/nfce/{idNota}/pdf", + "protocol": "https", + "host": [ + "api", + "sandbox", + "plugnotas", + "com", + "br" + ], + "path": [ + "nfce", + "{idNota}", + "pdf" + ] + } + }, + "response": [] + }, + { + "name": "Gerar XML", + "request": { + "method": "GET", + "header": [ + { + "key": "x-api-key", + "value": "{{token}}", + "type": "text" + }, + { + "key": "Content-Type", + "value": "application/xml", + "type": "text" + } + ], + "url": { + "raw": "https://api.sandbox.plugnotas.com.br/nfce/{idNota}/xml", + "protocol": "https", + "host": [ + "api", + "sandbox", + "plugnotas", + "com", + "br" + ], + "path": [ + "nfce", + "{idNota}", + "xml" + ] + } + }, + "response": [] + }, + { + "name": "Consulta Por Periodo NFCe", + "request": { + "method": "GET", + "header": [ + { + "key": "x-api-key", + "value": "{{token}}", + "type": "text" + } + ], + "url": { + "raw": "https://api.sandbox.plugnotas.com.br/nfce/consulta/periodo", + "protocol": "https", + "host": [ + "api", + "sandbox", + "plugnotas", + "com", + "br" + ], + "path": [ + "nfce", + "consulta", + "periodo" + ] + } + }, + "response": [] + } + ] + }, + { + "name": "Notas Destinadas (DF-e)", + "item": [ + { + "name": "Consultar notas destinadas", + "request": { + "method": "GET", + "header": [ + { + "key": "Content-Type", + "value": "application/json", + "type": "text" + }, + { + "key": "x-api-key", + "value": "{{token}}", + "type": "text" + } + ], + "url": { + "raw": "https://api.sandbox.plugnotas.com.br/nfe/destinada", + "protocol": "https", + "host": [ + "api", + "sandbox", + "plugnotas", + "com", + "br" + ], + "path": [ + "nfe", + "destinada" + ] + } + }, + "response": [] + }, + { + "name": "Consulta status da manifestação do destinatário", + "request": { + "method": "GET", + "header": [ + { + "key": "Content-Type", + "value": "application/json", + "type": "text" + }, + { + "key": "x-api-key", + "value": "{{token}}", + "type": "text" + } + ], + "url": { + "raw": "https://api.sandbox.plugnotas.com.br/nfe/{idNota}/manifestacao/status", + "protocol": "https", + "host": [ + "api", + "sandbox", + "plugnotas", + "com", + "br" + ], + "path": [ + "nfe", + "{idNota}", + "manifestacao", + "status" + ] + } + }, + "response": [] + }, + { + "name": "Manifestar Nota", + "request": { + "method": "POST", + "header": [ + { + "key": "Content-Type", + "value": "application/json", + "type": "text" + }, + { + "key": "x-api-key", + "value": "{{token}}", + "type": "text" + } + ], + "body": { + "mode": "raw", + "raw": "{\r\n \"operacao\": \"CONFIRMACAODAOPERACAO\"\r\n}" + }, + "url": { + "raw": "https://api.sandbox.plugnotas.com.br/nfe/{idNota}/manifestacao", + "protocol": "https", + "host": [ + "api", + "sandbox", + "plugnotas", + "com", + "br" + ], + "path": [ + "nfe", + "{idNota}", + "manifestacao" + ] + } + }, + "response": [] + } + ] + } + ] +} \ No newline at end of file diff --git a/backup/auxiliar.json b/backup/auxiliar.json new file mode 100644 index 0000000..e6988f6 --- /dev/null +++ b/backup/auxiliar.json @@ -0,0 +1,8 @@ +{ + "fila": "empresa", + "cliente": "Verta", + "dados": { + "cnpj": "45738057000120", + "file": "iVBORw0KGgoAAAANSUhEUgAAA7IAAABlCAYAAABwU9/+AAAAAXNSR0IArs4c6QAAAARnQU1BAACxjwv8YQUAAAAJcEhZcwAADsMAAA7DAcdvqGQAAApDSURBVHhe7d3BS5xnHgfwn5IK7aV/gUUwiMEihtxaCtvjEEgq5GC2XRcvsj1KIEMg22WzhTCB4HXJRZLWxEPAJiBz3IXF3CQSGpRQQdb+Ae0lgbFk9jAzzsw746gTTebpfj4wkHnfJ44Bf/m93+d53te+crlcDgAAAEhEf/YAAAAA9DJBFgAAgKQIsgAAACRFkAUAACApgiwAAABJEWQBAABIiiALAABAUgRZAAAAkiLIAgAAkBRBFgAAgKQIsgAAACRFkAUAACApgiwAAABJEWQBAABIiiALAABAUgRZAAAAkiLIAgAAkBRBFgAAgKQIsgAAACRFkAUAACApgiwAAABJEWQBAABIiiALAABAUgRZAAAAkiLIAgAAkBRBFgAAgKQIsgAAACRFkAUAACApgiwAAABJEWQBAABIiiALAABAUgRZAAAAkiLIAgAAkBRBFgAAgKQIsgAAACRFkAUAACApgiwAAABJEWQBAABIiiALAABAUgRZAAAAkiLIAgAAkBRBFgAAgKQIsgAAACRFkAUAACApgiwAAABJEWQBAABIiiALAABAUgRZAAAAkiLIAgAAkBRBFgAAgKQIsgAAACRFkAUAACApgiwAAABJEWQBAABIiiALAABAUgRZAAAAkiLIAgAAkBRBFgAAgKQIsgAAACRFkAUAACApgiwAAABJEWQBAABIiiALAABAUgRZAAAAktL3888/l7MHAQAAoFf1lctlQRYAAIBk2FoMAABAUgRZAAAAkiLIAgAAkBRBFgAAgKQIsgAAACRFkAUAACApgiwAAABJEWQBAABIiiALAABAUgRZAAAAkiLIAgAAkBRB9i1ZW1vLHgJ6gNqE3qQ2AehEkAUAACApgiwAAABJEWQBAABISl+5XC6/evUqdnd3o1wuZ8/TRl9fX7z33nvx/vvvZ0/ta21tLc6dO5c9DB2pzaNRm7wtavNo1CYAx63v5cuX5devX8epU6eiv98C7WG8fv06fvvtt+jv7z90U9aQOapXr16F2jwatcnboDaPTm0CcNz6d3d3NeMj6u/vj1OnTsXu7m72FBwbtXl0apO3QW0endoE4Lj1l8tlzbgL/f39tpRxotRmd9QmJ01tdkdtAnCcdGIAAACS8rsJssWZgRiYKVberEzHwMB0VN8BAEe0fXs8Bs7Ox3b2ROizALx7PRJkt2P+7EAMDDS/pley44Dfk+JMtu7HY34rO6pVxwts6AnFmG7Tx97lz+67/GwAOG49EmQrRm++iFKpVHltFGJ9ciDGb3fRcs/fi1LpXuSyx4EeUbnIvxiP6jVfKkVpYyYWLrnQBgCgs54Ksk2G5+LZRiHi2oWmFZrt2+P11Zv9ZpYbtjy1m4Fu2oacWQ1uDM6VcfN757sK1UCL7dv5WBorxIuFzHTT8Fw8ezoXQ7X3K9MNq7XVbYxb83Hh2mbE83yMNB6H1GzNx/jAeMzfbvg53+tN9f41X9u5UD23bx+sfb29nlnpb9MrHepmaz7GG3dFNHx+xdP4pqFHZleYG+37fQHACejdIBsRMTwZM2ObsfBDpR1u3x6PkWsT8ai6evNoIh8XDgiXQ1cKMfV8IZb3GnsxHiyORuF6rtrkRyI/UVsVehQTmeAciwsRDyuf9+zK3uU10LXtWP5+M0a/mqwH1nZWpmNgcj0KG5X6e3FzPS7OFCOG5+LxzdGIsUK8KJXsviBxm5H/8fJeD5pavNgcFp/n4+ml6o6FhVzEynRLHxw5TGjcp262f3gaM9UaK20UYrTl89fjbLUHlpanYmmy/cRRN/0ZAN5EbwfZGIrRidqfqxe/N2/sXbTmLk3F5vfLBzTwXFz+sh6GY+VBLI3NxORwRGwtx8LzWqhtMzYi4stCzA3X3wLHY2K0HmOb7pWtXpQXHy411d/QFzMxuvig7UU0pKuxB52Js2OZ02OFuHG+/rZSF5frffB6IUabJmuPZujKvXqPG56MmbGI9c2GHljrlxER5y/HVCzFg5ZV2W77MwB0r8eD7HZsrjcf2bw2Ur/gnVxqPrmPxoZafLiUWQnajPyZ+kX0xcWmvwqckMaL5dxCbdV1tGlMLF6s1/uZfGw2n4U0TYx23o1wgNGPz9TfDI/G3nxvVxpvrxmJ/PPs+UZtgnaDbvozAHSrt4NsdcV05ot6y59abngwTKkUpcb76fZz/nJ1e3ExHixORaFpi/DU3lao2ssW4u6sFnJx60n2KGQNxeRXo4darWl6AJxtxCSlfejb+PHNp2M2f9yov9najMx87xFkb695EYU233PdRjztEHS76s//5/RNgO71bpDdmo/xM/mIm4+r254qF7/N9+dsx/zMIe4NilzcuBmxcCkfSw1bsirbqJYq993VbM3H9Du+r+fC0oVDvTrZuT8buVyu8vp6KXayAzI6j9+Jpa+r53K5mL3f7qutxuq/P49PP8keh1aVe9fzMdLyYJm63KWp2Mzcs16cqdT/0OhExPOn0XA5Dz2mTc/amo/8YsTUpe6nY3KXpiIW83t1Ufw2H5u17b/DozERm/G0lpVXvmlaYd2vbuorvJ2Dau0hbY1bnSva/FsP3Z+Px1/+Pn+oVyed+2CrzuP1TYCT1lNBtmlb0pl8TCw3r44OXXlWeeBLbczASCx8fMADY6qGvpiJeL6ZuYAYirmnL6Kw3rh9cSHONqwAJ+nJrZj97nRcLxajWCzG9aG7MVtYzY6qO2D8amE27g5dj2KxGMXi9Tj93WzrDPKT1fjXHz6NTzOHob1c3CuV4lE01N7AQIx8PxMvaqs45+9FaXmieet/VCeizt+IwthS9f+C9g+fgXdt6MqzePRl7ee03tfutQTBIzh/L17cjL26uLheqNdM5OLe8lQsTVY/7+HZ5hXWlroZirlvpxp6bz7Wsyuye085Hqg8zGmfVdY36c894YA+2OKA8fomwMnr++WXX8offPBB9jiH8PLly/jwww+zh9taW1uLc+fOZQ+3ddBqa83jqcfZQxHVrUp3B+/EnT8OVg48uRW5f0RcL15t2zA7j1+NW7m78dGdOzFVPb1ayMW3cT2K+fpXWy3kYvWzYlytzizv3J+N2e/qM9Cf/7V+jsP59ddfQ21256RqE0JtvpGTqs2DVltr/vm3ueyhiAP7YKvO4/VNgLehp1ZkOQ47sZPdyzX4UQzGT7HTbmfTQeN3duKnzOmPBgcjtncatlFltkftLMU/Gmaqi3f+HLHd9sMB4B07oA+2OGC8vgnwVrxZkF2p/BL3Tr8gvae1/PL47LmBGH/H98t26/RQdRr4kDqPPx2DHU7v3L/bvD1q57+x03gBMDgVV2uz1vSo6pNLD/P7KHtFp/qF35sUelIK32MHnftgq87j9U2Ak/YGQbYY05PrUdh4w/t9Iipfq9cC8fBcPNsoRFz7Jsn773464kxu5/H7zUpHROzE6n924vPPGjZffXI17vwp4u5s7UEXt6LDnUb0gO3bFypPLt3n/jfS5smoqduO+Uutz404krcx8aNvNtA3AU7a/wAJTX+BdYrxlwAAAABJRU5ErkJggg==" + } +} diff --git a/backup/cadastrar_empresa.json b/backup/cadastrar_empresa.json new file mode 100644 index 0000000..06626ee --- /dev/null +++ b/backup/cadastrar_empresa.json @@ -0,0 +1,86 @@ +{ + "endereco": { + "codigoPais": "1058", + "descricaoPais": "Brasil", + "tipoLogradouro": "Rua", + "logradouro": "T55", + "numero": "882", + "complemento": "Quadra99 Lote 11/14 Sala 1507", + "tipoBairro": "Zona", + "bairro": "Setor Bueno", + "codigoCidade": "5208707", + "descricaoCidade": "Goiania", + "estado": "GO", + "cep": "74215170" + }, + "nfse": { + "config": { + "rps": { + "lote": 12, + "numeracao": [ + { + "numero": 2, + "serie": "CTBZ", + "numeracaoAtual": 2 + } + ] + }, + "email": { + "envio": false + }, + "producao": true, + "envioLimitado": false, + "integracoes": [] + }, + "ativo": true, + "tipoContrato": 0 + }, + "nfe": { + "config": { + "dfe": { + "primeiroNsu": true + }, + "email": { + "envio": false + }, + "integracoes": { + "plugStorage": { + "ativo": false + } + }, + "producao": false, + "impressaoFcp": false, + "impressaoPartilha": true, + "versaoManual": "6.0", + "versaoEsquema": "pl_009", + "numeracao": [] + }, + "ativo": false + }, + "nfce": { + "config": { + "email": { + "envio": false + }, + "integracoes": { + "plugStorage": { + "ativo": false + } + }, + "producao": false, + "versaoManual": "6.0", + "versaoEsquema": "pl_009", + "numeracao": [] + }, + "ativo": false + }, + "incentivoFiscal": false, + "incentivadorCultural": false, + "cpfCnpj": "80972076000118", + "inscricaoMunicipal": "108532836", + "certificado": "668698592698d7831c758d52", + "razaoSocial": "Teste", + "simplesNacional": true, + "regimeTributario": 1, + "regimeTributarioEspecial": 6 +} diff --git a/backup/collectionsForEduardo.json b/backup/collectionsForEduardo.json new file mode 100644 index 0000000..ac9d0f8 --- /dev/null +++ b/backup/collectionsForEduardo.json @@ -0,0 +1,125 @@ +{ + "info": { + "name": "APIs Frontend - Documentação de Payloads", + "description": "Estes são os JSONs puros extremamente completos com todas as tags mapeadas nos modelos Pydantic.", + "schema": "https://schema.getpostman.com/json/collection/v2.1.0/collection.json" + }, + "item": [ + { + "name": "1. Cadastrar NFe", + "request": { + "method": "POST", + "header": [ + { + "key": "Content-Type", + "value": "application/json" + } + ], + "body": { + "mode": "raw", + "raw": "{\n \"nfe\": {\n \"idIntegracao\": \"XXXYY99999_TESTE1\",\n \"presencial\": true,\n \"consumidorFinal\": true,\n \"natureza\": \"OPERAÇÃO INTERNA\",\n \"emitente\": {\n \"cpfCnpj\": \"08187168000160\"\n },\n \"destinatario\": {\n \"cpfCnpj\": \"08114280956\",\n \"razaoSocial\": \"NF-E EMITIDA EM HOMOLOGACAO\",\n \"email\": \"contato@tecnospeed.com.br\",\n \"endereco\": {\n \"logradouro\": \"AVENIDA DUQUE DE CAXIAS\",\n \"numero\": \"882\",\n \"bairro\": \"CENTRO\",\n \"codigoCidade\": \"4115200\",\n \"descricaoCidade\": \"MARINGA\",\n \"estado\": \"PR\",\n \"cep\": \"87020025\"\n }\n },\n \"itens\": [\n {\n \"codigo\": \"1\",\n \"descricao\": \"PRODUTO DE TESTE\",\n \"ncm\": \"06029090\",\n \"cest\": \"0123456\",\n \"cfop\": \"5101\",\n \"valorUnitario\": {\n \"comercial\": 4.67,\n \"tributavel\": 4.67\n },\n \"valor\": 4.67,\n \"tributos\": {\n \"icms\": {\n \"origem\": \"0\",\n \"cst\": \"00\",\n \"baseCalculo\": {\n \"modalidadeDeterminacao\": 0,\n \"valor\": 0\n },\n \"aliquota\": 0,\n \"valor\": 0\n },\n \"pis\": {\n \"cst\": \"99\",\n \"baseCalculo\": {\n \"valor\": 0,\n \"quantidade\": 0\n },\n \"aliquota\": 0,\n \"valor\": 0\n },\n \"cofins\": {\n \"cst\": \"07\",\n \"baseCalculo\": {\n \"valor\": 0\n },\n \"aliquota\": 0,\n \"valor\": 0\n }\n }\n }\n ],\n \"pagamentos\": [\n {\n \"aVista\": true,\n \"meio\": \"01\",\n \"valor\": 4.6\n }\n ],\n \"responsavelTecnico\": {\n \"cpfCnpj\": \"08187168000160\",\n \"nome\": \"Tecnospeed\",\n \"email\": \"contato@tecnospeed.com.br\",\n \"telefone\": {\n \"ddd\": \"44\",\n \"numero\": \"30379500\"\n }\n }\n }\n}" + }, + "url": { + "raw": "https://api.sandbox.plugnotas.com.br/nfe", + "protocol": "https", + "host": [ + "api.sandbox.plugnotas.com.br/nfe" + ] + } + } + }, + { + "name": "2. Cadastrar NFSe", + "request": { + "method": "POST", + "header": [ + { + "key": "Content-Type", + "value": "application/json" + } + ], + "body": { + "mode": "raw", + "raw": "{\n \"idIntegracao\": \"INTEGRACAO-NFSE-001\",\n \"prestador\": {\n \"cpfCnpj\": \"12345678000123\"\n },\n \"tomador\": {\n \"cpfCnpj\": \"98765432000198\",\n \"razaoSocial\": \"João Tomador da Silva\",\n \"inscricaoMunicipal\": \"isento\",\n \"email\": \"tomador@email.com\",\n \"endereco\": {\n \"descricaoCidade\": \"São Paulo\",\n \"cep\": \"00000000\",\n \"tipoLogradouro\": \"Rua\",\n \"logradouro\": \"Dois de Ouro\",\n \"tipoBairro\": \"Bairro\",\n \"codigoCidade\": \"3550308\",\n \"complemento\": \"Apto 13\",\n \"estado\": \"SP\",\n \"numero\": \"44\",\n \"bairro\": \"Centro\"\n }\n },\n \"servico\": {\n \"codigo\": \"1.01\",\n \"codigoTributacao\": \"1.01\",\n \"discriminacao\": \"Serviços prestados de tecnologia\",\n \"cnae\": \"6204000\",\n \"iss\": {\n \"tipoTributacao\": 6,\n \"exigibilidade\": 1,\n \"aliquota\": 2.5\n },\n \"valor\": {\n \"servico\": 1500.50,\n \"descontoCondicionado\": 0.0,\n \"descontoIncondicionado\": 0.0\n }\n }\n}" + }, + "url": { + "raw": "https://api.sandbox.plugnotas.com.br/nfse", + "protocol": "https", + "host": [ + "api.sandbox.plugnotas.com.br/nfse" + ] + } + } + }, + { + "name": "3. Cadastrar Empresa", + "request": { + "method": "POST", + "header": [ + { + "key": "Content-Type", + "value": "application/json" + } + ], + "body": { + "mode": "raw", + "raw": "{\n \"endereco\": {\n \"codigoPais\": \"1058\",\n \"descricaoPais\": \"BRASIL\",\n \"tipoLogradouro\": \"Rua\",\n \"logradouro\": \"Teste\",\n \"numero\": \"123\",\n \"complemento\": \"Terreo\",\n \"tipoBairro\": \"Bairro\",\n \"bairro\": \"Centro\",\n \"codigoCidade\": \"4106902\",\n \"descricaoCidade\": \"CURITIBA\",\n \"estado\": \"PR\",\n \"cep\": \"80000000\"\n },\n \"nfse\": {\n \"config\": {\n \"rps\": {\n \"lote\": 1,\n \"numeracao\": [\n 1\n ]\n },\n \"email\": {\n \"envio\": true\n },\n \"producao\": false,\n \"envioLimitado\": false,\n \"integracoes\": []\n },\n \"ativo\": true,\n \"tipoContrato\": 0\n },\n \"nfe\": {\n \"config\": {\n \"dfe\": {\n \"primeiroNsu\": false\n },\n \"email\": {\n \"envio\": false\n },\n \"integracoes\": {},\n \"producao\": false,\n \"impressaoFcp\": false,\n \"impressaoPartilha\": false,\n \"versaoManual\": \"6.0\",\n \"versaoEsquema\": \"PL_009_V4\",\n \"numeracao\": [\n 1\n ]\n },\n \"ativo\": false\n },\n \"nfce\": {\n \"config\": {\n \"email\": {\n \"envio\": false\n },\n \"integracoes\": {},\n \"producao\": false,\n \"versaoManual\": \"6.0\",\n \"versaoEsquema\": \"PL_009_V4\",\n \"numeracao\": [\n 1\n ]\n },\n \"ativo\": false\n },\n \"incentivoFiscal\": false,\n \"incentivadorCultural\": false,\n \"cpfCnpj\": \"12345678000123\",\n \"inscricaoMunicipal\": \"12345\",\n \"certificado\": \"60b8d5a1-UUID-DO-CERT\",\n \"razaoSocial\": \"Empresa Teste Completa SA\",\n \"simplesNacional\": true,\n \"regimeTributario\": 1,\n \"regimeTributarioEspecial\": 0\n}" + }, + "url": { + "raw": "https://api.sandbox.plugnotas.com.br/empresa", + "protocol": "https", + "host": [ + "api.sandbox.plugnotas.com.br/empresa" + ] + } + } + }, + { + "name": "4. Cadastrar Certificado Digital", + "request": { + "method": "POST", + "header": [ + { + "key": "Content-Type", + "value": "application/json" + } + ], + "body": { + "mode": "raw", + "raw": "{\n \"arquivo\": \"MIIJ/AIBAzCCC... (BASE64 STRING)\",\n \"senha\": \"minhasenha123\",\n \"id_empresa\": \"Id da Empresa (Ex: 105)\"\n}" + }, + "url": { + "raw": "https://api.sandbox.plugnotas.com.br/certificado", + "protocol": "https", + "host": [ + "api.sandbox.plugnotas.com.br/certificado" + ] + }, + "description": "Payload enxuto como refatorado." + } + }, + { + "name": "5. Cadastrar Serviço de NFSe", + "request": { + "method": "POST", + "header": [ + { + "key": "Content-Type", + "value": "application/json" + } + ], + "body": { + "mode": "raw", + "raw": "{\n \"codigo\": \"1.01\",\n \"idIntegracao\": \"SRV-101\",\n \"discriminacao\": \"An\\u00e1lise e desenvolvimento de sistemas\",\n \"codigoTributacao\": \"1.01\",\n \"cnae\": \"6204000\",\n \"codigoCidadeIncidencia\": \"3550308\",\n \"descricaoCidadeIncidencia\": \"S\\u00c3O PAULO\",\n \"unidade\": \"UN\",\n \"quantidade\": 1,\n \"iss\": {\n \"tipoTributacao\": 6,\n \"exigibilidade\": 1,\n \"retido\": false,\n \"aliquota\": 2.5,\n \"valor\": 37.5,\n \"valorRetido\": 0.0,\n \"processoSuspensao\": \"\"\n },\n \"obra\": {\n \"art\": \"\",\n \"codigo\": \"\"\n },\n \"valor\": {\n \"servico\": 1500.0,\n \"baseCalculo\": 1500.0,\n \"deducoes\": 0.0,\n \"descontoCondicionado\": 0.0,\n \"descontoIncondicionado\": 0.0,\n \"liquido\": 1500.0,\n \"unitario\": 1500.0,\n \"valorAproximadoTributos\": 0.0\n },\n \"deducao\": {\n \"tipo\": 0,\n \"descricao\": \"\"\n },\n \"retencao\": {\n \"pis\": {\n \"aliquota\": 0,\n \"valor\": 0\n },\n \"cofins\": {\n \"aliquota\": 0,\n \"valor\": 0\n },\n \"csll\": {\n \"aliquota\": 0,\n \"valor\": 0\n },\n \"inss\": {\n \"aliquota\": 0,\n \"valor\": 0\n },\n \"irrf\": {\n \"aliquota\": 0,\n \"valor\": 0\n },\n \"outrasRetencoes\": 0,\n \"cpp\": {\n \"aliquota\": 0,\n \"valor\": 0\n }\n },\n \"tributavel\": true,\n \"ibpt\": {\n \"simplificado\": {\n \"aliquota\": 0\n },\n \"detalhado\": {\n \"aliquota\": {\n \"municipal\": 0.0,\n \"estadual\": 0.0,\n \"federal\": 0.0\n }\n }\n },\n \"responsavelRetencao\": \"1\"\n}" + }, + "url": { + "raw": "https://api.sandbox.plugnotas.com.br/nfse/servico", + "protocol": "https", + "host": [ + "api.sandbox.plugnotas.com.br/nfse/servico" + ] + } + } + } + ] +} \ No newline at end of file diff --git a/backup/json_insercao_empresa.json b/backup/json_insercao_empresa.json new file mode 100644 index 0000000..076da61 --- /dev/null +++ b/backup/json_insercao_empresa.json @@ -0,0 +1,86 @@ +{ + "endereco": { + "codigoPais": "1058", + "descricaoPais": "Brasil", + "tipoLogradouro": "Rua", + "logradouro": "T55", + "numero": "882", + "complemento": "Quadra99 Lote 11/14 Sala 1507", + "tipoBairro": "Zona", + "bairro": "Setor Bueno", + "codigoCidade": "5208707", + "descricaoCidade": "Goiania", + "estado": "GO", + "cep": "74215170" + }, + "nfse": { + "config": { + "rps": { + "lote": 12, + "numeracao": [ + { + "numero": 2, + "serie": "CTBZ", + "numeracaoAtual": 2 + } + ] + }, + "email": { + "envio": false + }, + "producao": true, + "envioLimitado": false, + "integracoes": [] + }, + "ativo": true, + "tipoContrato": 0 + }, + "nfe": { + "config": { + "dfe": { + "primeiroNsu": true + }, + "email": { + "envio": false + }, + "integracoes": { + "plugStorage": { + "ativo": false + } + }, + "producao": false, + "impressaoFcp": false, + "impressaoPartilha": true, + "versaoManual": "6.0", + "versaoEsquema": "pl_009", + "numeracao": [] + }, + "ativo": false + }, + "nfce": { + "config": { + "email": { + "envio": false + }, + "integracoes": { + "plugStorage": { + "ativo": false + } + }, + "producao": false, + "versaoManual": "6.0", + "versaoEsquema": "pl_009", + "numeracao": [] + }, + "ativo": false + }, + "incentivoFiscal": false, + "incentivadorCultural": false, + "cpfCnpj": "45732212000100", + "inscricaoMunicipal": "108532836", + "certificado": "668698592698d7831c758d52", + "razaoSocial": "Teste", + "simplesNacional": true, + "regimeTributario": 1, + "regimeTributarioEspecial": 6 +} diff --git a/backup/modelo_json_rpc.json b/backup/modelo_json_rpc.json new file mode 100644 index 0000000..546acf9 --- /dev/null +++ b/backup/modelo_json_rpc.json @@ -0,0 +1,29 @@ +{ + "system": "integracao", + "target": "rpcclt", + "service": "tecnospeed", + "serviceId": "01234567890123456789012345678901", + "payload": { + "fila": "nfse", + "cliente": "Verta", + "metodo": "POST", + "dados": null, + "config":{ + "api_key": "2da392a6-79d2-4304-a8b7-959572c7e44d", + "user_postgres": "postgres", + "pass_postgres": "postgres", + "host_postgres": "localhost", + "port_postgres": "5432", + "database_postgres": "tecnospeed" + } + }, + "hmac": "38051e9be76496a05a237fdabc19898ef7ab3f469c5873b96a4fdbb5be30b10c", + "type": "rpc", + "correlationId": "884ea465-e2b6-4eab-bfdf-a08c9fb493d3", + "version": "1.0", + "origin": "kuririn", + "client": "verta", + "caller": "callRPC", + "logDate": "2025-02-24T16:41:40.882Z", + "msg": {"cliente":"Verta","servico":"webhooks"} +} \ No newline at end of file diff --git a/backup/modelo_system_config.json b/backup/modelo_system_config.json new file mode 100644 index 0000000..5b3bee6 --- /dev/null +++ b/backup/modelo_system_config.json @@ -0,0 +1,6 @@ +{ + "serviceid":"01234567890123456789012345678901", + "servicekey":"ABCDEFGHIJKLMNOPQRSTUVWXYZ123456", + "origin":"kuririn", + "amqps":"amqps://qyqordbr:5tDIJIX6z5k3qaMmpJGC4HIieqq4n29z@gull.rmq.cloudamqp.com/qyqordbr" +} \ No newline at end of file diff --git a/backup/servicoPlugnotasCollections.json b/backup/servicoPlugnotasCollections.json new file mode 100644 index 0000000..d1595f4 --- /dev/null +++ b/backup/servicoPlugnotasCollections.json @@ -0,0 +1,129 @@ +{ + "info": { + "_postman_id": "55e2d8ab-7890-4813-89bd-a287c7e44d1a", + "name": "Serviço PlugNotas — Messaging (RPC & Queue)", + "description": "Collection de mensageria para o Serviço PlugNotas.", + "schema": "https://schema.getpostman.com/json/collection/v2.1.0/collection.json" + }, + "item": [ + { + "name": "NFe", + "description": "Operações de Nota Fiscal Eletrônica (NFe) — emissão, consulta, download, cancelamento e resumo.", + "item": [ + { + "name": "POST - Emitir NFe", + "request": { + "method": "POST", + "header": [], + "body": { + "mode": "raw", + "raw": "{\n \"fila\": \"nfe\",\n \"metodo\": \"post\",\n \"cliente\": \"{{cliente}}\",\n \"url_base\": \"https://api.sandbox.plugnotas.com.br\",\n \"config\": {\n \"api_key\": \"{{api_key}}\",\n \"user_postgres\": \"{{user_postgres}}\",\n \"pass_postgres\": \"{{pass_postgres}}\",\n \"host_postgres\": \"{{host_postgres}}\",\n \"port_postgres\": \"{{port_postgres}}\",\n \"database_postgres\": \"{{database_postgres}}\"\n },\n \"dados\": {\n \"idIntegracao\": \"NFe_TESTE_001\",\n \"presencial\": true,\n \"consumidorFinal\": true,\n \"natureza\": \"OPERAÇÃO INTERNA\",\n \"emitente\": {\n \"cpfCnpj\": \"00000000000000\",\n \"inscricaoEstadual\": \"ISENTO\"\n },\n \"destinatario\": {\n \"cpfCnpj\": \"08114280956\",\n \"razaoSocial\": \"NF-E EMITIDA EM HOMOLOGACAO\",\n \"endereco\": {\n \"logradouro\": \"AVENIDA DUQUE DE CAXIAS\",\n \"numero\": \"882\",\n \"bairro\": \"CENTRO\",\n \"codigoCidade\": \"4115200\",\n \"descricaoCidade\": \"MARINGA\",\n \"estado\": \"PR\",\n \"cep\": \"87020025\"\n }\n },\n \"itens\": [\n {\n \"codigo\": \"1\",\n \"descricao\": \"PRODUTO DE TESTE\",\n \"ncm\": \"06029090\",\n \"cfop\": \"5101\",\n \"valorUnitario\": {\n \"comercial\": 4.67,\n \"tributavel\": 4.67\n },\n \"valor\": 4.67,\n \"tributos\": {\n \"icms\": {\n \"origem\": \"0\",\n \"cst\": \"00\",\n \"baseCalculo\": {\n \"modalidadeDeterminacao\": 0,\n \"valor\": 0\n },\n \"aliquota\": 0,\n \"valor\": 0\n }\n }\n }\n ],\n \"pagamentos\": [\n {\n \"aVista\": true,\n \"meio\": \"01\",\n \"valor\": 4.67\n }\n ]\n }\n}", + "options": { + "raw": { + "language": "json" + } + } + }, + "url": {}, + "description": "Emite uma NFe de acordo com as exigências da PlugNotas.\n\n**Campos obrigatórios em `dados`:** idIntegracao, presencial, consumidorFinal, natureza, emitente (cpfCnpj), destinatario (cpfCnpj, razaoSocial, endereco), itens, pagamentos." + }, + "response": [] + }, + { + "name": "GET - Consultar NFe", + "request": { + "method": "POST", + "header": [], + "body": { + "mode": "raw", + "raw": "{\n \"fila\": \"nfe\",\n \"metodo\": \"get\",\n \"cliente\": \"{{cliente}}\",\n \"config\": {\n \"api_key\": \"{{api_key}}\",\n \"user_postgres\": \"{{user_postgres}}\",\n \"pass_postgres\": \"{{pass_postgres}}\",\n \"host_postgres\": \"{{host_postgres}}\",\n \"port_postgres\": \"{{port_postgres}}\",\n \"database_postgres\": \"{{database_postgres}}\"\n },\n \"dados\": {\n \"id\": \"ID_DA_NOTA\"\n }\n}", + "options": { + "raw": { + "language": "json" + } + } + }, + "url": {}, + "description": "Consulta os dados de uma NFe pelo ID." + }, + "response": [] + } + ] + }, + { + "name": "NFSe", + "description": "Operações de NFSe — emissão, consulta, download, etc.", + "item": [ + { + "name": "POST - Emitir NFSe", + "request": { + "method": "POST", + "header": [], + "body": { + "mode": "raw", + "raw": "{\n \"fila\": \"nfse\",\n \"metodo\": \"post\",\n \"cliente\": \"{{cliente}}\",\n \"config\": {\n \"api_key\": \"{{api_key}}\",\n \"user_postgres\": \"{{user_postgres}}\",\n \"pass_postgres\": \"{{pass_postgres}}\",\n \"host_postgres\": \"{{host_postgres}}\",\n \"port_postgres\": \"{{port_postgres}}\",\n \"database_postgres\": \"{{database_postgres}}\"\n },\n \"dados\": {\n \"idIntegracao\": \"NFSe_001\",\n \"prestador\": {\n \"cpfCnpj\": \"00000000000000\"\n },\n \"tomador\": {\n \"cpfCnpj\": \"11111111111\",\n \"razaoSocial\": \"Razao Social do Tomador\",\n \"inscricaoMunicipal\": \"ISENTO\",\n \"email\": \"tomador@exemplo.com\",\n \"endereco\": {\n \"descricaoCidade\": \"Sao Paulo\",\n \"cep\": \"00000000\",\n \"tipoLogradouro\": \"Rua\",\n \"logradouro\": \"Rua de Teste\",\n \"tipoBairro\": \"Bairro\",\n \"codigoCidade\": \"3550308\",\n \"complemento\": \"\",\n \"estado\": \"SP\",\n \"numero\": \"123\",\n \"bairro\": \"Centro\"\n }\n },\n \"servico\": {\n \"codigo\": \"1.01\",\n \"codigoTributacao\": \"1.01.01\",\n \"discriminacao\": \"Descricao do servico\",\n \"cnae\": \"6201100\",\n \"iss\": {\n \"tipoTributacao\": 6,\n \"exigibilidade\": 1,\n \"aliquota\": 2.0\n },\n \"valor\": {\n \"servico\": 150.0,\n \"descontoCondicionado\": 0.0,\n \"descontoIncondicionado\": 0.0\n }\n }\n }\n}", + "options": { + "raw": { + "language": "json" + } + } + }, + "url": {}, + "description": "Emite uma NFSe." + }, + "response": [] + } + ] + }, + { + "name": "Empresa", + "description": "Cadastro e gerenciamento de empresas.", + "item": [ + { + "name": "POST - Cadastro Completo", + "request": { + "method": "POST", + "header": [], + "body": { + "mode": "raw", + "raw": "{\n \"fila\": \"empresa\",\n \"metodo\": \"post\",\n \"cliente\": \"{{cliente}}\",\n \"config\": {\n \"api_key\": \"{{api_key}}\",\n \"user_postgres\": \"{{user_postgres}}\",\n \"pass_postgres\": \"{{pass_postgres}}\",\n \"host_postgres\": \"{{host_postgres}}\",\n \"port_postgres\": \"{{port_postgres}}\",\n \"database_postgres\": \"{{database_postgres}}\"\n },\n \"dados\": {\n \"cpfCnpj\": \"00000000000000\",\n \"razaoSocial\": \"Empresa Teste S/A\",\n \"inscricaoMunicipal\": \"1234\",\n \"inscricaoEstadual\": \"ISENTO\",\n \"simplesNacional\": true,\n \"regimeTributario\": 1,\n \"regimeTributarioEspecial\": 0,\n \"incentivoFiscal\": false,\n \"incentivadorCultural\": false,\n \"certificado\": \"A1\",\n \"endereco\": {\n \"codigoPais\": \"1058\",\n \"descricaoPais\": \"Brasil\",\n \"tipoLogradouro\": \"Rua\",\n \"logradouro\": \"Rua das Industrias\",\n \"numero\": \"1050\",\n \"complemento\": \"Hangar 2\",\n \"tipoBairro\": \"Bairro\",\n \"bairro\": \"Parque Industrial\",\n \"codigoCidade\": \"3550308\",\n \"descricaoCidade\": \"Sao Paulo\",\n \"estado\": \"SP\",\n \"cep\": \"00000000\"\n },\n \"nfse\": {\n \"ativo\": true,\n \"tipoContrato\": 1,\n \"config\": {\n \"rps\": { \"lote\": 1, \"numeracao\": [] },\n \"email\": { \"envio\": true },\n \"producao\": false,\n \"envioLimitado\": false,\n \"integracoes\": [],\n \"prefeitura\": null\n }\n },\n \"nfe\": {\n \"ativo\": true,\n \"config\": {\n \"dfe\": { \"primeiroNsu\": true },\n \"email\": { \"envio\": true },\n \"integracoes\": {},\n \"producao\": false,\n \"impressaoFcp\": false,\n \"impressaoPartilha\": false,\n \"versaoManual\": \"4.0\",\n \"versaoEsquema\": \"4.00\",\n \"numeracao\": []\n }\n }\n }\n}", + "options": { + "raw": { + "language": "json" + } + } + }, + "url": {}, + "description": "Cadastro de empresa com NFe e NFSe." + }, + "response": [] + } + ] + }, + { + "name": "Tomador", + "description": "Cadastro de tomadores/clientes.", + "item": [ + { + "name": "POST - Criar Tomador", + "request": { + "method": "POST", + "header": [], + "body": { + "mode": "raw", + "raw": "{\n \"fila\": \"tomador\",\n \"metodo\": \"post\",\n \"cliente\": \"{{cliente}}\",\n \"config\": {\n \"api_key\": \"{{api_key}}\",\n \"user_postgres\": \"{{user_postgres}}\",\n \"pass_postgres\": \"{{pass_postgres}}\",\n \"host_postgres\": \"{{host_postgres}}\",\n \"port_postgres\": \"{{port_postgres}}\",\n \"database_postgres\": \"{{database_postgres}}\"\n },\n \"dados\": {\n \"cpfCnpj\": \"12345678901\",\n \"inscricaoMunicipal\": \"ISENTO\",\n \"inscricaoEstadual\": \"ISENTO\",\n \"inscricaoSuframa\": \"\",\n \"indicadorInscricaoEstadual\": 9,\n \"razaoSocial\": \"Cliete Teste\",\n \"nomeFantasia\": \"Empresa Teste\",\n \"email\": \"tomador@exemplo.com\",\n \"orgaoPublico\": false,\n \"endereco\": {\n \"tipoLogradouro\": \"Rua\",\n \"logradouro\": \"Rua de Teste\",\n \"numero\": \"100\",\n \"complemento\": \"\",\n \"tipoBairro\": \"Bairro\",\n \"bairro\": \"Centro\",\n \"codigoPais\": \"1058\",\n \"descricaoPais\": \"Brasil\",\n \"codigoCidade\": \"3550308\",\n \"descricaoCidade\": \"Sao Paulo\",\n \"estado\": \"SP\",\n \"cep\": \"00000000\"\n },\n \"telefone\": {\n \"ddd\": \"11\",\n \"numero\": \"999999999\"\n }\n }\n}", + "options": { + "raw": { + "language": "json" + } + } + }, + "url": {}, + "description": "Cadastro de tomador." + }, + "response": [] + } + ] + } + ] +} diff --git a/backup/src/Models/config.py b/backup/src/Models/config.py new file mode 100644 index 0000000..76500ee --- /dev/null +++ b/backup/src/Models/config.py @@ -0,0 +1,18 @@ +from pydantic import BaseModel + +# Dados de conexão ao banco de dados isolado do cliente e da API PlugNotas +class Config(BaseModel): + api_key: str | None = None + x_api_key: str | None = None + user_postgres: str + pass_postgres: str + host_postgres: str + port_postgres: str + database_postgres: str + +# Informações de mensageria para a troca de arquivos (AMQP) via subagent +class SystemConfig(BaseModel): + serviceid: str + servicekey: str + origin: str + amqps: str \ No newline at end of file diff --git a/backup/src/Models/empresa.py b/backup/src/Models/empresa.py new file mode 100644 index 0000000..5008bd0 --- /dev/null +++ b/backup/src/Models/empresa.py @@ -0,0 +1,96 @@ +from pydantic import BaseModel + + +################### +## POST /empresas # +################### +class Endereco(BaseModel): + codigoPais: str + descricaoPais: str + tipoLogradouro: str + logradouro: str + numero: str + complemento: str + tipoBairro: str + bairro: str + codigoCidade: str + descricaoCidade: str + estado: str + cep: str + +class Rps(BaseModel): + lote: int + numeracao: list + +class Email(BaseModel): + envio: bool + +class Config(BaseModel): + rps: Rps | None = None + email: Email | None = None + producao: bool | None = None + envioLimitado: bool | None = None + integracoes: list | None = None + prefeitura: dict | str | int | None = None + +class Nfse(BaseModel): + config: Config | None = None + ativo: bool = False + tipoContrato: int | None = 0 + +class Dfe(BaseModel): + primeiroNsu: bool + +class ConfigNfe(BaseModel): + dfe: Dfe | None = None + email: Email | None = None + integracoes: dict | None = None + producao: bool | None = None + impressaoFcp: bool | None = None + impressaoPartilha: bool | None = None + versaoManual: str | None = None + versaoEsquema: str | None = None + numeracao: list | None = None + +class Nfe(BaseModel): + config: ConfigNfe | None = None + ativo: bool = False + tipoContrato: int | None = 0 + +class ConfigNfce(BaseModel): + email: Email | None = None + integracoes: dict | None = None + producao: bool | None = None + versaoManual: str | None = None + versaoEsquema: str | None = None + numeracao: list | None = None + +class Nfce(BaseModel): + config: ConfigNfce | None = None + ativo: bool = False + tipoContrato: int | None = 0 + + +# Entidade Empresa que cadastra a credencial emissora no sistema da PlugNotas +class Empresa(BaseModel): + endereco: Endereco + nfse: Nfse | None = None + nfe: Nfe | None = None + nfce: Nfce | None = None + incentivoFiscal: bool + incentivadorCultural: bool + cpfCnpj: str + inscricaoMunicipal: str + inscricaoEstadual: str | None = None + certificado: str + razaoSocial: str + nomeFantasia: str | None = None + simplesNacional: bool + regimeTributario: int + regimeTributarioEspecial: int + id: str | None = None + banco: str | None = None + email: str | None = None + id_empresa: int | None = None + + diff --git a/backup/src/Models/nfe.py b/backup/src/Models/nfe.py new file mode 100644 index 0000000..6850c18 --- /dev/null +++ b/backup/src/Models/nfe.py @@ -0,0 +1,99 @@ +from pydantic import BaseModel +from typing import List, Optional + +# Grupo de dados correspondente ao prestador/emitente da nota +class Emitente(BaseModel): + cpfCnpj: str + inscricaoEstadual: str + +class Endereco(BaseModel): + logradouro: str + numero: str + bairro: str + codigoCidade: str + descricaoCidade: str + estado: str + cep: str + +class Destinatario(BaseModel): + cpfCnpj: str + razaoSocial: str + email: Optional[str] = None + endereco: Endereco + +class ValorTributavel(BaseModel): + comercial: float + tributavel: float + +class BaseCalculoIcms(BaseModel): + modalidadeDeterminacao: int + valor: float + +class Icms(BaseModel): + origem: str + cst: str + baseCalculo: BaseCalculoIcms + aliquota: float + valor: float + +class BaseCalculoPis(BaseModel): + valor: float + quantidade: int + +class Pis(BaseModel): + cst: str + baseCalculo: BaseCalculoPis + aliquota: float + valor: float + +class BaseCalculoCofins(BaseModel): + valor: float + +class Cofins(BaseModel): + cst: str + baseCalculo: BaseCalculoCofins + aliquota: float + valor: float + +class Tributos(BaseModel): + icms: Optional[Icms] = None + pis: Optional[Pis] = None + cofins: Optional[Cofins] = None + +# Representa o produto ou serviço vendido, incluindo valores, quantidades e tributação +class ItemNFe(BaseModel): + codigo: str + descricao: str + ncm: str + cest: Optional[str] = None + cfop: str + valorUnitario: ValorTributavel + valor: float + tributos: Tributos + +class Pagamento(BaseModel): + aVista: bool + meio: str + valor: float + +class Telefone(BaseModel): + ddd: str + numero: str + +class ResponsavelTecnico(BaseModel): + cpfCnpj: str + nome: str + email: str + telefone: Telefone + +# Root do JSON para envio da NFe; agrupa recebedor, compras, responsável, e pagamentos +class NFe(BaseModel): + idIntegracao: str + presencial: bool + consumidorFinal: bool + natureza: str + emitente: Emitente + destinatario: Destinatario + itens: List[ItemNFe] + pagamentos: List[Pagamento] + responsavelTecnico: Optional[ResponsavelTecnico] = None diff --git a/backup/src/Models/nfse.py b/backup/src/Models/nfse.py new file mode 100644 index 0000000..857ad97 --- /dev/null +++ b/backup/src/Models/nfse.py @@ -0,0 +1,49 @@ +from pydantic import BaseModel +from typing import Optional + +class Prestador(BaseModel): + cpfCnpj: str + +class Endereco(BaseModel): + descricaoCidade: str + cep: str + tipoLogradouro: str + logradouro: str + tipoBairro: Optional[str] = None + codigoCidade: str + complemento: Optional[str] = None + estado: str + numero: str + bairro: str + +class Tomador(BaseModel): + cpfCnpj: str + razaoSocial: str + inscricaoMunicipal: Optional[str] = None + email: str + endereco: dict + +class ISS(BaseModel): + tipoTributacao: int + exigibilidade: int + aliquota: float + +class Valor(BaseModel): + servico: float + descontoCondicionado: Optional[float] = 0 + descontoIncondicionado: Optional[float] = 0 + +class Servico(BaseModel): + codigo: str + codigoTributacao: str + discriminacao: str + cnae: str + iss: ISS + valor: Valor + +# Estrutura padrão de envio de uma NFSe contendo Prestador, Tomador e Serviço prestado +class NFSe(BaseModel): + idIntegracao: str + prestador: Prestador + tomador: Tomador + servico: Servico diff --git a/backup/src/Models/payload.py b/backup/src/Models/payload.py new file mode 100644 index 0000000..841551b --- /dev/null +++ b/backup/src/Models/payload.py @@ -0,0 +1,518 @@ +from enum import Enum +from pydantic import BaseModel, ConfigDict, Field + +from Models.config import Config +from Models.empresa import Empresa +from Models.nfe import NFe +from Models.nfse import NFSe +from Models.servico import Servico +from Models.tomador import Tomador + + +class Fila(str, Enum): + NFE = "nfe" + NFSE = "nfse" + CERTIFICADO_DIGITAL = "certificado_digital" + EMPRESA = "empresa" + SERVICO = "servico" + TOMADOR = "tomador" + RELATORIOS = "relatorios" + WEBHOOKS = "webhooks" + + +class NFeMetodo(str, Enum): + POST = "post" + GET = "get" + DOWNLOAD = "download" + DOWNLOAD_XML = "download_xml" + CANCELAR = "cancelar" + STATUS_CANCELAMENTO = "status_cancelamento" + CCE = "cce" + STATUS_CCE = "status_cce" + RESUMO = "resumo" + + +class NFSeMetodo(str, Enum): + POST = "post" + GET = "get" + DOWNLOAD = "download" + DOWNLOAD_XML = "download_xml" + CANCELAR = "cancelar" + STATUS_CANCELAMENTO = "status_cancelamento" + RESUMO = "resumo" + HISTORICO = "historico" + + +class CertificadoMetodo(str, Enum): + POST = "post" + GET = "get" + PUT = "put" + + +class EmpresaMetodo(str, Enum): + POST = "post" + GET = "get" + POST_FILE = "post_file" + PATCH = "patch" + + +class ServicoMetodo(str, Enum): + POST = "post" + GET = "get" + PUT = "put" + DELETE = "delete" + + +class TomadorMetodo(str, Enum): + POST = "post" + GET = "get" + PUT = "put" + DELETE = "delete" + + +class RelatorioMetodo(str, Enum): + NFE_EMITIDAS = "nfe_emitidas" + NFE_POR_CNPJ = "nfe_por_cnpj" + NFSE_EMITIDAS = "nfse_emitidas" + NFSE_POR_CNPJ = "nfse_por_cnpj" + + +class WebhookMetodo(str, Enum): + GET = "get" + POST = "post" + DELETE = "delete" + PUT = "put" + TEST = "test" + + +class BaseEnvelope(BaseModel): + cliente: str | None = None + config: Config + url_base: str | None = None + + +# --- Base Data Models --- + +class BaseDados(BaseModel): + model_config = ConfigDict(populate_by_name=True) + id_usuario: int | None = 1 + + +class NFePostDadosWrapped(BaseDados): + nfe: NFe + id_empresa: str | None = None + + +class NFePostDadosDirect(NFe): + id_usuario: int | None = 1 + id_empresa: str | None = None + + +class NFeIdDados(BaseDados): + id: str + + +class NFeCancelarDados(NFeIdDados): + justificativa: str | None = None + + +class NFeCCEDados(NFeIdDados): + correcao: str + + +class NFeResumoDados(BaseDados): + cnpj: str + id_integracao: str | None = None + id: str | None = None + + +class NFSePostDados(NFSe): + id_usuario: int | None = 1 + id_empresa: str | None = None + + +class NFSeIdDados(BaseDados): + id: str + + +class NFSeGetDados(BaseDados): + id: str | None = None + + +class NFSeResumoDados(NFSeIdDados): + cnpj: str + + +class NFSeHistoricoDados(NFSeIdDados): + cnpj: str | None = None + + +class CertificadoPostDados(BaseDados): + arquivo: str + senha: str + id_empresa: str | None = None + email: str | None = None + expiration_at: str | None = Field(None, alias="expirationAt") + data_expiracao: str | None = None + + +class CertificadoGetDados(BaseDados): + id: str | None = None + + +class CertificadoPutDados(CertificadoPostDados): + id: str + + +class EmpresaPostDados(Empresa): + id_usuario: int | None = 1 + + +class EmpresaGetDados(BaseDados): + cnpj: str | None = None + + +class EmpresaPostFileDados(BaseDados): + file: str + cnpj: str + + +class EmpresaPatchDados(BaseDados): + cnpj: str + certificado: str + + +class ServicoPostDados(Servico): + id_usuario: int | None = 1 + + +class ServicoGetDados(BaseDados): + id: str | None = None + + +class ServicoPutDados(Servico): + id: str + id_usuario: int | None = 1 + + +class ServicoDeleteDados(BaseDados): + id: str + + +class TomadorPostDados(Tomador): + id_usuario: int | None = 1 + + +class TomadorGetDados(BaseDados): + cpf_cnpj: str | None = None + + +class TomadorPutDados(Tomador): + cpf_cnpj: str + id_usuario: int | None = 1 + + +class TomadorDeleteDados(BaseDados): + cpf_cnpj: str + + +class RelatorioVazioDados(BaseDados): + pass + + +class RelatorioCnpjDados(BaseDados): + cnpj: str + + +class RelatorioPeriodoDados(BaseDados): + data_inicial: str | None = None + data_final: str | None = None + + +class RelatorioCnpjPeriodoDados(RelatorioPeriodoDados): + cnpj: str + + +class WebhookGetDeleteTestDados(BaseDados): + cnpj: str + + +class WebhookPostPutDados(WebhookGetDeleteTestDados): + url: str + + +# --- Envelopes --- + +class NFePostEnvelope(BaseEnvelope): + fila: Fila = Fila.NFE + metodo: NFeMetodo = NFeMetodo.POST + dados: NFePostDadosDirect | NFePostDadosWrapped + + +class NFeIdEnvelope(BaseEnvelope): + fila: Fila = Fila.NFE + metodo: NFeMetodo + dados: NFeIdDados + + +class NFeCancelarEnvelope(BaseEnvelope): + fila: Fila = Fila.NFE + metodo: NFeMetodo = NFeMetodo.CANCELAR + dados: NFeCancelarDados + + +class NFeCCEEnvelope(BaseEnvelope): + fila: Fila = Fila.NFE + metodo: NFeMetodo = NFeMetodo.CCE + dados: NFeCCEDados + + +class NFeResumoEnvelope(BaseEnvelope): + fila: Fila = Fila.NFE + metodo: NFeMetodo = NFeMetodo.RESUMO + dados: NFeResumoDados + + +class NFSePostEnvelope(BaseEnvelope): + fila: Fila = Fila.NFSE + metodo: NFSeMetodo = NFSeMetodo.POST + dados: NFSePostDados + + +class NFSeGetEnvelope(BaseEnvelope): + fila: Fila = Fila.NFSE + metodo: NFSeMetodo = NFSeMetodo.GET + dados: NFSeGetDados + + +class NFSeIdEnvelope(BaseEnvelope): + fila: Fila = Fila.NFSE + metodo: NFSeMetodo + dados: NFSeIdDados + + +class NFSeResumoEnvelope(BaseEnvelope): + fila: Fila = Fila.NFSE + metodo: NFSeMetodo = NFSeMetodo.RESUMO + dados: NFSeResumoDados + + +class NFSeHistoricoEnvelope(BaseEnvelope): + fila: Fila = Fila.NFSE + metodo: NFSeMetodo = NFSeMetodo.HISTORICO + dados: NFSeHistoricoDados + + +class CertificadoPostEnvelope(BaseEnvelope): + fila: Fila = Fila.CERTIFICADO_DIGITAL + metodo: CertificadoMetodo = CertificadoMetodo.POST + dados: CertificadoPostDados + + +class CertificadoGetEnvelope(BaseEnvelope): + fila: Fila = Fila.CERTIFICADO_DIGITAL + metodo: CertificadoMetodo = CertificadoMetodo.GET + dados: CertificadoGetDados + + +class CertificadoPutEnvelope(BaseEnvelope): + fila: Fila = Fila.CERTIFICADO_DIGITAL + metodo: CertificadoMetodo = CertificadoMetodo.PUT + dados: CertificadoPutDados + + +class EmpresaPostEnvelope(BaseEnvelope): + fila: Fila = Fila.EMPRESA + metodo: EmpresaMetodo = EmpresaMetodo.POST + dados: EmpresaPostDados + + +class EmpresaGetEnvelope(BaseEnvelope): + fila: Fila = Fila.EMPRESA + metodo: EmpresaMetodo = EmpresaMetodo.GET + dados: EmpresaGetDados + + +class EmpresaPostFileEnvelope(BaseEnvelope): + fila: Fila = Fila.EMPRESA + metodo: EmpresaMetodo = EmpresaMetodo.POST_FILE + dados: EmpresaPostFileDados + + +class EmpresaPatchEnvelope(BaseEnvelope): + fila: Fila = Fila.EMPRESA + metodo: EmpresaMetodo = EmpresaMetodo.PATCH + dados: EmpresaPatchDados + + +class ServicoPostEnvelope(BaseEnvelope): + fila: Fila = Fila.SERVICO + metodo: ServicoMetodo = ServicoMetodo.POST + dados: ServicoPostDados + + +class ServicoGetEnvelope(BaseEnvelope): + fila: Fila = Fila.SERVICO + metodo: ServicoMetodo = ServicoMetodo.GET + dados: ServicoGetDados + + +class ServicoPutEnvelope(BaseEnvelope): + fila: Fila = Fila.SERVICO + metodo: ServicoMetodo = ServicoMetodo.PUT + dados: ServicoPutDados + + +class ServicoDeleteEnvelope(BaseEnvelope): + fila: Fila = Fila.SERVICO + metodo: ServicoMetodo = ServicoMetodo.DELETE + dados: ServicoDeleteDados + + +class TomadorPostEnvelope(BaseEnvelope): + fila: Fila = Fila.TOMADOR + metodo: TomadorMetodo = TomadorMetodo.POST + dados: TomadorPostDados + + +class TomadorGetEnvelope(BaseEnvelope): + fila: Fila = Fila.TOMADOR + metodo: TomadorMetodo = TomadorMetodo.GET + dados: TomadorGetDados + + +class TomadorPutEnvelope(BaseEnvelope): + fila: Fila = Fila.TOMADOR + metodo: TomadorMetodo = TomadorMetodo.PUT + dados: TomadorPutDados + + +class TomadorDeleteEnvelope(BaseEnvelope): + fila: Fila = Fila.TOMADOR + metodo: TomadorMetodo = TomadorMetodo.DELETE + dados: TomadorDeleteDados + + +class RelatorioNFeEmitidasEnvelope(BaseEnvelope): + fila: Fila = Fila.RELATORIOS + metodo: RelatorioMetodo = RelatorioMetodo.NFE_EMITIDAS + dados: RelatorioVazioDados + + +class RelatorioNFeCnpjEnvelope(BaseEnvelope): + fila: Fila = Fila.RELATORIOS + metodo: RelatorioMetodo = RelatorioMetodo.NFE_POR_CNPJ + dados: RelatorioCnpjDados + + +class RelatorioNFSeEmitidasEnvelope(BaseEnvelope): + fila: Fila = Fila.RELATORIOS + metodo: RelatorioMetodo = RelatorioMetodo.NFSE_EMITIDAS + dados: RelatorioPeriodoDados + + +class RelatorioNFSeCnpjEnvelope(BaseEnvelope): + fila: Fila = Fila.RELATORIOS + metodo: RelatorioMetodo = RelatorioMetodo.NFSE_POR_CNPJ + dados: RelatorioCnpjPeriodoDados + + +class WebhookGetDeleteTestEnvelope(BaseEnvelope): + fila: Fila = Fila.WEBHOOKS + metodo: WebhookMetodo + dados: WebhookGetDeleteTestDados + + +class WebhookPostPutEnvelope(BaseEnvelope): + fila: Fila = Fila.WEBHOOKS + metodo: WebhookMetodo + dados: WebhookPostPutDados + + +# --- Generic Envelope for basic structural validation --- + +class GenericEnvelope(BaseEnvelope): + """Validates that minimum required fields exist before routing.""" + fila: Fila + metodo: str + dados: dict | None = None + + +# --- Payload Validation Router --- + +# Maps (fila, metodo) -> Envelope class +_ENVELOPE_MAP: dict[tuple[str, str], type[BaseEnvelope]] = { + # NFe + ("nfe", "post"): NFePostEnvelope, + ("nfe", "get"): NFeIdEnvelope, + ("nfe", "download"): NFeIdEnvelope, + ("nfe", "download_xml"): NFeIdEnvelope, + ("nfe", "cancelar"): NFeCancelarEnvelope, + ("nfe", "status_cancelamento"): NFeIdEnvelope, + ("nfe", "cce"): NFeCCEEnvelope, + ("nfe", "status_cce"): NFeIdEnvelope, + ("nfe", "resumo"): NFeResumoEnvelope, + # NFSe + ("nfse", "post"): NFSePostEnvelope, + ("nfse", "get"): NFSeGetEnvelope, + ("nfse", "download"): NFSeIdEnvelope, + ("nfse", "download_xml"): NFSeIdEnvelope, + ("nfse", "cancelar"): NFSeIdEnvelope, + ("nfse", "status_cancelamento"): NFSeIdEnvelope, + ("nfse", "resumo"): NFSeResumoEnvelope, + ("nfse", "historico"): NFSeHistoricoEnvelope, + # Certificado Digital + ("certificado_digital", "post"): CertificadoPostEnvelope, + ("certificado_digital", "get"): CertificadoGetEnvelope, + ("certificado_digital", "put"): CertificadoPutEnvelope, + # Empresa + ("empresa", "post"): EmpresaPostEnvelope, + ("empresa", "get"): EmpresaGetEnvelope, + ("empresa", "post_file"): EmpresaPostFileEnvelope, + ("empresa", "patch"): EmpresaPatchEnvelope, + # Serviço + ("servico", "post"): ServicoPostEnvelope, + ("servico", "get"): ServicoGetEnvelope, + ("servico", "put"): ServicoPutEnvelope, + ("servico", "delete"): ServicoDeleteEnvelope, + # Tomador + ("tomador", "post"): TomadorPostEnvelope, + ("tomador", "get"): TomadorGetEnvelope, + ("tomador", "put"): TomadorPutEnvelope, + ("tomador", "delete"): TomadorDeleteEnvelope, + # Relatórios + ("relatorios", "nfe_emitidas"): RelatorioNFeEmitidasEnvelope, + ("relatorios", "nfe_por_cnpj"): RelatorioNFeCnpjEnvelope, + ("relatorios", "nfse_emitidas"): RelatorioNFSeEmitidasEnvelope, + ("relatorios", "nfse_por_cnpj"): RelatorioNFSeCnpjEnvelope, + # Webhooks + ("webhooks", "get"): WebhookGetDeleteTestEnvelope, + ("webhooks", "post"): WebhookPostPutEnvelope, + ("webhooks", "delete"): WebhookGetDeleteTestEnvelope, + ("webhooks", "put"): WebhookPostPutEnvelope, + ("webhooks", "test"): WebhookGetDeleteTestEnvelope, +} + + +def validate_payload(payload_dict: dict) -> BaseEnvelope: + # Step 1: Basic structural validation + generic = GenericEnvelope(**payload_dict) + + fila = generic.fila.value + metodo = generic.metodo.lower() + + # Step 2: Find specific envelope + key = (fila, metodo) + envelope_cls = _ENVELOPE_MAP.get(key) + + if envelope_cls is None: + raise ValueError(f"Combinação fila/metodo desconhecida: {fila}/{metodo}") + + # Step 3: Normalize metodo in the dict to match enum lowercase values + payload_dict['metodo'] = metodo + + # Step 4: Full typed validation + return envelope_cls(**payload_dict) + + + diff --git a/backup/src/Models/servico.py b/backup/src/Models/servico.py new file mode 100644 index 0000000..afb717f --- /dev/null +++ b/backup/src/Models/servico.py @@ -0,0 +1,150 @@ +from pydantic import BaseModel +from enum import IntEnum + +class TipoTributacao(IntEnum): + NAO_DEFINIDO = 0 + ISENTO_ISS = 1 + IMUNE = 2 + NAO_INCIDENCIA = 3 + NAO_TRIBUTAVEL = 4 + RETIDO = 5 + TRIBUTAVEL_MUNICIPIO = 6 + TRIBUTAVEL_FORA_MUNICIPIO = 7 + TRIBUTAVEL_MUNICIPIO_TOMADOR = 8 + +class TipoExigibilidade(IntEnum): + EXIGIVEL = 1 + NAO_INCIDENCIA = 2 + ISENCAO = 3 + EXPORTACAO = 4 + IMUNIDADE = 5 + SUSPENSO_ACAO_JUDICIAL = 6 + SUSPENSO_ACAO_ADMINISTRATIVA = 7 + +class ISS(BaseModel): + tipoTributacao: int #TipoTributacao + exigibilidade: int #TipoExigibilidade + retido: bool | None = None # Se o ISS foi retido na fonte + aliquota: float # Aliquota do ISS + valor: float # Valor do ISS + valorRetido: float # Valor do ISS retido + processoSuspensao: str # Numero processo de suspensão do ISS +############################################################################## +# tipoTributacao integer +# Default: 6 +# Enum: 0 1 2 3 4 5 6 7 8 +# Tipo de Tributação do Serviço, valores: +# 0 - Não definido +# 1 - Isento de ISS +# 2 - Imune +# 3 - Não Incidência no Município +# 4 - Não Tributável +# 5 - Retido +# 6 - Tributável Dentro do Município +# 7 - Tributável Fora do Município +# 8 - Tributável Dentro do Município pelo tomador +############################################################################## +# exigibilidade integer +# Default: 1 +# Enum: 1 2 3 4 5 6 7 +# Exigibilidade do ISS, valores: +# 1 - Exigível +# 2 - Não Incidência +# 3 - Isenção +# 4 - Exportação +# 5 - Imunidade +# 6 - Supenso por Ação Judicial +# 7 - Suspenso por Ação Administrativa +############################################################################## +# responsavelRetencao string +# Valores aceitos: +# 1 - Tomador +# 2 - Intermediário +############################################################################## + +class Obra(BaseModel): + art: str # Anotação de Responsabilidade Técnica (Codigo ART) + codigo: str # Código da obra + +class Valor(BaseModel): + servico: float # Valor bruto do serviço prestado. + baseCalculo: float # Valor da base de cálculo dos impostos. + deducoes: float # Valor deduções. + descontoCondicionado: float # Valor do desconto condicionado. + descontoIncondicionado: float # Valor do desconto incondicionado. + liquido: float # Valor liquido do serviço, caso você não informe esse campo, vamos calcular automáticamente. + unitario: float # Valor unitário do serviço. + valorAproximadoTributos: float # Valor aproximado dos tributos. + +class Deducao(BaseModel): + tipo: int + descricao: str # Descrição da dedução +############################################################################## +# tipo integer +# Tipo de dedução: +# 0 - Sem Deduções +# 1 - Materiais +# 2 - Subempreitada de Mão de Obra +# 3 - Serviços +# 4 - Produção Externa +# 5 - Alimentação e Bebidas +# 6 - Reembolso de Despesas +# 7 - Repasse Consorciado +# 8 - Repasse Plano de Saúde +# 99 - Outras Deduções +############################################################################## + +class RetencaoInterno(BaseModel): + aliquota: float # Aliquota da retenção + valor: float # Valor da retenção + +class Retencao(BaseModel): + pis: RetencaoInterno + cofins: RetencaoInterno + csll: RetencaoInterno + inss: RetencaoInterno + irrf: RetencaoInterno + outrasRetencoes: float # Valor de outras retenções + cpp: RetencaoInterno + +class Aliquota(BaseModel): + municipal: float # Aliquota municipal + estadual: float # Aliquota estadual + federal: float # Aliquota federal + +class Detalhado(BaseModel): + aliquota: Aliquota + +class Simplificado(BaseModel): + aliquota: float # Aliquota geral de impostos + +class IBPT(BaseModel): + simplificado: Simplificado + detalhado: Detalhado + +# Representa o detalhamento do serviço prestado na NFSe, impostos incidentes e deduções +class Servico(BaseModel): + codigo: str # Código de serviço, também conhecido como Código Item Lista Serviço, geralmente segue a LC116, podendo váriar de acordo com a prefeitura.Você pode encontrar esse dado no portal da prefeitura, em uma nota emitida, ou junto ao contador. + idIntegracao: str # Código EXTERNO de integração do serviço. Utilizado SOMENTE quando se cadastra previamente um serviço. + discriminacao: str # Descrição do serviço prestado + codigoTributacao: str # Código tributação no Município + cnae: str # O código CNAE ou, por extenso, Classificação Nacional de Atividades Econômicas, é uma forma de padronizar, em todo o território nacional, os códigos de atividades econômicas e os critérios de enquadramento usados pelos mais diversos órgãos da administração tributária do Brasil. + codigoCidadeIncidencia: str # Código IBGE da cidade de incidência do ISS. + descricaoCidadeIncidencia: str # Descrição da cidade de incidência do ISS. + unidade: str # Unidade do serviço prestado. + quantidade: int # Quantidade dos serviços prestados. + iss: ISS + obra: Obra + valor: Valor + deducao: Deducao + retencao: Retencao + tributavel: bool # Indica se o serviço está sujeito a tributação. + ibpt: IBPT + responsavelRetencao: str + +############################################################################## +# responsavelRetencao Valores aceitos: +# 1 - Tomador +# 2 - Intermediário +# Caso o responsável pela retenção seja o próprio prestador, não gerar o campo. +############################################################################## diff --git a/backup/src/Models/tomador.py b/backup/src/Models/tomador.py new file mode 100644 index 0000000..4e3cf5a --- /dev/null +++ b/backup/src/Models/tomador.py @@ -0,0 +1,34 @@ +from pydantic import BaseModel + + +class Endereco(BaseModel): + tipoLogradouro: str + logradouro: str + numero: str + complemento: str + tipoBairro: str + bairro: str + codigoPais: str + descricaoPais: str + codigoCidade: str + descricaoCidade: str + estado: str + cep: str + +class Telefone(BaseModel): + ddd: str + numero: str + +# Representa o cliente (Tomador do Serviço) vinculado a uma nota fiscal de serviço +class Tomador(BaseModel): + cpfCnpj: str + inscricaoMunicipal: str + inscricaoEstadual: str + inscricaoSuframa: str + indicadorInscricaoEstadual: int + razaoSocial: str + nomeFantasia: str + endereco: Endereco + telefone: Telefone + email: str + orgaoPublico: bool diff --git a/backup/src/NFSe/Servico/servico.py b/backup/src/NFSe/Servico/servico.py new file mode 100644 index 0000000..4787089 --- /dev/null +++ b/backup/src/NFSe/Servico/servico.py @@ -0,0 +1,105 @@ +from Models.servico import Servico +from utils.api import Api +from utils.banco import Banco, ServicoTable +from utils.Exchange2 import LoggerExchange2 +from utils.exchangeModels import RealmType +from sqlalchemy.exc import SQLAlchemyError +import json + +# Controller responsável pelos cadastros e busca de Serviços a serem vinculados em NFSe +class ServicoController: + def __init__(self, url_base, api_key, banco: Banco, logger: LoggerExchange2 = None): + self.api = Api(api_key) + self.url = f"{url_base}/nfse/servico" + self.banco = banco + self.logger = logger + + def _log(self, realm: RealmType, msg: str): + if self.logger: + self.logger.logIC2(realm, (msg,)) + + def cadastrar_servico(self, servico: Servico, data: dict): + try: + resposta = self.api.post(self.url, servico.model_dump()) + if 'error' in resposta: + self._log(RealmType.ERRORS, f"Erro na API ao cadastrar serviço") + return resposta + + servico_novo = ServicoTable( + descricao=servico.discriminacao, + id_tecnospeed=resposta['data']['id'], + id_usuario=data.get('id_usuario', 1), + status=True + ) + self.banco.add(servico_novo) + self._log(RealmType.OPERATIONAL, f"Serviço cadastrado com sucesso - ID: {resposta['data']['id']}") + return resposta + except SQLAlchemyError as e: + self.banco.rollback() + self._log(RealmType.ERRORS, f"Erro no banco ao cadastrar serviço: {e}") + return {"error": f"Erro ao cadastrar serviço: {str(e)}"} + except Exception as e: + return {"error": f"Erro inesperado: {str(e)}"} + + def buscar_servico(self, id_servico, data: dict = None): + try: + resposta = self.api.get(f"{self.url}/{id_servico}") + self._log(RealmType.OPERATIONAL, f"Serviço consultado - ID: {id_servico}") + return resposta + except Exception as e: + self._log(RealmType.ERRORS, f"Erro ao buscar serviço: {e}") + return {"error": f"Erro ao buscar serviço: {str(e)}"} + + def listar_servicos(self, data: dict = None): + try: + servicos = self.banco.session.query(ServicoTable.id_tecnospeed, ServicoTable.descricao, ServicoTable.status).all() + resultado = [{"id_tecnospeed": servico.id_tecnospeed, "descricao": servico.descricao, "status": servico.status} for servico in servicos] + self._log(RealmType.OPERATIONAL, f"Listagem de serviços - {len(resultado)} registros") + return resultado + except SQLAlchemyError as e: + self._log(RealmType.ERRORS, f"Erro ao listar serviços: {e}") + return {"error": f"Erro ao listar serviços: {str(e)}"} + except Exception as e: + return {"error": f"Erro inesperado: {str(e)}"} + + def atualizar_servico(self, id_servico, servico: Servico): + try: + resposta = self.api.patch(f"{self.url}/{id_servico}", servico.model_dump()) + servico_existente = self.banco.session.query(ServicoTable).filter_by(id=id_servico).first() + + if not servico_existente: + self._log(RealmType.ERRORS, f"Serviço não encontrado - ID: {id_servico}") + return {"error": "Serviço não encontrado"} + + servico_existente.descricao = servico.discriminacao + self.banco.session.commit() + + self._log(RealmType.OPERATIONAL, f"Serviço atualizado com sucesso - ID: {id_servico}") + return resposta + except SQLAlchemyError as e: + self.banco.rollback() + self._log(RealmType.ERRORS, f"Erro ao atualizar serviço: {e}") + return {"error": f"Erro ao atualizar serviço: {str(e)}"} + except Exception as e: + return {"error": f"Erro inesperado: {str(e)}"} + + def deletar_servico(self, id_servico): + try: + resposta = self.api.delete(f"{self.url}/{id_servico}") + servico_existente = self.banco.session.query(ServicoTable).filter_by(id=id_servico).first() + + if not servico_existente: + self._log(RealmType.ERRORS, f"Serviço não encontrado para deletar - ID: {id_servico}") + return {"error": "Serviço não encontrado"} + + self.banco.session.delete(servico_existente) + self.banco.session.commit() + + self._log(RealmType.OPERATIONAL, f"Serviço deletado com sucesso - ID: {id_servico}") + return resposta + except SQLAlchemyError as e: + self.banco.rollback() + self._log(RealmType.ERRORS, f"Erro ao deletar serviço: {e}") + return {"error": f"Erro ao deletar serviço: {str(e)}"} + except Exception as e: + return {"error": f"Erro inesperado: {str(e)}"} \ No newline at end of file diff --git a/backup/src/NFSe/Tomador/tomador.py b/backup/src/NFSe/Tomador/tomador.py new file mode 100644 index 0000000..e10cc78 --- /dev/null +++ b/backup/src/NFSe/Tomador/tomador.py @@ -0,0 +1,91 @@ +from utils.api import Api +from utils.banco import Banco, TomadorTable +from utils.Exchange2 import LoggerExchange2 +from utils.exchangeModels import RealmType +from Models.tomador import Tomador + +import json + +# Controller para gerenciamento no banco e na API dos Tomadores (clientes finais) das notas +class TomadorController: + def __init__(self, url_base, api_key, banco: Banco, logger: LoggerExchange2 = None): + self.api = Api(api_key) + self.url = f"{url_base}/nfse/tomador" + self.banco = banco + self.logger = logger + + def _log(self, realm: RealmType, msg: str): + if self.logger: + self.logger.logIC2(realm, (msg,)) + + def cadastrar_tomador(self, tomador: Tomador, data: dict): + try: + retorno = self.api.post(self.url, tomador.model_dump()) + if 'error' in retorno: + self._log(RealmType.ERRORS, f"Erro na API ao cadastrar tomador") + return retorno + + tomador_novo = TomadorTable( + id_tecnospeed=retorno['id'], + nome=tomador.razaoSocial, + cpf_cnpj=tomador.cpfCnpj, + email=tomador.email, + telefone=f"({tomador.telefone.ddd}) {tomador.telefone.numero}", + id_usuario=data.get('id_usuario', 1), + status=True + ) + + self.banco.add(tomador_novo) + self._log(RealmType.OPERATIONAL, f"Tomador cadastrado com sucesso - CPF/CNPJ: {tomador.cpfCnpj}") + return retorno + except Exception as e: + self._log(RealmType.ERRORS, f"Erro ao cadastrar tomador: {e}") + return {"error": str(e)} + + def buscar_tomador(self, cpf_cnpj): + try: + tomador = self.api.get(f"{self.url}/{cpf_cnpj}") + self._log(RealmType.OPERATIONAL, f"Tomador consultado - CPF/CNPJ: {cpf_cnpj}") + return tomador + except Exception as e: + self._log(RealmType.ERRORS, f"Erro ao buscar tomador: {e}") + return {"error": str(e)} + + def atualizar_tomador(self, cpf_cnpj, tomador: Tomador): + try: + tomador_existente = self.banco.session.query(TomadorTable).filter_by(cpf_cnpj=cpf_cnpj).first() + if not tomador_existente: + self._log(RealmType.ERRORS, f"Tomador não encontrado - CPF/CNPJ: {cpf_cnpj}") + return {"error": "Tomador não encontrado"} + + resposta = self.api.patch(f"{self.url}/{cpf_cnpj}", tomador.model_dump()) + tomador_existente.nome = tomador.razaoSocial + tomador_existente.email = tomador.email + tomador_existente.telefone = f"({tomador.telefone.ddd}) {tomador.telefone.numero}" + + self.banco.session.commit() + self._log(RealmType.OPERATIONAL, f"Tomador atualizado com sucesso - CPF/CNPJ: {cpf_cnpj}") + return resposta + except Exception as e: + self.banco.session.rollback() + self._log(RealmType.ERRORS, f"Erro ao atualizar tomador: {e}") + return {"error": str(e)} + + def deletar_tomador(self, cpf_cnpj): + try: + tomador_existente = self.banco.session.query(TomadorTable).filter_by(cpf_cnpj=cpf_cnpj).first() + if not tomador_existente: + self._log(RealmType.ERRORS, f"Tomador não encontrado para deletar - CPF/CNPJ: {cpf_cnpj}") + return {"error": "Tomador não encontrado"} + + resposta = self.api.delete(f"{self.url}/{cpf_cnpj}") + self.banco.session.delete(tomador_existente) + self.banco.session.commit() + + self._log(RealmType.OPERATIONAL, f"Tomador deletado com sucesso - CPF/CNPJ: {cpf_cnpj}") + return resposta + except Exception as e: + self.banco.session.rollback() + self._log(RealmType.ERRORS, f"Erro ao deletar tomador: {e}") + return {"error": str(e)} + diff --git a/backup/src/NFSe/nfse.py b/backup/src/NFSe/nfse.py new file mode 100644 index 0000000..ae2da7d --- /dev/null +++ b/backup/src/NFSe/nfse.py @@ -0,0 +1,167 @@ +from Models.nfse import NFSe +from utils.api import Api +from utils.banco import Banco, NFSeTable +from utils.Exchange2 import LoggerExchange2 +from utils.exchangeModels import RealmType +from utils.notificador import NotificadorInterno + +import json + +# Serviço responsável pelo controle, emissão e cancelamento das Notas Fiscais de Serviço Eletrônicas (NFSe) +class NFSeService: + def __init__(self, api_key, url_base, banco: Banco, logger: LoggerExchange2 = None): + self.api = Api(api_key) + self.url = f"{url_base}/nfse" + self.banco = banco + self.logger = logger + self.notificador = NotificadorInterno(logger=logger) if logger else None + + def _log(self, realm: RealmType, msg: str): + if self.logger: + self.logger.logIC2(realm, (msg,)) + + def cadastrar_nfse(self, nfse: NFSe, data: dict): + try: + id_empresa = data.get('id_empresa', "0") + resposta = self.api.post(self.url, [nfse.model_dump()]) + if 'error' in resposta: + self._log(RealmType.ERRORS, f"Erro na API ao cadastrar NFSe: {resposta.get('error')}") + return resposta + + # A API da Plugnotas retorna 'documents' contendo uma lista com os sucessos + documentos = resposta.get('documents', []) + if not documentos: + self._log(RealmType.ERRORS, f"Nenhum documento retornado pela API. Resposta: {resposta}") + return resposta + + id_recebido = documentos[0].get('id') + if not id_recebido: + raise Exception("ID do documento não encontrado na resposta.") + + nfse_nova = NFSeTable( + id_tecnospeed=id_recebido, + id_empresa=id_empresa, + id_usuario=data.get('id_usuario', 1), + status=True + ) + + self.banco.add(nfse_nova) + self._log(RealmType.OPERATIONAL, f"NFSe cadastrada com sucesso - ID: {id_recebido}") + + # Notifica o listener interno para sincronização imediata + if self.notificador: + self.notificador.notificar_emissao(id_tecnospeed=id_recebido, documento="nfse", cnpj=id_empresa) + + return resposta + except Exception as e: + self._log(RealmType.ERRORS, f"Erro ao cadastrar NFSe: {e}") + raise Exception(f"Erro ao cadastrar NFSe: {e}") + + def buscar_nfse(self, id_nfse): + try: + resposta = self.api.get(f"{self.url}/{id_nfse}") + self._log(RealmType.OPERATIONAL, f"NFSe consultada com sucesso - ID: {id_nfse}") + return resposta + except Exception as e: + self._log(RealmType.ERRORS, f"Erro ao buscar NFSe: {e}") + raise Exception(f"Erro ao buscar NFSe: {e}") + + def listar_nfse(self): + try: + nfse = self.banco.session.query(NFSeTable.id, NFSeTable.id_tecnospeed).all() + resultado = [{"id": n[0], "descricao": n[1]} for n in nfse] + self._log(RealmType.OPERATIONAL, f"Listagem de NFSe - {len(resultado)} registros") + return resultado + except Exception as e: + self._log(RealmType.ERRORS, f"Erro ao listar NFSe: {e}") + raise Exception(f"Erro ao listar NFSe: {e}") + + def baixar_nfse(self, id_nfse): + try: + resposta = self.api.get(f"{self.url}/pdf/{id_nfse}") + self._log(RealmType.OPERATIONAL, f"PDF NFSe baixado com sucesso - ID: {id_nfse}") + return resposta + except Exception as e: + self._log(RealmType.ERRORS, f"Erro ao baixar NFSe: {e}") + raise Exception(f"Erro ao baixar NFSe: {e}") + + def baixar_xml_nfse(self, id_nfse): + try: + resposta = self.api.get(f"{self.url}/xml/{id_nfse}") + self._log(RealmType.OPERATIONAL, f"XML NFSe baixado com sucesso - ID: {id_nfse}") + return resposta + except Exception as e: + self._log(RealmType.ERRORS, f"Erro ao baixar XML NFSe: {e}") + raise Exception(f"Erro ao baixar XML NFSe: {e}") + + def solicitar_cancelamento_nfse(self, id_nfse): + try: + resposta = self.api.post(f"{self.url}/cancelar/{id_nfse}", {}) + self._log(RealmType.OPERATIONAL, f"Cancelamento NFSe solicitado - ID: {id_nfse}") + return resposta + except Exception as e: + self._log(RealmType.ERRORS, f"Erro ao solicitar cancelamento NFSe: {e}") + raise Exception(f"Erro ao solicitar cancelamento NFSe: {e}") + + def consultar_status_cancelamento_nfse(self, id_nfse): + try: + resposta = self.api.get(f"{self.url}/cancelar/status/{id_nfse}") + self._log(RealmType.OPERATIONAL, f"Status cancelamento NFSe consultado - ID: {id_nfse}") + return resposta + except Exception as e: + self._log(RealmType.ERRORS, f"Erro ao consultar status cancelamento NFSe: {e}") + raise Exception(f"Erro ao consultar status cancelamento NFSe: {e}") + + def consultar_resumo_nfse(self, id_nfse, cnpj): + try: + resposta = self.api.get(f"{self.url}/consultar/{id_nfse}/{cnpj}") + self._log(RealmType.OPERATIONAL, f"Resumo NFSe consultado - ID: {id_nfse}") + return resposta + except Exception as e: + self._log(RealmType.ERRORS, f"Erro ao consultar resumo NFSe: {e}") + raise Exception(f"Erro ao consultar resumo NFSe: {e}") + + def baixar_histotico_nfse(self, id_nfse, cnpj = None): + try: + resposta = self.api.get(f"{self.url}/historico/{id_nfse}") if cnpj is None else self.api.get(f"{self.url}/historico/{id_nfse}/{cnpj}") + self._log(RealmType.OPERATIONAL, f"Histórico NFSe baixado - ID: {id_nfse}") + return resposta + except Exception as e: + self._log(RealmType.ERRORS, f"Erro ao baixar histórico NFSe: {e}") + raise Exception(f"Erro ao baixar histórico NFSe: {e}") + + def enviar_email_nfse(self, id_nfse, email:list[str], reenvio:bool = False): + try: + resposta = self.api.post(f"{self.url}/email/{id_nfse}", {"reenvio": reenvio,"destinatarios": email}) + self._log(RealmType.OPERATIONAL, f"Email NFSe enviado para {email} - ID: {id_nfse}") + return resposta + except Exception as e: + self._log(RealmType.ERRORS, f"Erro ao enviar email NFSe: {e}") + raise Exception(f"Erro ao enviar email NFSe: {e}") + + def resolver_nfse(self, id_nfse, identificacaoNota:str): + try: + resposta = self.api.post(f"{self.url}/resolve/{id_nfse}", {"identificacaoNota": identificacaoNota}) + self._log(RealmType.OPERATIONAL, f"NFSe resolvida com sucesso - ID: {id_nfse}") + return resposta + except Exception as e: + self._log(RealmType.ERRORS, f"Erro ao resolver NFSe: {e}") + raise Exception(f"Erro ao resolver NFSe: {e}") + + def consultar_nfse_periodo(self, data_inicio, data_fim): + try: + resposta = self.api.get_paginated(f"{self.url}/periodo/{data_inicio}/{data_fim}") + self._log(RealmType.OPERATIONAL, f"NFSe consultada por período: {data_inicio} a {data_fim}") + return resposta + except Exception as e: + self._log(RealmType.ERRORS, f"Erro ao consultar NFSe por período: {e}") + raise Exception(f"Erro ao consultar NFSe por período: {e}") + + def baixar_recibo_provisorio_servico(self, id_nfse, cnpj = None): + try: + resposta = self.api.get(f"{self.url}/rps/pdf/{id_nfse}") if cnpj is None else self.api.get(f"{self.url}/rps/pdf/{id_nfse}/{cnpj}") + self._log(RealmType.OPERATIONAL, f"RPS baixado com sucesso - ID: {id_nfse}") + return resposta + except Exception as e: + self._log(RealmType.ERRORS, f"Erro ao baixar recibo provisório de serviço: {e}") + raise Exception(f"Erro ao baixar recibo provisório de serviço: {e}") diff --git a/backup/src/NFe/registroNFe.py b/backup/src/NFe/registroNFe.py new file mode 100644 index 0000000..349f03d --- /dev/null +++ b/backup/src/NFe/registroNFe.py @@ -0,0 +1,126 @@ +import json as json_lib +from utils.api import Api +from utils.banco import Banco, NFeTable +from Models.nfe import NFe +from typing import List +from utils.Exchange2 import LoggerExchange2 +from utils.exchangeModels import RealmType +from utils.notificador import NotificadorInterno + +class RegistroNFeService: + # Classe responsável por fornecer as operações de comunicação com a PlugNotas para o documento NFe + def __init__(self, api_key: str, url_base: str, banco: Banco, logger: LoggerExchange2 = None): + self.api = Api(api_key) + self.url = f"{url_base}/nfe" + self.banco = banco + self.logger = logger + self.notificador = NotificadorInterno(logger=logger) if logger else None + + def cadastrar_nfe(self, nfe_list: List[NFe], data: dict): + # Envia uma lista de NFes em lote ou unitária para emissão e registra no banco de dados + try: + id_empresa = data.get('id_empresa', "0") + payload = [nfe.model_dump() for nfe in nfe_list] + resposta = self.api.post(self.url, payload) + if 'error' in resposta: + id_usr = data.get('id_usuario', 1) + if self.logger: self.logger.logIC2(RealmType.OPERATIONAL, ('INSERT', 'nfe', id_usr, '0', 'ERRO', f"Erro na API: {resposta.get('error')}", json_lib.dumps(resposta))) + return resposta + if 'documents' in resposta: + for doc in resposta['documents']: + id_tecnospeed = doc.get('id') + if id_tecnospeed: + nfe_nova = NFeTable( + id_tecnospeed=id_tecnospeed, + id_empresa=id_empresa, + id_usuario=data.get('id_usuario', 1), + status=True + ) + self.banco.add(nfe_nova) + id_usr = data.get('id_usuario', 1) + if self.logger: self.logger.logIC2(RealmType.OPERATIONAL, ('INSERT', 'nfe', id_usr, str(nfe_nova.id), 'SUCESSO', 'NFe cadastrada com sucesso', json_lib.dumps(doc))) + + # Notifica o listener interno para sincronização imediata + if self.notificador: + self.notificador.notificar_emissao(id_tecnospeed=id_tecnospeed, documento="nfe", cnpj=id_empresa) + + return resposta + except Exception as e: + if self.logger: self.logger.logIC2(RealmType.OPERATIONAL, ('INSERT', 'nfe', data.get('id_usuario', 1), '0', 'ERRO', f"Erro ao cadastrar NFe: {e}", "{}")) + raise Exception(f"Erro ao cadastrar NFe: {e}") + + def cancelar_nfe(self, id_nota: str, justificativa: str): + # Solicita o cancelamento de uma NFe existente mediante uma justificativa + try: + resposta = self.api.post(f"{self.url}/{id_nota}/cancelamento", {"justificativa": justificativa}) + if self.logger: self.logger.logIC2(RealmType.OPERATIONAL, ('DELETE', 'nfe', 1, id_nota, 'SUCESSO', 'NFe cancelada com sucesso', json_lib.dumps(resposta))) + return resposta + except Exception as e: + if self.logger: self.logger.logIC2(RealmType.OPERATIONAL, ('DELETE', 'nfe', 1, id_nota, 'ERRO', f"Erro ao cancelar NFe: {e}", "{}")) + raise Exception(f"Erro ao cancelar NFe: {e}") + + def enviar_cce(self, id_nota: str, correcao: str): + # Envia uma Carta de Correção Eletrônica (CCe) para retificar uma NFe já emitida + try: + resposta = self.api.post(f"{self.url}/{id_nota}/cce", {"correcao": correcao}) + if self.logger: self.logger.logIC2(RealmType.OPERATIONAL, ('UPDATE', 'nfe', 1, id_nota, 'SUCESSO', 'CCe enviada com sucesso', json_lib.dumps(resposta))) + return resposta + except Exception as e: + if self.logger: self.logger.logIC2(RealmType.OPERATIONAL, ('UPDATE', 'nfe', 1, id_nota, 'ERRO', f"Erro ao enviar CCe NFe: {e}", "{}")) + raise Exception(f"Erro ao enviar CCe NFe: {e}") + + def consultar_resumo(self, id_nota: str): + # Consulta o resumo e o status atual da NFe na PlugNotas pelo seu ID + try: + resposta = self.api.get(f"{self.url}/{id_nota}/resumo") + if self.logger: self.logger.logIC2(RealmType.OPERATIONAL, ('SELECT', 'nfe', 1, id_nota, 'SUCESSO', 'Resumo de NFe consultado', json_lib.dumps(resposta))) + return resposta + except Exception as e: + if self.logger: self.logger.logIC2(RealmType.OPERATIONAL, ('SELECT', 'nfe', 1, id_nota, 'ERRO', f"Erro ao consultar resumo NFe: {e}", "{}")) + raise Exception(f"Erro ao consultar resumo NFe: {e}") + + def consultar_resumo_por_cnpj_integracao(self, cnpj: str, id_integracao: str): + try: + resposta = self.api.get(f"{self.url}/{cnpj}/{id_integracao}/resumo") + if self.logger: self.logger.logIC2(RealmType.OPERATIONAL, ('SELECT', 'nfe', id_integracao, id_integracao, 'SUCESSO', 'Resumo por CNPJ/Integração consultado', json_lib.dumps(resposta))) + return resposta + except Exception as e: + if self.logger: self.logger.logIC2(RealmType.OPERATIONAL, ('SELECT', 'nfe', id_integracao, id_integracao, 'ERRO', f"Erro ao consultar resumo NFe por CNPJ/Integração: {e}", "{}")) + raise Exception(f"Erro ao consultar resumo NFe por CNPJ/Integração: {e}") + + def consultar_status_cancelamento(self, id_nota: str): + try: + resposta = self.api.get(f"{self.url}/{id_nota}/cancelamento/status") + if self.logger: self.logger.logIC2(RealmType.OPERATIONAL, ('SELECT', 'nfe', id_nota, id_nota, 'SUCESSO', 'Status de cancelamento consultado', json_lib.dumps(resposta))) + return resposta + except Exception as e: + if self.logger: self.logger.logIC2(RealmType.OPERATIONAL, ('SELECT', 'nfe', id_nota, id_nota, 'ERRO', f"Erro ao consultar status de cancelamento NFe: {e}", "{}")) + raise Exception(f"Erro ao consultar status de cancelamento NFe: {e}") + + def consultar_status_cce(self, id_nota: str): + try: + resposta = self.api.get(f"{self.url}/{id_nota}/cce/status") + if self.logger: self.logger.logIC2(RealmType.OPERATIONAL, ('SELECT', 'nfe', id_nota, id_nota, 'SUCESSO', 'Status CCe consultado', json_lib.dumps(resposta))) + return resposta + except Exception as e: + if self.logger: self.logger.logIC2(RealmType.OPERATIONAL, ('SELECT', 'nfe', id_nota, id_nota, 'ERRO', f"Erro ao consultar status CCe NFe: {e}", "{}")) + raise Exception(f"Erro ao consultar status CCe NFe: {e}") + + def baixar_pdf(self, id_nota: str): + # Obtém o link para download ou o stream do PDF da Nota Fiscal processada + try: + resposta = self.api.get(f"{self.url}/{id_nota}/pdf") + if self.logger: self.logger.logIC2(RealmType.OPERATIONAL, ('DOWNLOAD (PDF)', 'nfe', id_nota, id_nota, 'SUCESSO', 'PDF baixado com sucesso', json_lib.dumps(resposta) if isinstance(resposta, dict) else '{}')) + return resposta + except Exception as e: + if self.logger: self.logger.logIC2(RealmType.OPERATIONAL, ('DOWNLOAD (PDF)', 'nfe', id_nota, id_nota, 'ERRO', f"Erro ao baixar PDF NFe: {e}", "{}")) + raise Exception(f"Erro ao baixar PDF NFe: {e}") + + def baixar_xml(self, id_nota: str): + try: + resposta = self.api.get(f"{self.url}/{id_nota}/xml") + if self.logger: self.logger.logIC2(RealmType.OPERATIONAL, ('DOWNLOAD (XML)', 'nfe', id_nota, id_nota, 'SUCESSO', 'XML baixado com sucesso', json_lib.dumps(resposta) if isinstance(resposta, dict) else '{}')) + return resposta + except Exception as e: + if self.logger: self.logger.logIC2(RealmType.OPERATIONAL, ('DOWNLOAD (XML)', 'nfe', id_nota, id_nota, 'ERRO', f"Erro ao baixar XML NFe: {e}", "{}")) + raise Exception(f"Erro ao baixar XML NFe: {e}") diff --git a/backup/src/Relatorios/relatorios.py b/backup/src/Relatorios/relatorios.py new file mode 100644 index 0000000..1155445 --- /dev/null +++ b/backup/src/Relatorios/relatorios.py @@ -0,0 +1,70 @@ +from utils.api import Api +from utils.banco import Banco +from utils.Exchange2 import LoggerExchange2 +from utils.exchangeModels import RealmType + +from datetime import datetime, timedelta +import json + +# Extração e consumo de relatórios massivos de emissões diretamente da API e consolidações em lotes +class Relatorios: + def __init__(self, url_base, api_key, banco: Banco, logger: LoggerExchange2 = None): + self.api = Api(api_key) + self.url = f"{url_base}/" + self.banco = banco + self.logger = logger + + def _log(self, realm: RealmType, msg: str): + if self.logger: + self.logger.logIC2(realm, (msg,)) + + def nfe_emitidas(self, data: dict = None): + try: + resposta = self.api.get_paginated(f"{self.url}nfe/relatorio") + self._log(RealmType.OPERATIONAL, "Relatório de NFe emitidas consultado") + return resposta + except Exception as e: + msg_erro = f"Erro ao consultar relatório NFe emitidas: {e}" + self._log(RealmType.ERRORS, msg_erro) + return {"error": msg_erro} + + def nfe_por_cnpj(self, cnpj, data: dict = None): + try: + resposta = self.api.get_paginated(f"{self.url}nfe/{cnpj}/relatorio") + self._log(RealmType.OPERATIONAL, f"Relatório de NFe por CNPJ: {cnpj}") + return resposta + except Exception as e: + msg_erro = f"Erro ao consultar relatório NFe por CNPJ: {e}" + self._log(RealmType.ERRORS, msg_erro) + return {"error": msg_erro} + + def nfse_emitidas(self, data_inicial=None, data_final=None, data: dict = None): + try: + if not data_inicial: + data_inicial = (datetime.now() - timedelta(days=30)).strftime('%Y-%m-%d') + if not data_final: + data_final = datetime.now().strftime('%Y-%m-%d') + + resposta = self.api.get_paginated(f"{self.url}nfse/relatorio?from={data_inicial}&to={data_final}") + self._log(RealmType.OPERATIONAL, f"Relatório NFSe emitidas: {data_inicial} a {data_final}") + return resposta + except Exception as e: + msg_erro = f"Erro ao consultar relatório NFSe emitidas: {e}" + self._log(RealmType.ERRORS, msg_erro) + return {"error": msg_erro} + + def nfse_por_cnpj(self, cnpj, data_inicial=None, data_final=None, data: dict = None): + try: + if not data_inicial: + data_inicial = (datetime.now() - timedelta(days=30)).strftime('%Y-%m-%d') + if not data_final: + data_final = datetime.now().strftime('%Y-%m-%d') + + resposta = self.api.get_paginated(f"{self.url}nfse/{cnpj}/relatorio?from={data_inicial}&to={data_final}") + self._log(RealmType.OPERATIONAL, f"Relatório NFSe por CNPJ: {cnpj}") + return resposta + except Exception as e: + msg_erro = f"Erro ao consultar relatório NFSe por CNPJ: {e}" + self._log(RealmType.ERRORS, msg_erro) + return {"error": msg_erro} + diff --git a/backup/src/certificado_digital/certificado_digital.py b/backup/src/certificado_digital/certificado_digital.py new file mode 100644 index 0000000..cfbd627 --- /dev/null +++ b/backup/src/certificado_digital/certificado_digital.py @@ -0,0 +1,165 @@ +from utils.api import Api +from utils.banco import Banco, CertificadoDigitalTable +from datetime import date +from utils.Exchange2 import LoggerExchange2 +from utils.exchangeModels import RealmType +import json +import base64 + +class CertificadoDigital: + # Serviço que gerencia o upload e atualização de certificados digitais A1/A3 na plataforma da PlugNotas + def __init__(self, url_base, banco: Banco, api_key: str, logger: LoggerExchange2 = None): + self.api = Api(api_key=api_key) + self.banco = banco + self.url = f"{url_base}/certificado" + self.logger = logger + + def post_form_data(self, data): + # Realiza o envio multipart/form-data do arquivo físico do certificado .pfx para a API e o salva no banco + try: + files = { + 'arquivo': ( + 'certificado.pfx', + base64.b64decode(data['arquivo']) if isinstance(data['arquivo'], str) else data['arquivo'], + 'application/x-pkcs12' + ), + 'senha': (None, data['senha']) + } + if 'email' in data: + files['email'] = (None, data['email']) + + response = self.api.post_form_data(self.url, files) + id_tecnospeed = response.get('data', {}).get('id') + + id_usr = data.get('id_usuario', 0) + id_emp = data.get('id_empresa', "0") + + if not id_tecnospeed: + if self.logger: self.logger.logIC2(RealmType.OPERATIONAL, ('INSERT', 'certificado_digital', id_usr, '0', 'ERRO', 'ID do certificado não encontrado na resposta da API', json.dumps(response))) + raise ValueError("Erro ao obter ID do certificado na resposta da API.") + + expiration_value = data.get('expirationAt') or data.get('data_expiracao') + certificado = CertificadoDigitalTable( + id_tecnospeed=id_tecnospeed, + certificado=data['arquivo'], + expirationAt=date.fromisoformat(expiration_value) if expiration_value else date.today(), + senha=data['senha'], + status=True, + id_empresa=id_emp, + id_usuario=id_usr, + email=data.get('email') + ) + + self.banco.add(certificado) + if self.logger: self.logger.logIC2(RealmType.OPERATIONAL, ('INSERT', 'certificado_digital', id_usr, id_tecnospeed, 'SUCESSO', 'Certificado digital cadastrado com sucesso', json.dumps(response))) + + return id_tecnospeed + except Exception as e: + if self.logger: self.logger.logIC2(RealmType.OPERATIONAL, ('INSERT', 'certificado_digital', data.get('id_usuario', 0), '0', 'ERRO', f"Erro ao adicionar certificado: {e}", "{}")) + raise + + def contar_vencidos(self, lista_certificados): + # Auxiliar para contar certificados ativos e vencidos na listagem + if isinstance(lista_certificados, dict) and "data" in lista_certificados: + lista = lista_certificados["data"] + elif isinstance(lista_certificados, list): + lista = lista_certificados + else: + return {"certificados_ativos": 0, "certificados_vencidos": 0} + + ativos = 0 + vencidos = 0 + hoje = date.today() + + for cert in lista: + if not isinstance(cert, dict): continue + + # Tenta encontrar o campo de expiração (vencimento ou outros nomes conhecidos) + exp_str = cert.get("vencimento") or cert.get("expirationAt") or cert.get("dataVencimento") or cert.get("data_expiracao") + + if exp_str: + try: + # Tenta converter a string de data: + # 1. Formato DD/MM/YYYY (do snipped do usuário) + # 2. Formato ISO YYYY-MM-DD + if "/" in exp_str: + partes = exp_str.split(" ")[0].split("/") + if len(partes) == 3: + data_exp = date(int(partes[2]), int(partes[1]), int(partes[0])) + else: + data_exp = date.fromisoformat(exp_str[:10]) + + if data_exp >= hoje: + ativos += 1 + else: + vencidos += 1 + except: + continue + + return { + "certificados_ativos": ativos, + "certificados_vencidos": vencidos + } + + def get(self, id=None): + # Consulta a situação de um certificado digital específico pelo ID ou lista todos vinculados + try: + if id: + resultado = self.api.get(f"{self.url}/{id}") + if self.logger: self.logger.logIC2(RealmType.OPERATIONAL, ('SELECT', 'certificado_digital', id, id, 'SUCESSO', f"Certificado consultado - ID: {id}", json.dumps(resultado))) + return resultado + else: + resposta = self.api.get(self.url) + + # Calcula totais + totais = self.contar_vencidos(resposta) + + # Organiza a resposta unificada + resultado = { + "certificados": resposta if isinstance(resposta, list) else resposta.get("data", []), + "totais": totais + } + + if self.logger: self.logger.logIC2(RealmType.OPERATIONAL, ('SELECT', 'certificado_digital', 'GERAL', '0', 'SUCESSO', 'Listagem de certificados digitais', json.dumps(resultado))) + return resultado + except Exception as e: + if self.logger: self.logger.logIC2(RealmType.OPERATIONAL, ('SELECT', 'certificado_digital', id or 'GERAL', '0', 'ERRO', f"Erro ao buscar certificado: {e}", "{}")) + raise + + def put_certificado(self, id, data): + # Atualiza o arquivo físico ou a senha de um certificado já existente + try: + certificado = self.banco.session.query(CertificadoDigitalTable).filter_by(id_tecnospeed=id).first() + id_usr = data.get('id_usuario', 0) + + if not certificado: + if self.logger: self.logger.logIC2(RealmType.OPERATIONAL, ('UPDATE', 'certificado_digital', id_usr, id, 'ERRO', f"Certificado com ID {id} não encontrado", "{}")) + raise ValueError(f"Certificado com ID {id} não encontrado no banco.") + + files = { + 'arquivo': ( + 'certificado.pfx', + base64.b64decode(data['arquivo']) if isinstance(data['arquivo'], str) else data['arquivo'], + 'application/x-pkcs12' + ), + 'senha': (None, data['senha']) + } + if 'email' in data: + files['email'] = (None, data['email']) + + self.api.put_form_data(f"{self.url}/{id}", files) + + certificado.certificado = data['arquivo'] + certificado.senha = data['senha'] + expiration_value = data.get('expirationAt') or data.get('data_expiracao') + if expiration_value: + certificado.expirationAt = date.fromisoformat(expiration_value) + certificado.updatedAt = date.today() + + self.banco.update(certificado) + if self.logger: self.logger.logIC2(RealmType.OPERATIONAL, ('UPDATE', 'certificado_digital', id_usr, id, 'SUCESSO', 'Certificado digital atualizado com sucesso', json.dumps({"id": id}))) + + return id + except Exception as e: + if self.logger: self.logger.logIC2(RealmType.OPERATIONAL, ('UPDATE', 'certificado_digital', data.get('id_usuario', 0), id, 'ERRO', f"Erro ao atualizar certificado: {e}", "{}")) + raise diff --git a/backup/src/empresas/empresa.py b/backup/src/empresas/empresa.py new file mode 100644 index 0000000..1db0e75 --- /dev/null +++ b/backup/src/empresas/empresa.py @@ -0,0 +1,177 @@ +from Models.empresa import Empresa +from utils.api import Api +from utils.banco import Banco, EnterpriseTable, LogoTable +from utils.Exchange2 import LoggerExchange2 +from utils.exchangeModels import RealmType +from sqlalchemy.exc import SQLAlchemyError +import json +import string +import random +import time + +def generate_cuid(): + """Gera um ID no formato aproximado de CUID (c + timestamp + random).""" + ts = hex(int(time.time() * 1000))[2:] + rand = ''.join(random.choices(string.ascii_lowercase + string.digits, k=12)) + return f"c{ts}{rand}"[:25] + +# Gerenciador de contas de Empresas emissoras de notas e seus respectivos certificados e logos +class EmpresaController: + def __init__(self, url_base, api_key: str, banco: Banco, logger: LoggerExchange2 = None): + self.api = Api(api_key=api_key) + self.banco = banco + self.url = f"{url_base}/empresa" + self.logger = logger + + def _log(self, realm: RealmType, msg: str): + if self.logger: + self.logger.logIC2(realm, (msg,)) + + def post(self, empresa: Empresa, data: dict): + try: + dict_api = empresa.model_dump(exclude={'id', 'id_empresa', 'banco', 'email'}, exclude_none=True) + + # Injeção das configurações fixas e obrigatórias para a PlugNotas + if dict_api.get("nfe") and dict_api["nfe"].get("ativo"): + nfe_config_front = dict_api["nfe"].get("config") or {} + dict_api["nfe"]["config"] = { + "producao": False, + "email": {"envio": False}, + "dfe": {"primeiroNsu": False}, + "impressaoFcp": False, + "impressaoPartilha": False, + **nfe_config_front + } + dict_api["nfe"]["tipoContrato"] = 0 + + if dict_api.get("nfse") and dict_api["nfse"].get("ativo"): + nfse_config_front = dict_api["nfse"].get("config") or {} + dict_api["nfse"]["config"] = { + "producao": False, + "email": {"envio": False}, + "rps": {"lote": 1, "numeracao": [{"numero": 1, "serie": "1"}]}, + "prefeitura": {"login": "12345", "senha": "123"}, + **nfse_config_front + } + dict_api["nfse"]["tipoContrato"] = 0 + print("===========================") + print("ENVIANDO PARA A API:", json.dumps(dict_api, indent=2)) + print("===========================") + retorno = self.api.post(data=dict_api, url=self.url) + if 'error' in retorno: + err_data = retorno.get('response', {}).get('error', {}) + msg = err_data.get('message', 'Erro desconhecido') + detalhes = err_data.get('data', []) + if detalhes: + msg += f" Detalhes: {detalhes}" + + print(f"\n[ERRO API PLUGNOTAS] {msg}\n", flush=True) + self._log(RealmType.ERRORS, f"Erro na API ao cadastrar empresa: {msg}") + raise Exception(msg) + + # Gerar ID CUID se não vier no objeto ou se for nulo + empresa_id = empresa.id if empresa.id else generate_cuid() + + empresa_nova = EnterpriseTable( + id=empresa_id, + cnpj=empresa.cpfCnpj, + name=empresa.razaoSocial, + banco=empresa.banco, + email=empresa.email, + status=True + ) + self.banco.add(empresa_nova) + self._log(RealmType.OPERATIONAL, f"Empresa cadastrada com sucesso - CNPJ: {empresa.cpfCnpj} (ID: {empresa_id})") + + if isinstance(retorno, dict) and 'data' in retorno: + retorno['data']['id'] = empresa_nova.id + retorno['data']['id_plugnotas'] = retorno['data'].get('_id') + retorno['data']['name'] = empresa_nova.name + retorno['data']['status'] = empresa_nova.status + retorno['data'].pop('razaoSocial', None) + retorno['data'].pop('id_empresa', None) + + return retorno + except SQLAlchemyError as e: + self.banco.rollback() + self._log(RealmType.ERRORS, f"Erro no banco ao cadastrar empresa: {e}") + return {"error": f"Erro ao cadastrar empresa: {str(e)}"} + except Exception as e: + return {"error": f"Erro inesperado: {str(e)}"} + + def get(self, cnpj: str = None, data: dict = None): + try: + if cnpj: + resposta = self.api.get(f"{self.url}/{cnpj}") + + if isinstance(resposta, dict) and "error" not in resposta: + empresa_local = self.banco.session.query(EnterpriseTable).filter_by(cnpj=cnpj).first() + if empresa_local: + resposta["id"] = empresa_local.id + resposta["name"] = empresa_local.name + resposta["status"] = empresa_local.status + resposta.pop("razaoSocial", None) + + if "_id" in resposta: + resposta["id_plugnotas"] = resposta["_id"] + + else: + resposta = self.api.get_paginated(self.url) + # Injetar o ID do banco local em cada empresa listada + if isinstance(resposta, dict) and "data" in resposta: + for emp in resposta["data"]: + empresa_local = self.banco.session.query(EnterpriseTable).filter_by(cnpj=emp.get("cpfCnpj")).first() + if empresa_local: + emp["id"] = empresa_local.id + emp["name"] = empresa_local.name + emp["status"] = empresa_local.status + emp.pop("razaoSocial", None) + if "_id" in emp: + emp["id_plugnotas"] = emp["_id"] + + self._log(RealmType.OPERATIONAL, f"Empresa consultada - CNPJ: {cnpj or 'TODAS'}") + return resposta + except Exception as e: + self._log(RealmType.ERRORS, f"Erro ao buscar empresa: {e}") + return {"error": f"Erro ao buscar empresa: {str(e)}"} + + def post_file(self, file, cnpj, data=None): + try: + empresa = self.banco.session.query(EnterpriseTable).filter_by(cnpj=cnpj).first() + if not empresa: + self._log(RealmType.ERRORS, f"Empresa não encontrada para upload de logo - CNPJ: {cnpj}") + return {"error": "Empresa não encontrada"} + + logo = LogoTable( + id_empresa=empresa.id, + logo=file, + status=True + ) + self.banco.add(logo) + + files = {'arquivo': ('logo.png', file, 'image/png')} + resposta = self.api.post_file(file=files, url=f"{self.url}/{cnpj}/logotipo") + self._log(RealmType.OPERATIONAL, f"Logo enviada com sucesso - CNPJ: {cnpj}") + return resposta + + except SQLAlchemyError as e: + self.banco.rollback() + self._log(RealmType.ERRORS, f"Erro no banco ao salvar logo: {e}") + return {"error": f"Erro ao salvar logo: {str(e)}"} + except Exception as e: + self._log(RealmType.ERRORS, f"Erro inesperado ao enviar logo: {e}") + return {"error": f"Erro inesperado: {str(e)}"} + + def patch_certificado(self, cnpj: str, certificado: str, data: dict): + try: + empresa = self.banco.session.query(EnterpriseTable).filter_by(cnpj=cnpj).first() + if not empresa: + self._log(RealmType.ERRORS, f"Empresa não encontrada para atualizar certificado - CNPJ: {cnpj}") + return {"error": "Empresa não encontrada"} + resposta = self.api.patch(url=f"{self.url}/{cnpj}/certificado", data={"certificado": certificado}) + self._log(RealmType.OPERATIONAL, f"Certificado da empresa atualizado - CNPJ: {cnpj}") + return resposta + except Exception as e: + self._log(RealmType.ERRORS, f"Erro ao atualizar certificado: {e}") + return {"error": f"Erro ao atualizar certificado: {str(e)}"} + diff --git a/backup/src/listenerEndpoint.py b/backup/src/listenerEndpoint.py new file mode 100644 index 0000000..9a848a5 --- /dev/null +++ b/backup/src/listenerEndpoint.py @@ -0,0 +1,173 @@ +import sys +import os +import json +from contextlib import asynccontextmanager +from fastapi import FastAPI, Request +from fastapi.responses import JSONResponse +import uvicorn + +sys.path.append(os.path.dirname(os.path.abspath(__file__))) +from Models.config import SystemConfig +from utils.redis import RedisClient +from utils.Exchange2 import RPCExchange2, LoggerExchange2, AppIC2Builder +from utils.exchangeModels import RealmType + + +# Campos obrigatórios que o PlugNotas envia no Endpoint +CAMPOS_OBRIGATORIOS = ["id", "status", "documento"] + +# Status válidos que o PlugNotas envia +STATUS_VALIDOS = ["CONCLUIDO", "REJEITADO", "CANCELADO"] + +# Tipos de documento/entidades aceitos +DOCUMENTOS_VALIDOS = ["nfe", "nfse", "certificado_digital"] + +# Variáveis globais +redis_client = None +system_config = None +exchange = None +logger = None + + +@asynccontextmanager +async def ciclo_app(app: FastAPI): + # Gerencia o ciclo de vida da aplicação, inicializando conexões com Redis e RabbitMQ (Exchange) na inicialização + global redis_client, system_config, exchange + + try: + redis_client = RedisClient(host=os.getenv('REDIS_HOST', 'localhost'), port=6379) + system_config_data = redis_client.get("system_config") + + if not system_config_data: + print("Erro: system_config não encontrado no Redis.") + else: + system_config = SystemConfig(**json.loads(system_config_data)) + + builder = AppIC2Builder(service_name="tecnospeed", versionstr="2.0.1", origin=system_config.origin) + global logger + logger = LoggerExchange2(ic2_builder=builder, amqps=system_config.amqps, system="integracao", service="tecnospeed", version="2.0.1", origin=system_config.origin) + logger.logIC2(RealmType.LOGS, (f"[{system_config.origin}] Configuração lida com sucesso pelo Listener Endpoint.",)) + + exchange = RPCExchange2( + amqps=system_config.amqps, + origin=system_config.origin, + system="integracao", + service="controller", + version="2.0.1", + client="" + ) + servicekey = system_config.servicekey.ljust(32, '0')[:32] + exchange.setCrypto(serviceid=system_config.serviceid, servicekey=servicekey) + logger.logIC2(RealmType.LOGS, ("Exchange inicializado com sucesso para o Listener Endpoint.",)) + + except Exception as e: + if logger: + logger.logIC2(RealmType.ERRORS, (f"Erro ao inicializar o Listener Endpoint: {e}",)) + else: + print(f"Erro ao inicializar o Listener Endpoint: {e}") + + yield # Aplicação rodando + + if logger: + logger.logIC2(RealmType.LOGS, ("Listener Endpoint encerrando...",)) + logger.close() + + +app = FastAPI( + title="Listener Endpoint PlugNotas", + description="Endpoint para receber notificações Endpoint do PlugNotas", + version="2.0.1", + lifespan=ciclo_app +) + + +@app.get("/health") +async def healthy(): + # Verifica a saúde da aplicação, validando se as dependências (Redis e Exchange) estão conectadas + status = { + "status": "ok", + "redis": redis_client is not None, + "exchange": exchange is not None, + "system_config": system_config is not None + } + if not exchange: + return JSONResponse(status_code=503, content={**status, "status": "degraded", "message": "Exchange não inicializado"}) + return status + + +@app.post("/listenerPlugnotas") +@app.post("/listenerPlugnotas/{cnpj}") +async def receber_Endpoint_plugnotas(request: Request, cnpj: str = None): + # Rota principal que recebe a notificação da PlugNotas e repassa para processamento no RabbitMQ + try: + try: + payload = await request.json() + except Exception as e: + logger.logIC2(RealmType.ERRORS, (f"Payload inválido recebido (não é JSON válido): {e}",)) + return JSONResponse( + status_code=400, + content={"status": "error", "message": "Payload não é um JSON válido"} + ) + + documento = payload.get("documento", "desconhecido") + status_nota = payload.get("status", "desconhecido") + id_nota = payload.get("id", "") + id_integracao = payload.get("idIntegracao", "") + emitente = payload.get("emitente", cnpj or "") + + logger.logIC2(RealmType.LOGS, ( + f"Endpoint recebido - Documento: {documento} | Status: {status_nota} | " + f"ID: {id_nota} | IDIntegracao: {id_integracao} | Emitente: {emitente}", + )) + + campos_faltantes = [campo for campo in CAMPOS_OBRIGATORIOS if campo not in payload] + if campos_faltantes: + logger.logIC2(RealmType.ERRORS, ( + f"Endpoint com campos obrigatórios faltantes: {campos_faltantes} | " + f"Payload recebido: {json.dumps(payload)}", + )) + return {"status": "warning", "message": f"Campos faltantes: {campos_faltantes}"} + + if documento not in DOCUMENTOS_VALIDOS: + logger.logIC2(RealmType.ERRORS, ( + f"Tipo de documento desconhecido: {documento} | Payload: {json.dumps(payload)}", + )) + if status_nota not in STATUS_VALIDOS: + logger.logIC2(RealmType.ERRORS, ( + f"Status desconhecido: {status_nota} | Documento: {documento} | ID: {id_nota}", + )) + + if not exchange: + logger.logIC2(RealmType.ERRORS, ("Exchange não inicializado no momento da chegada do Endpoint.",)) + return JSONResponse( + status_code=500, + content={"status": "error", "message": "Serviço temporariamente indisponível"} + ) + + # Envia o EXATO objeto dict recebido do PlugNotas + exchange.setPayload(json.dumps(payload)) + + # Chama a rota RPC aguardando retorno + try: + resposta_rpc = exchange.callRPC("Endpoint PlugNotas Recebido", timeout=15) + + logger.logIC2(RealmType.LOGS, ( + f"Endpoint enviado ao RPC Controller. " + f"Documento: {documento} | Status: {status_nota} | ID: {id_nota} | Resposta: {resposta_rpc}", + )) + + return {"status": "ok", "message": "Enviado ao Controller com sucesso", "controller_response": resposta_rpc} + + except Exception as e: + logger.logIC2(RealmType.ERRORS, (f"Erro ou Timeout ao conversar com o Controller (RPC): {e}",)) + return JSONResponse( + status_code=502, + content={"status": "error", "message": "Falha na comunicação RPC com o Controller"} + ) + + except Exception as e: + logger.logIC2(RealmType.ERRORS, (f"Erro inesperado no Listener Endpoint: {e}",)) + return JSONResponse( + status_code=500, + content={"status": "error", "message": "Erro interno no processamento do Endpoint"} + ) diff --git a/backup/src/logs/integracao_01 b/backup/src/logs/integracao_01 new file mode 100644 index 0000000..ee7eea0 --- /dev/null +++ b/backup/src/logs/integracao_01 @@ -0,0 +1,24 @@ +2026-04-01 09:09:33,082 - integracao - INFO - { 'caller': 'ciclo_app', + 'client': '', + 'logDate': '2026-04-01 09:09:33.082579-03:00', + 'msg': '[local] Configuração lida com sucesso pelo Listener Webhook.', + 'origin': '', + 'service': 'tecnospeed', + 'system': 'integracao', + 'version': '2.0.1'} +2026-04-01 09:09:33,700 - integracao - INFO - { 'caller': 'ciclo_app', + 'client': '', + 'logDate': '2026-04-01 09:09:33.700242-03:00', + 'msg': 'Exchange inicializado com sucesso para o Listener Webhook.', + 'origin': '', + 'service': 'tecnospeed', + 'system': 'integracao', + 'version': '2.0.1'} +2026-04-01 09:14:24,402 - integracao - INFO - { 'caller': 'ciclo_app', + 'client': '', + 'logDate': '2026-04-01 09:14:24.402193-03:00', + 'msg': 'Listener Webhook encerrando...', + 'origin': '', + 'service': 'tecnospeed', + 'system': 'integracao', + 'version': '2.0.1'} diff --git a/backup/src/logs/integracao_02 b/backup/src/logs/integracao_02 new file mode 100644 index 0000000..3fc1caa --- /dev/null +++ b/backup/src/logs/integracao_02 @@ -0,0 +1,194 @@ +2026-04-02 09:14:16,362 - integracao - INFO - { 'caller': 'ciclo_app', + 'client': '', + 'logDate': '2026-04-02 09:14:16.362304-03:00', + 'msg': '[local] Configuração lida com sucesso pelo Listener Webhook.', + 'origin': '', + 'service': 'tecnospeed', + 'system': 'integracao', + 'version': '2.0.1'} +2026-04-02 09:14:16,932 - integracao - INFO - { 'caller': 'ciclo_app', + 'client': '', + 'logDate': '2026-04-02 09:14:16.932459-03:00', + 'msg': 'Exchange inicializado com sucesso para o Listener Webhook.', + 'origin': '', + 'service': 'tecnospeed', + 'system': 'integracao', + 'version': '2.0.1'} +2026-04-02 14:14:30,014 - integracao - INFO - { 'caller': 'ciclo_app', + 'client': '', + 'logDate': '2026-04-02 14:14:30.013914-03:00', + 'msg': '[local] Configuração lida com sucesso pelo Listener Webhook.', + 'origin': '', + 'service': 'tecnospeed', + 'system': 'integracao', + 'version': '2.0.1'} +2026-04-02 14:14:31,040 - integracao - INFO - { 'caller': 'ciclo_app', + 'client': '', + 'logDate': '2026-04-02 14:14:31.039753-03:00', + 'msg': 'Exchange inicializado com sucesso para o Listener Webhook.', + 'origin': '', + 'service': 'tecnospeed', + 'system': 'integracao', + 'version': '2.0.1'} +2026-04-02 14:22:20,024 - integracao - INFO - { 'caller': 'receber_webhook_plugnotas', + 'client': '', + 'logDate': '2026-04-02 14:22:20.024167-03:00', + 'msg': 'Webhook recebido - Documento: nfe | Status: CONCLUIDO | ID: ' + '69cab453... | IDIntegracao: | Emitente:', + 'origin': '', + 'service': 'tecnospeed', + 'system': 'integracao', + 'version': '2.0.1'} +2026-04-02 14:22:20,263 - integracao - INFO - { 'caller': 'receber_webhook_plugnotas', + 'client': '', + 'logDate': '2026-04-02 14:22:20.263204-03:00', + 'msg': 'Webhook repassado ao worker com sucesso - Documento: nfe | Status: ' + 'CONCLUIDO | ID: 69cab453...', + 'origin': '', + 'service': 'tecnospeed', + 'system': 'integracao', + 'version': '2.0.1'} +2026-04-02 14:23:45,608 - integracao - INFO - { 'caller': 'receber_webhook_plugnotas', + 'client': '', + 'logDate': '2026-04-02 14:23:45.608300-03:00', + 'msg': 'Webhook recebido - Documento: nfe | Status: CONCLUIDO | ID: ' + '69cab45329f21a7de7691961 | IDIntegracao: XXXYY99999_TESTE445 | ' + 'Emitente: 08187168000160', + 'origin': '', + 'service': 'tecnospeed', + 'system': 'integracao', + 'version': '2.0.1'} +2026-04-02 14:23:46,107 - integracao - INFO - { 'caller': 'receber_webhook_plugnotas', + 'client': '', + 'logDate': '2026-04-02 14:23:46.107170-03:00', + 'msg': 'Webhook repassado ao worker com sucesso - Documento: nfe | Status: ' + 'CONCLUIDO | ID: 69cab45329f21a7de7691961', + 'origin': '', + 'service': 'tecnospeed', + 'system': 'integracao', + 'version': '2.0.1'} +2026-04-02 14:26:07,402 - integracao - INFO - { 'caller': 'ciclo_app', + 'client': '', + 'logDate': '2026-04-02 14:26:07.401683-03:00', + 'msg': 'Listener Webhook encerrando...', + 'origin': '', + 'service': 'tecnospeed', + 'system': 'integracao', + 'version': '2.0.1'} +2026-04-02 14:37:36,191 - integracao - INFO - { 'caller': 'ciclo_app', + 'client': '', + 'logDate': '2026-04-02 14:37:36.190896-03:00', + 'msg': '[local] Configuração lida com sucesso pelo Listener Webhook.', + 'origin': '', + 'service': 'tecnospeed', + 'system': 'integracao', + 'version': '2.0.1'} +2026-04-02 14:37:36,969 - integracao - INFO - { 'caller': 'ciclo_app', + 'client': '', + 'logDate': '2026-04-02 14:37:36.969622-03:00', + 'msg': 'Exchange inicializado com sucesso para o Listener Webhook.', + 'origin': '', + 'service': 'tecnospeed', + 'system': 'integracao', + 'version': '2.0.1'} +2026-04-02 14:37:37,459 - integracao - INFO - { 'caller': 'ciclo_app', + 'client': '', + 'logDate': '2026-04-02 14:37:37.459047-03:00', + 'msg': 'Listener Webhook encerrando...', + 'origin': '', + 'service': 'tecnospeed', + 'system': 'integracao', + 'version': '2.0.1'} +2026-04-02 14:37:42,582 - integracao - INFO - { 'caller': 'ciclo_app', + 'client': '', + 'logDate': '2026-04-02 14:37:42.582792-03:00', + 'msg': '[local] Configuração lida com sucesso pelo Listener Webhook.', + 'origin': '', + 'service': 'tecnospeed', + 'system': 'integracao', + 'version': '2.0.1'} +2026-04-02 14:37:43,391 - integracao - INFO - { 'caller': 'ciclo_app', + 'client': '', + 'logDate': '2026-04-02 14:37:43.391071-03:00', + 'msg': 'Exchange inicializado com sucesso para o Listener Webhook.', + 'origin': '', + 'service': 'tecnospeed', + 'system': 'integracao', + 'version': '2.0.1'} +2026-04-02 14:37:43,782 - integracao - INFO - { 'caller': 'ciclo_app', + 'client': '', + 'logDate': '2026-04-02 14:37:43.781995-03:00', + 'msg': 'Listener Webhook encerrando...', + 'origin': '', + 'service': 'tecnospeed', + 'system': 'integracao', + 'version': '2.0.1'} +2026-04-02 16:03:48,010 - integracao - INFO - { 'caller': 'run', + 'client': '', + 'logDate': '2026-04-02 16:03:48.009993-03:00', + 'msg': 'Conectando ao RabbitMQ', + 'origin': '', + 'service': 'tecnospeed', + 'system': 'integracao', + 'version': '2.0.1'} +2026-04-02 16:03:48,217 - integracao - INFO - { 'caller': 'run', + 'client': '', + 'logDate': '2026-04-02 16:03:48.216535-03:00', + 'msg': "Configuração do sistema carregada - serviceid='local' " + "servicekey='local' origin='local' " + "amqps='amqp://guest:guest@localhost:5672/'", + 'origin': '', + 'service': 'tecnospeed', + 'system': 'integracao', + 'version': '2.0.1'} +2026-04-02 16:12:17,812 - integracao - INFO - { 'caller': 'run', + 'client': '', + 'logDate': '2026-04-02 16:12:17.812369-03:00', + 'msg': 'Conectando ao RabbitMQ', + 'origin': '', + 'service': 'tecnospeed', + 'system': 'integracao', + 'version': '2.0.1'} +2026-04-02 16:12:18,488 - integracao - INFO - { 'caller': 'run', + 'client': '', + 'logDate': '2026-04-02 16:12:18.487913-03:00', + 'msg': "Configuração do sistema carregada - serviceid='local' " + "servicekey='local' origin='local' " + "amqps='amqp://guest:guest@localhost:5672/'", + 'origin': '', + 'service': 'tecnospeed', + 'system': 'integracao', + 'version': '2.0.1'} +2026-04-02 16:12:18,864 - integracao - INFO - { 'caller': 'run', + 'client': '', + 'logDate': '2026-04-02 16:12:18.863889-03:00', + 'msg': 'Erro fatal ao conectar ao RabbitMQ - VERIFY PARAMETERS: ' + 'is_amqp_url: True\n' + ' amqps[amqp://guest:guest@localhost:5672/]\n' + ' origin[local]\n' + ' system[integracao]\n' + ' service[tecnospeed]\n' + ' version[2.0.1\n' + ' client[]', + 'origin': '', + 'service': 'tecnospeed', + 'system': 'integracao', + 'version': '2.0.1'} +2026-04-02 16:12:30,426 - integracao - INFO - { 'caller': 'run', + 'client': '', + 'logDate': '2026-04-02 16:12:30.426681-03:00', + 'msg': 'Conectando ao RabbitMQ', + 'origin': '', + 'service': 'tecnospeed', + 'system': 'integracao', + 'version': '2.0.1'} +2026-04-02 16:12:30,947 - integracao - INFO - { 'caller': 'run', + 'client': '', + 'logDate': '2026-04-02 16:12:30.947199-03:00', + 'msg': "Configuração do sistema carregada - serviceid='local' " + "servicekey='local' origin='local' " + "amqps='amqp://guest:guest@localhost:5672/'", + 'origin': '', + 'service': 'tecnospeed', + 'system': 'integracao', + 'version': '2.0.1'} diff --git a/backup/src/logs/integracao_06 b/backup/src/logs/integracao_06 new file mode 100644 index 0000000..d64ebb0 --- /dev/null +++ b/backup/src/logs/integracao_06 @@ -0,0 +1,56 @@ +2026-04-06 08:25:17,847 - teste_wrapper.integracao - INFO - params: amqps: amqps://iawkzmhh:4iIVXCgLDsLy2oVyVWeV-o6shg-BZ89z@jackal.rmq.cloudamqp.com/iawkzmhh +2026-04-06 08:25:18,327 - teste_wrapper.integracao - INFO - Logger Messages topics: logs.teste_wrapper.integracao. +2026-04-06 08:25:18,361 - teste_wrapper.integracao - INFO - {'caller': 'IC2', + 'client': '', + 'hmac': '862c786853960277c4d518d1eb01143b2fe7782092dbf5403fb61b52127c7829', + 'logDate': '2026-04-06 08:25:18.361486-03:00', + 'msg': "LOGIC2 LOGS [('Testando o novo fo ...]", + 'origin': '', + 'payload': '44102feb05c76aac7b2b', + 'service': 'teste_wrapper', + 'serviceid': 'AF1699F719F1B6C81DE2BD38AB811994E3089B35C8E9314813A770AC7AE25D8D', + 'system': 'integracao', + 'target': '', + 'type': 'logger', + 'version': ''} +2026-04-06 08:25:18,368 - teste_wrapper.integracao - INFO - {'caller': 'logMsg', + 'client': '', + 'hmac': '', + 'logDate': '2026-04-06 08:25:18.367868-03:00', + 'msg': 'Testando o novo formato IC2', + 'origin': '', + 'payload': '', + 'service': 'teste_wrapper', + 'serviceid': 'AF1699F719F1B6C81DE2BD38AB811994E3089B35C8E9314813A770AC7AE25D8D', + 'system': 'integracao', + 'target': '', + 'type': 'logger', + 'version': ''} +2026-04-06 08:28:37,324 - teste_wrapper.integracao - INFO - params: amqps: amqps://iawkzmhh:4iIVXCgLDsLy2oVyVWeV-o6shg-BZ89z@jackal.rmq.cloudamqp.com/iawkzmhh +2026-04-06 08:28:37,834 - teste_wrapper.integracao - INFO - Logger Messages topics: logs.teste_wrapper.integracao. +2026-04-06 08:28:37,855 - teste_wrapper.integracao - INFO - {'caller': 'IC2', + 'client': '', + 'hmac': '841681411efce4ae99ff2602ae79da6284e6c3ca247a27da3a87dedf3771244e', + 'logDate': '2026-04-06 08:28:37.855637-03:00', + 'msg': "LOGIC2 LOGS [('Testando o formato ...]", + 'origin': '', + 'payload': '353fd214129406d78301', + 'service': 'teste_wrapper', + 'serviceid': 'AF1699F719F1B6C81DE2BD38AB811994E3089B35C8E9314813A770AC7AE25D8D', + 'system': 'integracao', + 'target': '', + 'type': 'logger', + 'version': ''} +2026-04-06 08:28:37,865 - teste_wrapper.integracao - INFO - {'caller': 'logMsg', + 'client': '', + 'hmac': '', + 'logDate': '2026-04-06 08:28:37.865799-03:00', + 'msg': 'Testando o formato IC2', + 'origin': '', + 'payload': '', + 'service': 'teste_wrapper', + 'serviceid': 'AF1699F719F1B6C81DE2BD38AB811994E3089B35C8E9314813A770AC7AE25D8D', + 'system': 'integracao', + 'target': '', + 'type': 'logger', + 'version': ''} diff --git a/backup/src/logs/integracao_25 b/backup/src/logs/integracao_25 new file mode 100644 index 0000000..e69de29 diff --git a/backup/src/logs/integracao_26 b/backup/src/logs/integracao_26 new file mode 100644 index 0000000..e69de29 diff --git a/backup/src/logs/integracao_27 b/backup/src/logs/integracao_27 new file mode 100644 index 0000000..f61511b --- /dev/null +++ b/backup/src/logs/integracao_27 @@ -0,0 +1,140 @@ +2026-03-27 14:57:57,807 - integracao - INFO - { 'caller': 'run', + 'client': '', + 'logDate': '2026-03-27 14:57:57.807604-03:00', + 'msg': 'Conectando ao RabbitMQ', + 'origin': '', + 'service': 'tecnospeed', + 'system': 'integracao', + 'version': '2.0.1'} +2026-03-27 14:57:58,282 - integracao - INFO - { 'caller': 'run', + 'client': '', + 'logDate': '2026-03-27 14:57:58.282491-03:00', + 'msg': "Configuração do sistema carregada - serviceid='local' " + "servicekey='local' origin='local' " + "amqps='amqp://guest:guest@localhost:5672/'", + 'origin': '', + 'service': 'tecnospeed', + 'system': 'integracao', + 'version': '2.0.1'} +2026-03-27 14:59:07,273 - integracao - INFO - { 'caller': 'callback', + 'client': 'Test', + 'logDate': '2026-03-27 14:59:07.273291-03:00', + 'msg': "Configuração recebida - {'api_key': None, 'x_api_key': " + "'2da392a6-79d2-4304-a8b7-959572c7e44d', 'user_postgres': " + "'postgres', 'pass_postgres': 'postgres', 'host_postgres': " + "'localhost', 'port_postgres': '5432', 'database_postgres': " + "'tecnospeed'}", + 'origin': '', + 'service': 'tecnospeed', + 'system': 'integracao', + 'version': '2.0.1'} +2026-03-27 14:59:08,015 - integracao - INFO - { 'caller': 'callback', + 'client': 'Test', + 'logDate': '2026-03-27 14:59:08.014993-03:00', + 'msg': 'Configuração incompleta', + 'origin': '', + 'service': 'tecnospeed', + 'system': 'integracao', + 'version': '2.0.1'} +2026-03-27 15:00:56,891 - integracao - INFO - { 'caller': 'callback', + 'client': 'Test', + 'logDate': '2026-03-27 15:00:56.890566-03:00', + 'msg': "Configuração recebida - {'api_key': None, 'x_api_key': " + "'2da392a6-79d2-4304-a8b7-959572c7e44d', 'user_postgres': " + "'postgres', 'pass_postgres': 'postgres', 'host_postgres': " + "'localhost', 'port_postgres': '5432', 'database_postgres': " + "'sg_servico_tecnospeed'}", + 'origin': '', + 'service': 'tecnospeed', + 'system': 'integracao', + 'version': '2.0.1'} +2026-03-27 15:00:57,787 - integracao - INFO - { 'caller': 'callback', + 'client': 'Test', + 'logDate': '2026-03-27 15:00:57.787472-03:00', + 'msg': 'Configuração incompleta', + 'origin': '', + 'service': 'tecnospeed', + 'system': 'integracao', + 'version': '2.0.1'} +2026-03-27 15:02:14,102 - integracao - INFO - { 'caller': 'callback', + 'client': 'Test', + 'logDate': '2026-03-27 15:02:14.101859-03:00', + 'msg': "Configuração recebida - {'api_key': None, 'x_api_key': " + "'2da392a6-79d2-4304-a8b7-959572c7e44d', 'user_postgres': " + "'postgres', 'pass_postgres': 'postgres', 'host_postgres': " + "'localhost', 'port_postgres': '5432', 'database_postgres': " + "'tecnospeed'}", + 'origin': '', + 'service': 'tecnospeed', + 'system': 'integracao', + 'version': '2.0.1'} +2026-03-27 15:02:14,792 - integracao - INFO - { 'caller': 'callback', + 'client': 'Test', + 'logDate': '2026-03-27 15:02:14.791942-03:00', + 'msg': 'Configuração incompleta', + 'origin': '', + 'service': 'tecnospeed', + 'system': 'integracao', + 'version': '2.0.1'} +2026-03-27 15:09:33,329 - integracao - INFO - { 'caller': 'run', + 'client': '', + 'logDate': '2026-03-27 15:09:33.329870-03:00', + 'msg': 'Conectando ao RabbitMQ', + 'origin': '', + 'service': 'tecnospeed', + 'system': 'integracao', + 'version': '2.0.1'} +2026-03-27 15:09:33,698 - integracao - INFO - { 'caller': 'run', + 'client': '', + 'logDate': '2026-03-27 15:09:33.698183-03:00', + 'msg': "Configuração do sistema carregada - serviceid='local' " + "servicekey='local' origin='local' " + "amqps='amqp://guest:guest@localhost:5672/'", + 'origin': '', + 'service': 'tecnospeed', + 'system': 'integracao', + 'version': '2.0.1'} +2026-03-27 15:09:39,159 - integracao - INFO - { 'caller': 'callback', + 'client': 'Test', + 'logDate': '2026-03-27 15:09:39.158573-03:00', + 'msg': "Configuração recebida - {'api_key': None, 'x_api_key': " + "'2da392a6-79d2-4304-a8b7-959572c7e44d', 'user_postgres': " + "'postgres', 'pass_postgres': 'postgres', 'host_postgres': " + "'localhost', 'port_postgres': '5432', 'database_postgres': " + "'tecnospeed'}", + 'origin': '', + 'service': 'tecnospeed', + 'system': 'integracao', + 'version': '2.0.1'} +2026-03-27 15:09:44,975 - integracao - INFO - { 'caller': 'callback', + 'client': 'Test', + 'logDate': '2026-03-27 15:09:44.975168-03:00', + 'msg': 'Configuração completa para o cliente Test', + 'origin': '', + 'service': 'tecnospeed', + 'system': 'integracao', + 'version': '2.0.1'} +2026-03-27 15:09:45,763 - integracao - INFO - { 'caller': 'nfe', + 'client': 'Test', + 'logDate': '2026-03-27 15:09:45.763614-03:00', + 'msg': 'Iniciando serviço de NFe - Método: GET', + 'origin': '', + 'service': 'tecnospeed', + 'system': 'integracao', + 'version': '2.0.1'} +2026-03-27 15:09:46,610 - integracao - INFO - { 'caller': 'nfe', + 'client': 'Test', + 'logDate': '2026-03-27 15:09:46.610671-03:00', + 'msg': 'Consultando resumo de NFe', + 'origin': '', + 'service': 'tecnospeed', + 'system': 'integracao', + 'version': '2.0.1'} +2026-03-27 15:09:47,761 - integracao - INFO - { 'caller': 'nfe', + 'client': 'Test', + 'logDate': '2026-03-27 15:09:47.761417-03:00', + 'msg': 'Operação realizada com sucesso', + 'origin': '', + 'service': 'tecnospeed', + 'system': 'integracao', + 'version': '2.0.1'} diff --git a/backup/src/main.py b/backup/src/main.py new file mode 100644 index 0000000..ca6b9e2 --- /dev/null +++ b/backup/src/main.py @@ -0,0 +1,686 @@ +# IMPORT DAS CLASSES DE TRABALHO +from certificado_digital.certificado_digital import CertificadoDigital +from empresas.empresa import EmpresaController +from NFSe.nfse import NFSeService +from NFSe.Servico.servico import ServicoController +from NFSe.Tomador.tomador import TomadorController +from Relatorios.relatorios import Relatorios +from webhooks.webhooks import Webhooks +from NFe.registroNFe import RegistroNFeService + +# IMPORTS DE MODELS +from Models.config import Config, SystemConfig +from Models.empresa import Empresa +from Models.nfse import NFSe +from Models.servico import Servico +from Models.nfe import NFe +from Models.tomador import Tomador +from Models.payload import validate_payload + +# IMPORTS DE BIBLIOTECAS +from pydantic import ValidationError +import json, sys, time, random, os, uuid + +# IMPORTS DE CLASSES AUXILIARES +from utils.Exchange2 import RPCExchange2, LoggerExchange2, AppIC2Builder +from utils.exchangeModels import RealmType +from utils.banco import Banco + +from utils.redis import RedisClient + +# VERSÃO DO SISTEMA +version = "1.0.0" # Versão inicial do sistema +version = "1.0.1" # Implementando ORM +version = "1.0.2" # Implementando RPC +version = "1.0.3" # Implementando configuração via Redis +version = "2.0.0" # NFe e refatoração da Main +version = "2.0.1" # Refatoração para RPCExchange + + +class Main: + def __init__(self): + self.client = "#" + self.api_key = "" + self.exchange = None + self.banco = None + self.system_config: SystemConfig = None + self.redis = RedisClient(host=os.getenv('REDIS_HOST', 'localhost'), port=6379) + self.service = "tecnospeed" + self.system = "integracao" + self.version = version + self.logger = None + self.url_base = os.getenv('PLUGNOTAS_URL', 'https://api.sandbox.plugnotas.com.br') + self.funcoes = { + "nfe": self.nfe, + "nfse": self.nfse, + "certificado_digital": self.certificado_digital, + "empresa": self.empresa, + "servico": self.servico, + "tomador": self.tomador, + "relatorios": self.relatorios, + "webhooks": self.webhooks + } + + # Métodos de serviços orientado a eventos. + + def nfe(self, metodo, dados=None, correlation_id=None): + # Roteador de emissões, baixas e cancelamentos via NFe + id_usr = dados.get('id_usuario', 1) if dados else 1 + try: + self.logger.logIC2(RealmType.LOGS, (f"Iniciando serviço de NFe - Método: {metodo.upper()}",)) + + nfe_service = RegistroNFeService(api_key=self.api_key, + url_base=self.url_base, + banco=self.banco, + logger=self.logger) + + metodo = metodo.lower() + resultado = None + + match metodo: + case "post": + self.logger.logIC2(RealmType.LOGS, ("Cadastrando NFe",)) + nfe = NFe(**dados.get('nfe', dados)) + resultado = nfe_service.cadastrar_nfe(nfe_list=[nfe], data=dados) + + case "get": + self.logger.logIC2(RealmType.LOGS, ("Consultando resumo de NFe",)) + resultado = nfe_service.consultar_resumo(id_nota=dados.get('id')) + + case "download": + self.logger.logIC2(RealmType.LOGS, ("Baixando PDF NFe",)) + resultado = nfe_service.baixar_pdf(id_nota=dados.get('id')) + + case "download_xml": + self.logger.logIC2(RealmType.LOGS, ("Baixando XML NFe",)) + resultado = nfe_service.baixar_xml(id_nota=dados.get('id')) + + case "cancelar": + self.logger.logIC2(RealmType.LOGS, ("Cancelando NFe",)) + resultado = nfe_service.cancelar_nfe(id_nota=dados.get('id'), justificativa=dados.get('justificativa', 'Cancelamento padrão')) + + case "status_cancelamento": + self.logger.logIC2(RealmType.LOGS, ("Consultando status de cancelamento",)) + resultado = nfe_service.consultar_status_cancelamento(id_nota=dados.get('id')) + + case "cce": + self.logger.logIC2(RealmType.LOGS, ("Enviando Carta de Correção NFe",)) + resultado = nfe_service.enviar_cce(id_nota=dados.get('id'), correcao=dados.get('correcao')) + + case "status_cce": + self.logger.logIC2(RealmType.LOGS, ("Consultando status CCe NFe",)) + resultado = nfe_service.consultar_status_cce(id_nota=dados.get('id')) + + case "resumo": + self.logger.logIC2(RealmType.LOGS, ("Consultando resumo de NFe por CNPJ de integracao",)) + resultado = nfe_service.consultar_resumo_por_cnpj_integracao(cnpj=dados.get('cnpj'), id_integracao=dados.get('id_integracao', dados.get('id'))) + + case _: + self.logger.logIC2(RealmType.ERRORS, ("Método não encontrado",)) + self.banco.logar_sistema(id_usuario=id_usr, acao=metodo.upper(), tabela='nfe', status='ERRO', mensagem='Método não encontrado', cliente=self.client, correlation_id=correlation_id) + return json.dumps({"status": "error", "message": "Método não encontrado"}) + + if isinstance(resultado, dict) and "error" in resultado: + msg = resultado["error"] + if "response" in resultado: + msg = f"{msg} | Detalhes: {resultado['response']}" + raise Exception(msg) + + self.logger.logIC2(RealmType.LOGS, ("Operação realizada com sucesso",)) + self.banco.logar_sistema(id_usuario=id_usr, acao=metodo.upper(), tabela='nfe', status='SUCESSO', mensagem='Operação concluída', json=json.dumps(resultado) if resultado else '{}', cliente=self.client, correlation_id=correlation_id) + return self._responder("ok", resultado) + except Exception as e: + self.logger.logIC2(RealmType.ERRORS, (f"Erro ao executar método {metodo} - {e}",)) + self.banco.logar_sistema(id_usuario=id_usr, acao=metodo.upper() if isinstance(metodo, str) else 'NFE', tabela='nfe', status='ERRO', mensagem=str(e), cliente=self.client, correlation_id=correlation_id) + raise e + + def nfse(self, metodo, dados=None, correlation_id=None): + # Roteador de Notas Fiscais de Roteiro/Serviço (NFSe) + id_usr = dados.get('id_usuario', 1) if dados else 1 + try: + self.logger.logIC2(RealmType.LOGS, (f"Iniciando serviço de NFSe - Método: {metodo.upper()}",)) + + nfse_service = NFSeService(api_key=self.api_key, + banco=self.banco, + url_base=self.url_base, + logger=self.logger) + + metodo = metodo.lower() + resultado = None + + match metodo: + case "post": + self.logger.logIC2(RealmType.LOGS, ("Cadastrando NFSe",)) + nfse = NFSe(**dados) + resultado = nfse_service.cadastrar_nfse(nfse=nfse, data=dados) + + case "get": + self.logger.logIC2(RealmType.LOGS, ("Listando NFSe",)) + resultado = nfse_service.buscar_nfse(id_nfse=dados.get('id')) if dados and 'id' in dados else nfse_service.listar_nfse() + + case "download": + self.logger.logIC2(RealmType.LOGS, ("Baixando NFSe",)) + resultado = nfse_service.baixar_nfse(id_nfse=dados.get('id')) + + case "download_xml": + self.logger.logIC2(RealmType.LOGS, ("Baixando XML NFSe",)) + resultado = nfse_service.baixar_xml_nfse(id_nfse=dados.get('id')) + + case "cancelar": + self.logger.logIC2(RealmType.LOGS, ("Cancelando NFSe",)) + resultado = nfse_service.solicitar_cancelamento_nfse(id_nfse=dados.get('id')) + + case "status_cancelamento": + self.logger.logIC2(RealmType.LOGS, ("Consultando status de cancelamento",)) + resultado = nfse_service.consultar_status_cancelamento_nfse(id_nfse=dados.get('id')) + + case "resumo": + self.logger.logIC2(RealmType.LOGS, ("Consultando resumo de NFSe",)) + resultado = nfse_service.consultar_resumo_nfse(id_nfse=dados.get('id'), cnpj=dados.get('cnpj')) + + case "historico": + self.logger.logIC2(RealmType.LOGS, ("Baixando histórico de NFSe",)) + resultado = nfse_service.baixar_histotico_nfse(id_nfse=dados.get('id'), cnpj=dados.get('cnpj')) + + case _: + self.logger.logIC2(RealmType.ERRORS, ("Método não encontrado",)) + self.banco.logar_sistema(id_usuario=id_usr, acao=metodo.upper(), tabela='nfse', status='ERRO', mensagem='Método não encontrado', cliente=self.client, correlation_id=correlation_id) + return json.dumps({"status": "error", "message": "Método não encontrado"}) + + if isinstance(resultado, dict) and "error" in resultado: + raise Exception(json.dumps(resultado)) + + self.logger.logIC2(RealmType.LOGS, ("Operação realizada com sucesso",)) + self.banco.logar_sistema(id_usuario=id_usr, acao=metodo.upper(), tabela='nfse', status='SUCESSO', mensagem='Operação concluída', json=json.dumps(resultado) if resultado else '{}', cliente=self.client, correlation_id=correlation_id) + return self._responder("ok", resultado) + except Exception as e: + self.logger.logIC2(RealmType.ERRORS, (f"Erro ao executar método {metodo} - {e}",)) + self.banco.logar_sistema(id_usuario=id_usr, acao=metodo.upper() if isinstance(metodo, str) else 'NFSE', tabela='nfse', status='ERRO', mensagem=str(e), cliente=self.client, correlation_id=correlation_id) + raise e + + def certificado_digital(self, metodo, dados=None, correlation_id=None): + # Gerenciamento de envio dos certificados A3/A1 + id_usr = dados.get('id_usuario', 1) if dados else 1 + try: + self.logger.logIC2(RealmType.LOGS, (f"Iniciando serviço de Certificado Digital - Método: {metodo.upper()}",)) + + certificado_digital = CertificadoDigital(api_key=self.api_key, banco=self.banco, url_base=self.url_base, logger=self.logger) + + metodo = metodo.lower() + resultado = None + + match metodo: + case "post": + self.logger.logIC2(RealmType.LOGS, ("Cadastrando certificado digital",)) + resultado = certificado_digital.post_form_data(data=dados) + case "get": + self.logger.logIC2(RealmType.LOGS, ("Listando certificados digitais",)) + resultado = certificado_digital.get(id=dados.get('id')) if dados and 'id' in dados else certificado_digital.get() + case "put": + self.logger.logIC2(RealmType.LOGS, ("Atualizando certificado digital",)) + resultado = certificado_digital.put_certificado(id=dados.get('id'), data=dados) + case _: + self.logger.logIC2(RealmType.ERRORS, ("Método não encontrado",)) + self.banco.logar_sistema(id_usuario=id_usr, acao=metodo.upper(), tabela='certificado_digital', status='ERRO', mensagem='Método não encontrado', cliente=self.client, correlation_id=correlation_id) + return json.dumps({"status": "error", "message": "Método não encontrado"}) + + if isinstance(resultado, dict) and "error" in resultado: + msg = resultado["error"] + if "response" in resultado: + msg = f"{msg} | Detalhes: {resultado['response']}" + raise Exception(msg) + + self.logger.logIC2(RealmType.LOGS, ("Operação realizada com sucesso",)) + self.banco.logar_sistema(id_usuario=id_usr, acao=metodo.upper(), tabela='certificado_digital', status='SUCESSO', mensagem='Operação concluída', cliente=self.client, correlation_id=correlation_id) + return self._responder("ok", resultado) + + except Exception as e: + self.logger.logIC2(RealmType.ERRORS, (f"Erro ao executar método {metodo} - {e}",)) + self.banco.logar_sistema(id_usuario=id_usr, acao=metodo.upper() if isinstance(metodo, str) else 'CERT', tabela='certificado_digital', status='ERRO', mensagem=str(e), cliente=self.client, correlation_id=correlation_id) + raise e + + def empresa(self, metodo, dados=None, correlation_id=None): + # Intermediador do cadastro e listagem das Empresas emissoras + id_usr = dados.get('id_usuario', 1) if dados else 1 + try: + self.logger.logIC2(RealmType.LOGS, (f"Iniciando serviço de Empresa - Método: {metodo.upper()}",)) + + empresa_controller = EmpresaController( + api_key=self.api_key, + banco=self.banco, + url_base=self.url_base, + logger=self.logger + ) + + metodo = metodo.lower() + resultado = None + + match metodo: + case "post": + self.logger.logIC2(RealmType.LOGS, ("Cadastrando empresa",)) + if dados and 'incentivoCultural' in dados: + dados['incentivadorCultural'] = dados.pop('incentivoCultural') + empresa = Empresa(**dados) + resultado = empresa_controller.post(empresa=empresa, data=dados) + + case "get": + self.logger.logIC2(RealmType.LOGS, ("Listando empresas",)) + resultado = empresa_controller.get(cnpj=dados.get('cnpj'), data=dados) if dados and 'cnpj' in dados else empresa_controller.get(data=dados) + + case "post_file": + self.logger.logIC2(RealmType.LOGS, ("Cadastrando empresa via arquivo",)) + if not dados or 'file' not in dados or 'cnpj' not in dados: + raise ValueError("Parâmetros 'file' e 'cnpj' são obrigatórios para 'post_file'") + resultado = empresa_controller.post_file(file=dados['file'], cnpj=dados['cnpj']) + + case "patch": + self.logger.logIC2(RealmType.LOGS, ("Atualizando certificado da empresa",)) + if not dados or 'cnpj' not in dados or 'certificado' not in dados: + raise ValueError("Parâmetros 'cnpj' e 'certificado' (ID) são obrigatórios para 'patch'") + resultado = empresa_controller.patch_certificado(cnpj=dados['cnpj'], certificado=dados['certificado'], data=dados) + + case _: + self.logger.logIC2(RealmType.ERRORS, ("Método não encontrado",)) + self.banco.logar_sistema(id_usuario=id_usr, acao=metodo.upper(), tabela='empresa', status='ERRO', mensagem='Método não encontrado', cliente=self.client, correlation_id=correlation_id) + return json.dumps({"status": "error", "message": "Método não encontrado"}) + + if isinstance(resultado, dict) and "error" in resultado: + msg = resultado["error"] + if "response" in resultado: + msg = f"{msg} | Detalhes: {resultado['response']}" + raise Exception(msg) + + self.logger.logIC2(RealmType.LOGS, ("Operação realizada com sucesso",)) + self.banco.logar_sistema(id_usuario=id_usr, acao=metodo.upper(), tabela='empresa', status='SUCESSO', mensagem='Operação concluída', json=json.dumps(resultado) if resultado else '{}', cliente=self.client, correlation_id=correlation_id) + return self._responder("ok", resultado) + + except Exception as e: + self.logger.logIC2(RealmType.ERRORS, (f"Erro ao executar método {metodo.upper()} - {str(e)}",)) + self.banco.logar_sistema(id_usuario=id_usr, acao=metodo.upper() if isinstance(metodo, str) else 'EMPRESA', tabela='empresa', status='ERRO', mensagem=str(e), cliente=self.client, correlation_id=correlation_id) + return json.dumps({"status": "error", "message": str(e)}) + + def servico(self, metodo, dados=None, correlation_id=None): + # Intermediador do CRUD de Serviços com configurações de impostos + id_usr = dados.get('id_usuario', 1) if dados else 1 + try: + self.logger.logIC2(RealmType.LOGS, (f"Iniciando serviço de Serviço - Método: {metodo.upper()}",)) + + servico_controller = ServicoController( + api_key=self.api_key, + banco=self.banco, + url_base=self.url_base, + logger=self.logger + ) + + metodo = metodo.lower() + resultado = None + + match metodo: + case "post": + self.logger.logIC2(RealmType.LOGS, ("Cadastrando serviço",)) + try: + servico = Servico(**dados) + except ValidationError as e: + self.logger.logIC2(RealmType.ERRORS, (f"Erro ao validar dados do serviço: {e}",)) + self.banco.logar_sistema(id_usuario=id_usr, acao='POST', tabela='servico', status='ERRO', mensagem=f'Validação: {e}', cliente=self.client, correlation_id=correlation_id) + return json.dumps({"status": "error", "message": str(e)}) + resultado = servico_controller.cadastrar_servico(servico=servico, data=dados) + + case "get": + self.logger.logIC2(RealmType.LOGS, ("Listando serviços",)) + if dados and "id" in dados: + resultado = servico_controller.buscar_servico(id_servico=dados.get('id'), data=dados) + else: + resultado = servico_controller.listar_servicos(data=dados) + + case "put": + self.logger.logIC2(RealmType.LOGS, ("Atualizando serviço",)) + resultado = servico_controller.atualizar_servico(id_servico=dados.get('id'), servico=dados) + + case "delete": + self.logger.logIC2(RealmType.LOGS, ("Deletando serviço",)) + resultado = servico_controller.deletar_servico(id_servico=dados.get('id')) + + case _: + self.logger.logIC2(RealmType.ERRORS, ("Método não encontrado",)) + self.banco.logar_sistema(id_usuario=id_usr, acao=metodo.upper(), tabela='servico', status='ERRO', mensagem='Método não encontrado', cliente=self.client, correlation_id=correlation_id) + return json.dumps({"status": "error", "message": "Método não encontrado"}) + + if isinstance(resultado, dict) and "error" in resultado: + msg = resultado["error"] + if "response" in resultado: + msg = f"{msg} | Detalhes: {resultado['response']}" + raise Exception(msg) + + self.logger.logIC2(RealmType.LOGS, ("Operação realizada com sucesso",)) + self.banco.logar_sistema(id_usuario=id_usr, acao=metodo.upper(), tabela='servico', status='SUCESSO', mensagem='Operação concluída', cliente=self.client, correlation_id=correlation_id) + return self._responder("ok", resultado) + + except Exception as e: + self.logger.logIC2(RealmType.ERRORS, (f"Erro ao executar método {metodo} - {e}",)) + self.banco.logar_sistema(id_usuario=id_usr, acao=metodo.upper() if isinstance(metodo, str) else 'SERVICO', tabela='servico', status='ERRO', mensagem=str(e), cliente=self.client, correlation_id=correlation_id) + raise e + + def tomador(self, metodo, dados=None, correlation_id=None): + # Gerencia clientes/clientes fiscais envolvidos nas transações (Tomadores) + id_usr = dados.get('id_usuario', 1) if dados else 1 + try: + self.logger.logIC2(RealmType.LOGS, (f"Iniciando serviço de Tomador - Método: {metodo.upper()}",)) + + tomador_controller = TomadorController( + api_key=self.api_key, + banco=self.banco, + url_base=self.url_base, + logger=self.logger + ) + + metodo = metodo.lower() + resultado = None + + match metodo: + case "post": + self.logger.logIC2(RealmType.LOGS, ("Cadastrando tomador",)) + tomador_model = Tomador(**dados) + resultado = tomador_controller.cadastrar_tomador(tomador=tomador_model, data=dados) + + case "get": + self.logger.logIC2(RealmType.LOGS, ("Listando tomadores",)) + resultado = tomador_controller.buscar_tomador(cpf_cnpj=dados.get('cpf_cnpj')) if dados and 'cpf_cnpj' in dados else tomador_controller.buscar_tomador() + + case "put": + self.logger.logIC2(RealmType.LOGS, ("Atualizando tomador",)) + tomador_model = Tomador(**dados) + resultado = tomador_controller.atualizar_tomador(cpf_cnpj=dados.get('cpf_cnpj'), tomador=tomador_model) + + case "delete": + self.logger.logIC2(RealmType.LOGS, ("Deletando tomador",)) + resultado = tomador_controller.deletar_tomador(cpf_cnpj=dados.get('cpf_cnpj')) + + case _: + self.logger.logIC2(RealmType.ERRORS, ("Método não encontrado",)) + self.banco.logar_sistema(id_usuario=id_usr, acao=metodo.upper(), tabela='tomador', status='ERRO', mensagem='Método não encontrado', cliente=self.client, correlation_id=correlation_id) + return json.dumps({"status": "error", "message": "Método não encontrado"}) + + if isinstance(resultado, dict) and "error" in resultado: + msg = resultado["error"] + if "response" in resultado: + msg = f"{msg} | Detalhes: {resultado['response']}" + raise Exception(msg) + + self.logger.logIC2(RealmType.LOGS, ("Operação realizada com sucesso",)) + self.banco.logar_sistema(id_usuario=id_usr, acao=metodo.upper(), tabela='tomador', status='SUCESSO', mensagem='Operação concluída', cliente=self.client, correlation_id=correlation_id) + return self._responder("ok", resultado) + except Exception as e: + self.logger.logIC2(RealmType.ERRORS, (f"Erro ao executar método {metodo} - {e}",)) + self.banco.logar_sistema(id_usuario=id_usr, acao=metodo.upper() if isinstance(metodo, str) else 'TOMADOR', tabela='tomador', status='ERRO', mensagem=str(e), cliente=self.client, correlation_id=correlation_id) + raise e + + def relatorios(self, metodo, dados=None, correlation_id=None): + # Extrai planilhas mensais/períodicas e relatórios aglomerados + id_usr = dados.get('id_usuario', 1) if dados else 1 + try: + self.logger.logIC2(RealmType.LOGS, (f"Iniciando serviço de Relatórios - Método: {metodo.upper()}",)) + + relatorios = Relatorios( + url_base=self.url_base, + api_key=self.api_key, + banco=self.banco, + logger=self.logger + ) + metodo = metodo.lower() + resultado = None + + match metodo: + case "nfe_emitidas": + self.logger.logIC2(RealmType.LOGS, ("Relatório de NF-e emitidas",)) + resultado = relatorios.nfe_emitidas() + + case "nfe_por_cnpj": + self.logger.logIC2(RealmType.LOGS, ("Relatório de NF-e por CNPJ",)) + resultado = relatorios.nfe_por_cnpj(cnpj=dados.get('cnpj')) + + case "nfse_emitidas": + self.logger.logIC2(RealmType.LOGS, ("Relatório de NFS-e emitidas",)) + resultado = relatorios.nfse_emitidas( + data_inicial=dados.get('data_inicial'), + data_final=dados.get('data_final') + ) + + case "nfse_por_cnpj": + self.logger.logIC2(RealmType.LOGS, ("Relatório de NFS-e por CNPJ",)) + resultado = relatorios.nfse_por_cnpj( + cnpj=dados.get('cnpj'), + data_inicial=dados.get('data_inicial'), + data_final=dados.get('data_final') + ) + + case _: + self.logger.logIC2(RealmType.ERRORS, ("Método não encontrado",)) + self.banco.logar_sistema(id_usuario=id_usr, acao=metodo.upper(), tabela='relatorios', status='ERRO', mensagem='Método não encontrado', cliente=self.client, correlation_id=correlation_id) + return json.dumps({"status": "error", "message": "Método não encontrado"}) + + if isinstance(resultado, dict) and "error" in resultado: + msg = resultado["error"] + if "response" in resultado: + msg = f"{msg} | Detalhes: {resultado['response']}" + raise Exception(msg) + + self.logger.logIC2(RealmType.LOGS, ("Operação realizada com sucesso",)) + self.banco.logar_sistema(id_usuario=id_usr, acao=metodo.upper(), tabela='relatorios', status='SUCESSO', mensagem='Operação concluída', cliente=self.client, correlation_id=correlation_id) + return self._responder("ok", resultado) + + except Exception as e: + self.logger.logIC2(RealmType.ERRORS, (f"Erro ao executar método {metodo} - {e}",)) + self.banco.logar_sistema(id_usuario=id_usr, acao=metodo.upper() if isinstance(metodo, str) else 'RELATORIOS', tabela='relatorios', status='ERRO', mensagem=str(e), cliente=self.client, correlation_id=correlation_id) + raise e + + def webhooks(self, metodo, dados=None, correlation_id=None): + # Gerencia as rotas externas para qual a PlugNotas envia atualização + id_usr = dados.get('id_usuario', 1) if dados else 1 + try: + self.logger.logIC2(RealmType.LOGS, (f"Iniciando serviço de Webhooks - Método: {metodo.upper()}",)) + + webhooks = Webhooks( + url_base=self.url_base, + api_key=self.api_key, + banco=self.banco, + logger=self.logger + ) + + metodo = metodo.lower() + resultado = None + + match metodo: + case "get": + self.logger.logIC2(RealmType.LOGS, ("Consultando webhooks",)) + resultado = webhooks.consultar_webhooks(cnpj=dados.get('cnpj')) + + case "post": + self.logger.logIC2(RealmType.LOGS, ("Criando webhook",)) + resultado = webhooks.criar_webhook(cnpj=dados.get('cnpj'), url=dados.get('url')) + + case "delete": + self.logger.logIC2(RealmType.LOGS, ("Deletando webhook",)) + resultado = webhooks.deletar_webhook(cnpj=dados.get('cnpj')) + + case "put": + self.logger.logIC2(RealmType.LOGS, ("Atualizando webhook",)) + resultado = webhooks.atualizar_webhook(cnpj=dados.get('cnpj'), url=dados.get('url')) + + case "test": + self.logger.logIC2(RealmType.LOGS, ("Testando webhook",)) + resultado = webhooks.testar_webhook(cnpj=dados.get('cnpj')) + + case _: + self.logger.logIC2(RealmType.ERRORS, ("Método não encontrado",)) + self.banco.logar_sistema(id_usuario=id_usr, acao=metodo.upper(), tabela='webhooks', status='ERRO', mensagem='Método não encontrado', cliente=self.client, correlation_id=correlation_id) + return json.dumps({"status": "error", "message": "Método não encontrado"}) + + if isinstance(resultado, dict) and "error" in resultado: + msg = resultado["error"] + if "response" in resultado: + msg = f"{msg} | Detalhes: {resultado['response']}" + raise Exception(msg) + + self.logger.logIC2(RealmType.LOGS, ("Operação realizada com sucesso",)) + self.banco.logar_sistema(id_usuario=id_usr, acao=metodo.upper(), tabela='webhooks', status='SUCESSO', mensagem='Operação concluída', cliente=self.client, correlation_id=correlation_id) + return self._responder("ok", resultado) + + except Exception as e: + self.logger.logIC2(RealmType.ERRORS, (f"Erro ao executar método {metodo} - {e}",)) + self.banco.logar_sistema(id_usuario=id_usr, acao=metodo.upper() if isinstance(metodo, str) else 'WEBHOOKS', tabela='webhooks', status='ERRO', mensagem=str(e), cliente=self.client, correlation_id=correlation_id) + raise e + + def callback(self, message): + #Descriptografa a msg, e repassa pra função correta (ex: nfe) + try: + dado = json.loads(message) + + hmac = dado.get('hmac') + payload = dado.get('payload') + + if hmac and payload: + servicekey = self.system_config.servicekey.ljust(32, '0')[:32] + self.exchange.setCrypto(serviceid=self.system_config.serviceid, + servicekey=servicekey) + + if self.exchange.getPayload(hmac, payload): + payload_dict = json.loads(self.exchange.payload) + + # Validação do payload contra os modelos tipados + try: + if isinstance(payload_dict.get('dados'), dict) and 'incentivoCultural' in payload_dict['dados']: + payload_dict['dados']['incentivadorCultural'] = payload_dict['dados'].pop('incentivoCultural') + + validated_envelope = validate_payload(payload_dict) + self.logger.logIC2(RealmType.LOGS, (f"Payload validado: {payload_dict.get('fila')}/{payload_dict.get('metodo')}",)) + except (ValidationError, ValueError) as ve: + self.logger.logIC2(RealmType.ERRORS, (f"Payload inválido: {str(ve)}",)) + return json.dumps({"status": "error", "message": f"Payload inválido: {str(ve)}"}) + + config = Config(**payload_dict['config']) + + self.client = payload_dict.get('cliente') + self.logger.client = self.client + self.logger.logIC2(RealmType.LOGS, (f"Configuração recebida para cliente {self.client}",)) + + api_key_extraida = config.api_key or config.x_api_key + expected_pg_user = os.getenv("POSTGRES_USER", "tecnospeed") + if config.user_postgres != expected_pg_user: + self.logger.logIC2( + RealmType.ERRORS, + (f"Usuário PostgreSQL inválido: {config.user_postgres}. Permitido: {expected_pg_user}",) + ) + return json.dumps({"status": "error", "message": f"Apenas o usuário PostgreSQL '{expected_pg_user}' é permitido"}) + + if api_key_extraida and config.user_postgres and config.pass_postgres and config.host_postgres and config.port_postgres and config.database_postgres: + + self.api_key = api_key_extraida + self.banco = Banco(user=config.user_postgres, + password=config.pass_postgres, + host=config.host_postgres, + port=config.port_postgres, + database=config.database_postgres) + + self.banco.create_tables() + + self.logger.logIC2(RealmType.LOGS, (f"Configuração completa para o cliente {self.client}",)) + if 'url_base' in payload_dict: + self.url_base = payload_dict['url_base'] + + if payload_dict['fila'] not in self.funcoes: + self.logger.logIC2(RealmType.ERRORS, ("Fila não encontrada",)) + return json.dumps({"status":"error","message":"Fila não encontrada"}) + + corr_id = self.banco.gerar_correlation_id() + self.logger.logIC2(RealmType.LOGS, (f"[{corr_id}] Processando {payload_dict['fila']}/{payload_dict['metodo']} para cliente {self.client}",)) + retorno = self.funcoes[payload_dict['fila']](payload_dict['metodo'], payload_dict['dados'], correlation_id=corr_id) + return retorno + + else: + self.logger.logIC2(RealmType.ERRORS, ("Configuração incompleta",)) + return json.dumps({"status":"error","message":"Configuração incompleta"}) + else: + self.logger.logIC2(RealmType.ERRORS, (f"Erro ao decodificar payload - {payload}",)) + return json.dumps({"status":"error","message":"Erro ao decodificar payload"}) + + else: + if self.logger: + self.logger.logIC2(RealmType.ERRORS, (f"Erro ao receber dado (sem hmac/payload) - {dado}",)) + return json.dumps({"status":"error","message":"Erro ao receber dado"}) + + except Exception as e: + if self.logger: + self.logger.logIC2(RealmType.ERRORS, (f"Erro ao executar callback - {e}",)) + return json.dumps({"status":"error","message":str(e)}) + finally: + if hasattr(self, 'banco') and self.banco: + self.banco.close() + + def run(self): + # Ponto de Partida. Conecta no Redis, constrói os parâmetros AMQP (URL) e inicia o loop RabbitMQ + try: + print("[STATUS] Conectando ao RabbitMQ") + + try: + system_config = self.redis.get("system_config") + if system_config: + system_config = json.loads(system_config) + self.system_config = SystemConfig(**system_config) + print(f"[INFO] Configuração do sistema carregada - {self.system_config}") + else: + msg = "Configuração do sistema não encontrada no Redis" + print(f"[ERRO] {msg}") + raise Exception(msg) + except Exception as e: + msg = f"Erro ao conectar ao Redis - {e}" + print(f"[ERRO] {msg}") + raise + + # Inicializa LoggerExchange2 após obter a URL AMQP do Redis + builder = AppIC2Builder(service_name=self.service, versionstr=self.version, origin=self.system_config.origin) + self.logger = LoggerExchange2( + ic2_builder=builder, + amqps=self.system_config.amqps, + system=self.system, + service=self.service, + version=self.version, + origin=self.system_config.origin + ) + self.logger.logIC2(RealmType.LOGS, ("LoggerExchange2 inicializado com sucesso",)) + + self.exchange = RPCExchange2( + amqps=self.system_config.amqps, + origin=self.system_config.origin, + system=self.system, + service=self.service, + version=self.version, + client=self.client, + workfunction=self.callback, + delivery_mode = 2 + ) + self.logger.logIC2(RealmType.LOGS, (f"Consumindo da fila {self.exchange.queue}",)) + self.exchange.start_consuming() + + except Exception as e: + if self.logger: + self.logger.logIC2(RealmType.ERRORS, (f"Erro fatal ao conectar ao RabbitMQ - {e}",)) + else: + print(f"[ERRO] Erro ao conectar ao RabbitMQ - {e}") + time.sleep(5) + sys.exit(1) + + def _responder(self, status, resultado): + # Auxiliar para formatar a resposta RPC criptografada conforme esperado pelo Exchange2 + print(f"[RPC] Preparando resposta criptografada para o cliente (status: {status})", flush=True) + servicekey = self.system_config.servicekey.ljust(32, '0')[:32] + return json.dumps({ + "status": status, + "payload": resultado, # A classe RPCExchange2 vai criptografar este campo + "serviceid": self.system_config.serviceid, + "servicekey": servicekey + }) + +if __name__ == "__main__": + main = Main() + main.run() diff --git a/backup/src/mainQUEUE.py b/backup/src/mainQUEUE.py new file mode 100644 index 0000000..82c653f --- /dev/null +++ b/backup/src/mainQUEUE.py @@ -0,0 +1,660 @@ +# IMPORT DAS CLASSES DE TRABALHO +from certificado_digital.certificado_digital import CertificadoDigital +from empresas.empresa import EmpresaController +from NFSe.nfse import NFSeService +from NFSe.Servico.servico import ServicoController +from NFSe.Tomador.tomador import TomadorController +from Relatorios.relatorios import Relatorios +from webhooks.webhooks import Webhooks +from NFe.registroNFe import RegistroNFeService + +# IMPORTS DE MODELS +from Models.config import Config, SystemConfig +from Models.empresa import Empresa +from Models.nfse import NFSe +from Models.servico import Servico +from Models.nfe import NFe +from Models.tomador import Tomador +from Models.payload import validate_payload + +# IMPORTS DE BIBLIOTECAS +from pydantic import ValidationError +import json, sys, time, random, os, uuid + +# IMPORTS DE CLASSES AUXILIARES +from utils.Exchange2 import QueueExchange2, LoggerExchange2, AppIC2Builder +from utils.exchangeModels import RealmType +from utils.banco import Banco + +from utils.redis import RedisClient + +# VERSÃO DO SISTEMA +version = "1.0.0" # Versão inicial do sistema +version = "1.0.1" # Implementando ORM +version = "1.0.2" # Implementando RPC +version = "1.0.3" # Implementando configuração via Redis +version = "2.0.0" # NFe e refatoração da Main +version = "2.0.1" # Refatoração para QueueExchange + + +class Main: + def __init__(self): + self.client = "#" + self.api_key = "" + self.exchange = None + self.banco = None + self.system_config: SystemConfig = None + self.redis = RedisClient(host=os.getenv('REDIS_HOST', 'localhost'), port=6379) + self.service = "tecnospeed" + self.system = "integracao" + self.version = version + self.logger = None + self.url_base = os.getenv('PLUGNOTAS_URL', 'https://api.sandbox.plugnotas.com.br') + self.funcoes = { + "nfe": self.nfe, + "nfse": self.nfse, + "certificado_digital": self.certificado_digital, + "empresa": self.empresa, + "servico": self.servico, + "tomador": self.tomador, + "relatorios": self.relatorios, + "webhooks": self.webhooks + } + + # Métodos de serviços orientado a eventos. + + def nfe(self, metodo, dados=None, correlation_id=None): + # Roteador de emissões, baixas e cancelamentos via NFe + id_usr = dados.get('id_usuario', 1) if dados else 1 + try: + self.logger.logIC2(RealmType.LOGS, (f"Iniciando serviço de NFe - Método: {metodo.upper()}",)) + + nfe_service = RegistroNFeService(api_key=self.api_key, + url_base=self.url_base, + banco=self.banco, + logger=self.logger) + + metodo = metodo.lower() + resultado = None + + match metodo: + case "post": + self.logger.logIC2(RealmType.LOGS, ("Cadastrando NFe",)) + nfe = NFe(**dados.get('nfe', dados)) + resultado = nfe_service.cadastrar_nfe(nfe_list=[nfe], data=dados) + + case "get": + self.logger.logIC2(RealmType.LOGS, ("Consultando resumo de NFe",)) + resultado = nfe_service.consultar_resumo(id_nota=dados.get('id')) + + case "download": + self.logger.logIC2(RealmType.LOGS, ("Baixando PDF NFe",)) + resultado = nfe_service.baixar_pdf(id_nota=dados.get('id')) + + case "download_xml": + self.logger.logIC2(RealmType.LOGS, ("Baixando XML NFe",)) + resultado = nfe_service.baixar_xml(id_nota=dados.get('id')) + + case "cancelar": + self.logger.logIC2(RealmType.LOGS, ("Cancelando NFe",)) + resultado = nfe_service.cancelar_nfe(id_nota=dados.get('id'), justificativa=dados.get('justificativa', 'Cancelamento padrão')) + + case "status_cancelamento": + self.logger.logIC2(RealmType.LOGS, ("Consultando status de cancelamento",)) + resultado = nfe_service.consultar_status_cancelamento(id_nota=dados.get('id')) + + case "cce": + self.logger.logIC2(RealmType.LOGS, ("Enviando Carta de Correção NFe",)) + resultado = nfe_service.enviar_cce(id_nota=dados.get('id'), correcao=dados.get('correcao')) + + case "status_cce": + self.logger.logIC2(RealmType.LOGS, ("Consultando status CCe NFe",)) + resultado = nfe_service.consultar_status_cce(id_nota=dados.get('id')) + + case "resumo": + self.logger.logIC2(RealmType.LOGS, ("Consultando resumo de NFe por CNPJ de integracao",)) + resultado = nfe_service.consultar_resumo_por_cnpj_integracao(cnpj=dados.get('cnpj'), id_integracao=dados.get('id_integracao', dados.get('id'))) + + case _: + self.logger.logIC2(RealmType.ERRORS, ("Método não encontrado",)) + self.banco.logar_sistema(id_usuario=id_usr, acao=metodo.upper(), tabela='nfe', status='ERRO', mensagem='Método não encontrado', cliente=self.client, correlation_id=correlation_id) + return json.dumps({"status": "error", "message": "Método não encontrado"}) + + if isinstance(resultado, dict) and "error" in resultado: + msg_erro = resultado["error"] + if "response" in resultado and resultado["response"]: + msg_erro += f" - Detalhes: {json.dumps(resultado['response'])}" + raise Exception(msg_erro) + + self.exchange.setPayload(json.dumps(resultado)) + self.logger.logIC2(RealmType.LOGS, ("Operação realizada com sucesso",)) + self.banco.logar_sistema(id_usuario=id_usr, acao=metodo.upper(), tabela='nfe', status='SUCESSO', mensagem='Operação concluída', json=json.dumps(resultado) if resultado else '{}', cliente=self.client, correlation_id=correlation_id) + return json.dumps({"status": "ok", "data": resultado}) + except Exception as e: + msg_completa = str(e) + self.logger.logIC2(RealmType.ERRORS, (f"Erro ao executar método {metodo} - {msg_completa}",)) + self.banco.logar_sistema(id_usuario=id_usr, acao=metodo.upper() if isinstance(metodo, str) else 'NFE', tabela='nfe', status='ERRO', mensagem=msg_completa, cliente=self.client, correlation_id=correlation_id) + raise e + + def nfse(self, metodo, dados=None, correlation_id=None): + # Roteador de Notas Fiscais de Roteiro/Serviço (NFSe) + id_usr = dados.get('id_usuario', 1) if dados else 1 + try: + self.logger.logIC2(RealmType.LOGS, (f"Iniciando serviço de NFSe - Método: {metodo.upper()}",)) + + nfse_service = NFSeService(api_key=self.api_key, + banco=self.banco, + url_base=self.url_base, + logger=self.logger) + + metodo = metodo.lower() + resultado = None + + match metodo: + case "post": + self.logger.logIC2(RealmType.LOGS, ("Cadastrando NFSe",)) + nfse = NFSe(**dados.get('nfse', dados)) + resultado = nfse_service.cadastrar_nfse(nfse=nfse, data=dados) + + case "get": + self.logger.logIC2(RealmType.LOGS, ("Listando NFSe",)) + resultado = nfse_service.buscar_nfse(id_nfse=dados.get('id')) if dados and 'id' in dados else nfse_service.listar_nfse() + + case "download": + self.logger.logIC2(RealmType.LOGS, ("Baixando NFSe",)) + resultado = nfse_service.baixar_nfse(id_nfse=dados.get('id')) + + case "download_xml": + self.logger.logIC2(RealmType.LOGS, ("Baixando XML NFSe",)) + resultado = nfse_service.baixar_xml_nfse(id_nfse=dados.get('id')) + + case "cancelar": + self.logger.logIC2(RealmType.LOGS, ("Cancelando NFSe",)) + resultado = nfse_service.solicitar_cancelamento_nfse(id_nfse=dados.get('id')) + + case "status_cancelamento": + self.logger.logIC2(RealmType.LOGS, ("Consultando status de cancelamento",)) + resultado = nfse_service.consultar_status_cancelamento_nfse(id_nfse=dados.get('id')) + + case "resumo": + self.logger.logIC2(RealmType.LOGS, ("Consultando resumo de NFSe",)) + resultado = nfse_service.consultar_resumo_nfse(id_nfse=dados.get('id'), cnpj=dados.get('cnpj')) + + case "historico": + self.logger.logIC2(RealmType.LOGS, ("Baixando histórico de NFSe",)) + resultado = nfse_service.baixar_histotico_nfse(id_nfse=dados.get('id'), cnpj=dados.get('cnpj')) + + case _: + self.logger.logIC2(RealmType.ERRORS, ("Método não encontrado",)) + self.banco.logar_sistema(id_usuario=id_usr, acao=metodo.upper(), tabela='nfse', status='ERRO', mensagem='Método não encontrado', cliente=self.client, correlation_id=correlation_id) + return json.dumps({"status": "error", "message": "Método não encontrado"}) + + if isinstance(resultado, dict) and "error" in resultado: + raise Exception(json.dumps(resultado)) + + self.exchange.setPayload(json.dumps(resultado)) + self.logger.logIC2(RealmType.LOGS, ("Operação realizada com sucesso",)) + self.banco.logar_sistema(id_usuario=id_usr, acao=metodo.upper(), tabela='nfse', status='SUCESSO', mensagem='Operação concluída', json=json.dumps(resultado) if resultado else '{}', cliente=self.client, correlation_id=correlation_id) + return json.dumps({"status": "ok", "data": resultado}) + except Exception as e: + self.logger.logIC2(RealmType.ERRORS, (f"Erro ao executar método {metodo} - {e}",)) + self.banco.logar_sistema(id_usuario=id_usr, acao=metodo.upper() if isinstance(metodo, str) else 'NFSE', tabela='nfse', status='ERRO', mensagem=str(e), cliente=self.client, correlation_id=correlation_id) + raise e + + def certificado_digital(self, metodo, dados=None, correlation_id=None): + # Gerenciamento de envio dos certificados A3/A1 + id_usr = dados.get('id_usuario', 1) if dados else 1 + try: + self.logger.logIC2(RealmType.LOGS, (f"Iniciando serviço de Certificado Digital - Método: {metodo.upper()}",)) + + certificado_digital = CertificadoDigital(api_key=self.api_key, banco=self.banco, url_base=self.url_base, logger=self.logger) + + metodo = metodo.lower() + resultado = None + + match metodo: + case "post": + self.logger.logIC2(RealmType.LOGS, ("Cadastrando certificado digital",)) + resultado = certificado_digital.post_form_data(data=dados) + case "get": + self.logger.logIC2(RealmType.LOGS, ("Listando certificados digitais",)) + resultado = certificado_digital.get(id=dados.get('id')) if dados and 'id' in dados else certificado_digital.get() + case "put": + self.logger.logIC2(RealmType.LOGS, ("Atualizando certificado digital",)) + resultado = certificado_digital.put_certificado(id=dados.get('id'), data=dados) + case _: + self.logger.logIC2(RealmType.ERRORS, ("Método não encontrado",)) + self.banco.logar_sistema(id_usuario=id_usr, acao=metodo.upper(), tabela='certificado_digital', status='ERRO', mensagem='Método não encontrado', cliente=self.client, correlation_id=correlation_id) + return json.dumps({"status": "error", "message": "Método não encontrado"}) + + if isinstance(resultado, dict) and "error" in resultado: + raise Exception(resultado["error"]) + + self.exchange.setPayload(json.dumps(resultado)) + self.logger.logIC2(RealmType.LOGS, ("Operação realizada com sucesso",)) + self.banco.logar_sistema(id_usuario=id_usr, acao=metodo.upper(), tabela='certificado_digital', status='SUCESSO', mensagem='Operação concluída', cliente=self.client, correlation_id=correlation_id) + return json.dumps({"status": "ok", "data": resultado}) + + except Exception as e: + self.logger.logIC2(RealmType.ERRORS, (f"Erro ao executar método {metodo} - {e}",)) + self.banco.logar_sistema(id_usuario=id_usr, acao=metodo.upper() if isinstance(metodo, str) else 'CERT', tabela='certificado_digital', status='ERRO', mensagem=str(e), cliente=self.client, correlation_id=correlation_id) + raise e + + def empresa(self, metodo, dados=None, correlation_id=None): + # Intermediador do cadastro e listagem das Empresas emissoras + id_usr = dados.get('id_usuario', 1) if dados else 1 + try: + self.logger.logIC2(RealmType.LOGS, (f"Iniciando serviço de Enterprise - Método: {metodo.upper()}",)) + + empresa_controller = EmpresaController( + api_key=self.api_key, + banco=self.banco, + url_base=self.url_base, + logger=self.logger + ) + + metodo = metodo.lower() + resultado = None + + match metodo: + case "post": + self.logger.logIC2(RealmType.LOGS, ("Cadastrando empresa",)) + empresa = Empresa(**dados) + resultado = empresa_controller.post(empresa=empresa, data=dados) + + case "get": + self.logger.logIC2(RealmType.LOGS, ("Listando empresas",)) + resultado = empresa_controller.get(cnpj=dados.get('cnpj'), data=dados) if dados and 'cnpj' in dados else empresa_controller.get(data=dados) + + case "post_file": + self.logger.logIC2(RealmType.LOGS, ("Cadastrando empresa via arquivo",)) + if not dados or 'file' not in dados or 'cnpj' not in dados: + raise ValueError("Parâmetros 'file' e 'cnpj' são obrigatórios para 'post_file'") + resultado = empresa_controller.post_file(file=dados['file'], cnpj=dados['cnpj']) + + case _: + self.logger.logIC2(RealmType.ERRORS, ("Método não encontrado",)) + self.banco.logar_sistema(id_usuario=id_usr, acao=metodo.upper(), tabela='Enterprise', status='ERRO', mensagem='Método não encontrado', cliente=self.client, correlation_id=correlation_id) + return json.dumps({"status": "error", "message": "Método não encontrado"}) + + if isinstance(resultado, dict) and "error" in resultado: + raise Exception(resultado["error"]) + + self.exchange.setPayload(json.dumps(resultado)) + self.logger.logIC2(RealmType.LOGS, ("Operação realizada com sucesso",)) + self.banco.logar_sistema(id_usuario=id_usr, acao=metodo.upper(), tabela='Enterprise', status='SUCESSO', mensagem='Operação concluída', json=json.dumps(resultado) if resultado else '{}', cliente=self.client, correlation_id=correlation_id) + return json.dumps({"status": "ok", "data": resultado}) + + except Exception as e: + print(f"\n[ERRO CRITICAL] {metodo.upper()} -> {str(e)}\n", flush=True); self.logger.logIC2(RealmType.ERRORS, (f"Erro ao executar método {metodo.upper()} - {str(e)}",)) + self.banco.logar_sistema(id_usuario=id_usr, acao=metodo.upper() if isinstance(metodo, str) else 'ENTERPRISE', tabela='Enterprise', status='ERRO', mensagem=str(e), cliente=self.client, correlation_id=correlation_id) + return json.dumps({"status": "error", "message": str(e)}) + + def servico(self, metodo, dados=None, correlation_id=None): + # Intermediador do CRUD de Serviços com configurações de impostos + id_usr = dados.get('id_usuario', 1) if dados else 1 + try: + self.logger.logIC2(RealmType.LOGS, (f"Iniciando serviço de Serviço - Método: {metodo.upper()}",)) + + servico_controller = ServicoController( + api_key=self.api_key, + banco=self.banco, + url_base=self.url_base, + logger=self.logger + ) + + metodo = metodo.lower() + resultado = None + + match metodo: + case "post": + self.logger.logIC2(RealmType.LOGS, ("Cadastrando serviço",)) + try: + servico = Servico(**dados) + except ValidationError as e: + self.logger.logIC2(RealmType.ERRORS, (f"Erro ao validar dados do serviço: {e}",)) + self.banco.logar_sistema(id_usuario=id_usr, acao='POST', tabela='servico', status='ERRO', mensagem=f'Validação: {e}', cliente=self.client, correlation_id=correlation_id) + return json.dumps({"status": "error", "message": str(e)}) + resultado = servico_controller.cadastrar_servico(servico=servico, data=dados) + + case "get": + self.logger.logIC2(RealmType.LOGS, ("Listando serviços",)) + if dados and "id" in dados: + resultado = servico_controller.buscar_servico(id_servico=dados.get('id'), data=dados) + else: + resultado = servico_controller.listar_servicos(data=dados) + + case "put": + self.logger.logIC2(RealmType.LOGS, ("Atualizando serviço",)) + resultado = servico_controller.atualizar_servico(id_servico=dados.get('id'), servico=dados) + + case "delete": + self.logger.logIC2(RealmType.LOGS, ("Deletando serviço",)) + resultado = servico_controller.deletar_servico(id_servico=dados.get('id')) + + case _: + self.logger.logIC2(RealmType.ERRORS, ("Método não encontrado",)) + self.banco.logar_sistema(id_usuario=id_usr, acao=metodo.upper(), tabela='servico', status='ERRO', mensagem='Método não encontrado', cliente=self.client, correlation_id=correlation_id) + return json.dumps({"status": "error", "message": "Método não encontrado"}) + + if isinstance(resultado, dict) and "error" in resultado: + raise Exception(resultado["error"]) + + self.exchange.setPayload(json.dumps(resultado)) + self.logger.logIC2(RealmType.LOGS, ("Operação realizada com sucesso",)) + self.banco.logar_sistema(id_usuario=id_usr, acao=metodo.upper(), tabela='servico', status='SUCESSO', mensagem='Operação concluída', cliente=self.client, correlation_id=correlation_id) + return json.dumps({"status": "ok", "data": resultado}) + + except Exception as e: + self.logger.logIC2(RealmType.ERRORS, (f"Erro ao executar método {metodo} - {e}",)) + self.banco.logar_sistema(id_usuario=id_usr, acao=metodo.upper() if isinstance(metodo, str) else 'SERVICO', tabela='servico', status='ERRO', mensagem=str(e), cliente=self.client, correlation_id=correlation_id) + raise e + + def tomador(self, metodo, dados=None, correlation_id=None): + # Gerencia clientes/clientes fiscais envolvidos nas transações (Tomadores) + id_usr = dados.get('id_usuario', 1) if dados else 1 + try: + self.logger.logIC2(RealmType.LOGS, (f"Iniciando serviço de Tomador - Método: {metodo.upper()}",)) + + tomador_controller = TomadorController( + api_key=self.api_key, + banco=self.banco, + url_base=self.url_base, + logger=self.logger + ) + + metodo = metodo.lower() + resultado = None + + match metodo: + case "post": + self.logger.logIC2(RealmType.LOGS, ("Cadastrando tomador",)) + tomador_model = Tomador(**dados) + resultado = tomador_controller.cadastrar_tomador(tomador=tomador_model, data=dados) + + case "get": + self.logger.logIC2(RealmType.LOGS, ("Listando tomadores",)) + resultado = tomador_controller.buscar_tomador(cpf_cnpj=dados.get('cpf_cnpj')) if dados and 'cpf_cnpj' in dados else tomador_controller.buscar_tomador() + + case "put": + self.logger.logIC2(RealmType.LOGS, ("Atualizando tomador",)) + tomador_model = Tomador(**dados) + resultado = tomador_controller.atualizar_tomador(cpf_cnpj=dados.get('cpf_cnpj'), tomador=tomador_model) + + case "delete": + self.logger.logIC2(RealmType.LOGS, ("Deletando tomador",)) + resultado = tomador_controller.deletar_tomador(cpf_cnpj=dados.get('cpf_cnpj')) + + case _: + self.logger.logIC2(RealmType.ERRORS, ("Método não encontrado",)) + self.banco.logar_sistema(id_usuario=id_usr, acao=metodo.upper(), tabela='tomador', status='ERRO', mensagem='Método não encontrado', cliente=self.client, correlation_id=correlation_id) + return json.dumps({"status": "error", "message": "Método não encontrado"}) + + if isinstance(resultado, dict) and "error" in resultado: + raise Exception(resultado["error"]) + + self.exchange.setPayload(json.dumps(resultado)) + self.logger.logIC2(RealmType.LOGS, ("Operação realizada com sucesso",)) + self.banco.logar_sistema(id_usuario=id_usr, acao=metodo.upper(), tabela='tomador', status='SUCESSO', mensagem='Operação concluída', cliente=self.client, correlation_id=correlation_id) + return json.dumps({"status": "ok", "data": resultado}) + except Exception as e: + self.logger.logIC2(RealmType.ERRORS, (f"Erro ao executar método {metodo} - {e}",)) + self.banco.logar_sistema(id_usuario=id_usr, acao=metodo.upper() if isinstance(metodo, str) else 'TOMADOR', tabela='tomador', status='ERRO', mensagem=str(e), cliente=self.client, correlation_id=correlation_id) + raise e + + def relatorios(self, metodo, dados=None, correlation_id=None): + # Extrai planilhas mensais/períodicas e relatórios aglomerados + id_usr = dados.get('id_usuario', 1) if dados else 1 + try: + self.logger.logIC2(RealmType.LOGS, (f"Iniciando serviço de Relatórios - Método: {metodo.upper()}",)) + + relatorios = Relatorios( + url_base=self.url_base, + api_key=self.api_key, + banco=self.banco, + logger=self.logger + ) + metodo = metodo.lower() + resultado = None + + match metodo: + case "nfe_emitidas": + self.logger.logIC2(RealmType.LOGS, ("Relatório de NF-e emitidas",)) + resultado = relatorios.nfe_emitidas() + + case "nfe_por_cnpj": + self.logger.logIC2(RealmType.LOGS, ("Relatório de NF-e por CNPJ",)) + resultado = relatorios.nfe_por_cnpj(cnpj=dados.get('cnpj')) + + case "nfse_emitidas": + self.logger.logIC2(RealmType.LOGS, ("Relatório de NFS-e emitidas",)) + resultado = relatorios.nfse_emitidas(data_inicial=dados.get('data_inicial'), data_final=dados.get('data_final')) + + case "nfse_por_cnpj": + self.logger.logIC2(RealmType.LOGS, ("Relatório de NFS-e por CNPJ",)) + resultado = relatorios.nfse_por_cnpj(cnpj=dados.get('cnpj'), data_inicial=dados.get('data_inicial'), data_final=dados.get('data_final')) + + case _: + self.logger.logIC2(RealmType.ERRORS, ("Método não encontrado",)) + self.banco.logar_sistema(id_usuario=id_usr, acao=metodo.upper(), tabela='relatorios', status='ERRO', mensagem='Método não encontrado', cliente=self.client, correlation_id=correlation_id) + return json.dumps({"status": "error", "message": "Método não encontrado"}) + + if isinstance(resultado, dict) and "error" in resultado: + raise Exception(resultado["error"]) + + self.exchange.setPayload(json.dumps(resultado)) + self.logger.logIC2(RealmType.LOGS, ("Operação realizada com sucesso",)) + self.banco.logar_sistema(id_usuario=id_usr, acao=metodo.upper(), tabela='relatorios', status='SUCESSO', mensagem='Operação concluída', cliente=self.client, correlation_id=correlation_id) + return json.dumps({"status": "ok", "data": resultado}) + + except Exception as e: + self.logger.logIC2(RealmType.ERRORS, (f"Erro ao executar método {metodo} - {e}",)) + self.banco.logar_sistema(id_usuario=id_usr, acao=metodo.upper() if isinstance(metodo, str) else 'RELATORIOS', tabela='relatorios', status='ERRO', mensagem=str(e), cliente=self.client, correlation_id=correlation_id) + raise e + + def webhooks(self, metodo, dados=None, correlation_id=None): + # Gerencia as rotas externas para qual a PlugNotas envia atualização + id_usr = dados.get('id_usuario', 1) if dados else 1 + try: + self.logger.logIC2(RealmType.LOGS, (f"Iniciando serviço de Webhooks - Método: {metodo.upper()}",)) + + webhooks = Webhooks( + url_base=self.url_base, + api_key=self.api_key, + banco=self.banco, + logger=self.logger + ) + + metodo = metodo.lower() + resultado = None + + match metodo: + case "get": + self.logger.logIC2(RealmType.LOGS, ("Consultando webhooks",)) + resultado = webhooks.consultar_webhooks(cnpj=dados.get('cnpj')) + + case "post": + self.logger.logIC2(RealmType.LOGS, ("Criando webhook",)) + resultado = webhooks.criar_webhook(cnpj=dados.get('cnpj'), url=dados.get('url')) + + case "delete": + self.logger.logIC2(RealmType.LOGS, ("Deletando webhook",)) + resultado = webhooks.deletar_webhook(cnpj=dados.get('cnpj')) + + case "put": + self.logger.logIC2(RealmType.LOGS, ("Atualizando webhook",)) + resultado = webhooks.atualizar_webhook(cnpj=dados.get('cnpj'), url=dados.get('url')) + + case "test": + self.logger.logIC2(RealmType.LOGS, ("Testando webhook",)) + resultado = webhooks.testar_webhook(cnpj=dados.get('cnpj')) + + case _: + self.logger.logIC2(RealmType.ERRORS, ("Método não encontrado",)) + self.banco.logar_sistema(id_usuario=id_usr, acao=metodo.upper(), tabela='webhooks', status='ERRO', mensagem='Método não encontrado', cliente=self.client, correlation_id=correlation_id) + return json.dumps({"status": "error", "message": "Método não encontrado"}) + + if isinstance(resultado, dict) and "error" in resultado: + raise Exception(resultado["error"]) + + self.exchange.setPayload(json.dumps(resultado)) + self.logger.logIC2(RealmType.LOGS, ("Operação realizada com sucesso",)) + self.banco.logar_sistema(id_usuario=id_usr, acao=metodo.upper(), tabela='webhooks', status='SUCESSO', mensagem='Operação concluída', cliente=self.client, correlation_id=correlation_id) + + except Exception as e: + self.logger.logIC2(RealmType.ERRORS, (f"Erro ao executar método {metodo} - {e}",)) + self.banco.logar_sistema(id_usuario=id_usr, acao=metodo.upper() if isinstance(metodo, str) else 'WEBHOOKS', tabela='webhooks', status='ERRO', mensagem=str(e), cliente=self.client, correlation_id=correlation_id) + raise e + + def callback(self, ch, method, properties, body): + #Descriptografa a msg, e repassa pra função correta (ex: nfe) + try: + message = body.decode("utf-8") + dado = json.loads(message) + + hmac = dado.get('hmac') + payload = dado.get('payload') + + if hmac and payload: + time.sleep(random.uniform(1,5)) + servicekey = self.system_config.servicekey.ljust(32, '0')[:32] + self.exchange.setCrypto(serviceid=self.system_config.serviceid, + servicekey=servicekey) + + if self.exchange.getPayload(hmac, payload): + payload_dict = json.loads(self.exchange.payload) + + # Validação do payload contra os modelos tipados + try: + if isinstance(payload_dict.get('dados'), dict) and 'incentivoCultural' in payload_dict['dados']: + payload_dict['dados']['incentivadorCultural'] = payload_dict['dados'].pop('incentivoCultural') + + validated_envelope = validate_payload(payload_dict) + self.logger.logIC2(RealmType.LOGS, (f"Payload validado: {payload_dict.get('fila')}/{payload_dict.get('metodo')}",)) + except (ValidationError, ValueError) as ve: + self.logger.logIC2(RealmType.ERRORS, (f"Payload inválido: {str(ve)}",)) + ch.basic_nack(delivery_tag=method.delivery_tag, requeue=False) + return json.dumps({"status": "error", "message": f"Payload inválido: {str(ve)}"}) + + config = Config(**payload_dict['config']) + + self.client = payload_dict.get('cliente') + self.logger.client = self.client + self.logger.logIC2(RealmType.LOGS, (f"Configuração recebida para cliente {self.client}",)) + + api_key_extraida = config.api_key or config.x_api_key + expected_pg_user = os.getenv("POSTGRES_USER", "tecnospeed") + if config.user_postgres != expected_pg_user: + self.logger.logIC2( + RealmType.ERRORS, + (f"Usuário PostgreSQL inválido: {config.user_postgres}. Permitido: {expected_pg_user}",) + ) + ch.basic_nack(delivery_tag=method.delivery_tag, requeue=False) + return json.dumps({"status": "error", "message": f"Apenas o usuário PostgreSQL '{expected_pg_user}' é permitido"}) + + if api_key_extraida and config.user_postgres and config.pass_postgres and config.host_postgres and config.port_postgres and config.database_postgres: + + self.api_key = api_key_extraida + self.banco = Banco(user=config.user_postgres, + password=config.pass_postgres, + host=config.host_postgres, + port=config.port_postgres, + database=config.database_postgres) + + self.banco.create_tables() + + self.logger.logIC2(RealmType.LOGS, (f"Configuração completa para o cliente {self.client}",)) + if 'url_base' in payload_dict: + self.url_base = payload_dict['url_base'] + + if payload_dict['fila'] not in self.funcoes: + self.logger.logIC2(RealmType.ERRORS, ("Fila não encontrada",)) + ch.basic_nack(delivery_tag=method.delivery_tag, requeue=False) + return json.dumps({"status":"error","message":"Fila não encontrada"}) + + corr_id = self.banco.gerar_correlation_id() + self.logger.logIC2(RealmType.LOGS, (f"[{corr_id}] Processando {payload_dict['fila']}/{payload_dict['metodo']} para cliente {self.client}",)) + retorno = self.funcoes[payload_dict['fila']](payload_dict['metodo'], payload_dict['dados'], correlation_id=corr_id) + ch.basic_ack(delivery_tag=method.delivery_tag) + return retorno + + else: + self.logger.logIC2(RealmType.ERRORS, ("Configuração incompleta",)) + ch.basic_nack(delivery_tag=method.delivery_tag, requeue=False) + return json.dumps({"status":"error","message":"Configuração incompleta"}) + else: + self.logger.logIC2(RealmType.ERRORS, (f"Erro ao decodificar payload - {payload}",)) + ch.basic_nack(delivery_tag=method.delivery_tag, requeue=False) + return json.dumps({"status":"error","message":"Erro ao decodificar payload"}) + + else: + if self.logger: + self.logger.logIC2(RealmType.ERRORS, (f"Erro ao receber dado (sem hmac/payload) - {dado}",)) + ch.basic_nack(delivery_tag=method.delivery_tag, requeue=False) + return json.dumps({"status":"error","message":"Erro ao receber dado"}) + + except Exception as e: + if self.logger: + self.logger.logIC2(RealmType.ERRORS, (f"Erro ao executar callback - {e}",)) + ch.basic_nack(delivery_tag=method.delivery_tag, requeue=False) + return json.dumps({"status":"error","message":str(e)}) + finally: + if hasattr(self, 'banco') and self.banco: + self.banco.close() + + def run(self): + # Ponto de Partida. Conecta no Redis, constrói os parâmetros AMQP (URL) e inicia o loop RabbitMQ + try: + print("[STATUS] Conectando ao RabbitMQ") + + try: + system_config = self.redis.get("system_config") + if system_config: + system_config = json.loads(system_config) + self.system_config = SystemConfig(**system_config) + print(f"[INFO] Configuração do sistema carregada - {self.system_config}") + else: + msg = "Configuração do sistema não encontrada no Redis" + print(f"[ERRO] {msg}") + raise Exception(msg) + except Exception as e: + msg = f"Erro ao conectar ao Redis - {e}" + print(f"[ERRO] {msg}") + raise + + # Inicializa LoggerExchange2 após obter a URL AMQP do Redis + builder = AppIC2Builder(service_name=self.service, versionstr=self.version, origin=self.system_config.origin) + self.logger = LoggerExchange2( + ic2_builder=builder, + amqps=self.system_config.amqps, + system=self.system, + service=self.service, + version=self.version, + origin=self.system_config.origin + ) + self.logger.logIC2(RealmType.LOGS, ("LoggerExchange2 inicializado com sucesso",)) + + self.exchange = QueueExchange2( + amqps=self.system_config.amqps, + origin=self.system_config.origin, + system=self.system, + client=self.client, + service=self.service, + version=self.version, + callback=self.callback, + delivery_mode = 1) + self.logger.logIC2(RealmType.LOGS, (f"Consumindo da fila {self.exchange.queue}",)) + self.exchange.start_consuming() + + except Exception as e: + print(f"[ERRO FATAL (CONSOLE)] Erro ao conectar ao RabbitMQ - {e}") + if self.logger: + self.logger.logIC2(RealmType.ERRORS, (f"Erro fatal ao conectar ao RabbitMQ - {e}",)) + else: + print(f"[ERRO] Erro ao conectar ao RabbitMQ - {e}") + time.sleep(5) + sys.exit(1) + +if __name__ == "__main__": + main = Main() + main.run() diff --git a/backup/src/main_listener.py b/backup/src/main_listener.py new file mode 100644 index 0000000..582883b --- /dev/null +++ b/backup/src/main_listener.py @@ -0,0 +1,21 @@ +import sys +import os +import uvicorn + +sys.path.append(os.path.dirname(os.path.abspath(__file__))) + +from listenerEndpoint import app + +# O EndpointListener tem a função de manter a aplicação servindo de webhooks ativa recebendo HTTP e convertendo para RabbitMQ +class EndpointListener: + def __init__(self): + self.host = "0.0.0.0" + self.port = int(os.getenv("WEBHOOK_PORT", 8000)) + + def run(self): + print(f"[STATUS] Endpoint Listener rodando na porta {self.port}...") + uvicorn.run(app, host=self.host, port=self.port) + +if __name__ == "__main__": + main = EndpointListener() + main.run() \ No newline at end of file diff --git a/backup/src/tests/__init__.py b/backup/src/tests/__init__.py new file mode 100644 index 0000000..65140f2 --- /dev/null +++ b/backup/src/tests/__init__.py @@ -0,0 +1 @@ +# tests package diff --git a/backup/src/tests/conftest.py b/backup/src/tests/conftest.py new file mode 100644 index 0000000..db70125 --- /dev/null +++ b/backup/src/tests/conftest.py @@ -0,0 +1,119 @@ +""" +Fixtures compartilhados para os testes E2E do servico_plugnotas. + +Cria um banco SQLite in-memory, mocks do Logger e da Api, +e uma instância pré-configurada da classe Main para cada teste. +""" +import pytest +import json +import sys +from unittest.mock import MagicMock, patch + +mock_pika = MagicMock() +sys.modules["pika"] = mock_pika + +mock_redis_module = MagicMock() +sys.modules["redis"] = mock_redis_module + +from utils.banco import Base, Banco, LogTable +from utils.api import Api + +@pytest.fixture +def banco_teste(): + """Cria um Banco real com SQLite in-memory para cada teste (isolado).""" + from sqlalchemy import create_engine + from sqlalchemy.orm import sessionmaker + + engine = create_engine("sqlite:///:memory:") + Base.metadata.create_all(engine) + Session = sessionmaker(bind=engine) + + banco = Banco.__new__(Banco) + banco.engine = engine + banco.session = Session() + + yield banco + + banco.session.close() + engine.dispose() + +@pytest.fixture +def mock_logger(): + """Logger que grava todas as chamadas de log em uma lista para inspeção.""" + logger = MagicMock() + logger.logs = [] + logger.client = "TestClient" + + def capture_log(realm, msg_tuple): + logger.logs.append({"realm": str(realm), "message": msg_tuple[0] if msg_tuple else ""}) + + logger.logIC2.side_effect = capture_log + return logger + +@pytest.fixture +def mock_api(): + """Api que retorna respostas configuráveis sem fazer chamadas HTTP reais.""" + api = MagicMock(spec=Api) + api.get.return_value = {"status": "ok"} + api.post.return_value = {"documents": [{"id": "PLUG-001"}]} + api.put.return_value = {"status": "ok"} + api.patch.return_value = {"status": "ok"} + api.delete.return_value = {"status": "ok"} + api.post_file.return_value = {"status": "ok"} + api.post_form_data.return_value = {"data": {"id": "CERT-001"}} + api.get_paginated.return_value = {"data": [], "total": 0} + return api + +@pytest.fixture +def mock_exchange(): + """Exchange que simula setPayload sem enviar nada.""" + exchange = MagicMock() + exchange.payload = "" + exchange.payloads_set = [] + + def capture_payload(p): + exchange.payloads_set.append(p) + exchange.payload = p + + exchange.setPayload.side_effect = capture_payload + return exchange + +@pytest.fixture +def main_instance(banco_teste, mock_logger, mock_exchange): + with patch("utils.Exchange2.RPCExchange2"), \ + patch("utils.Exchange2.QueueExchange2"), \ + patch("utils.Exchange2.LoggerExchange2"), \ + patch("utils.redis.RedisClient"): + + from main import Main + + with patch.object(Main, '__init__', lambda self: None): + main = Main() + + main.client = "TestClient" + main.api_key = "test-api-key-fake" + main.exchange = mock_exchange + main.banco = banco_teste + main.system_config = MagicMock() + main.system_config.serviceid = "test-id" + main.system_config.servicekey = "test-key-32-chars-long-12345678" + main.redis = MagicMock() + main.service = "tecnospeed" + main.system = "integracao" + main.version = "2.0.1-test" + main.logger = mock_logger + main.url_base = "https://api.sandbox.plugnotas.com.br" + + main.funcoes = { + "nfe": main.nfe, + "nfse": main.nfse, + "certificado_digital": main.certificado_digital, + "empresa": main.empresa, + "servico": main.servico, + "tomador": main.tomador, + "relatorios": main.relatorios, + "webhooks": main.webhooks, + } + + return main + diff --git a/backup/src/tests/debug_payload.py b/backup/src/tests/debug_payload.py new file mode 100644 index 0000000..611e9bb --- /dev/null +++ b/backup/src/tests/debug_payload.py @@ -0,0 +1,67 @@ +import json +import os +import sys + +# Garante que o Python encontre os módulos do projeto +sys.path.append(os.path.dirname(os.path.dirname(os.path.abspath(__file__)))) + +from Models.payload import validate_payload + +sample_payload = { + "fila": "nfse", + "cliente": "MassiveNFSeTest", + "metodo": "POST", + "dados": { + "id_empresa": "some-uuid", + "id_usuario": 1, + "idIntegracao": "TEST_123", + "prestador": { + "cpfCnpj": "00000000000000" + }, + "tomador": { + "cpfCnpj": "08114280956", + "razaoSocial": "NFSE EMITIDA EM HOMOLOGACAO", + "email": "contato@tecnospeed.com.br", + "endereco": { + "logradouro": "AVENIDA DUQUE DE CAXIAS", + "numero": "882", + "bairro": "CENTRO", + "codigoCidade": "4115200", + "descricaoCidade": "MARINGA", + "estado": "PR", + "cep": "87020025", + "tipoLogradouro": "Avenida" + } + }, + "servico": { + "codigo": "1.01", + "codigoTributacao": "1.01", + "discriminacao": "SERVICO DE TESTE DE CARGA MASSIVA", + "cnae": "6201501", + "iss": { + "tipoTributacao": 1, + "exigibilidade": 1, + "aliquota": 2.0 + }, + "valor": { + "servico": 100.0 + } + } + }, + "config": { + "x_api_key": "2da392a6-79d2-4304-a8b7-959572c7e44d", + "user_postgres": "tecnospeed", + "pass_postgres": "u7QimG7E!SF//JC2c8", + "host_postgres": "banco", + "port_postgres": "5432", + "database_postgres": "tecnospeed" + }, + "url_base": "https://api.sandbox.plugnotas.com.br" +} + +try: + print("Tentando validar payload...") + validated = validate_payload(sample_payload) + print("Sucesso! Payload válido.") +except Exception as e: + print(f"Erro de validação: {e}") diff --git a/backup/src/tests/debug_report_api.py b/backup/src/tests/debug_report_api.py new file mode 100644 index 0000000..69bc364 --- /dev/null +++ b/backup/src/tests/debug_report_api.py @@ -0,0 +1,33 @@ +import requests +import os +import json + +def debug_report(): + api_key = "2da392a6-79d2-4304-a8b7-959572c7e44d" # From src/utils/api.py comment + url = "https://api.sandbox.plugnotas.com.br/nfse/relatorio?from=2026-04-01&to=2026-04-30" + + headers = { + "x-api-key": api_key + } + + print(f"Testing URL: {url}") + response = requests.get(url, headers=headers) + + print(f"Status Code: {response.status_code}") + try: + print(f"Response Body: {json.dumps(response.json(), indent=2)}") + except: + print(f"Response Body: {response.text}") + + # Test with ISO format + url_iso = "https://api.sandbox.plugnotas.com.br/nfse/relatorio?from=2026-04-01T00:00:00&to=2026-04-30T23:59:59" + print(f"\nTesting URL (ISO): {url_iso}") + response = requests.get(url_iso, headers=headers) + print(f"Status Code: {response.status_code}") + try: + print(f"Response Body: {json.dumps(response.json(), indent=2)}") + except: + print(f"Response Body: {response.text}") + +if __name__ == "__main__": + debug_report() diff --git a/backup/src/tests/purge_queue.py b/backup/src/tests/purge_queue.py new file mode 100644 index 0000000..e5c00c5 --- /dev/null +++ b/backup/src/tests/purge_queue.py @@ -0,0 +1,46 @@ +import pika +import os +import sys +import json + +# Garante que o Python encontre os módulos do projeto +sys.path.append(os.path.dirname(os.path.dirname(os.path.abspath(__file__)))) + +from utils.redis import RedisClient + +def purge_queue(): + # 1. Obter URL do RabbitMQ do Redis + redis_host = os.getenv('REDIS_HOST', 'localhost') + redis_client = RedisClient(host=redis_host, port=6379) + try: + system_config_data = redis_client.get("system_config") + if not system_config_data: + print("Erro: system_config não encontrado no Redis.") + return + system_config = json.loads(system_config_data) + amqps_url = system_config.get("amqps") + except Exception as e: + print(f"Erro ao ler configs do Redis: {e}") + return + + if not amqps_url: + print("Erro: AMQPS URL não encontrada na configuração.") + return + + # 2. Conectar e limpar a fila + queue_name = "integracao_tecnospeed" + try: + params = pika.URLParameters(amqps_url) + connection = pika.BlockingConnection(params) + channel = connection.channel() + + # Tenta realizar o purge + result = channel.queue_purge(queue=queue_name) + print(f"Sucesso! Fila [{queue_name}] limpa. Mensagens removidas: {result.method.message_count}") + + connection.close() + except Exception as e: + print(f"Erro ao limpar a fila: {e}") + +if __name__ == "__main__": + purge_queue() diff --git a/backup/src/tests/test.py b/backup/src/tests/test.py new file mode 100644 index 0000000..257b05d --- /dev/null +++ b/backup/src/tests/test.py @@ -0,0 +1,134 @@ +import sys +import os +import json + +sys.path.append(os.path.dirname(os.path.abspath(__file__))) + +from Models.config import SystemConfig +from utils.redis import RedisClient +from utils.Exchange2 import RPCExchange2 + +def main(): + print("Iniciando script de testes da mensageria...") + + # Carregando configuração do Redis + redis_client = RedisClient(host=os.getenv('REDIS_HOST', 'localhost'), port=6379) + try: + system_config_data = redis_client.get("system_config") + if not system_config_data: + print("Erro: system_config não encontrado no Redis. Certifique-se de que a variável existe.") + return + + system_config = SystemConfig(**json.loads(system_config_data)) + print(f"[{system_config.origin}] Configuração do sistema lida com sucesso.") + + except Exception as e: + print(f"Erro ao ler configs do Redis: {e}") + return + + # Inicializando mensageria RPC + try: + amqps_local = system_config.amqps.replace("host.docker.internal", "localhost") + exchange = RPCExchange2( + amqps=amqps_local, + origin=system_config.origin, + system="integracao", + service="tecnospeed", + version="2.0.1", + client="#" + ) + except Exception as e: + print(f"Falha ao inicializar mensageria: {e}") + return + + # Configurando criptografia e payload + servicekey = system_config.servicekey.ljust(32, '0')[:32] + exchange.setCrypto(serviceid=system_config.serviceid, servicekey=servicekey) + + # Payload da mensageria + + payload_interno = { + "fila": "nfse", + "cliente": "Test", + "metodo": "POST", + "dados": { + "idIntegracao": "INTEGRACAO-NFSE-002", + "prestador": { + "cpfCnpj": "08187168000160" + }, + "tomador": { + "cpfCnpj": "08114280956", + "razaoSocial": "João Tomador da Silva", + "inscricaoMunicipal": "isento", + "email": "tomador@email.com", + "endereco": { + "descricaoCidade": "São Paulo", + "cep": "00000000", + "tipoLogradouro": "Rua", + "logradouro": "Dois de Ouro", + "tipoBairro": "Bairro", + "codigoCidade": "3550308", + "complemento": "Apto 13", + "estado": "SP", + "numero": "44", + "bairro": "Centro" + } + }, + "servico": { + "codigo": "1.01", + "codigoTributacao": "1.01", + "discriminacao": "Serviços prestados de tecnologia", + "cnae": "6204000", + "iss": { + "tipoTributacao": 6, + "exigibilidade": 1, + "aliquota": 2.5 + }, + "valor": { + "servico": 1500.50, + "descontoCondicionado": 0.0, + "descontoIncondicionado": 0.0 + } + } + }, + "config": { + "x_api_key": os.getenv('PLUGNOTAS_API_KEY', ''), + "user_postgres": os.getenv('POSTGRES_USER', 'tecnospeed'), + "pass_postgres": os.getenv('POSTGRES_PASSWORD', 'tecnospeed'), + "host_postgres": os.getenv('POSTGRES_HOST', 'localhost'), + "port_postgres": os.getenv('POSTGRES_PORT', '5432'), + "database_postgres": os.getenv('POSTGRES_DB', 'tecnospeed') + }, + "url_base": "https://api.sandbox.plugnotas.com.br" + } + + + # Criptografando e enviando tarefa + exchange.setPayload(json.dumps(payload_interno)) + + print("Disparando chamada RPC para o RabbitMQ...") + # Executando chamada RPC + response = exchange.callRPC("Disparo de testes executado pelo test.py") + print("\n[RAW RPC RESPONSE]:") + print(response) + + # Descriptografando a resposta + try: + resp_obj = json.loads(response) + hmac_resp = resp_obj.get('hmac') + payload_resp = resp_obj.get('payload') + + if hmac_resp and payload_resp: + if exchange.getPayload(hmac_resp, payload_resp): + print("\n[DECRYPTED RPC DATA]:") + print(json.dumps(json.loads(exchange.payload), indent=2)) + else: + print("\n[ERROR]: Failed to decrypt response.") + else: + print("\n[INFO]: Response does not contain an encrypted payload.") + + except Exception as e: + print(f"\n[ERROR]: Error processing response: {e}") + +if __name__ == "__main__": + main() diff --git a/backup/src/tests/testArch.py b/backup/src/tests/testArch.py new file mode 100644 index 0000000..1fe2d01 --- /dev/null +++ b/backup/src/tests/testArch.py @@ -0,0 +1,58 @@ +import pytest +from unittest.mock import patch, MagicMock +from fastapi.testclient import TestClient +from src.listenerEndpoint import app + +client = TestClient(app) + +PAYLOAD_VALIDO = { + "id": "60a7c4f1c9d2a00015...", + "status": "CONCLUIDO", + "documento": "nfe" +} +HEADERS_VALIDOS = {"x-plugnotas-signature": "assinatura_hmac_valida"} + +class TestPlugNotasWebhookEndpoint: + + @patch("src.listenerEndpoint.exchange") + def test_deve_receber_payload_e_enfileirar_com_sucesso(self, mock_exchange): + """Critério: Caminho feliz. Valida, enfileira e responde com sucesso (200).""" + mock_exchange.sendTask.return_value = True + + response = client.post("/listenerPlugnotas", json=PAYLOAD_VALIDO, headers=HEADERS_VALIDOS) + + assert response.status_code == 200 + assert response.json() == {"status": "ok", "message": "Webhook recebido e encaminhado para processamento"} + + mock_exchange.setPayload.assert_called_once() + mock_exchange.sendTask.assert_called_once_with("Webhook PlugNotas Recebido") + + @patch("src.listenerEndpoint.exchange") + def test_deve_rejeitar_payload_invalido_antes_de_processar(self, mock_exchange): + """Critério: Integridade de Dados. Valida campos obrigatórios recebidos do Hook.""" + payload_invalido = {"status": "CONCLUIDO"} # Faltando ID e documento + + response = client.post("/listenerPlugnotas", json=payload_invalido, headers=HEADERS_VALIDOS) + + assert response.status_code == 200 + assert response.json()["status"] == "warning" + assert "Campos faltantes" in response.json()["message"] + + @patch("src.listenerEndpoint.exchange") + def test_deve_retornar_erro_se_internal_error_no_rabbitmq(self, mock_exchange): + """Critério: Resiliência. Retorna 500 em caso de erro ao enviar para fila.""" + # Simulando uma queda ao enviar task + mock_exchange.sendTask.side_effect = Exception("RabbitMQ is down") + + response = client.post("/listenerPlugnotas", json=PAYLOAD_VALIDO, headers=HEADERS_VALIDOS) + + assert response.status_code == 500 + assert response.json()["message"] == "Erro interno no processamento do webhook" + + def test_deve_retornar_erro_se_exchange_nao_inicializado(self): + """Critério: Resiliência. Retorna 500 se o exchange global for nulo.""" + # Sem patch de exchange, então pegará o exchange default (None) em client sem lifespan completo + response = client.post("/listenerPlugnotas", json=PAYLOAD_VALIDO) + + assert response.status_code == 500 + assert response.json()["message"] == "Serviço temporariamente indisponível" \ No newline at end of file diff --git a/backup/src/tests/test_1000_empresas.py b/backup/src/tests/test_1000_empresas.py new file mode 100644 index 0000000..c24a82a --- /dev/null +++ b/backup/src/tests/test_1000_empresas.py @@ -0,0 +1,144 @@ +import os +import sys +import json +import time + +# Garante que o Python encontre os módulos do projeto +sys.path.append(os.path.dirname(os.path.dirname(os.path.abspath(__file__)))) + +from Models.config import SystemConfig +from utils.redis import RedisClient +from utils.Exchange2 import RPCExchange2, QueueExchange2 + +def get_empresa_payload(i): + """Gera um payload de empresa com CNPJ e Razão Social baseados no índice i""" + # Gerando um CNPJ fictício baseado no loop para evitar duplicidade no banco + # Ex: 94074986000001, 94074986000002... + cnpj = f"94074986{str(i).zfill(6)}" + + return { + "id_usuario": 1, + "cpfCnpj": cnpj, + "razaoSocial": f"Varejo de Teste {i} LTDA", + "nomeFantasia": f"Varejo {i}", + "inscricaoMunicipal": "0001", + "inscricaoEstadual": "123456789", + "simplesNacional": True, + "regimeTributario": 1, + "regimeTributarioEspecial": 1, + "incentivadorCultural": False, + "incentivoFiscal": True, + "certificado": "5af59d271f6e8f409178fbf3", + "endereco": { + "codigoPais": "1058", + "descricaoPais": "BRASIL", + "tipoLogradouro": "Avenida", + "logradouro": "Avenida de Teste", + "numero": str(i), + "complemento": "Casa", + "tipoBairro": "Bairro", + "bairro": "Centro", + "codigoCidade": "4115200", + "descricaoCidade": "Maringá", + "estado": "PR", + "cep": "87020025" + }, + "nfe": {"ativo": True, "tipoContrato": 0}, + "nfse": {"ativo": False} + } + +def run_rpc_test(exchange, config_rpc): + """Envia uma única empresa via RPC e aguarda a resposta (bom para debug)""" + print("\n--- INICIANDO TESTE RPC INDIVIDUAL ---") + payload_dados = get_empresa_payload(1) + + full_payload = { + "fila": "empresa", + "metodo": "post", + "cliente": "tray", + "url_base": "https://api.sandbox.plugnotas.com.br", + "config": config_rpc, + "dados": payload_dados + } + + print(f"Enviando empresa {payload_dados['cpfCnpj']} via RPC...") + exchange.setPayload(json.dumps(full_payload)) + resposta = exchange.callRPC(json.dumps(full_payload), timeout=30) + + print("\nRESPOSTA DO SERVIDOR:") + print(resposta) + print("--------------------------------------\n") + +def run_mass_test(exchange, config_rpc, total=1000): + """Envia N empresas via Fila (sem esperar resposta), ideal para teste de carga""" + print(f"\n--- INICIANDO CARGA MASSIVA: {total} Mensagens ---") + start_time = time.time() + + for i in range(1, total + 1): + payload_dados = get_empresa_payload(i) + + full_payload = { + "fila": "empresa", + "metodo": "post", + "cliente": "MassTest", + "url_base": "https://api.sandbox.plugnotas.com.br", + "config": config_rpc, + "dados": payload_dados + } + + exchange.setPayload(json.dumps(full_payload)) + exchange.sendMsg(json.dumps(full_payload), service="tecnospeed") + + if i % 100 == 0: + print(f"Enviadas: {i}/{total}...") + + end_time = time.time() + print(f"Carga concluída em {end_time - start_time:.2f} segundos.") + print("--------------------------------------\n") + +def main(): + # 1. Carrega configurações do Redis + redis_host = os.getenv('REDIS_HOST', 'localhost') + redis_client = RedisClient(host=redis_host, port=6379) + + try: + config_data = redis_client.get("system_config") + if not config_data: + print("Erro: system_config não encontrado no Redis.") + return + sc = SystemConfig(**json.loads(config_data)) + except Exception as e: + print(f"Erro ao carregar configs: {e}") + return + + # Configurações de Banco e API para o Worker + config_rpc = { + "x_api_key": "2da392a6-79d2-4304-a8b7-959572c7e44d", + "user_postgres": "tecnospeed", + "pass_postgres": "u7QimG7E%21SF%2F%2FJC2c8", + "host_postgres": "banco", + "port_postgres": "5432", + "database_postgres": "tecnospeed" + } + + print(f"Conectando ao RabbitMQ de {sc.origin}...") + + # 2. Escolha do Modo de Operação + # Mude para True para testar o retorno (RPC Server) ou False para carga assíncrona (Queue Worker) + MODO_DEBUG_RPC = True + + if MODO_DEBUG_RPC: + # Usa RPCExchange2 para modo Síncrono (Request-Response) + exchange = RPCExchange2(sc.amqps, sc.origin, "integracao", "tecnospeed", "2.1.0", client="") + servicekey = sc.servicekey.ljust(32, '0')[:32] + exchange.setCrypto(sc.serviceid, servicekey) + run_rpc_test(exchange, config_rpc) + else: + # Usa QueueExchange2 para modo Assíncrono (Fire and Forget) + exchange = QueueExchange2(sc.amqps, sc.origin, "integracao", "tecnospeed", "2.1.0", client="#") + servicekey = sc.servicekey.ljust(32, '0')[:32] + exchange.setCrypto(sc.serviceid, servicekey) + run_mass_test(exchange, config_rpc, total=1000) + +if __name__ == "__main__": + main() \ No newline at end of file diff --git a/backup/src/tests/test_1000_nfe.py b/backup/src/tests/test_1000_nfe.py new file mode 100644 index 0000000..feac5a8 --- /dev/null +++ b/backup/src/tests/test_1000_nfe.py @@ -0,0 +1,219 @@ +import sys +import os +import json +import time + +# Adiciona o diretório atual ao path para importar as classes locais +sys.path.append(os.path.dirname(os.path.abspath(__file__))) + +from Models.config import SystemConfig +from utils.redis import RedisClient +from utils.Exchange2 import QueueExchange2 +from utils.banco import Banco, EnterpriseTable +import random + +def main(): + print("Iniciando script de teste de carga: 1000 NFes...", flush=True) + + # 1. Carregando configuração do Redis para obter dados do RabbitMQ + redis_host = os.getenv('REDIS_HOST', 'localhost') + redis_client = RedisClient(host=redis_host, port=6379) + try: + system_config_data = redis_client.get("system_config") + if not system_config_data: + print("Erro: system_config não encontrado no Redis.") + return + + system_config = SystemConfig(**json.loads(system_config_data)) + print(f"Configuração [{system_config.origin}] carregada.", flush=True) + except Exception as e: + print(f"Erro ao ler configs do Redis: {e}") + return + + # 2. Inicializando a conexão com a FILA (RabbitMQ) + try: + print("Tentando conectar ao RabbitMQ...", flush=True) + exchange = QueueExchange2( + amqps=system_config.amqps, + origin=system_config.origin, + system="integracao", + service="tecnospeed", + version="2.1.0", + client="#" + ) + except Exception as e: + print(f"Falha ao iniciar mensageria: {e}") + return + + # 3. Configurando criptografia (necessário para o worker aceitar a mensagem) + servicekey = system_config.servicekey.ljust(32, '0')[:32] + exchange.setCrypto(serviceid=system_config.serviceid, servicekey=servicekey) + + # 4. Buscando empresas válidas no banco para o teste + try: + # Configuração do banco - Força conexão interna se estiver no Docker + # O .env às vezes aponta para o IP externo que não resolve dentro do container + db_host = os.getenv('POSTGRES_HOST', 'postgres') + db_port = int(os.getenv('POSTGRES_PORT', 5432)) + + # Se estivermos DENTRO do docker, o host 'postgres' é o correto. + if db_host != 'postgres': + print(f"Aviso: POSTGRES_HOST no ambiente é {db_host}. Tentando 'postgres' primeiro por estar no Docker...") + db_host = 'postgres' + db_port = 5432 + + print(f"Conectando ao banco em {db_host}:{db_port}...") + + banco = Banco( + host=db_host, + port=db_port, + user=os.getenv('POSTGRES_USER', 'tecnospeed'), + password=os.getenv('POSTGRES_PASSWORD', 'u7QimG7E!SF//JC2c8'), + database=os.getenv('POSTGRES_DB', 'tecnospeed') + ) + print("Executando query na tabela Enterprise...", flush=True) + empresas = banco.session.query(EnterpriseTable.id, EnterpriseTable.cnpj).filter(EnterpriseTable.cnpj != None, EnterpriseTable.cnpj != "").limit(100).all() + lista_empresas = [{"id": emp.id, "cnpj": emp.cnpj} for emp in empresas] + if not lista_empresas: + print("Erro: Nenhuma empresa com CNPJ válido encontrada na tabela 'Enterprise'.", flush=True) + return + print(f"Sucesso: {len(lista_empresas)} empresas válidas encontradas para o teste.", flush=True) + except Exception as e: + print(f"Erro ao conectar ao banco de dados: {e}") + return + + # Base do payload fornecido pelo usuário (omitindo emitente fixo para preencher no loop) + payload_base = { + "nfe": { + "idIntegracao": "", # Será preenchido no loop + "presencial": True, + "consumidorFinal": True, + "natureza": "OPERAÇÃO INTERNA", + "destinatario": { + "cpfCnpj": "08114280956", + "razaoSocial": "NF-E EMITIDA EM HOMOLOGACAO", + "email": "contato@tecnospeed.com.br", + "endereco": { + "logradouro": "AVENIDA DUQUE DE CAXIAS", + "numero": "882", + "bairro": "CENTRO", + "codigoCidade": "4115200", + "descricaoCidade": "MARINGA", + "estado": "PR", + "cep": "87020025" + } + }, + "itens": [ + { + "codigo": "1", + "descricao": "PRODUTO DE TESTE", + "ncm": "06029090", + "cest": "0123456", + "cfop": "5101", + "valorUnitario": { + "comercial": 4.67, + "tributavel": 4.67 + }, + "valor": 4.67, + "tributos": { + "icms": { + "origem": "0", + "cst": "00", + "baseCalculo": { + "modalidadeDeterminacao": 0, + "valor": 0 + }, + "aliquota": 0, + "valor": 0 + }, + "pis": { + "cst": "99", + "baseCalculo": { + "valor": 0, + "quantidade": 0 + }, + "aliquota": 0, + "valor": 0 + }, + "cofins": { + "cst": "07", + "baseCalculo": { + "valor": 0 + }, + "aliquota": 0, + "valor": 0 + } + } + } + ], + "pagamentos": [ + { + "aVista": True, + "meio": "01", + "valor": 4.6 + } + ], + "responsavelTecnico": { + "cpfCnpj": "08187168000160", + "nome": "Tecnospeed", + "email": "contato@tecnospeed.com.br", + "telefone": { + "ddd": "44", + "numero": "30379500" + } + } + } + } + + # Estrutura completa exigida pelo mainQUEUE.py + def get_full_payload(id_num, id_empresa, cnpj_empresa): + # Gerando um timestamp para garantir idIntegracao único + ts = int(time.time()) + payload = json.loads(json.dumps(payload_base)) # Deep copy simples + payload["nfe"]["idIntegracao"] = f"TESTE_{ts}_{id_num}" + payload["nfe"]["emitente"] = { + "cpfCnpj": cnpj_empresa, + "inscricaoEstadual": "ISENTO" + } + + return { + "fila": "nfe", + "cliente": "LoadTest1000", + "metodo": "POST", + "dados": { + "id_empresa": id_empresa, + **payload + }, + "config": { + "x_api_key": "2da392a6-79d2-4304-a8b7-959572c7e44d", + "user_postgres": "tecnospeed", + "pass_postgres": "u7QimG7E!SF//JC2c8", + "host_postgres": "postgres", + "port_postgres": "5432", + "database_postgres": "tecnospeed" + }, + "url_base": "https://api.sandbox.plugnotas.com.br" + } + + print(f"Enviando 1000 mensagens para a fila...") + + start_time = time.time() + + for i in range(1, 1001): + empresa_sorteada = random.choice(lista_empresas) + full_payload = get_full_payload(i, id_empresa=empresa_sorteada["id"], cnpj_empresa=empresa_sorteada["cnpj"]) + # É necessário criptografar o payload antes de cada envio + exchange.setPayload(json.dumps(full_payload)) + exchange.sendMsg(json.dumps(full_payload), service="tecnospeed") + + if i % 100 == 0: + print(f"Enviadas: {i}/1000...", flush=True) + + end_time = time.time() + + print("\nConcluído!") + print(f"Tempo total de envio: {end_time - start_time:.2f} segundos.") + print("Agora você pode acompanhar o processamento nos logs do worker (tecnospeed-queue).") + +if __name__ == "__main__": + main() diff --git a/backup/src/tests/test_1000_nfse.py b/backup/src/tests/test_1000_nfse.py new file mode 100644 index 0000000..62b6a41 --- /dev/null +++ b/backup/src/tests/test_1000_nfse.py @@ -0,0 +1,173 @@ +import sys +import os +import json +import time +import random + +# Adiciona o diretório atual ao path para importar as classes locais +sys.path.append(os.path.dirname(os.path.abspath(__file__))) + +# Adiciona o diretório src ao path para encontrar os Models e Utils +sys.path.append(os.path.dirname(os.path.dirname(os.path.abspath(__file__)))) + +from Models.config import SystemConfig +from utils.redis import RedisClient +from utils.Exchange2 import QueueExchange2 +from utils.banco import Banco, EnterpriseTable + +def main(): + print("Iniciando script de teste de carga: 1000 NFSe...", flush=True) + + # 1. Carregando configuração do Redis para obter dados do RabbitMQ + redis_host = os.getenv('REDIS_HOST', 'localhost') + redis_client = RedisClient(host=redis_host, port=6379) + try: + system_config_data = redis_client.get("system_config") + if not system_config_data: + print("Erro: system_config não encontrado no Redis.") + return + + system_config = SystemConfig(**json.loads(system_config_data)) + print(f"Configuração [{system_config.origin}] carregada.", flush=True) + except Exception as e: + print(f"Erro ao ler configs do Redis: {e}") + return + + # 2. Inicializando a conexão com a FILA (RabbitMQ) + try: + print("Tentando conectar ao RabbitMQ...", flush=True) + exchange = QueueExchange2( + amqps=system_config.amqps, + origin=system_config.origin, + system="integracao", + service="tecnospeed", + version="2.1.0", + client="#" + ) + except Exception as e: + print(f"Falha ao iniciar mensageria: {e}") + return + + # 3. Configurando criptografia (necessário para o worker aceitar a mensagem) + servicekey = system_config.servicekey.ljust(32, '0')[:32] + exchange.setCrypto(serviceid=system_config.serviceid, servicekey=servicekey) + + # 4. Buscando empresas válidas no banco para o teste + try: + db_host = os.getenv('POSTGRES_HOST', 'banco') + db_port = int(os.getenv('POSTGRES_PORT', 5432)) + + # Se estivermos DENTRO do docker e o host configurado for o IP externo ou porta mapeada, forçamos 'banco' e '5432' + if os.path.exists('/.dockerenv') or os.path.exists('/proc/1/cgroup'): + print(f"Aviso: Ambiente Docker detectado. Forçando host 'banco' e porta 5432.") + db_host = 'banco' + db_port = 5432 + + print(f"Conectando ao banco em {db_host}:{db_port}...") + + banco = Banco( + host=db_host, + port=db_port, + user=os.getenv('POSTGRES_USER', 'tecnospeed'), + password=os.getenv('POSTGRES_PASSWORD', 'u7QimG7E!SF//JC2c8'), + database=os.getenv('POSTGRES_DB', 'tecnospeed') + ) + print("Executando query na tabela Enterprise...", flush=True) + empresas = banco.session.query(EnterpriseTable.id, EnterpriseTable.cnpj).filter(EnterpriseTable.cnpj != None, EnterpriseTable.cnpj != "").limit(100).all() + lista_empresas = [{"id": emp.id, "cnpj": emp.cnpj} for emp in empresas] + if not lista_empresas: + print("Erro: Nenhuma empresa com CNPJ válido encontrada na tabela 'Enterprise'.", flush=True) + return + print(f"Sucesso: {len(lista_empresas)} empresas válidas encontradas para o teste.", flush=True) + except Exception as e: + print(f"Erro ao conectar ao banco de dados: {e}") + return + + # Base do payload de NFSe + payload_base = { + "nfse": { + "idIntegracao": "", # Será preenchido no loop + "prestador": { + "cpfCnpj": "" # Será preenchido no loop + }, + "tomador": { + "cpfCnpj": "08114280956", + "razaoSocial": "NFSE EMITIDA EM HOMOLOGACAO", + "email": "contato@tecnospeed.com.br", + "endereco": { + "logradouro": "AVENIDA DUQUE DE CAXIAS", + "numero": "882", + "bairro": "CENTRO", + "codigoCidade": "4115200", + "descricaoCidade": "MARINGA", + "estado": "PR", + "cep": "87020025", + "tipoLogradouro": "Avenida" + } + }, + "servico": { + "codigo": "1.01", + "codigoTributacao": "1.01", + "discriminacao": "SERVICO DE TESTE DE CARGA MASSIVA", + "cnae": "6201501", + "iss": { + "tipoTributacao": 1, + "exigibilidade": 1, + "aliquota": 2.0 + }, + "valor": { + "servico": 100.0 + } + } + } + } + + # Estrutura completa exigida pelo mainQUEUE.py + def get_full_payload(id_num, id_empresa, cnpj_empresa): + ts = int(time.time() * 1000) + payload = json.loads(json.dumps(payload_base)) # Deep copy + payload["nfse"]["idIntegracao"] = f"LOAD_NFSE_{ts}_{id_num}" + payload["nfse"]["prestador"]["cpfCnpj"] = cnpj_empresa + + return { + "fila": "nfse", + "cliente": "MassiveNFSeTest", + "metodo": "POST", + "dados": { + "id_empresa": id_empresa, + "id_usuario": 1, + **payload["nfse"] + }, + "config": { + "x_api_key": "2da392a6-79d2-4304-a8b7-959572c7e44d", + "user_postgres": "tecnospeed", + "pass_postgres": "u7QimG7E!SF//JC2c8", + "host_postgres": "banco", + "port_postgres": "5432", + "database_postgres": "tecnospeed" + }, + "url_base": "https://api.sandbox.plugnotas.com.br" + } + + print(f"Enviando 1000 NFSe para a fila...") + + start_time = time.time() + + for i in range(1, 1001): + empresa_sorteada = random.choice(lista_empresas) + full_payload = get_full_payload(i, id_empresa=empresa_sorteada["id"], cnpj_empresa=empresa_sorteada["cnpj"]) + + exchange.setPayload(json.dumps(full_payload)) + exchange.sendMsg(json.dumps(full_payload), service="tecnospeed") + + if i % 100 == 0: + print(f"Enviadas: {i}/1000...", flush=True) + + end_time = time.time() + + print("\nConcluído!") + print(f"Tempo total de envio: {end_time - start_time:.2f} segundos.") + print("Acompanhe o processamento nos logs do worker (tecnospeed-queue).") + +if __name__ == "__main__": + main() diff --git a/backup/src/tests/test_api.py b/backup/src/tests/test_api.py new file mode 100644 index 0000000..820ebef --- /dev/null +++ b/backup/src/tests/test_api.py @@ -0,0 +1,29 @@ +import json +import sys +import os + +sys.path.append(os.path.dirname(os.path.abspath(__file__))) +from utils.api import Api + +api = Api("2da392a6-79d2-4304-a8b7-959572c7e44d") + +print("\n--- Consultando Certificados ---") +# GET /certificado (Lista todos) +certificados = api.get("https://api.sandbox.plugnotas.com.br/certificado") +print("GET /certificado response:") +print(json.dumps(certificados, indent=2)) + +# Se houver certificados, faz um GET específico no primeiro +if isinstance(certificados, list) and len(certificados) > 0: + first_cert_id = certificados[0].get('id') + print(f"\nConsultando certificado específico (ID: {first_cert_id}):") + spec_cert = api.get(f"https://api.sandbox.plugnotas.com.br/certificado/{first_cert_id}") + print(json.dumps(spec_cert, indent=2)) +elif isinstance(certificados, dict) and "data" in certificados: + # Caso a resposta venha com envelope "data" + data_list = certificados["data"] + if data_list: + first_cert_id = data_list[0].get('id') + print(f"\nConsultando certificado específico (ID: {first_cert_id}):") + spec_cert = api.get(f"https://api.sandbox.plugnotas.com.br/certificado/{first_cert_id}") + print(json.dumps(spec_cert, indent=2)) diff --git a/backup/src/tests/test_callback.py b/backup/src/tests/test_callback.py new file mode 100644 index 0000000..82531bd --- /dev/null +++ b/backup/src/tests/test_callback.py @@ -0,0 +1,96 @@ +import json +import pytest +from unittest.mock import patch, MagicMock + + +class TestRoteamento: + """Verifica que todas as filas registradas no dict existem e são callable.""" + + def test_todas_filas_registradas(self, main_instance): + filas_esperadas = ["nfe", "nfse", "certificado_digital", "empresa", + "servico", "tomador", "relatorios", "webhooks"] + for fila in filas_esperadas: + assert fila in main_instance.funcoes, f"Fila '{fila}' não está registrada" + assert callable(main_instance.funcoes[fila]), f"Fila '{fila}' não é callable" + + def test_fila_inexistente_nao_registrada(self, main_instance): + assert "webhook_callback" not in main_instance.funcoes + assert "fila_fantasma" not in main_instance.funcoes + + +class TestCallbackErros: + """Testa que o callback lida corretamente com payloads inválidos.""" + + def test_callback_json_invalido(self, main_instance): + """Payload que não é JSON válido deve retornar erro.""" + resultado = json.loads(main_instance.callback("isso não é json")) + assert resultado["status"] == "error" + + def test_callback_sem_hmac(self, main_instance): + """Payload sem HMAC deve retornar erro.""" + resultado = json.loads(main_instance.callback(json.dumps({"dados": "algo"}))) + assert resultado["status"] == "error" + + def test_callback_com_hmac_e_payload(self, main_instance, mock_exchange): + """Payload com hmac+payload mas sem crypto configurada deve retornar erro.""" + mock_exchange.getPayload.return_value = False + + main_instance.system_config = MagicMock() + main_instance.system_config.servicekey = "testkey1234567890" + main_instance.system_config.serviceid = "test-service" + + resultado = json.loads(main_instance.callback(json.dumps({ + "hmac": "fake-hmac", + "payload": "fake-encrypted-payload" + }))) + + assert resultado["status"] == "error" + + +class TestBancoIntegracao: + """Testa que os logs são gravados no banco durante as operações.""" + + def test_log_gravado_no_banco(self, main_instance, banco_teste, mock_api): + """Ao executar uma operação, deve gravar log no banco.""" + mock_api.get.return_value = {"id": "PLUG-NFE-001", "status": "CONCLUIDO"} + + with patch("NFe.registroNFe.Api", return_value=mock_api): + main_instance.nfe("get", {"id": "PLUG-NFE-001", "id_usuario": 42}) + + from utils.banco import LogTable + logs = banco_teste.session.query(LogTable).all() + + # Deve ter pelo menos 2 logs: INICIO + SUCESSO + assert len(logs) >= 2 + acoes = [log.acao for log in logs] + assert "GET" in acoes + + # Verifica que o id_usuario foi gravado + assert any(log.id_usuario == 42 for log in logs) + + def test_log_erro_gravado(self, main_instance, banco_teste, mock_api): + """Erros também devem ser gravados no banco.""" + mock_api.get.side_effect = Exception("Timeout na API") + + with patch("NFe.registroNFe.Api", return_value=mock_api): + with pytest.raises(Exception): + main_instance.nfe("get", {"id": "PLUG-NFE-001", "id_usuario": 7}) + + from utils.banco import LogTable + logs = banco_teste.session.query(LogTable).filter_by(status="ERRO").all() + assert len(logs) >= 1 + + +class TestExchangePayload: + """Testa que o exchange.setPayload é chamado com o resultado correto.""" + + def test_payload_set_no_sucesso(self, main_instance, mock_api, mock_exchange): + """Ao concluir com sucesso, setPayload deve ser chamado com o resultado.""" + mock_api.get.return_value = {"id": "PLUG-NFE-001", "status": "CONCLUIDO"} + + with patch("NFe.registroNFe.Api", return_value=mock_api): + main_instance.nfe("get", {"id": "PLUG-NFE-001", "id_usuario": 1}) + + assert mock_exchange.setPayload.called + payload_enviado = json.loads(mock_exchange.payloads_set[-1]) + assert payload_enviado["status"] == "CONCLUIDO" diff --git a/backup/src/tests/test_nfe.py b/backup/src/tests/test_nfe.py new file mode 100644 index 0000000..771402d --- /dev/null +++ b/backup/src/tests/test_nfe.py @@ -0,0 +1,179 @@ +import json +import pytest +from unittest.mock import patch, MagicMock + +class TestNFePost: + + @pytest.fixture + def dados_nfe(self): + return { + "id_usuario": 1, + "id_empresa": 10, + "nfe": { + "idIntegracao": "INT-NFE-001", + "presencial": True, + "consumidorFinal": True, + "natureza": "Venda de mercadoria", + "emitente": {"cpfCnpj": "08187168000160"}, + "destinatario": { + "cpfCnpj": "08114280956", + "razaoSocial": "Destinatário Teste", + "endereco": { + "logradouro": "Rua Teste", + "numero": "100", + "bairro": "Centro", + "codigoCidade": "5201405", + "descricaoCidade": "GOIANIA", + "estado": "GO", + "cep": "74000000" + } + }, + "itens": [{ + "codigo": "001", + "descricao": "Produto Teste", + "ncm": "61091000", + "cfop": "5102", + "valorUnitario": {"comercial": 100.0, "tributavel": 100.0}, + "valor": 100.0, + "tributos": { + "icms": { + "origem": "0", + "cst": "00", + "baseCalculo": {"modalidadeDeterminacao": 0, "valor": 100.0}, + "aliquota": 18.0, + "valor": 18.0 + }, + "pis": { + "cst": "01", + "baseCalculo": {"valor": 100.0, "quantidade": 1}, + "aliquota": 1.65, + "valor": 1.65 + }, + "cofins": { + "cst": "01", + "baseCalculo": {"valor": 100.0}, + "aliquota": 7.6, + "valor": 7.6 + } + } + }], + "pagamentos": [{"aVista": True, "meio": "01", "valor": 100.0}] + } + } + + def test_post_nfe_sucesso(self, main_instance, mock_api, dados_nfe): + mock_api.post.return_value = { + "documents": [{"id": "PLUG-NFE-001"}] + } + + with patch("NFe.registroNFe.Api", return_value=mock_api): + resultado = json.loads(main_instance.nfe("post", dados_nfe)) + + assert resultado["status"] == "ok" + assert resultado["data"]["documents"][0]["id"] == "PLUG-NFE-001" + + def test_post_nfe_erro_api(self, main_instance, mock_api, dados_nfe): + mock_api.post.return_value = {"error": "CPF/CNPJ inválido"} + + with patch("NFe.registroNFe.Api", return_value=mock_api): + with pytest.raises(Exception, match="CPF/CNPJ inválido"): + main_instance.nfe("post", dados_nfe) + +class TestNFeGet: + + def test_get_nfe_resumo(self, main_instance, mock_api): + mock_api.get.return_value = { + "id": "PLUG-NFE-001", + "status": "CONCLUIDO", + "xml": "https://..." + } + + with patch("NFe.registroNFe.Api", return_value=mock_api): + resultado = json.loads(main_instance.nfe("get", {"id": "PLUG-NFE-001", "id_usuario": 1})) + + assert resultado["status"] == "ok" + assert resultado["data"]["status"] == "CONCLUIDO" + +class TestNFeDownload: + """Testa download de PDF e XML de NFe.""" + + def test_download_pdf(self, main_instance, mock_api): + mock_api.get.return_value = {"url": "https://cdn.plugnotas.com.br/nfe/pdf/123"} + + with patch("NFe.registroNFe.Api", return_value=mock_api): + resultado = json.loads(main_instance.nfe("download", {"id": "PLUG-NFE-001", "id_usuario": 1})) + + assert resultado["status"] == "ok" + + def test_download_xml(self, main_instance, mock_api): + mock_api.get.return_value = {"url": "https://cdn.plugnotas.com.br/nfe/xml/123"} + + with patch("NFe.registroNFe.Api", return_value=mock_api): + resultado = json.loads(main_instance.nfe("download_xml", {"id": "PLUG-NFE-001", "id_usuario": 1})) + + assert resultado["status"] == "ok" + +class TestNFeCancelar: + + def test_cancelar_nfe_sucesso(self, main_instance, mock_api): + mock_api.post.return_value = {"id": "PLUG-NFE-001", "status": "CANCELAMENTO_SOLICITADO"} + + with patch("NFe.registroNFe.Api", return_value=mock_api): + resultado = json.loads(main_instance.nfe("cancelar", { + "id": "PLUG-NFE-001", + "justificativa": "Erro de emissão", + "id_usuario": 1 + })) + + assert resultado["status"] == "ok" + + def test_status_cancelamento(self, main_instance, mock_api): + mock_api.get.return_value = {"id": "PLUG-NFE-001", "status": "CANCELADO"} + + with patch("NFe.registroNFe.Api", return_value=mock_api): + resultado = json.loads(main_instance.nfe("status_cancelamento", {"id": "PLUG-NFE-001", "id_usuario": 1})) + + assert resultado["status"] == "ok" + +class TestNFeCCe: + + def test_enviar_cce(self, main_instance, mock_api): + mock_api.post.return_value = {"id": "PLUG-NFE-001", "status": "CCE_ENVIADA"} + + with patch("NFe.registroNFe.Api", return_value=mock_api): + resultado = json.loads(main_instance.nfe("cce", { + "id": "PLUG-NFE-001", + "correcao": "Corrigir endereço do destinatário", + "id_usuario": 1 + })) + + assert resultado["status"] == "ok" + + def test_status_cce(self, main_instance, mock_api): + mock_api.get.return_value = {"id": "PLUG-NFE-001", "status": "CCE_AUTORIZADA"} + + with patch("NFe.registroNFe.Api", return_value=mock_api): + resultado = json.loads(main_instance.nfe("status_cce", {"id": "PLUG-NFE-001", "id_usuario": 1})) + + assert resultado["status"] == "ok" + +class TestNFeResumo: + + def test_resumo_por_cnpj(self, main_instance, mock_api): + mock_api.get.return_value = {"id": "PLUG-NFE-001", "chave": "3524..."} + + with patch("NFe.registroNFe.Api", return_value=mock_api): + resultado = json.loads(main_instance.nfe("resumo", { + "cnpj": "08187168000160", + "id_integracao": "INT-001", + "id_usuario": 1 + })) + + assert resultado["status"] == "ok" + +class TestNFeInvalido: + + def test_metodo_invalido(self, main_instance): + resultado = json.loads(main_instance.nfe("inexistente", {"id_usuario": 1})) + assert resultado["status"] == "error" + assert "não encontrado" in resultado["message"].lower() diff --git a/backup/src/tests/test_nfse.py b/backup/src/tests/test_nfse.py new file mode 100644 index 0000000..be1045a --- /dev/null +++ b/backup/src/tests/test_nfse.py @@ -0,0 +1,146 @@ +""" +Testes E2E para o serviço NFSe. +Testa: POST, GET, DOWNLOAD, DOWNLOAD_XML, CANCELAR, +STATUS_CANCELAMENTO, RESUMO, HISTORICO. +""" +import json +import pytest +from unittest.mock import patch, MagicMock + + +@pytest.fixture +def dados_nfse(): + return { + "id_usuario": 1, + "id_empresa": 10, + "idIntegracao": "INT-NFSE-001", + "prestador": {"cpfCnpj": "08187168000160"}, + "tomador": { + "cpfCnpj": "08114280956", + "razaoSocial": "Tomador Teste", + "inscricaoMunicipal": "isento", + "email": "tomador@teste.com", + "endereco": { + "descricaoCidade": "São Paulo", + "cep": "01000000", + "tipoLogradouro": "Rua", + "logradouro": "Teste", + "tipoBairro": "Bairro", + "codigoCidade": "3550308", + "complemento": "", + "estado": "SP", + "numero": "100", + "bairro": "Centro" + } + }, + "servico": { + "codigo": "1.01", + "codigoTributacao": "1.01", + "discriminacao": "Serviços de tecnologia", + "cnae": "6204000", + "iss": {"tipoTributacao": 6, "exigibilidade": 1, "aliquota": 2.5}, + "valor": {"servico": 1500.0, "descontoCondicionado": 0, "descontoIncondicionado": 0} + } + } + + +class TestNFSePost: + def test_post_nfse_sucesso(self, main_instance, mock_api, dados_nfse): + mock_api.post.return_value = {"documents": [{"id": "PLUG-NFSE-001"}]} + + with patch("NFSe.nfse.Api", return_value=mock_api): + resultado = json.loads(main_instance.nfse("post", dados_nfse)) + + assert resultado["status"] == "ok" + assert resultado["data"]["documents"][0]["id"] == "PLUG-NFSE-001" + + def test_post_nfse_erro_api(self, main_instance, mock_api, dados_nfse): + mock_api.post.return_value = {"error": "CNPJ do prestador não cadastrado"} + + with patch("NFSe.nfse.Api", return_value=mock_api): + with pytest.raises(Exception): + main_instance.nfse("post", dados_nfse) + + +class TestNFSeGet: + def test_get_nfse_por_id(self, main_instance, mock_api): + mock_api.get.return_value = {"id": "PLUG-NFSE-001", "status": "CONCLUIDO"} + + with patch("NFSe.nfse.Api", return_value=mock_api): + resultado = json.loads(main_instance.nfse("get", {"id": "PLUG-NFSE-001", "id_usuario": 1})) + + assert resultado["status"] == "ok" + + def test_listar_nfse(self, main_instance, banco_teste): + """Listar NFSe sem ID deve buscar do banco.""" + resultado = json.loads(main_instance.nfse("get", {"id_usuario": 1})) + assert resultado["status"] == "ok" + + +class TestNFSeDownload: + def test_download_pdf(self, main_instance, mock_api): + mock_api.get.return_value = {"pdf_url": "https://..."} + + with patch("NFSe.nfse.Api", return_value=mock_api): + resultado = json.loads(main_instance.nfse("download", {"id": "PLUG-NFSE-001", "id_usuario": 1})) + + assert resultado["status"] == "ok" + + def test_download_xml(self, main_instance, mock_api): + mock_api.get.return_value = {"xml_url": "https://..."} + + with patch("NFSe.nfse.Api", return_value=mock_api): + resultado = json.loads(main_instance.nfse("download_xml", {"id": "PLUG-NFSE-001", "id_usuario": 1})) + + assert resultado["status"] == "ok" + + +class TestNFSeCancelar: + def test_cancelar_nfse(self, main_instance, mock_api): + mock_api.post.return_value = {"status": "CANCELAMENTO_SOLICITADO"} + + with patch("NFSe.nfse.Api", return_value=mock_api): + resultado = json.loads(main_instance.nfse("cancelar", {"id": "PLUG-NFSE-001", "id_usuario": 1})) + + assert resultado["status"] == "ok" + + def test_status_cancelamento(self, main_instance, mock_api): + mock_api.get.return_value = {"status": "CANCELADO"} + + with patch("NFSe.nfse.Api", return_value=mock_api): + resultado = json.loads(main_instance.nfse("status_cancelamento", {"id": "PLUG-NFSE-001", "id_usuario": 1})) + + assert resultado["status"] == "ok" + + +class TestNFSeResumo: + def test_consultar_resumo(self, main_instance, mock_api): + mock_api.get.return_value = {"id": "PLUG-NFSE-001", "numero": "12345"} + + with patch("NFSe.nfse.Api", return_value=mock_api): + resultado = json.loads(main_instance.nfse("resumo", { + "id": "PLUG-NFSE-001", + "cnpj": "08187168000160", + "id_usuario": 1 + })) + + assert resultado["status"] == "ok" + + +class TestNFSeHistorico: + def test_historico_nfse(self, main_instance, mock_api): + mock_api.get.return_value = [{"data": "2026-01-01", "status": "CONCLUIDO"}] + + with patch("NFSe.nfse.Api", return_value=mock_api): + resultado = json.loads(main_instance.nfse("historico", { + "id": "PLUG-NFSE-001", + "id_usuario": 1 + })) + + assert resultado["status"] == "ok" + + +class TestNFSeInvalido: + def test_metodo_invalido(self, main_instance): + resultado = json.loads(main_instance.nfse("inexistente", {"id_usuario": 1})) + assert resultado["status"] == "error" diff --git a/backup/src/tests/test_queue.py b/backup/src/tests/test_queue.py new file mode 100644 index 0000000..9bc6adb --- /dev/null +++ b/backup/src/tests/test_queue.py @@ -0,0 +1,164 @@ +import sys +import os +import json +import time + +sys.path.append(os.path.dirname(os.path.abspath(__file__))) + +from Models.config import SystemConfig +from utils.redis import RedisClient +from utils.Exchange2 import QueueExchange2 + +def main(): + print("Iniciando script de testes da FILA (Queue Exchange)...") + + # Carregando configuração do Redis + redis_host = os.getenv('REDIS_HOST', 'localhost') + redis_client = RedisClient(host=redis_host, port=6379) + try: + system_config_data = redis_client.get("system_config") + if not system_config_data: + print("Erro: system_config não encontrado no Redis.") + return + + system_config = SystemConfig(**json.loads(system_config_data)) + print(f"[{system_config.origin}] Configuração carregada.") + except Exception as e: + print(f"Erro ao ler configs: {e}") + return + + # Inicializando mensageria de FILA + try: + # Nota: O QueueExchange2 envia para 'system_service_client' + exchange = QueueExchange2( + amqps=system_config.amqps, + origin=system_config.origin, + system="integracao", + service="tecnospeed", + version="2.1.0", + client="#" # '#' para bater com a fila padrão do worker (evita erro de validação length) + ) + except Exception as e: + print(f"Falha ao iniciar mensageria: {e}") + return + + # Configurando criptografia + servicekey = system_config.servicekey.ljust(32, '0')[:32] + exchange.setCrypto(serviceid=system_config.serviceid, servicekey=servicekey) + + # Payload (mesmo do teste RPC) + payload_interno = { + "fila": "empresa", + "cliente": "TestQueue", + "metodo": "POST", + "dados": { + "endereco" : { + "codigoPais": "1058", + "descricaoPais": "Brasil", + "tipoLogradouro": "Rua", + "logradouro": "T55", + "numero": "882", + "complemento": "Quadra99 Lote 11/14 Sala 1507", + "tipoBairro": "Zona", + "bairro": "Setor Bueno", + "codigoCidade": "5208707", + "descricaoCidade": "Goiania", + "estado": "GO", + "cep": "74215170" + }, + "nfse" : { + "config" : { + "rps" : { + "lote" : 12, + "numeracao" : [ + { + "numero" : 2, + "serie" : "CTBZ", + "numeracaoAtual" : 2 + } + ] + }, + "email" : { + "envio" : False + }, + "producao" : True, + "envioLimitado" : False, + "integracoes" : [] + }, + "ativo" : True, + "tipoContrato" : 0 + }, + "nfe" : { + "config" : { + "dfe" : { + "primeiroNsu" : True + }, + "email" : { + "envio" : False + }, + "integracoes" : { + "plugStorage" : { + "ativo" : False + } + }, + "producao" : False, + "impressaoFcp" : False, + "impressaoPartilha" : True, + "versaoManual" : "6.0", + "versaoEsquema" : "pl_009", + "numeracao" : [] + }, + "ativo" : False + }, + "nfce" : { + "config" : { + "email" : { + "envio" : False + }, + "integracoes" : { + "plugStorage" : { + "ativo" : False + } + }, + "producao" : False, + "versaoManual" : "6.0", + "versaoEsquema" : "pl_009", + "numeracao" : [] + }, + "ativo" : False + }, + "incentivoFiscal" : False, + "incentivadorCultural" : False, + "cpfCnpj" : "14358405000100", + "inscricaoMunicipal" : "1085328365", + "certificado" : "668698592698d7831c758d52", + "razaoSocial" : "Teste da SIlva do teste", + "simplesNacional" : True, + "regimeTributario" : 1, + "regimeTributarioEspecial" : 6 + }, + "config": { + "x_api_key": "2da392a6-79d2-4304-a8b7-959572c7e44d", + "user_postgres": "tecnospeed", + "pass_postgres": "u7QimG7E!SF//JC2c8", + "host_postgres": "postgres", + "port_postgres": "5432", + "database_postgres": "tecnospeed" + }, + "url_base": "https://api.sandbox.plugnotas.com.br" + } + + # Criptografando e enviando + exchange.setPayload(json.dumps(payload_interno)) + + print("\n--- DEBUG ENVIO ---") + print(f"Alvo (targetQueue): integracao_tecnospeed") + print(f"Config Enviada: {payload_interno['config']['host_postgres']}:{payload_interno['config']['port_postgres']}") + print("-------------------\n") + + print("Disparando mensagem para a FILA...") + exchange.sendMsg(json.dumps(payload_interno), service="tecnospeed") + print("Mensagem enviada com sucesso. Verifique os logs do worker mainQUEUE.py") + +if __name__ == "__main__": + main() diff --git a/backup/src/tests/test_servicos.py b/backup/src/tests/test_servicos.py new file mode 100644 index 0000000..1228a2a --- /dev/null +++ b/backup/src/tests/test_servicos.py @@ -0,0 +1,328 @@ +import json +import pytest +from unittest.mock import patch, MagicMock +from utils.banco import EmpresaTable, ServicoTable, TomadorTable + +class TestEmpresaPost: + @pytest.fixture + def dados_empresa(self): + return { + "id_usuario": 1, + "endereco": { + "codigoPais": "1058", "descricaoPais": "BRASIL", + "tipoLogradouro": "Rua", "logradouro": "Teste", "numero": "123", + "complemento": "", "tipoBairro": "Bairro", "bairro": "Centro", + "codigoCidade": "5201405", "descricaoCidade": "GOIANIA", + "estado": "GO", "cep": "74000000" + }, + "nfse": { + "config": {"rps": {"lote": 1, "numeracao": [1]}, "email": {"envio": True}, + "producao": False, "envioLimitado": False, "integracoes": []}, + "ativo": True, "tipoContrato": 0 + }, + "nfe": { + "config": {"dfe": {"primeiroNsu": False}, "email": {"envio": False}, + "integracoes": {}, "producao": False, "impressaoFcp": False, + "impressaoPartilha": False, "versaoManual": "6.0", + "versaoEsquema": "PL_009_V4", "numeracao": [1]}, + "ativo": False + }, + "nfce": { + "config": {"email": {"envio": False}, "integracoes": {}, "producao": False, + "versaoManual": "6.0", "versaoEsquema": "PL_009_V4", "numeracao": [1]}, + "ativo": False + }, + "incentivoFiscal": False, "incentivadorCultural": False, + "cpfCnpj": "17956143000174", "inscricaoMunicipal": "12345", + "certificado": "CERT-UUID", "razaoSocial": "Empresa Teste SA", + "simplesNacional": True, "regimeTributario": 1, "regimeTributarioEspecial": 0 + } + + def test_post_empresa_sucesso(self, main_instance, mock_api, dados_empresa): + mock_api.post.return_value = {"_id": "PLUG-EMP-001", "cpfCnpj": "17956143000174"} + + with patch("empresas.empresa.Api", return_value=mock_api): + resultado = json.loads(main_instance.empresa("post", dados_empresa)) + + assert resultado["status"] == "ok" + + def test_post_empresa_erro(self, main_instance, mock_api, dados_empresa): + mock_api.post.return_value = { + "error": "Erro", + "response": {"error": {"message": "CNPJ já cadastrado", "data": []}} + } + + with patch("empresas.empresa.Api", return_value=mock_api): + resultado = json.loads(main_instance.empresa("post", dados_empresa)) + + assert resultado["status"] == "error" + + +class TestEmpresaGet: + def test_get_empresa_por_cnpj(self, main_instance, mock_api): + mock_api.get.return_value = {"_id": "PLUG-EMP-001", "cpfCnpj": "17956143000174", "razaoSocial": "Teste SA"} + + with patch("empresas.empresa.Api", return_value=mock_api): + resultado = json.loads(main_instance.empresa("get", {"cnpj": "17956143000174", "id_usuario": 1})) + + assert resultado["status"] == "ok" + + def test_listar_empresas(self, main_instance, mock_api): + mock_api.get_paginated.return_value = {"data": [{"cpfCnpj": "111", "razaoSocial": "Emp A"}], "total": 1} + + with patch("empresas.empresa.Api", return_value=mock_api): + resultado = json.loads(main_instance.empresa("get", {"id_usuario": 1})) + + assert resultado["status"] == "ok" + + +class TestEmpresaInvalido: + def test_metodo_invalido(self, main_instance): + resultado = json.loads(main_instance.empresa("inexistente", {"id_usuario": 1})) + assert resultado["status"] == "error" + + +class TestCertificadoPost: + @pytest.fixture + def dados_certificado(self): + return { + "id_usuario": 1, + "id_empresa": 10, + "arquivo": "CONTEUDO_PFX_BASE64", + "senha": "12345678", + "expirationAt": "2027-12-31" + } + + def test_post_certificado_sucesso(self, main_instance, mock_api, dados_certificado): + mock_api.post_form_data.return_value = {"data": {"id": "CERT-001"}} + + with patch("certificado_digital.certificado_digital.Api", return_value=mock_api): + resultado = json.loads(main_instance.certificado_digital("post", dados_certificado)) + + assert resultado["status"] == "ok" + assert resultado["data"] == "CERT-001" + + +class TestCertificadoGet: + def test_get_certificado_por_id(self, main_instance, mock_api): + mock_api.get.return_value = {"id": "CERT-001", "status": "VALIDO"} + + with patch("certificado_digital.certificado_digital.Api", return_value=mock_api): + resultado = json.loads(main_instance.certificado_digital("get", {"id": "CERT-001", "id_usuario": 1})) + + assert resultado["status"] == "ok" + + def test_listar_certificados(self, main_instance, mock_api): + mock_api.get.return_value = [{"id": "CERT-001"}, {"id": "CERT-002"}] + + with patch("certificado_digital.certificado_digital.Api", return_value=mock_api): + resultado = json.loads(main_instance.certificado_digital("get", {"id_usuario": 1})) + + assert resultado["status"] == "ok" + + +class TestCertificadoInvalido: + def test_metodo_invalido(self, main_instance): + resultado = json.loads(main_instance.certificado_digital("inexistente", {"id_usuario": 1})) + assert resultado["status"] == "error" + +class TestServicoPost: + """Testa criação de Serviço na API + banco.""" + + @pytest.fixture + def dados_servico_completo(self): + """Payload completo do Model Servico com todos campos obrigatórios.""" + return { + "id_usuario": 1, + "codigo": "1.01", + "idIntegracao": "SERV-INT-001", + "discriminacao": "Consultoria em TI", + "codigoTributacao": "1.01", + "cnae": "6204000", + "codigoCidadeIncidencia": "3550308", + "descricaoCidadeIncidencia": "São Paulo", + "unidade": "UN", + "quantidade": 1, + "iss": { + "tipoTributacao": 6, "exigibilidade": 1, + "retido": False, "aliquota": 2.5, "valor": 37.5, + "valorRetido": 0, "processoSuspensao": "" + }, + "obra": {"art": "", "codigo": ""}, + "valor": { + "servico": 1500.0, "baseCalculo": 1500.0, "deducoes": 0, + "descontoCondicionado": 0, "descontoIncondicionado": 0, + "liquido": 1500.0, "unitario": 1500.0, "valorAproximadoTributos": 0 + }, + "deducao": {"tipo": 0, "descricao": ""}, + "retencao": { + "pis": {"aliquota": 0, "valor": 0}, + "cofins": {"aliquota": 0, "valor": 0}, + "csll": {"aliquota": 0, "valor": 0}, + "inss": {"aliquota": 0, "valor": 0}, + "irrf": {"aliquota": 0, "valor": 0}, + "outrasRetencoes": 0, + "cpp": {"aliquota": 0, "valor": 0} + }, + "tributavel": True, + "ibpt": { + "simplificado": {"aliquota": 8.65}, + "detalhado": {"aliquota": {"municipal": 2.5, "estadual": 0, "federal": 6.15}} + }, + "responsavelRetencao": "" + } + + def test_post_servico_sucesso(self, main_instance, mock_api, dados_servico_completo): + mock_api.post.return_value = {"data": {"id": "SERV-001"}} + + with patch("NFSe.Servico.servico.Api", return_value=mock_api): + resultado = json.loads(main_instance.servico("post", dados_servico_completo)) + + assert resultado["status"] == "ok" + + +class TestServicoGet: + def test_listar_servicos(self, main_instance, banco_teste): + """Deve retornar lista vazia quando não há serviços.""" + resultado = json.loads(main_instance.servico("get", {"id_usuario": 1})) + assert resultado["status"] == "ok" + + def test_buscar_servico_por_id(self, main_instance, mock_api): + mock_api.get.return_value = {"id": "SERV-001", "discriminacao": "Consultoria"} + + with patch("NFSe.Servico.servico.Api", return_value=mock_api): + resultado = json.loads(main_instance.servico("get", {"id": "SERV-001", "id_usuario": 1})) + + assert resultado["status"] == "ok" + + +class TestServicoInvalido: + def test_metodo_invalido(self, main_instance): + resultado = json.loads(main_instance.servico("inexistente", {"id_usuario": 1})) + assert resultado["status"] == "error" + +class TestTomadorPost: + @pytest.fixture + def dados_tomador(self): + return { + "id_usuario": 1, + "cpfCnpj": "08114280956", + "inscricaoMunicipal": "isento", + "inscricaoEstadual": "", + "inscricaoSuframa": "", + "indicadorInscricaoEstadual": 9, + "razaoSocial": "Tomador Teste LTDA", + "nomeFantasia": "Tomador Teste", + "endereco": { + "tipoLogradouro": "Rua", "logradouro": "Teste", "numero": "100", + "complemento": "", "tipoBairro": "Bairro", "bairro": "Centro", + "codigoPais": "1058", "descricaoPais": "BRASIL", + "codigoCidade": "3550308", "descricaoCidade": "São Paulo", + "estado": "SP", "cep": "01000000" + }, + "telefone": {"ddd": "11", "numero": "999999999"}, + "email": "tomador@teste.com", + "orgaoPublico": False + } + + def test_post_tomador_sucesso(self, main_instance, mock_api, dados_tomador): + mock_api.post.return_value = {"id": "TOM-001", "cpfCnpj": "08114280956"} + + with patch("NFSe.Tomador.tomador.Api", return_value=mock_api): + resultado = json.loads(main_instance.tomador("post", dados_tomador)) + + assert resultado["status"] == "ok" + + +class TestTomadorGet: + def test_buscar_tomador(self, main_instance, mock_api): + mock_api.get.return_value = {"cpfCnpj": "08114280956", "razaoSocial": "Tomador Teste"} + + with patch("NFSe.Tomador.tomador.Api", return_value=mock_api): + resultado = json.loads(main_instance.tomador("get", {"cpf_cnpj": "08114280956", "id_usuario": 1})) + + assert resultado["status"] == "ok" + + +class TestTomadorInvalido: + def test_metodo_invalido(self, main_instance): + resultado = json.loads(main_instance.tomador("inexistente", {"id_usuario": 1})) + assert resultado["status"] == "error" + +class TestRelatorios: + def test_nfe_emitidas(self, main_instance, mock_api): + mock_api.get_paginated.return_value = {"data": [{"id": "NFE-1"}], "total": 1} + + with patch("Relatorios.relatorios.Api", return_value=mock_api): + resultado = json.loads(main_instance.relatorios("nfe_emitidas", {"id_usuario": 1})) + + assert resultado["status"] == "ok" + + def test_nfe_por_cnpj(self, main_instance, mock_api): + mock_api.get_paginated.return_value = {"data": [], "total": 0} + + with patch("Relatorios.relatorios.Api", return_value=mock_api): + resultado = json.loads(main_instance.relatorios("nfe_por_cnpj", {"cnpj": "08187168000160", "id_usuario": 1})) + + assert resultado["status"] == "ok" + + def test_nfse_emitidas(self, main_instance, mock_api): + mock_api.get_paginated.return_value = {"data": [], "total": 0} + + with patch("Relatorios.relatorios.Api", return_value=mock_api): + resultado = json.loads(main_instance.relatorios("nfse_emitidas", { + "data_inicial": "2026-01-01", "data_final": "2026-12-31", "id_usuario": 1 + })) + + assert resultado["status"] == "ok" + + def test_nfse_por_cnpj(self, main_instance, mock_api): + mock_api.get_paginated.return_value = {"data": [], "total": 0} + + with patch("Relatorios.relatorios.Api", return_value=mock_api): + resultado = json.loads(main_instance.relatorios("nfse_por_cnpj", { + "cnpj": "08187168000160", + "data_inicial": "2026-01-01", "data_final": "2026-12-31", + "id_usuario": 1 + })) + + assert resultado["status"] == "ok" + + def test_metodo_invalido(self, main_instance): + resultado = json.loads(main_instance.relatorios("inexistente", {"id_usuario": 1})) + assert resultado["status"] == "error" + +class TestWebhooks: + def test_consultar_webhooks(self, main_instance, mock_api): + mock_api.get.return_value = {"url": " ", "method": "POST"} + + with patch("webhooks.webhooks.Api", return_value=mock_api): + main_instance.webhooks("get", {"cnpj": "08187168000160", "id_usuario": 1}) + + def test_criar_webhook(self, main_instance, mock_api): + mock_api.post.return_value = {"status": "ok"} + + with patch("webhooks.webhooks.Api", return_value=mock_api): + main_instance.webhooks("post", {"cnpj": "08187168000160", "url": " ", "id_usuario": 1}) + + def test_deletar_webhook(self, main_instance, mock_api): + mock_api.delete.return_value = {"status": "ok"} + + with patch("webhooks.webhooks.Api", return_value=mock_api): + main_instance.webhooks("delete", {"cnpj": "08187168000160", "id_usuario": 1}) + + def test_atualizar_webhook(self, main_instance, mock_api): + mock_api.put.return_value = {"status": "ok"} + + with patch("webhooks.webhooks.Api", return_value=mock_api): + main_instance.webhooks("put", {"cnpj": "08187168000160", "url": " ", "id_usuario": 1}) + + def test_testar_webhook(self, main_instance, mock_api): + mock_api.post.return_value = {"status": "ok", "message": "Webhook testado"} + + with patch("webhooks.webhooks.Api", return_value=mock_api): + main_instance.webhooks("test", {"cnpj": "08187168000160", "id_usuario": 1}) + + def test_metodo_invalido(self, main_instance): + resultado = json.loads(main_instance.webhooks("inexistente", {"id_usuario": 1})) + assert resultado["status"] == "error" diff --git a/backup/src/tests/test_simplified_payload.py b/backup/src/tests/test_simplified_payload.py new file mode 100644 index 0000000..d40c494 --- /dev/null +++ b/backup/src/tests/test_simplified_payload.py @@ -0,0 +1,80 @@ +import os +import sys +import json +import time + +sys.path.append(os.path.dirname(os.path.dirname(os.path.abspath(__file__)))) + +from Models.config import SystemConfig +from utils.redis import RedisClient +from utils.Exchange2 import RPCExchange2 + +def run_rpc_test(): + redis_host = os.getenv('REDIS_HOST', 'localhost') + redis_client = RedisClient(host=redis_host, port=6379) + try: + config_data = redis_client.get("system_config") + sc = SystemConfig(**json.loads(config_data)) + except Exception as e: + print(f"Erro ao carregar configs: {e}") + return + + config_rpc = { + "x_api_key": "2da392a6-79d2-4304-a8b7-959572c7e44d", + "user_postgres": "tecnospeed", + "pass_postgres": "u7QimG7E%21SF%2F%2FJC2c8", + "host_postgres": "banco", + "port_postgres": "5432", + "database_postgres": "tecnospeed" + } + + exchange = RPCExchange2(sc.amqps, sc.origin, "integracao", "tecnospeed", "2.1.0", client="#") + servicekey = sc.servicekey.ljust(32, '0')[:32] + exchange.setCrypto(sc.serviceid, servicekey) + + # Payload "simplificado" do front-end + full_payload = { + "fila": "empresa", + "metodo": "POST", + "cliente": "tray", + "url_base": "https://api.sandbox.plugnotas.com.br", + "config": config_rpc, + "dados": { + "cpfCnpj": "94074986000198", + "razaoSocial": "Tech nux", + "nomeFantasia": "Tech nux ltda", + "inscricaoMunicipal": "0001", + "simplesNacional": True, + "regimeTributario": 1, + "regimeTributarioEspecial": 1, + "incentivoCultural": False, + "incentivoFiscal": True, + "certificado": "cert-mock-01", + "endereco": { + "codigoPais": "1058", + "descricaoPais": "BRASIL", + "tipoLogradouro": "Avenida", + "logradouro": "Avenida", + "numero": "123", + "complemento": "Casa", + "tipoBairro": "Bairro", + "bairro": "Centro", + "codigoCidade": "5200175", + "descricaoCidade": "ÁGUA FRIA DE GOIÁS", + "estado": "GO", + "cep": "68907-460" + }, + "nfe": { "ativo": True }, + "nfse": { "ativo": False } + } + } + + print("Enviando payload via RPC...") + exchange.setPayload(json.dumps(full_payload)) + resposta = exchange.callRPC(json.dumps(full_payload), timeout=30) + + print("\nRESPOSTA DO SERVIDOR (RPC):") + print(resposta) + +if __name__ == "__main__": + run_rpc_test() diff --git a/backup/src/tests/test_upload_certificado.py b/backup/src/tests/test_upload_certificado.py new file mode 100644 index 0000000..c3c7b9d --- /dev/null +++ b/backup/src/tests/test_upload_certificado.py @@ -0,0 +1,88 @@ +import sys +import os +import json +import time +from dotenv import load_dotenv + +# Carrega variáveis de ambiente +load_dotenv() + +# Adiciona o diretório atual ao path para importar as classes locais +sys.path.append(os.path.dirname(os.path.abspath(__file__))) + +from Models.config import SystemConfig +from utils.redis import RedisClient +from utils.Exchange2 import QueueExchange2 + +# Dados do Certificado (Preenchidos dinamicamente) +PFX_BASE64 = " " +PFX_PASSWORD = str(sys.argv[1]) if len(sys.argv) > 1 else " " + +def main(): + print(f"Iniciando teste de upload de certificado para senha: {PFX_PASSWORD}") + + # 1. Carregando configuração do Redis + redis_host = os.getenv('REDIS_HOST', 'localhost') + redis_client = RedisClient(host=redis_host, port=6379) + try: + system_config_data = redis_client.get("system_config") + if not system_config_data: + print("Erro: system_config não encontrado no Redis.") + return + + system_config = SystemConfig(**json.loads(system_config_data)) + except Exception as e: + print(f"Erro ao ler configs do Redis: {e}") + return + + # 2. Inicializando QueueExchange2 + try: + exchange = QueueExchange2( + amqps=system_config.amqps, + origin=system_config.origin, + system="integracao", + service="tecnospeed", + version="1.0.0", + client="#" + ) + + servicekey = system_config.servicekey.ljust(32, '0')[:32] + exchange.setCrypto(serviceid=system_config.serviceid, servicekey=servicekey) + except Exception as e: + print(f"Falha ao iniciar mensageria: {e}") + return + + # 3. Montando o payload interno + internal_payload = { + "fila": "certificado_digital", + "cliente": "UploadTest", + "metodo": "POST", + "dados": { + "arquivo": PFX_BASE64, + "senha": PFX_PASSWORD, + "id_empresa": "c19d874447b1kxlapg1xv783", + "id_usuario": 1, + "expirationAt": "2027-04-13" + }, + "config": { + "x_api_key": os.getenv("PLUGNOTAS_API_KEY"), + "user_postgres": os.getenv("POSTGRES_USER", "tecnospeed"), + "pass_postgres": os.getenv("POSTGRES_PASSWORD", "u7QimG7E!SF//JC2c8"), + "host_postgres": "postgres", + "port_postgres": "5432", + "database_postgres": os.getenv("POSTGRES_DB", "tecnospeed") + }, + "url_base": "https://api.sandbox.plugnotas.com.br" + } + + # 4. Enviando com criptografia (hmac/payload) + try: + msg_json = json.dumps(internal_payload) + exchange.setPayload(msg_json) + exchange.sendMsg(msg_json, service="tecnospeed") + print("Mensagem enviada com sucesso (criptografada)!") + except Exception as e: + print(f"Erro ao enviar mensagem: {e}") + +if __name__ == "__main__": + main() diff --git a/backup/src/utils/Exchange2.py b/backup/src/utils/Exchange2.py new file mode 100644 index 0000000..d6af010 --- /dev/null +++ b/backup/src/utils/Exchange2.py @@ -0,0 +1,1041 @@ +import json, time, sys, inspect, os +import hmac, hashlib, uuid, re + +import pika, pprint +from datetime import datetime +from pytz import timezone +from cryptography.hazmat.primitives.ciphers import Cipher, algorithms, modes +from cryptography.hazmat.primitives import padding +from cryptography.hazmat.backends import default_backend + +from queue import Queue, Full, Empty +from threading import Thread, Event +from abc import ABC, abstractmethod +from typing import Sequence, Any +import logging + +from utils.exchangeModels import * + +versao = "v1.0.0 14/12/2023 - Baltazar - Prefetch count" +versao = "v1.1.0 06/02/2024 - MJ. - Detecta Serviço rabbitmq ausente no inicio" +versao = "v1.2.0 20/02/2024 - MJ. - Possibilita uma taksqueue por serviço" +versao = "v1.3.0 06/03/2024 - MJ. - Verboso apenas nos erros" +versao = "v1.5.0 07/03/2024 - MJ. - External Rabbitmq Connection" +versao = "v1.6.0 26/06/2024 - MJ. - Conversao dos parametros host, service e client para lowercase" +versao = "v1.7.0 26/07/2024 - MJ. - Prepacao para utilizar Cloudamqp ou Rabbitmq local amqps/system" +versao = "v2.0.0 16/08/2024 - MJ. - Nova classe RPC e restruturação de Exchange2 e Tasks" +versao = "v2.1.0 21/11/2024 - MJ. - Clareza no log e tratamento de erros" +versao = "v2.2.0 31/01/2025 - MJ. - Remoção de alguns logs e melhorias de performance" +versao = "v2.3.0 12/02/2025 - MJ. - Melhorias na conexão com o Rabbitmq" +versao = "v2.4.0 14/02/2025 - MJ. - Check amqp url parameters" +versao = "v2.4.1 19/02/2025 - MJ. - Correcao prefetch_count=1" +versao = "v2.5.1 13/05/2025 - MJ. - Payload até 128kb - ajuste documentação e visual" +versao = "v2.5.2 01/10/2025 - MJ. - Evita especificar um client vazio" +versao = "v3" +versaostr = f"{versao}0.0 16/01/2026 S.D.G. - SG_IC2 LoggerExchange" + +def is_amqp_url(url): + pattern = r"^amqps?:\/\/([a-zA-Z0-9_\-\.]+):([^@]+)@([a-zA-Z0-9_\-\.]+)(:[0-9]+)?(\/[a-zA-Z0-9_\-\.]*)?$" + + return bool(re.match(pattern, url)) + +# QUEUE EXCHANGE - Apoia no comunicação inter-serviços de um sistema +# EXCHANGE TIPO DIRECT +# NAO DURAVEL - EXCLUSIVO - AUTODELETE +# SE NAO HOUVER QUEM RECEBA A MSG A MSG SERÁ PERDIDA -> O ENVIO FUNCIONA MESMO QUE NINGUEM EXISTA PARA RECEBER +class QueueExchange2: + def __init__( self, + amqps:str, + origin:str, + system:str, + service:str, + version:str, + client:str = '#', + callback: object = None, + delivery_mode = 1): + + paramOK = len(service) and len(system) and len(amqps) and len(origin) and len(version) and len(client) + if not (paramOK) or not is_amqp_url(amqps): + if not is_amqp_url(amqps): + msg = f"NOT A VALID AMQP URL: {amqps}" + else: + msg = f"VERIFY PARAMETERS: is_amqp_url: {is_amqp_url(amqps)}\n amqps[{amqps}]\n origin[{origin}]\n system[{system}]\n service[{service}]\n version[{version}\n client[{client}]" + raise Exception(msg) + + self.delivery = delivery_mode + self.payload = "" + self.hmac = "" + self.version = version + self.serviceid = "" + self.type = "msg" + self.origin = origin.lower() + self.client = client.lower() + self.service = service.lower() + self.system = system.lower() + self.client = client + self.params = pika.URLParameters(amqps) + self.callback = callback + self.queue = f"{self.system}_{service}_{self.client}" if client != '#' else f"{self.system}_{service}" + if callback is not None: + try: + self.connection = pika.BlockingConnection(self.params) + self.channelIn = self.connection.channel() # start main channel + self.channelIn.exchange_declare (exchange = f"{self.system}.direct", exchange_type = 'direct') + self.channelIn.queue_declare (queue = self.queue, durable = True) + self.channelIn.queue_bind (queue = self.queue, exchange = f"{self.system}.direct", routing_key= self.queue) + self.channelIn.basic_qos (prefetch_count=1) + self.consumerTag = self.channelIn.basic_consume (queue = self.queue, + auto_ack = False, + on_message_callback = self.callback) + except Exception as error: + raise(error) + else: + conexao = pika.BlockingConnection(self.params) #testa conexao + conexao.close() + + def cancel(self): + self.channelIn.basic_cancel(self.consumerTag) + + # METODOS INTERNOS _ + # MODELO MSG QUE TRAFEGA ENTRE SERVIÇOS E CONTROLLER + """ + MSG JSON CONTROLADOR + data: + { + "salesContractId": , + "client": "bambui", + "service": "SALES_CONTRACT_CREATED" + } + + message: + { ..., + "serviceid": , + "payload": aes-256 , + "hmac": hmac , + "type": "work", + ... + } + """ + def _setMsg(self, msg:str, caller:str): + now = datetime.now(timezone('America/Sao_Paulo')) + message =msg.strip('"') + + self.Msg = { + "system": self.system, + "target": self.target, + "service": self.service, + "serviceid": self.serviceid, + "payload": self.payload, + "hmac": self.hmac, + "type": self.type, + "version": self.version, + "origin": self.origin, + "client": self.client, + "caller": caller, + "logDate": f"{now}", + "msg": f"{message}" + } + + # Envia uma mensagem para um serviço associado ao sistema + # controller é o nome padrão do serviço, caso nao seja informado + def sendMsg(self, message: str, service:str = 'controller'): + trials = 5 + waitTime = 12 + messageSent = False + caller = inspect.stack()[1].function + if caller == '': caller = 'main' + + self.target = service + self._setMsg(message, caller) + targetQueue = f"{self.system}_{self.target}_{self.client}" if self.client != '#' else f"{self.system}_{self.target}" + while not messageSent and trials: + try: + connection = pika.BlockingConnection(self.params) + channelOut = connection.channel() # start a channel + #channelOut.exchange_declare(exchange = f"{self.system}.direct", exchange_type = 'direct') + #channelOut.queue_declare (queue= targetQueue, durable= True) + #channelOut.queue_bind (queue= targetQueue, exchange= f"{self.system}.direct", routing_key= targetQueue) + channelOut.basic_publish( + exchange = f"{self.system}.direct", + routing_key = targetQueue, + body = json.dumps(self.Msg), + properties = pika.BasicProperties(content_type = 'text/plain', delivery_mode= self.delivery)) + messageSent = True + connection.close() + except Exception as ex: + print(f"EXCH2: TROUBLE CNX RABBITMQ REMAIN ATTEMPT: {trials}") + print(ex, flush=True) + sys.stdout.flush() + time.sleep(waitTime) + trials -= 1 + if messageSent: + self.payload = "" + self.hmac = "" + else: + print(f"EXCH2: ERROR MESSAGE NOT SENT TO [{targetQueue}]", flush=True) + + def start_consuming(self): + if self.callback is not None: + self.consumerTag = self.channelIn.start_consuming() + + def setCrypto(self, serviceid:str, servicekey:str): + self.serviceid = serviceid + self.servicekey = servicekey + self.Crypto = Payload(serviceid, servicekey) + + def setPayload(self, data:str): + try: + self.hmac = "" + self.payload = "" + self.hmac, self.payload = self.Crypto.setPayload(data) + except Exception as ex: + print(f"EXCH2: CRYPTO ERROR: {ex}", flush=True) + + def getPayload(self, hmac:str, safeData:str): + try: + self.hmac = "" + self.payload = "" + self.hmac, self.payload = self.Crypto.getPayload(safeData) + except Exception as ex: + print(ex) + return False + + return hmac == self.hmac + +# CRIPTOGRAFIA DO PAYLOAD +class Payload: + def __init__(self, serviceid:str, servicekey:str): + self.serviceid = serviceid + self.servicekey = servicekey + + + def setPayload(self, data:str): + data = data.encode('utf-8') + tamanho = len(data) + if tamanho > 1024 * 1024 * 10 or tamanho <= 0: + raise Exception("EXCHANGE2 Payload setPayload: Invalid Payload size") + + hmacData= hmac.new(self.serviceid.encode('utf-8'), data, 'sha256').hexdigest() + iv = os.urandom(16) + padder = padding.PKCS7(128).padder() + datarw = padder.update(data) + padder.finalize() + cipher = Cipher(algorithms.AES(self.servicekey.encode('utf-8')), modes.CBC(iv)) + encrypt = cipher.encryptor() + safeData = encrypt.update(datarw) + encrypt.finalize() + payload = (iv + safeData) + + return (hmacData, payload.hex()) + + def getPayload(self, safeData:str): + safeData = bytes.fromhex(safeData) + iv = safeData[:16] + cipher = Cipher(algorithms.AES(self.servicekey.encode('utf-8')), modes.CBC(iv)) + decrypt = cipher.decryptor() + rawdata = decrypt.update(safeData[16:]) + decrypt.finalize() + if len(rawdata)%16 != 0: + raise Exception("EXCHANGE2 Payload getPayload: Invalid Payload size block {0}".format(len(rawdata))) + + unpadder = padding.PKCS7(128).unpadder() + data = unpadder.update(rawdata) + unpadder.finalize() + hmacData = hmac.new(self.serviceid.encode('utf-8'), data, 'sha256').hexdigest() + + return (hmacData, data.decode('utf-8')) + +# TASK EXCHANGE +# PERMITE CRIAR UM DISTRIBUIDOR DE TAREFAS +# O DISTRIBUIDOR DEVE RECEBER UMA FUNCAO DE CALLBACK +class TaskExchange2: + def __init__(self, + amqps:str, + origin:str, + system:str, + service:str, + version:str, + client:str = '#', callback: object = None, delivery_mode:int = 2): + paramOK = len(service) and len(system) and len(amqps) and len(origin) and len(version) + if not (paramOK) or not is_amqp_url(amqps): + if not is_amqp_url(amqps): + msg = f"NOT A VALID AMQP URL {amqps}" + else: + msg = f"VERIFY PARAMETERS: is_amqp_url: {is_amqp_url(amqps)}\n amqps[{amqps}]\n origin[{origin}]\n system[{system}]\n service[{service}]\n version[{version}]" + raise Exception(msg) + + self.delivery = delivery_mode + self.payload = "" + self.hmac = "" + self.version = version + self.serviceid = "" + self.type = "task" + self.origin = origin.lower() + self.service = service.lower() + self.system = system.lower() + self.client = client.lower() + self.params = pika.URLParameters(amqps) + self.callback = callback + self.queue = f'{self.system}_{self.service}_tasks_{client}' if client != '#' else f'{self.system}_{self.service}_tasks' + print(f"EXCH2: TASK EXCHANGE: [{self.queue}] [{amqps}]", flush=True) + if callback is not None: + self.connection = pika.BlockingConnection(self.params) + self.channelIn = self.connection.channel() # start main channel + self.channelIn.exchange_declare (exchange = f"{self.system}.direct", exchange_type = 'direct') + self.channelIn.queue_declare (queue = self.queue, durable = True) + self.channelIn.queue_bind (queue = self.queue, exchange = f"{self.system}.direct", routing_key= self.queue) + self.channelIn.basic_qos (prefetch_count=1) + self.consumerTag = self.channelIn.basic_consume (queue = self.queue, + auto_ack = False, + on_message_callback = self.callback) + else: + conexao = pika.BlockingConnection(self.params) #testa conexao + conexao.close() + def _setTask(self, msg:str, caller:str): + now = datetime.now(timezone('America/Sao_Paulo')) + message =msg.strip('"') + + self.Msg = { + "system": self.system, + "target": self.target, + "service": self.service, + "serviceid": self.serviceid, + "payload": self.payload, + "hmac": self.hmac, + "type": self.type, + "version": self.version, + "origin": self.origin, + "client": self.client, + "caller": caller, + "logDate": f"{now}", + "msg": f"{message}" + } + + def cancel(self): + self.channelIn.basic_cancel(self.consumerTag) + + def sendTask(self, message:str): + trials = 5 + waitTime = 12 + messageSent = False + caller = 'TaskExchange2' + self.target = 'workers' + self._setTask(message, caller) + + while not messageSent and trials: + try: + connection = pika.BlockingConnection(self.params) + channelOut = connection.channel() # start a channel + channelOut.exchange_declare(exchange = f"{self.system}.direct", exchange_type = 'direct') + channelOut.queue_declare (queue = self.queue, durable = True) + channelOut.queue_bind (queue = self.queue, exchange = f"{self.system}.direct", routing_key= self.queue) + + channelOut.basic_publish (exchange = f"{self.system}.direct", + routing_key = self.queue, + body = json.dumps(self.Msg), + properties = pika.BasicProperties(content_type = 'text/plain', + delivery_mode= self.delivery)) + messageSent = True + connection.close() + except Exception as ex: + print(f"EXCH2: TROUBLE CNX RABBITMQ REMAIN ATTEMPT: {trials}") + print(ex, flush=True) + time.sleep(waitTime) + trials -= 1 + if not messageSent: + print(f"EXCH2: ERROR TASK NOT SENT TO: {self.queue}", flush=True) + + def start_consuming(self): + if self.callback is not None: + print(f"EXCH2: SEND TASKS TO: [{self.queue}]", flush=True) + self.consumerTag = self.channelIn.start_consuming() + + def setCrypto(self, serviceid:str, servicekey:str): + self.serviceid = serviceid + self.servicekey = servicekey + self.Crypto = Payload(serviceid, servicekey) + + def setPayload(self, data:str): + try: + self.hmac = "" + self.payload = "" + self.hmac, self.payload = self.Crypto.setPayload(data) + except Exception as ex: + print(f"EXCH2: CRYPTO ERROR: {ex}", flush=True) + + def getPayload(self, hmac:str, safeData:str): + try: + self.hmac = "" + self.payload = "" + self.hmac, self.payload = self.Crypto.getPayload(safeData) + except Exception as ex: + print(f"EXCH2: CRYPTO ERROR: {ex}", flush=True) + return False + + return hmac == self.hmac + +# RPC - QUANDO FOR UTILIZAR COMO ENDPOINT +# FORNECER FUNCAO DE TRABALHO NA INICIALIZACAO +# COMO RPC Server: FILA ONDE RECEBERA AS TAREFAS => SISTEM_SERVICE_RPC ou SISTEM_SERVICE_RPC_CLIENT +# COMO RPC Client: UTILIZA callRPC +class RPCExchange2: + def __init__(self, + amqps:str, # ENDERECO URL RABBITMQ - CLOUDAMQP + origin:str, # NOME DO HOST ONDE O SCRIPT ESTÁ RODANDO + system:str, # NOME DO SISTEMA QUE O SERVIÇO FAZ PARTE + service:str, # NOME DO SERVIÇO DENTRO DO SISTEMA + version:str, # VERSÃO DO SERVIÇO + client:str = '#', # CASO ESPECIFICO A UM CLIENTE, COMO NO CASO DO SG_ETL + workfunction=None, # FUNCAO DE TRABALHO PARA O USO COMO RCP Server, i.e. para substituir um endpoint fastapi + delivery_mode=2 # VAI PERSISTIR AS MENSAGENS NO MODO 2 + ): + paramOK = len(service) and len(system) and len(amqps) and len(origin) and len(version) + + if not (paramOK) or not is_amqp_url(amqps): + if not is_amqp_url(amqps): + msg = f"RPCExchange2: NOT A VALID AMQP URL: {amqps}" + else: + msg = f"RPCExchange2: VERIFY PARAMETERS: is_amqp_url: {is_amqp_url(amqps)}\n amqps[{amqps}]\n origin[{origin}]\n system[{system}]\n service[{service}]\n version[{version}]" + raise Exception(msg) + + self.callback = workfunction + self.delivery = delivery_mode + self.payload = "" + self.hmac = "" + self.version = version + self.serviceid = "" + self.servicekey = "" + self.correlation_id = "" + self.consumerTag = None + self.type = "rpc" + self.origin = origin.lower() + self.service = service.lower() + self.system = system.lower() + self.client = client.lower() + self.params = pika.URLParameters(amqps) + # Server ouve nessa fila - Cliente publica nesta fila + self.queue = f'{self.system}_{self.service}_rpc_{client}' if client != '#' else f'{self.system}_{self.service}_rpc' + self.connection = pika.BlockingConnection(self.params) + self.channel = self.connection.channel() # start main channel + self.channel.exchange_declare (exchange = f"{self.system}.direct", exchange_type = 'direct') + + if (workfunction is not None): # FUNCIONAMENTO COMO RPC SERVER + self.target = 'rpcclt' # SINALIZA O DESTINO DA MSG SER UM RPC CLIENT + self.channel.queue_declare (queue = self.queue, exclusive = False, durable=True, auto_delete = True) + self.channel.queue_bind (exchange = f"{self.system}.direct", queue = self.queue, routing_key = self.queue) + self.channel.basic_qos (prefetch_count = 1) + self.consumerTag = self.channel.basic_consume(queue = self.queue, + auto_ack = True, + on_message_callback = self._RPCResp) # de lá chamamos self.callback + else: # FUNCIONAMENTO COMO RPC CLIENT + self.target = 'rpcsrv' # SINALIZA O DESTINO DA MSG SER UM RPC SERVER + + # PREPARA QUEUE DE RETORNO + result = self.channel.queue_declare(queue='', exclusive=True, auto_delete=True) + self.callback_queue = result.method.queue + self.channel.queue_bind(exchange = f"{self.system}.direct", + queue = self.callback_queue, + routing_key = self.callback_queue) + + self.channel.basic_consume( + queue = self.callback_queue, + on_message_callback = self._on_response, + auto_ack = True + ) + #print("EXCH2: RPC CLT - QUEUE:", self.callback_queue, flush=True) + + # RPC - QUANDO FOR UTILIZAR COMO ENDPOINT + def _RPCResp(self, ch, method, props, body): + #ch.basic_ack(delivery_tag = method.delivery_tag) + message = body.decode("utf-8") + reply_to = getattr(props, 'reply_to', None) + correlation_id = getattr(props, 'correlation_id', None) + resposta = "{}" # SEM WORKFUNCTION + # ENVIA A MENSAGEM AO CALLBACK + self.payload = "" + self.hmac = "" + try: + if self.callback is not None: + resposta = self.callback(message) + rspObj = json.loads(resposta) + # para enviar um payload criptografado na resposta ao caller + # retorne um objeto contendo serviceid, servicekey e payload + # essas chaves serao retiradas do objeto e o restante da informacao + # irá no chave msg + if 'payload' in rspObj and 'serviceid' in rspObj and 'servicekey' in rspObj: + self.setCrypto(rspObj['serviceid'], rspObj['servicekey']) + self.setPayload(json.dumps(rspObj['payload'])) + rspObj.pop('payload', None) + rspObj.pop('serviceid', None) + rspObj.pop('servicekey', None) + resposta = json.dumps(rspObj) + + + except Exception as ex: + erro = f"ERROR CALLBACK: {str(ex)}" + print(erro, flush=True) + resposta = json.dumps({"status": "error", "message": erro}) + + # ENVIA RESPOSTA AO CALLER + msg = json.loads(message) + msg = msg['msg'] + #print(f"RPC <== {msg}", flush=True) + #print(f"RPC ==> {resposta}", flush=True) + try: + if reply_to: + self.target = "rpcclt" + self._setJSON(resposta, 'RPCResp') + + ch.basic_publish(exchange = f"{self.system}.direct", + routing_key = reply_to, + body = json.dumps(self.Msg), + properties = pika.BasicProperties(content_type = 'application/json', + delivery_mode = self.delivery, + correlation_id = correlation_id)) + else: + print(f"RPCExchange2: NO TARGET FOUND: {resposta}", props, flush=True) + + except Exception as ex: + print(ex) + resposta = f"RPCExchange2: ERRO MSG CALLER: {str(ex)}" + raise Exception(resposta) + + + def _setJSON(self, msg:str, caller:str): + now = datetime.now(timezone('America/Sao_Paulo')) + message =msg.strip('"') + + self.Msg = { + "system": self.system, + "target": self.target, + "service": self.service, + "serviceid": self.serviceid, + "payload": self.payload, + "hmac": self.hmac, + "type": self.type, + "correlation_id": self.correlation_id, + "version": self.version, + "origin": self.origin, + "client": self.client, + "caller": caller, + "logDate": f"{now}", + "msg": f"{message}" + } + self.hmac = "" + self.payload = "" + + + def _on_response(self, ch, method, properties, body): + try: + if self.correlation_id == properties.correlation_id: + self.response = body.decode("utf-8") + except Exception as ex: + print(f"RPCExchange2: RPC ERROR: {ex}", flush=True) + sys.exit(0) + + return self.response + + # ENVIA A MENSAGEM AO CALLBACK + # RPC CLIENT CHAMA O RPC SERVER + def callRPC(self, message:str, timeout:int=10): + # ENVIA A MENSAGEM + self.response = None + self.correlation_id = str(uuid.uuid4()) + self._setJSON(message, 'callRPC') # COLOCA A message DENTRO DA CHAVE msg em self.Msg + + self.channel.exchange_declare(exchange = f"{self.system}.direct", exchange_type = 'direct') + self.channel.queue_declare (queue= self.queue, durable=True, exclusive=False, auto_delete=True) + self.channel.queue_bind (queue= self.queue, exchange= f"{self.system}.direct", routing_key= self.queue) + + self.channel.basic_publish (exchange = f"{self.system}.direct", + routing_key = self.queue, + body = json.dumps(self.Msg), + properties = pika.BasicProperties(content_type = 'application/json', + delivery_mode = self.delivery, + reply_to = self.callback_queue, + correlation_id= self.correlation_id)) + # AGUARDA O RETORNO + while self.response is None: + self.connection.process_data_events(time_limit=timeout) + return self.response + + def cancel(self): + print(f"RPCExchange2: CONSUMER TAG CANCELED: {self.consumerTag}", flush=True) + self.channel.basic_cancel(self.consumerTag) + + def start_consuming(self): + if self.callback is not None: + print(f"RPCExchange2: {self.service.upper()} RPC SRV QUEUE: [{self.queue}]", flush=True) + self.channel.start_consuming() + + def setCrypto(self, serviceid:str, servicekey:str): + self.serviceid = serviceid + self.servicekey = servicekey + self.Crypto = Payload(serviceid, servicekey) + + def setPayload(self, data:str): + print(f"RPCExchange2: SET PAYLOAD", flush=True) + try: + self.hmac = "" + self.payload = "" + self.hmac, self.payload = self.Crypto.setPayload(data) + except Exception as ex: + print(ex) + + def getPayload(self, hmac:str, safeData:str): + print(f"RPCExchange2: GET PAYLOAD", flush=True) + try: + self.hmac = "" + self.payload = "" + self.hmac, self.payload = self.Crypto.getPayload(safeData) + except Exception as ex: + print(ex) + return False + + return hmac == self.hmac + +# CADA APLICAÇÃO CRIA UMA CLASSE QUE ESTENDE IC2Builder IMPLEMENTANDO +# OS METODOS ABSTRATOS DE CADA REALMTYPE +class IC2Builder(ABC): + @abstractmethod + def _setLogs(self, message: tuple) -> InfoConnector: ... + @abstractmethod + def _setErrors(self, message: tuple) -> InfoConnector: ... + @abstractmethod + def _setOperational(self, message: tuple) -> InfoConnector: ... + @abstractmethod + def _setStatus(self, message: tuple) -> InfoConnector: ... + @abstractmethod + def _setMetrics(self, message: tuple) -> InfoConnector: ... + + def build(self, realm: RealmType, message: Sequence[Any]) -> InfoConnector: + handlers = { + RealmType.LOGS: self._setLogs, + RealmType.ERRORS: self._setErrors, + RealmType.OPERATIONAL: self._setOperational, + RealmType.STATUS: self._setStatus, + RealmType.METRICS: self._setMetrics, + } + + try: + return handlers[realm](message) + except KeyError: + raise ValueError(f"RealmType não suportado: {realm}") + +class LoggerExchange2: + def __enter__(self): return self + def __exit__(self, exc_type, exc, tb): self.close() + + def __init__(self, + ic2_builder: IC2Builder, + amqps : str, + system : str, + service : str, + version : str, + origin : str = "", + client : str = "#", + logType : str = 'logs', + logLocal : bool = False, + callback : object = None): + self.callback = callback + self.ic2_builder = ic2_builder + self.system = system.lower() + self.target = "" + self.service = service.lower() + self.serviceid = "AF1699F719F1B6C81DE2BD38AB811994E3089B35C8E9314813A770AC7AE25D8D" + self.servicekey = "KoG704lAafBA7Yafj8jyg5lAHfarrQ12" + self.payload = "" + self.hmac = "" + self.type = "logger" + self.version = version + self.origin = origin + self.client = client.lower() + self.params = pika.URLParameters(amqps) + self.params.socket_timeout = 15 + self.params.heartbeat = 60 + self.logType = logType.lower() + self.logTypeSvd = logType.lower() + self.logLocal = logLocal + self.pp = pprint.PrettyPrinter() + self.diaMes = datetime.now().strftime("%d") # PARA SABER SE ATUALIZA OU NÃO O ARQUIVO DE LOG LOCAL + self.flush = True + self.base_routeKey = f"{self.service}.{self.system}" + self.connection = None + self.channel = None + self.logging = logging.getLogger(self.base_routeKey) + self.Crypto = Payload(self.serviceid, self.servicekey) + self.exchange_name = 'info_connector.topic' + + try: + self._connect() + except Exception as err: + self.logging.error(f"ERROR CONNECTION {err}") + raise + + if self.logLocal: self.setLogLocalName() + + # --- async logging --- + self.queue_size = 1000 + self.queue = Queue(maxsize=self.queue_size) + self.stop_event = Event() + self.worker_thread = Thread( + target = self._worker, + name = "logger-worker", + daemon = True + ) + self.worker_thread.start() + + self.logging.info(f"params: {self.params} amqps: {amqps}") + if self.callback is not None: + try: + self.queueIC2 = "info_connector_collector" + self.channelIn = self.connection.channel() + self.channelIn.queue_declare (queue = self.queueIC2, exclusive=True, durable=True) + self.channelIn.queue_bind (queue = self.queueIC2, exchange = self.exchange_name, routing_key = "#") + self.channelIn.basic_qos (prefetch_count = 1) + self.consumerTag = self.channelIn.basic_consume (queue = self.queueIC2, + auto_ack = False, + on_message_callback = self.callback) + self.logging.info("COLLECTING TOPIC MESSAGES VHOST info_connector_host EXCHANGE info_connector.topic KEY #") + except Exception as error: + raise(error) + else: + conexao = pika.BlockingConnection(self.params) #testa conexao + conexao.close() + self.logging.info(f"Logger Messages topics: {self.logType}.{self.base_routeKey}.{self.client}") + + + # PERSISTENCIA LOGS MESSGERIA + def _worker(self): + while not self.stop_event.is_set() or not self.queue.empty(): + try: + payload, client = self.queue.get(timeout=0.5) + except Empty: + continue + + try: + self.sendMsgWork(payload, client) + except Exception as e: + # última linha de defesa + self.logging.error(f"ASYNC LOGGER FAILURE: {e}") + if self.logLocal: + self.logging.info(payload) + finally: + self.queue.task_done() + + # PERSISTENCIA CONEXAO AMQPS + def _connect(self): + """Conecta ou reconecta ao RabbitMQ.""" + if self.connection and self.connection.is_open: + return + try: + if self.connection: + self.connection.close() + except Exception: + pass + try: + self.connection = pika.BlockingConnection(self.params) + self.channel = self.connection.channel() + self.channel.exchange_declare(exchange = self.exchange_name, + exchange_type = 'topic', + auto_delete = False) + + self.logging.info("Conexão ao RabbitMQ estabelecida.") + except pika.exceptions.AMQPConnectionError as e: + self.logging.error(f"Falha na conexão: {e}") + raise + + def _setMsg(self, msg:str, caller:str): + now = datetime.now(timezone('America/Sao_Paulo')) # melhoria: acrescentar timezone info no system_config + return { + "system" : self.system, + "target" : self.target, + "service" : self.service, + "serviceid": self.serviceid, + "payload" : self.payload, + "hmac" : self.hmac, + "type" : self.type, + "version" : self.version, + "origin" : self.origin, + "client" : self.client, + "caller" : caller, + "logDate" : f"{now}", + "msg" : msg + } + + def _sendMsg(self, msg: str, caller: str, *args): + # Avalia arquivo de logs + if self.logLocal: + current_dia = datetime.now().strftime("%d") + if current_dia != self.diaMes: + self.diaMes = current_dia + self.setLogLocalName() + + message = msg.strip() + + try: + if args and isinstance(args[0], (list, tuple)): + for item in args[0]: + msgArg = pprint.pformat(item).replace("'", "").strip() + message = f"{message}, {msgArg}" + + Msg = self._setMsg(message, caller) + payload = json.dumps(Msg) + + # console / docker logs continuam síncronos + self.pp.pprint(f"{Msg['logDate'][0:21]} {Msg['msg']}") + + # log local imediato (garantia mínima) + if self.logLocal: + Msg['payload'] = Msg['payload'][:20] + self.logging.info(self.pp.pformat(Msg)) + + # envio assíncrono + try: + self.queue.put_nowait((payload, self.client)) + except Full: + # fila cheia → degrada com dignidade + self.logging.warning("LOGGER QUEUE FULL — DROPPING MESSAGE") + + if self.flush: + sys.stdout.flush() + + except Exception as e: + erro = f"LOGGER ERROR: {e}" + self.logging.error(erro) + print(erro, flush=True) + + + def _sendMsgLocal(self, msg: str, caller: str, *args): + # Avalia arquivo de logs + if self.logLocal: + current_dia = datetime.now().strftime("%d") + if current_dia != self.diaMes: + self.diaMes = current_dia + self.setLogLocalName() + + message = msg.strip() + + try: + if args and isinstance(args[0], (list, tuple)): + for item in args[0]: + msgArg = pprint.pformat(item).replace("'", "").strip() + message = f"{message}, {msgArg}" + + Msg = self._setMsg(message, caller) + payload = json.dumps(Msg) + + # console / docker logs continuam síncronos + self.pp.pprint(f"{Msg['logDate'][0:21]} {Msg['msg']}") + + # log local imediato (garantia mínima) + if self.logLocal: + Msg['payload'] = Msg['payload'][:20] + self.logging.info(self.pp.pformat(Msg)) + + except Exception as e: + erro = f"LOGGER ERROR: {e}" + self.logging.error(erro) + print(erro, flush=True) + + def start_consuming(self): + if self.callback is not None: + self.consumerTag = self.channelIn.start_consuming() + + def close(self): + # sinaliza parada + self.stop_event.set() + + # aguarda flush da fila (timeout defensivo) + try: + self.queue.join() + except Exception: + pass + + if self.worker_thread.is_alive(): + self.worker_thread.join(timeout=2) + + if self.channel: + self.channel.close() + if self.connection: + self.connection.close() + + def setLogLocalName(self): + self.logLocalName = 'logs/'+ self.system.split()[0] + '_' + self.diaMes + formatter = logging.Formatter("%(asctime)s - %(name)s - %(levelname)s - %(message)s") + fhandler = logging.FileHandler(self.logLocalName) + fhandler.setLevel(logging.INFO) + fhandler.setFormatter(formatter) + self.logging.propagate = False + self.logging.setLevel(logging.INFO) + if not any(isinstance(h, logging.FileHandler) for h in self.logging.handlers): + self.logging.addHandler(fhandler) + + # Funcionamento: caso nao seja possivel enviar a msg, tentar nova conexao + # e envio da msg original mais uma vez + def sendMsgWork(self, message:str, client:str = "#"): + messageSent = False + trials = 5 + waitTime = 5 + while not messageSent and trials > 0: + self._connect() + # Cliente informado - MAIOR PRECEDENCIA + if client != "#": + routeKey = self.base_routeKey + f".{client.lower()}" + elif self.client != "#": + routeKey = self.base_routeKey + f".{self.client.lower()}" + else: + routeKey = self.base_routeKey + + try: + routing_key = f"{self.logType}.{routeKey}" + self.channel.basic_publish( + exchange = "info_connector.topic", + routing_key = routing_key, + body = message, + properties = pika.BasicProperties( + content_type = 'text/plain', + delivery_mode= pika.DeliveryMode.Transient)) + messageSent = True + except (pika.exceptions.ConnectionClosed, pika.exceptions.ChannelClosed, pika.exceptions.AMQPConnectionError) as ex: + self.logging.warning(f"TROUBLE CNX LOGGER REMAIN ATTEMPT: {trials} {ex}") + self._connect() + except Exception as ex: + self.logging.error(f"LOGGER ERROR: {ex}") + self.logging.warning(str(self.params)) + finally: + if not messageSent: + trials -= 1 + time.sleep(waitTime) + waitTime *= 2 + + if not messageSent: + self.logging.error(f"LOGGER ERROR: {message}") + + # Para quando desejar trocar o header do topico + # Salva o tópico padrão especificado no momento da inicialização + def setLogType(self, logType:str): + if not logType or len(logType) > 25: + return + self.logTypeSvd = self.logType + self.logType = logType.lower() + + # Volta ao header topico padrao + def clrLogType(self): + self.logType = self.logTypeSvd + + def logIC2(self, realm: RealmType, msg: tuple = ()): + ic_obj = self.ic2_builder.build(realm, msg) + payload = ic_obj.model_dump_json() + self.setPayload(data=payload) + self.setLogType(logType=realm.value) + self._sendMsg(f"LOGIC2 {realm.value.upper()} [{str(msg)[:20]} ...]", caller="IC2") + self.clrLogType() + self.clrPayload() + + def logMsgError(self, msg:str, *args): + self.setLogType('error.logs') + caller = inspect.stack()[1].function + if caller == '': caller = 'main' + + self._sendMsgLocal(msg, caller, args) + self.clrLogType() + + # LOGS GERAIS + # INFORM logs...# + # Log geral de acordo com o valor logType + def logMsg (self, msg: str, *args): + caller = inspect.stack()[1].function + if caller == '': caller = 'main' + Msg = f"{msg}" + self._sendMsgLocal(Msg, caller, args) + + # FUNÇÕES PARA CRYPTOGRAFIA + def clrPayload(self): + self.hmac = "" + self.payload = "" + + def setPayload(self, data:str): + try: + self.hmac = "" + self.payload = "" + self.hmac, self.payload = self.Crypto.setPayload(data) + except Exception as ex: + print(f"EXCH2: CRYPTO ERROR: {ex}", flush=True) + + def getPayload(self, hmac:str, safeData:str): + try: + self.hmac = "" + self.payload = "" + self.hmac, self.payload = self.Crypto.getPayload(safeData) + except Exception as ex: + print(f"EXCH2: CRYPTO ERROR: {ex}", flush=True) + return False + + return hmac == self.hmac + +class AppIC2Builder(IC2Builder): + def __init__(self, service_name: str, versionstr: str, origin: str): + self.service_name = service_name + self.versionstr = versionstr + self.origin = origin + + def _get_common_fields(self): + now = datetime.now().strftime("%Y-%m-%d %H:%M:%S") + return { + "log_date": now, + "service_date": now, + "service": self.service_name, + "versionstr": self.versionstr, + "origin": self.origin + } + + def _setLogs(self, message: tuple) -> InfoConnector: + msg_str = str(message[0]) if len(message) > 0 else "" + ent_id = str(message[1]) if len(message) > 1 else "0" + ent_name = str(message[2]) if len(message) > 2 else "Unknown" + tenant = str(message[3]) if len(message) > 3 else "#" + + ic_msg = ICLogs(**self._get_common_fields(), enterprise_id=ent_id, enterprise_name=ent_name, message=msg_str, tenant=tenant) + return InfoConnector(realm=RealmType.LOGS, messageIC=ic_msg) + + def _setErrors(self, message: tuple) -> InfoConnector: + msg_str = str(message[0]) if len(message) > 0 else "" + ent_id = str(message[1]) if len(message) > 1 else "0" + ent_name = str(message[2]) if len(message) > 2 else "Unknown" + tenant = str(message[3]) if len(message) > 3 else "#" + + ic_msg = ICErrors(**self._get_common_fields(), enterprise_id=ent_id, enterprise_name=ent_name, message=msg_str, tenant=tenant) + return InfoConnector(realm=RealmType.ERRORS, messageIC=ic_msg) + + def _setOperational(self, message: tuple) -> InfoConnector: + acao = str(message[0]) if len(message) > 0 else "" + tabela = str(message[1]) if len(message) > 1 else "" + id_usuario = str(message[2]) if len(message) > 2 else "0" + id_registro = str(message[3]) if len(message) > 3 else "0" + status_str = str(message[4]).upper() if len(message) > 4 else "INICIO" + mensagem = str(message[5]) if len(message) > 5 else "" + json_data = str(message[6]) if len(message) > 6 else "{}" + + if status_str == 'SUCESSO': op_status = OpStatus.OK + elif status_str == 'ERRO': op_status = OpStatus.ERROR + else: op_status = OpStatus.ACTIVE + + common = self._get_common_fields() + ic_msg = ICOperational( + log_date=common["log_date"], service_date=common["service_date"], service_name=self.service_name, + versionstr=self.versionstr, origin=self.origin, enterprise_id=id_usuario, enterprise_name=f"User {id_usuario}", + status=op_status, step_status=acao, message=f"{mensagem} | Tabela: {tabela} | ID_Reg: {id_registro} | Data: {json_data}" + ) + return InfoConnector(realm=RealmType.OPERATIONAL, messageIC=ic_msg) + + def _setStatus(self, message: tuple) -> InfoConnector: + common = self._get_common_fields() + ic_msg = ICStatus( service=self.service_name, versionstr=self.versionstr, system_name="integracao", + startup_date=common["log_date"], access_date=common["log_date"], origin=self.origin ) + return InfoConnector(realm=RealmType.STATUS, messageIC=ic_msg) + + def _setMetrics(self, message: tuple) -> InfoConnector: + metric_name = str(message[0]) if len(message) > 0 else "" + value = str(message[1]) if len(message) > 1 else "0" + unit = str(message[2]) if len(message) > 2 else "" + + now = datetime.now().strftime("%Y-%m-%d %H:%M:%S") + ic_msg = ICMetrics( metric_date=now, metric=metric_name, metric_name=metric_name, value=value, unit=unit, tenant="#" ) + return InfoConnector(realm=RealmType.METRICS, messageIC=ic_msg) diff --git a/backup/src/utils/api.py b/backup/src/utils/api.py new file mode 100644 index 0000000..e7c48ce --- /dev/null +++ b/backup/src/utils/api.py @@ -0,0 +1,158 @@ +import requests + +# Wrapper base para realizar requests (GET/POST/PUT/DELETE) à API da PlugNotas de forma padronizada +class Api: + def __init__(self, api_key): + self.api_key = api_key + + def get(self, url): + try: + headers = { + "x-api-key": self.api_key + } + response = requests.get(url, headers=headers, timeout=30) + response.raise_for_status() + return response.json() + except requests.exceptions.RequestException as e: + resp_data = {} + if 'response' in locals() and response is not None: + try: + resp_data = response.json() + except ValueError: + resp_data = response.text + return {"error": str(e), "response": resp_data} + + def get_paginated(self, url, max_pages=100): + try: + headers = { + "x-api-key": self.api_key + } + all_data = [] + current_url = url + page = 0 + + while current_url and page < max_pages: + separator = "&" if "?" in current_url else "?" + request_url = current_url if page == 0 else current_url + + response = requests.get(request_url, headers=headers, timeout=30) + response.raise_for_status() + result = response.json() + + if isinstance(result, list): + all_data.extend(result) + break + elif isinstance(result, dict): + page_data = result.get("data", []) + if isinstance(page_data, list): + all_data.extend(page_data) + elif page_data: + all_data.append(page_data) + + hash_proxima = result.get("hashProximaPagina") + if hash_proxima: + separator = "&" if "?" in url else "?" + current_url = f"{url}{separator}hashProximaPagina={hash_proxima}" + page += 1 + else: + break + else: + break + + return {"data": all_data, "total_paginas": page + 1, "total_registros": len(all_data)} + except requests.exceptions.RequestException as e: + resp_data = {} + if 'response' in locals() and response is not None: + try: + resp_data = response.json() + except ValueError: + resp_data = response.text + return {"error": str(e), "response": resp_data} + + def post(self, url, data): + try: + headers = { + "Content-Type": "application/json", + "x-api-key": self.api_key + } + response = requests.post(url, headers=headers, json=data, timeout=30) + response.raise_for_status() + return response.json() + except requests.exceptions.RequestException as e: + resp_data = {} + if 'response' in locals() and response is not None: + try: + resp_data = response.json() + except ValueError: + resp_data = response.text + return {"error": str(e), "response": resp_data} + + def put(self, url, data): + try: + headers = { + "x-api-key": self.api_key + } + response = requests.put(url, headers=headers, json=data, timeout=30) + response.raise_for_status() + return response.json() + except requests.exceptions.RequestException as e: + return {"error": str(e)} + + def patch(self, url, data): + try: + headers = { + "x-api-key": self.api_key + } + response = requests.patch(url, headers=headers, json=data, timeout=30) + response.raise_for_status() + return response.json() + except requests.exceptions.RequestException as e: + return {"error": str(e)} + + def delete(self, url): + try: + headers = { + "x-api-key": self.api_key + } + response = requests.delete(url, headers=headers, timeout=30) + response.raise_for_status() + return response.json() + except requests.exceptions.RequestException as e: + return {"error": str(e)} + + def post_file(self, url, file): + try: + headers = { + "x-api-key": self.api_key + } + response = requests.post(url, headers=headers, files=file, timeout=60) + response.raise_for_status() + return response.json() + except requests.exceptions.RequestException as e: + return {"error": str(e)} + + def post_form_data(self, url, data): + try: + headers = { + "x-api-key": self.api_key + } + response = requests.post(url, headers=headers, files=data, timeout=60) + response.raise_for_status() + return response.json() + except requests.exceptions.RequestException as e: + return {"error": str(e)} + + def put_form_data(self, url, data): + try: + headers = { + "x-api-key": self.api_key + } + response = requests.put(url, headers=headers, files=data, timeout=60) + response.raise_for_status() + return response.json() + except requests.exceptions.RequestException as e: + return {"error": str(e)} + +# api = Api("2da392a6-79d2-4304-a8b7-959572c7e44d") +# response = api.get("https://api.sandbox.plugnotas.com.br/certificado") +# print(response) diff --git a/backup/src/utils/banco.py b/backup/src/utils/banco.py new file mode 100644 index 0000000..b51c9a6 --- /dev/null +++ b/backup/src/utils/banco.py @@ -0,0 +1,241 @@ +from datetime import datetime +import uuid +from sqlalchemy import Column, Integer, String, DateTime, Text, create_engine, Boolean +from sqlalchemy.orm import declarative_base, sessionmaker +from sqlalchemy.exc import SQLAlchemyError + +Base = declarative_base() + +class CertificadoDigitalTable(Base): + __tablename__ = 'certificado_digital' + id = Column(Integer, primary_key=True, autoincrement=True) + id_tecnospeed = Column(String(50), nullable=False) + certificado = Column(Text, nullable=False) + createdAt = Column(DateTime, default=datetime.now) + updatedAt = Column(DateTime, default=datetime.now, onupdate=datetime.now) + deletedAt = Column(DateTime, nullable=True) + expirationAt = Column(DateTime, nullable=False) + senha = Column(String(50), nullable=False) + status = Column(Boolean, nullable=False) + id_empresa = Column(String(50), nullable=False) + id_usuario = Column(Integer, nullable=False) + email = Column(String(255), nullable=True) + +class EnterpriseTable(Base): + __tablename__ = 'Enterprise' + id = Column(String(50), primary_key=True) + name = Column(String(255), nullable=False) + cnpj = Column(String(50), nullable=False) + banco = Column(Text, nullable=True) + email = Column(String(255), nullable=True) + createdAt = Column(DateTime, default=datetime.now) + updatedAt = Column(DateTime, default=datetime.now, onupdate=datetime.now) + deletedAt = Column(DateTime, nullable=True) + status = Column(Boolean, nullable=False, default=True) + +class LogoTable(Base): + __tablename__ = 'logo' + id = Column(Integer, primary_key=True, autoincrement=True) + logo = Column(Text, nullable=False) + id_empresa = Column(String(50), nullable=False) + createdAt = Column(DateTime, default=datetime.now) + updatedAt = Column(DateTime, default=datetime.now, onupdate=datetime.now) + deletedAt = Column(DateTime, nullable=True) + status = Column(Boolean, nullable=False) + +class LogTable(Base): + __tablename__ = 'log' + id = Column(Integer, primary_key=True, autoincrement=True) + id_usuario = Column(Integer, nullable=False) + createdAt = Column(DateTime, default=datetime.now) + updatedAt = Column(DateTime, default=datetime.now, onupdate=datetime.now) + deletedAt = Column(DateTime, nullable=True) + acao = Column(String(50), nullable=False) + tabela = Column(String(50), nullable=False) + id_registro = Column(String(50), nullable=False) + json = Column(Text, nullable=False) + status = Column(String(20), nullable=False, default='INICIO') + mensagem = Column(Text, nullable=False, default='') + cliente = Column(String(100), nullable=True) + correlation_id = Column(String(50), nullable=True) + +class ServicoTable(Base): + __tablename__ = 'servico' + id = Column(Integer, primary_key=True, autoincrement=True) + id_tecnospeed = Column(String(50), nullable=False) + descricao = Column(Text, nullable=False) + id_usuario = Column(Integer, nullable=False) + createdAt = Column(DateTime, default=datetime.now) + updatedAt = Column(DateTime, default=datetime.now, onupdate=datetime.now) + deletedAt = Column(DateTime, nullable=True) + status = Column(Boolean, nullable=False) + +class TomadorTable(Base): + __tablename__ = 'tomador' + id = Column(Integer, primary_key=True, autoincrement=True) + id_tecnospeed = Column(String(50), nullable=False) + nome = Column(String(50), nullable=False) + cpf_cnpj = Column(String(50), nullable=False) + email = Column(String(50), nullable=False) + telefone = Column(String(50), nullable=False) + id_usuario = Column(Integer, nullable=False) + createdAt = Column(DateTime, default=datetime.now) + updatedAt = Column(DateTime, default=datetime.now, onupdate=datetime.now) + deletedAt = Column(DateTime, nullable=True) + status = Column(Boolean, nullable=False) + +class NFSeTable(Base): + __tablename__ = 'nfse' + id = Column(Integer, primary_key=True, autoincrement=True) + id_tecnospeed = Column(String(50), nullable=False) + id_empresa = Column(String(50), nullable=False) + id_usuario = Column(Integer, nullable=False) + createdAt = Column(DateTime, default=datetime.now) + updatedAt = Column(DateTime, default=datetime.now, onupdate=datetime.now) + deletedAt = Column(DateTime, nullable=True) + status = Column(Boolean, nullable=False) + +class NFeTable(Base): + __tablename__ = 'nfe' + id = Column(Integer, primary_key=True, autoincrement=True) + id_tecnospeed = Column(String(50), nullable=False) + id_empresa = Column(String(50), nullable=False) + id_usuario = Column(Integer, nullable=False) + createdAt = Column(DateTime, default=datetime.now) + updatedAt = Column(DateTime, default=datetime.now, onupdate=datetime.now) + deletedAt = Column(DateTime, nullable=True) + status = Column(Boolean, nullable=False) + +# Cache de motores criados pelo SQLAlchemy por URL de conexao +_engines = {} + +# Abstração de conexão base e de persistência de instâncias de metadados relacionais usando PostgreSQL +class Banco: + def __init__(self, host, port, user, password, database): + conn_str = f'postgresql://{user}:{password}@{host}:{port}/{database}' + if conn_str not in _engines: + _engines[conn_str] = create_engine( + conn_str, + pool_pre_ping=True, + pool_recycle=3600 + ) + self.engine = _engines[conn_str] + self.Session = sessionmaker(bind=self.engine) + self.session = self.Session() + + def close(self): + """Encerra a sessão SQLAlchemy atual liberando do pooler.""" + if hasattr(self, 'session') and self.session is not None: + self.session.close() + + def create_tables(self): + """Cria as tabelas no banco de dados.""" + try: + Base.metadata.create_all(self.engine) + except SQLAlchemyError as e: + print(f"Erro ao criar tabelas (SQLAlchemy): {e}") + except Exception as e: + print(f"Erro ao criar tabelas: {e}") + + def add(self, obj): + """Adiciona um novo registro no banco.""" + try: + self.session.add(obj) + self.session.commit() + return obj + except SQLAlchemyError as e: + self.session.rollback() + print(f"Erro ao adicionar registro (SQLAlchemy): {e}") + raise e + except Exception as e: + self.session.rollback() + print(f"Erro ao adicionar registro: {e}") + raise e + + def get(self, model, obj_id): + """Busca um registro pelo ID.""" + try: + return self.session.query(model).get(obj_id) + except SQLAlchemyError as e: + self.session.rollback() + print(f"Erro ao buscar registro (SQLAlchemy): {e}") + raise e + except Exception as e: + self.session.rollback() + print(f"Erro ao buscar registro: {e}") + raise e + + def get_all(self, model): + """Retorna todos os registros de um modelo.""" + try: + return self.session.query(model).all() + except SQLAlchemyError as e: + print(f"Erro ao buscar registros (SQLAlchemy): {e}") + raise e + except Exception as e: + print(f"Erro ao buscar registros: {e}") + raise e + + def update(self, obj): + """Atualiza um registro no banco.""" + try: + self.session.commit() + return obj + except SQLAlchemyError as e: + self.session.rollback() + print(f"Erro ao atualizar registro (SQLAlchemy): {e}") + raise e + except Exception as e: + self.session.rollback() + print(f"Erro ao atualizar registro: {e}") + raise e + + def delete(self, obj): + """Deleta um registro do banco.""" + try: + self.session.delete(obj) + self.session.commit() + return True + except SQLAlchemyError as e: + self.session.rollback() + print(f"Erro ao deletar registro (SQLAlchemy): {e}") + raise e + except Exception as e: + self.session.rollback() + print(f"Erro ao deletar registro: {e}") + raise e + + def rollback(self): + """Desfaz as alterações pendentes.""" + try: + self.session.rollback() + except SQLAlchemyError as e: + print(f"Erro ao desfazer alterações (SQLAlchemy): {e}") + except Exception as e: + print(f"Erro ao desfazer alterações: {e}") + + def logar_sistema(self, id_usuario, acao, tabela, id_registro=0, json="{}", status="INICIO", mensagem="", cliente=None, correlation_id=None): + """Registra uma ação no log do sistema.""" + try: + log = LogTable( + id_usuario=id_usuario, + acao=acao, + tabela=tabela, + id_registro=id_registro, + json=json, + status=status, + mensagem=mensagem, + cliente=cliente, + correlation_id=correlation_id + ) + self.add(log) + except SQLAlchemyError as e: + print(f"Erro ao logar ação (SQLAlchemy): {e}") + except Exception as e: + print(f"Erro ao logar ação: {e}") + + @staticmethod + def gerar_correlation_id(): + """Gera um ID único para rastrear toda a cadeia de uma operação.""" + return str(uuid.uuid4())[:8] + diff --git a/backup/src/utils/enviaListerner.py b/backup/src/utils/enviaListerner.py new file mode 100644 index 0000000..42ba65c --- /dev/null +++ b/backup/src/utils/enviaListerner.py @@ -0,0 +1,35 @@ +import requests +import json +import os + +class enviaListerner: + + def __init__(self, logger=None): + self.url = os.getenv("LISTENER_INTERNAL_URL", "http://webhook_listener:8000/listenerPlugnotas") + self.logger = logger + + def notificar_emissao(self, id_tecnospeed: str, documento: str, cnpj: str, status: str = "PROCESSANDO"): + """ + Envia um POST para o listener local para sincronização imediata. + """ + payload = { + "id": id_tecnospeed, + "status": status, + "documento": documento, + "emitente": cnpj + } + + try: + response = requests.post(self.url, json=payload, timeout=5) + + if self.logger: + if response.status_code in [200, 201]: + self.logger.log_logs(f"Notificação interna enviada com sucesso - ID: {id_tecnospeed} | Status: {status}") + else: + self.logger.log_errors(f"Falha na notificação interna - Status: {response.status_code} | Resposta: {response.text}") + + return response.status_code in [200, 201] + except Exception as e: + if self.logger: + self.logger.log_errors(f"Erro ao tentar notificar listener interno: {e}") + return False diff --git a/backup/src/utils/exchangeModels.py b/backup/src/utils/exchangeModels.py new file mode 100644 index 0000000..1e19116 --- /dev/null +++ b/backup/src/utils/exchangeModels.py @@ -0,0 +1,127 @@ + +__version__ = "1.0.0 - 18/02/2025" # VERSAO PARA TODOS OS SISTEMAS TRANSMITINDO PARA O SERVIÇO LOGGER_CONTROLLER DO SG_IC2 +__version__ = "1.0.1 - 15/01/2026 - S.D.G. - Ajustes" +__version__ = "1.0.2 - 23/01/2026 - S.D.G. - ForwardCompatibleModel & SerializeAsAny" +from pydantic import BaseModel, field_validator, ValidationInfo, Field, ConfigDict, SerializeAsAny +from enum import StrEnum +from typing import Union, Any, Optional + +class ForwardCompatibleModel(BaseModel): + """ + Base para todos os modelos que permite campos extras. + O Pydantic V2 gerencia automaticamente o dicionário 'model_extra'. + """ + model_config = ConfigDict(extra='allow') + +class RealmType(StrEnum): + LOGS = "LOGS" + METRICS = "METRICS" + STATUS = "STATUS" + ERRORS = "ERRORS" + OPERATIONAL = "OPERATIONAL" + +class OpStatus(StrEnum): + ACTIVE = "ACTIVE" + OK = "OK" + ERROR = "ERROR" + +class ICLogs(ForwardCompatibleModel): + log_date : str + service_date : str + service : str + versionstr : str + origin : str + enterprise_id : str + enterprise_name : str + message : str + tenant : Optional[str] = Field(default='#', description="Tenant ou '#' para global") + +class ICErrors(ForwardCompatibleModel): + log_date : str + service_date : str + service : str + versionstr : str + origin : str + enterprise_id : str + enterprise_name : str + message : str + tenant : Optional[str] = Field(default='#', description="Tenant ou '#' para global") + +class ICMetrics(ForwardCompatibleModel): + metric_date : str + metric : str + metric_name : str + value : str + unit : str + tenant : Optional[str] = Field(default='#', description="Tenant ou '#' para global") + +class ICOperational(ForwardCompatibleModel): + log_date : str # YYYY-MM-DD HH:MM:S + service_date : str # YYYY-MM-DD HH:MM:S + service_name : str + versionstr : str + origin : str + enterprise_id : str + enterprise_name : str + status : OpStatus + step_status : str + message : str + +class ICStatus(ForwardCompatibleModel): + service : str + versionstr : str + system_name : str + startup_date : str + access_date : str + origin : str + sid : Optional[str] = "" + task_count : Optional[int] = 0 + cpu : Optional[float] = 0.0 + memory : Optional[float] = 0.0 + disk : Optional[float] = 0.0 + network : Optional[float] = 0.0 + +# Modelo polimórfico gerador de metadados JSON para o Exchange de Logs do Sistema Inteirador +class InfoConnector(ForwardCompatibleModel): + realm: RealmType + + # SerializeAsAny garante que se 'messageIC' for uma subclasse (como 'Operation'), + # os campos extras da subclasse (como 'reserva_id') aparecerão no JSON final. + messageIC: SerializeAsAny[Union[ICLogs, ICErrors, ICMetrics, ICStatus, ICOperational]] + + @field_validator('messageIC', mode='before') + @classmethod + def validate_schema(cls, v: Any, info: ValidationInfo) -> Any: + realm = info.data.get("realm") + if realm is None: + # Se realm não existe, o próprio Pydantic lançará erro de validação do campo 'realm' + return v + + # Mapeamento de Realm para Classe Esperada + target_map = { + RealmType.LOGS : ICLogs, + RealmType.ERRORS : ICErrors, + RealmType.METRICS : ICMetrics, + RealmType.STATUS : ICStatus, + RealmType.OPERATIONAL: ICOperational, + } + + expected_class = target_map.get(realm) + if not expected_class: + raise ValueError(f"Invalid realm: {realm}") + + # Se for um dicionário (vindo de um JSON), validamos contra a classe mapeada + if isinstance(v, dict): + return expected_class.model_validate(v) + + # Se já for um objeto instanciado (como no seu Builder), verificamos a herança + if not isinstance(v, expected_class): + raise ValueError(f"Expected {expected_class.__name__} for realm {realm}, got {type(v).__name__}") + + return v + + model_config = ConfigDict( + extra="allow", + frozen=False, + str_strip_whitespace=True + ) \ No newline at end of file diff --git a/backup/src/utils/redis.py b/backup/src/utils/redis.py new file mode 100644 index 0000000..a712eb1 --- /dev/null +++ b/backup/src/utils/redis.py @@ -0,0 +1,107 @@ +import redis + +# Fornece mecanismos de Caching e Persistência Volátil/PubSub na aplicação usando Redis +class RedisClient: + def __init__(self, host='localhost', port=6379, db=0): + """ + Inicializa o cliente Redis com as configurações fornecidas. + """ + self.client = redis.Redis(host=host, port=port, db=db, decode_responses=True) + + def set(self, key, value): + """ + Define um valor para uma chave no Redis. + """ + self.client.set(key, value) + + def setex(self, key, value, ex): + """ + Define um valor para uma chave no Redis com um tempo de expiração em segundos. + """ + self.client.set(key, value, ex) + + def get(self, key): + """ + Obtém o valor de uma chave no Redis. + """ + return self.client.get(key) + + def get_keys(self, pattern="*"): + """ + Obtém todas as chaves que correspondem a um padrão. + """ + return self.client.keys(pattern) + + def get_all(self): + """ + Obtém todas as chaves e seus valores no Redis. + """ + keys = self.client.keys('*') + return {key: self.client.get(key) for key in keys} + + def delete(self, key): + """ + Remove uma chave do Redis. + """ + self.client.delete(key) + + def exists(self, key): + """ + Verifica se uma chave existe no Redis. + Retorna True se a chave existir, caso contrário, False. + """ + return self.client.exists(key) > 0 + + def close(self): + """ + Fecha a conexão com o Redis. + """ + self.client.close() + + def set_expiration(self, key, value, seconds): + """ + Define um valor para uma chave e configura um tempo de expiração em segundos. + """ + self.client.set(key, value, ex=seconds) + + def increment(self, key, amount=1): + """ + Incrementa o valor de uma chave numérica no Redis. + """ + return self.client.incr(key, amount) + + def decrement(self, key, amount=1): + """ + Decrementa o valor de uma chave numérica no Redis. + """ + return self.client.decr(key, amount) + + def hset(self, name, key, value): + """ + Define um campo em um hash no Redis. + """ + self.client.hset(name, key, value) + + def hget(self, name, key): + """ + Obtém o valor de um campo em um hash no Redis. + """ + return self.client.hget(name, key) + + def hgetall(self, name): + """ + Obtém todos os campos e valores de um hash no Redis. + """ + return self.client.hgetall(name) + + def expire(self, key, seconds): + """ + Define um tempo de expiração para uma chave existente. + """ + self.client.expire(key, seconds) + + def ttl(self, key): + """ + Obtém o tempo restante de vida (TTL) de uma chave. + """ + return self.client.ttl(key) \ No newline at end of file diff --git a/backup/src/webhooks/webhooks.py b/backup/src/webhooks/webhooks.py new file mode 100644 index 0000000..4106a1c --- /dev/null +++ b/backup/src/webhooks/webhooks.py @@ -0,0 +1,64 @@ +from utils.api import Api +from utils.banco import Banco +from utils.Exchange2 import LoggerExchange2 +from utils.exchangeModels import RealmType +import json + +# Serviço responsável por vincular rotas (webhooks) por CNPJ onde a PlugNotas enviará notificações +class Webhooks: + def __init__(self, url_base, api_key, banco:Banco, logger: LoggerExchange2 = None): + self.api = Api(api_key) + self.url = f"{url_base}/empresa/" + self.banco = banco + self.logger = logger + + def _log(self, realm: RealmType, msg: str): + if self.logger: + self.logger.logIC2(realm, (msg,)) + + def consultar_webhooks(self, cnpj): + try: + resposta = self.api.get(f"{self.url}{cnpj}/webhook") + self._log(RealmType.OPERATIONAL, f"Webhook consultado - CNPJ: {cnpj}") + return resposta + except Exception as e: + self._log(RealmType.ERRORS, f"Erro ao consultar webhook: {e}") + return {"error": str(e)} + + def criar_webhook(self, cnpj, url): + try: + resposta = self.api.post(f"{self.url}{cnpj}/webhook", {"url": url}) + self._log(RealmType.OPERATIONAL, f"Webhook criado - CNPJ: {cnpj}") + return resposta + except Exception as e: + self._log(RealmType.ERRORS, f"Erro ao criar webhook: {e}") + return {"error": str(e)} + + def deletar_webhook(self, cnpj): + try: + resposta = self.api.delete(f"{self.url}{cnpj}/webhook") + self._log(RealmType.OPERATIONAL, f"Webhook deletado - CNPJ: {cnpj}") + return resposta + except Exception as e: + self._log(RealmType.ERRORS, f"Erro ao deletar webhook: {e}") + return {"error": str(e)} + + def atualizar_webhook(self, cnpj, url): + try: + resposta = self.api.put(f"{self.url}{cnpj}/webhook", {"url": url,"method": "POST"}) + self._log(RealmType.OPERATIONAL, f"Webhook atualizado - CNPJ: {cnpj}") + return resposta + except Exception as e: + self._log(RealmType.ERRORS, f"Erro ao atualizar webhook: {e}") + return {"error": str(e)} + + def testar_webhook(self, cnpj): + try: + resposta = self.api.post(f"{self.url}{cnpj}/webhook/verify", {}) + self._log(RealmType.OPERATIONAL, f"Webhook testado - CNPJ: {cnpj}") + return resposta + except Exception as e: + self._log(RealmType.ERRORS, f"Erro ao testar webhook: {e}") + return {"error": str(e)} + + \ No newline at end of file diff --git a/backup/test_payload.py b/backup/test_payload.py new file mode 100644 index 0000000..b1b3391 --- /dev/null +++ b/backup/test_payload.py @@ -0,0 +1,52 @@ +import json +from src.Models.payload import validate_payload + +payload = { + "fila": "empresa", + "metodo": "POST", + "cliente": "tray", + "url_base": "https://api.sandbox.plugnotas.com.br", + "config": { + "x_api_key": "2da392a6-79d2-4304-a8b7-959572c7e44d", + "user_postgres": "tecnospeed", + "pass_postgres": "u7QimG7E%21SF%2F%2FJC2c8", + "host_postgres": "postgres", + "port_postgres": "5432", + "database_postgres": "tecnospeed" + }, + "dados": { + "cpfCnpj": "94074986000198", + "razaoSocial": "Tech nux", + "nomeFantasia": "Tech nux ltda", + "inscricaoMunicipal": "0001", + "simplesNacional": True, + "regimeTributario": 1, + "regimeTributarioEspecial": 1, + "incentivadorCultural": False, + "incentivoFiscal": True, + "certificado": "cert-mock-01", + "endereco": { + "codigoPais": "1058", + "descricaoPais": "BRASIL", + "tipoLogradouro": "Avenida", + "logradouro": "Avenida", + "numero": "123", + "complemento": "Casa", + "tipoBairro": "Bairro", + "bairro": "Centro", + "codigoCidade": "5200175", + "descricaoCidade": "ÁGUA FRIA DE GOIÁS", + "estado": "GO", + "cep": "68907-460" + }, + "nfe": { "ativo": True }, + "nfse": { "ativo": False } + } +} + +try: + env = validate_payload(payload) + print("VALIDO") +except Exception as e: + print("ERRO:", type(e)) + print(e) diff --git a/buildImages.sh b/buildImages.sh new file mode 100644 index 0000000..8272623 --- /dev/null +++ b/buildImages.sh @@ -0,0 +1,86 @@ +# GERA AS IMAGENS QUE SERAO PUBLICADAS NO HUB DOCKER +# +# TODOS OS PROJETOS COMPARTILHAM DA MAJOR VERSION DA IMAGEM +# EMBORA INTERNAMENTE TENHA MINOR E PATCH VERSION DIFERENTES +# +# FORMATO MAJOR.MINOR.PATCH +# +# A IMAGEM LISTA APENAS A MAJOR VERSION COMEÇANDO DA v1 +# OPCOES DE EXECUÇÃO - git clone prune +version=':v1' # Versão de Inicial +# BUILD IMAGE 1/7/2025 - Caso não aja o diretorio utils ou Models clone é compulsorio +# BUILD IMAGE 1/7/2025 - Identifica o comando se docker compose ou docker-compose +# BUILD IMAGE 1/7/2025 - Permite mudar a configuração do Dockerfile com argumentos + +buildarg='INSTALL_GCC=false' + +# Try to find the correct $DOCKER_COMPOSE_CMD command +if command -v docker-compose &> /dev/null; then + DOCKER_COMPOSE_CMD="docker-compose" +elif command -v docker &> /dev/null && docker compose version &> /dev/null; then + DOCKER_COMPOSE_CMD="docker compose" +else + echo "$DOCKER_COMPOSE_CMD is not installed." + exit 1 +fi + +clone() { + echo "VERSAO ATUAL CLASSES ACESSORIAS Exchange2 SoftGOModel" + $DOCKER_COMPOSE_CMD down + + if [ -d "utils" ]; then + rm -rf utils + fi + if [ -d "Model" ]; then + rm -rf Model + fi + + git clone git@github.com:desenvolvimentopsa/Exchange2.git utils/Exchange2 + git clone git@github.com:desenvolvimentopsa/SoftGOModel.git Model + cd utils + find . -name "*.py" | xargs -I {} cp {} . + rm -rf Exchange2 + cd .. + +} + +for arg in "$@"; do + if [ -n "$arg" ] && [ "$arg" == "git" ]; then + echo "VERSAO ATUAL SERVICO_TECNOSPEED DO GIT ${version}" + git stash + git pull + fi +done + +if [ ! -d "utils" ] || [ ! -d "Model" ]; then + echo "DOWNLOAD CLASSES ACESSORIAS" + clone +else + for arg in "$@"; do + if [ -n "$arg" ] && [ "$arg" == "clone" ]; then + echo "DOWNLOAD CLASSES ACESSORIAS" + clone + fi + done +fi + +for arg in "$@"; do + if [ -n "$arg" ] && [ "$arg" == "prune" ]; then + echo "REMOVER IMAGENS SERVICO_TECNOSPEED ${version}" + $DOCKER_COMPOSE_CMD down + docker image ls -q -f "reference=servico_tecnospeed-*${version}" | xargs -I {} docker image rm -f {} + docker system prune -f + fi +done + +echo "GERAR IMAGENS SERVICO_TECNOSPEED ${version}" + +echo "SERVICO_TECNOSPEED SERVICE" +cd src +cp ../utils/Exchange2.py ./utils +cp ../Model/sg_tecnospeed_models/*.py ./Models +echo servico_tecnospeed${version} +cd .. +docker build --build-arg ${buildarg} -t sg_servico_tecnospeed${version} . +rm -rf ./utils +rm -rf ./Model \ No newline at end of file diff --git a/docker-compose.yml b/docker-compose.yml new file mode 100644 index 0000000..0994806 --- /dev/null +++ b/docker-compose.yml @@ -0,0 +1,111 @@ +services: + redis: + image: redis + command: redis-server --save 300 1 + restart: always + volumes: + - ./redis-data:/data + + banco: + image: postgres:15 + command: postgres -c listen_addresses='*' + restart: always + env_file: .env + environment: + POSTGRES_DB: ${POSTGRES_DB:-tecnospeed} + POSTGRES_USER: ${POSTGRES_USER:-tecnospeed} + POSTGRES_PASSWORD: ${POSTGRES_PASSWORD:-tecnospeed} + volumes: + - ./postgres-data:/var/lib/postgresql/data + ports: + - "15432:5432" + + tecnospeed_rpc: + build: ./tecnospeed_rpc + image: servico_plugnotas-tecnospeed_rpc:v2 + command: python tecnospeed_rpc.py + restart: always + env_file: .env + environment: + - "TZ=America/Sao_Paulo" + - "REDIS_HOST=redis" + - "PLUGNOTAS_URL=https://api.sandbox.plugnotas.com.br" + - "LISTENER_INTERNAL_URL=http://endpoint_listener:8000/listenerPlugnotas" + volumes: + - ./tecnospeed_rpc/src:/app + extra_hosts: + - "host.docker.internal:host-gateway" + logging: + driver: "json-file" + options: + max-size: "100m" + max-file: "3" + shm_size: "500m" + deploy: + resources: + limits: + cpus: "1" + memory: "500m" + depends_on: + - redis + - banco + + tecnospeed_queue: + build: ./tecnospeed_queue + image: servico_plugnotas-tecnospeed_queue:v2 + command: python tecnospeed_queue.py + restart: always + env_file: .env + environment: + - "TZ=America/Sao_Paulo" + - "REDIS_HOST=redis" + - "PLUGNOTAS_URL=https://api.sandbox.plugnotas.com.br" + - "LISTENER_INTERNAL_URL=http://endpoint_listener:8000/listenerPlugnotas" + volumes: + - ./tecnospeed_queue/src:/app + extra_hosts: + - "host.docker.internal:host-gateway" + logging: + driver: "json-file" + options: + max-size: "100m" + max-file: "3" + shm_size: "500m" + deploy: + resources: + limits: + cpus: "1" + memory: "500m" + networks: + - default + depends_on: + - redis + - banco + + endpoint_listener: + build: ./endpoint_listener + image: servico_plugnotas-endpoint_listener:v2 + command: python endpoint_listener.py + restart: always + environment: + - "TZ=America/Sao_Paulo" + - "REDIS_HOST=redis" + - "WEBHOOK_PORT=8000" + ports: + - "8000:8000" + volumes: + - ./endpoint_listener/src:/app + extra_hosts: + - "host.docker.internal:host-gateway" + logging: + driver: "json-file" + options: + max-size: "10k" + max-file: "3" + depends_on: + - redis + shm_size: "200m" + +networks: + default: + name: servico_plugnotas_default diff --git a/endpoint_listener/Dockerfile b/endpoint_listener/Dockerfile new file mode 100644 index 0000000..f96a16a --- /dev/null +++ b/endpoint_listener/Dockerfile @@ -0,0 +1,28 @@ +FROM python:3.12-slim +ARG INSTALL_GCC + +ENV LANG=C.UTF-8 \ + LC_ALL=C.UTF-8 + +RUN apt-get update && apt-get install -y --no-install-recommends \ + locales \ + vim \ + iputils-ping \ + && locale-gen en_US.UTF-8 \ + && if [ "$INSTALL_GCC" = "true" ]; then \ + apt-get install -y --no-install-recommends build-essential curl; \ + fi \ + && apt-get clean && apt-get autoremove -y \ + && rm -rf /var/lib/apt/lists/* + +RUN pip install --no-cache-dir unidecode + +# PARTE ESPECIFICA DA IMAGEM +WORKDIR /app +RUN mkdir /app/logs +COPY requirements.txt ./ +RUN pip install --no-cache-dir -r requirements.txt + +COPY src . + +CMD [ "python", "endpoint_listener.py"] diff --git a/endpoint_listener/requirements.txt b/endpoint_listener/requirements.txt new file mode 100644 index 0000000..cd6db41 --- /dev/null +++ b/endpoint_listener/requirements.txt @@ -0,0 +1,13 @@ +sqlalchemy +pydantic +requests +datetime +redis +pytz +pika +cryptography +psycopg2-binary +fastapi +uvicorn[standard] +pytest +pytest-cov \ No newline at end of file diff --git a/endpoint_listener/src/Models/config.py b/endpoint_listener/src/Models/config.py new file mode 100644 index 0000000..76500ee --- /dev/null +++ b/endpoint_listener/src/Models/config.py @@ -0,0 +1,18 @@ +from pydantic import BaseModel + +# Dados de conexão ao banco de dados isolado do cliente e da API PlugNotas +class Config(BaseModel): + api_key: str | None = None + x_api_key: str | None = None + user_postgres: str + pass_postgres: str + host_postgres: str + port_postgres: str + database_postgres: str + +# Informações de mensageria para a troca de arquivos (AMQP) via subagent +class SystemConfig(BaseModel): + serviceid: str + servicekey: str + origin: str + amqps: str \ No newline at end of file diff --git a/endpoint_listener/src/Models/empresa.py b/endpoint_listener/src/Models/empresa.py new file mode 100644 index 0000000..0aad758 --- /dev/null +++ b/endpoint_listener/src/Models/empresa.py @@ -0,0 +1,97 @@ +from pydantic import BaseModel + + +################### +## POST /empresas # +################### +class Endereco(BaseModel): + codigoPais: str + descricaoPais: str + tipoLogradouro: str | None = None + logradouro: str + numero: str + complemento: str | None = None + tipoBairro: str | None = None + bairro: str + codigoCidade: str + descricaoCidade: str + estado: str + cep: str + +class Rps(BaseModel): + lote: int + numeracao: list + +class Email(BaseModel): + envio: bool + +class Config(BaseModel): + rps: Rps | None = None + email: Email | None = None + producao: bool | None = None + envioLimitado: bool | None = None + integracoes: list | None = None + prefeitura: dict | str | int | None = None + +class Nfse(BaseModel): + config: Config | None = None + ativo: bool = False + tipoContrato: int | None = 0 + +class Dfe(BaseModel): + primeiroNsu: bool + +class ConfigNfe(BaseModel): + dfe: Dfe | None = None + email: Email | None = None + integracoes: dict | None = None + producao: bool | None = None + impressaoFcp: bool | None = None + impressaoPartilha: bool | None = None + versaoManual: str | None = None + versaoEsquema: str | None = None + numeracao: list | None = None + calculoAutomaticoIbpt: bool | None = True + +class Nfe(BaseModel): + config: ConfigNfe | None = None + ativo: bool = False + tipoContrato: int | None = 0 + +class ConfigNfce(BaseModel): + email: Email | None = None + integracoes: dict | None = None + producao: bool | None = None + versaoManual: str | None = None + versaoEsquema: str | None = None + numeracao: list | None = None + +class Nfce(BaseModel): + config: ConfigNfce | None = None + ativo: bool = False + tipoContrato: int | None = 0 + + +# Entidade Empresa que cadastra a credencial emissora no sistema da PlugNotas +class Empresa(BaseModel): + endereco: Endereco + nfse: Nfse | None = None + nfe: Nfe | None = None + nfce: Nfce | None = None + incentivoFiscal: bool + incentivadorCultural: bool + cpfCnpj: str + inscricaoMunicipal: str + inscricaoEstadual: str | None = None + certificado: str + razaoSocial: str + nomeFantasia: str | None = None + simplesNacional: bool + regimeTributario: int + regimeTributarioEspecial: int + id: str | None = None + banco: str | None = None + email: str | None = None + id_empresa: int | None = None + + diff --git a/endpoint_listener/src/Models/nfe.py b/endpoint_listener/src/Models/nfe.py new file mode 100644 index 0000000..b6e186a --- /dev/null +++ b/endpoint_listener/src/Models/nfe.py @@ -0,0 +1,115 @@ +from pydantic import BaseModel +from typing import List, Optional, Literal + +EstadoEnum = Literal[ + "AC", "AL", "AM", "AP", "BA", "CE", "DF", "ES", "GO", "MA", "MG", "MS", "MT", + "PA", "PB", "PE", "PI", "PR", "RJ", "RN", "RO", "RR", "RS", "SC", "SE", "SP", "TO" +] + +TipoLogradouroEnum = Literal[ + "Alameda", "Avenida", "Chácara", "Colônia", "Condomínio", "Estância", "Estrada", + "Fazenda", "Praça", "Prolongamento", "Rodovia", "Rua", "Sítio", "Travessa", "Vicinal", "Eqnp" +] + +class Emitente(BaseModel): + cpfCnpj: str + inscricaoEstadual: Optional[str] = None + +class Endereco(BaseModel): + logradouro: str + numero: str + bairro: str + codigoCidade: str + estado: EstadoEnum + cep: str + tipoLogradouro: TipoLogradouroEnum + codigoPais: Optional[str] = "1058" + complemento: Optional[str] = None + descricaoCidade: Optional[str] = None + descricaoPais: Optional[str] = "Brasil" + +class Destinatario(BaseModel): + cpfCnpj: str + razaoSocial: str + nomeFantasia: Optional[str] = None + email: Optional[str] = None + inscricaoEstadual: Optional[str] = None + inscricaoMunicipal: Optional[str] = None + inscricaoSuframa: Optional[str] = None + orgaoPublico: Optional[bool] = False + endereco: Endereco + +class ValorTributavel(BaseModel): + comercial: float + tributavel: float + +class BaseCalculoIcms(BaseModel): + modalidadeDeterminacao: int + valor: float + +class Icms(BaseModel): + origem: str + cst: str + baseCalculo: BaseCalculoIcms + aliquota: float + valor: float + +class BaseCalculoPis(BaseModel): + valor: float + quantidade: int + +class Pis(BaseModel): + cst: str + baseCalculo: BaseCalculoPis + aliquota: float + valor: float + +class BaseCalculoCofins(BaseModel): + valor: float + +class Cofins(BaseModel): + cst: str + baseCalculo: BaseCalculoCofins + aliquota: float + valor: float + +class Tributos(BaseModel): + icms: Optional[Icms] = None + pis: Optional[Pis] = None + cofins: Optional[Cofins] = None + +class ItemNFe(BaseModel): + codigo: str + descricao: str + ncm: str + cest: Optional[str] = None + cfop: str + valorUnitario: ValorTributavel + valor: float + tributos: Tributos + +class Pagamento(BaseModel): + aVista: Optional[bool] = True + meio: Optional[str] = "01" + valor: float + +class Telefone(BaseModel): + ddd: str + numero: str + +class ResponsavelTecnico(BaseModel): + cpfCnpj: str + nome: str + email: str + telefone: Telefone + +class NFe(BaseModel): + idIntegracao: str + presencial: bool + consumidorFinal: bool + natureza: str + emitente: Emitente + destinatario: Destinatario + itens: List[ItemNFe] + pagamentos: List[Pagamento] + responsavelTecnico: Optional[ResponsavelTecnico] = None diff --git a/endpoint_listener/src/Models/nfse.py b/endpoint_listener/src/Models/nfse.py new file mode 100644 index 0000000..857ad97 --- /dev/null +++ b/endpoint_listener/src/Models/nfse.py @@ -0,0 +1,49 @@ +from pydantic import BaseModel +from typing import Optional + +class Prestador(BaseModel): + cpfCnpj: str + +class Endereco(BaseModel): + descricaoCidade: str + cep: str + tipoLogradouro: str + logradouro: str + tipoBairro: Optional[str] = None + codigoCidade: str + complemento: Optional[str] = None + estado: str + numero: str + bairro: str + +class Tomador(BaseModel): + cpfCnpj: str + razaoSocial: str + inscricaoMunicipal: Optional[str] = None + email: str + endereco: dict + +class ISS(BaseModel): + tipoTributacao: int + exigibilidade: int + aliquota: float + +class Valor(BaseModel): + servico: float + descontoCondicionado: Optional[float] = 0 + descontoIncondicionado: Optional[float] = 0 + +class Servico(BaseModel): + codigo: str + codigoTributacao: str + discriminacao: str + cnae: str + iss: ISS + valor: Valor + +# Estrutura padrão de envio de uma NFSe contendo Prestador, Tomador e Serviço prestado +class NFSe(BaseModel): + idIntegracao: str + prestador: Prestador + tomador: Tomador + servico: Servico diff --git a/endpoint_listener/src/Models/payload.py b/endpoint_listener/src/Models/payload.py new file mode 100644 index 0000000..841551b --- /dev/null +++ b/endpoint_listener/src/Models/payload.py @@ -0,0 +1,518 @@ +from enum import Enum +from pydantic import BaseModel, ConfigDict, Field + +from Models.config import Config +from Models.empresa import Empresa +from Models.nfe import NFe +from Models.nfse import NFSe +from Models.servico import Servico +from Models.tomador import Tomador + + +class Fila(str, Enum): + NFE = "nfe" + NFSE = "nfse" + CERTIFICADO_DIGITAL = "certificado_digital" + EMPRESA = "empresa" + SERVICO = "servico" + TOMADOR = "tomador" + RELATORIOS = "relatorios" + WEBHOOKS = "webhooks" + + +class NFeMetodo(str, Enum): + POST = "post" + GET = "get" + DOWNLOAD = "download" + DOWNLOAD_XML = "download_xml" + CANCELAR = "cancelar" + STATUS_CANCELAMENTO = "status_cancelamento" + CCE = "cce" + STATUS_CCE = "status_cce" + RESUMO = "resumo" + + +class NFSeMetodo(str, Enum): + POST = "post" + GET = "get" + DOWNLOAD = "download" + DOWNLOAD_XML = "download_xml" + CANCELAR = "cancelar" + STATUS_CANCELAMENTO = "status_cancelamento" + RESUMO = "resumo" + HISTORICO = "historico" + + +class CertificadoMetodo(str, Enum): + POST = "post" + GET = "get" + PUT = "put" + + +class EmpresaMetodo(str, Enum): + POST = "post" + GET = "get" + POST_FILE = "post_file" + PATCH = "patch" + + +class ServicoMetodo(str, Enum): + POST = "post" + GET = "get" + PUT = "put" + DELETE = "delete" + + +class TomadorMetodo(str, Enum): + POST = "post" + GET = "get" + PUT = "put" + DELETE = "delete" + + +class RelatorioMetodo(str, Enum): + NFE_EMITIDAS = "nfe_emitidas" + NFE_POR_CNPJ = "nfe_por_cnpj" + NFSE_EMITIDAS = "nfse_emitidas" + NFSE_POR_CNPJ = "nfse_por_cnpj" + + +class WebhookMetodo(str, Enum): + GET = "get" + POST = "post" + DELETE = "delete" + PUT = "put" + TEST = "test" + + +class BaseEnvelope(BaseModel): + cliente: str | None = None + config: Config + url_base: str | None = None + + +# --- Base Data Models --- + +class BaseDados(BaseModel): + model_config = ConfigDict(populate_by_name=True) + id_usuario: int | None = 1 + + +class NFePostDadosWrapped(BaseDados): + nfe: NFe + id_empresa: str | None = None + + +class NFePostDadosDirect(NFe): + id_usuario: int | None = 1 + id_empresa: str | None = None + + +class NFeIdDados(BaseDados): + id: str + + +class NFeCancelarDados(NFeIdDados): + justificativa: str | None = None + + +class NFeCCEDados(NFeIdDados): + correcao: str + + +class NFeResumoDados(BaseDados): + cnpj: str + id_integracao: str | None = None + id: str | None = None + + +class NFSePostDados(NFSe): + id_usuario: int | None = 1 + id_empresa: str | None = None + + +class NFSeIdDados(BaseDados): + id: str + + +class NFSeGetDados(BaseDados): + id: str | None = None + + +class NFSeResumoDados(NFSeIdDados): + cnpj: str + + +class NFSeHistoricoDados(NFSeIdDados): + cnpj: str | None = None + + +class CertificadoPostDados(BaseDados): + arquivo: str + senha: str + id_empresa: str | None = None + email: str | None = None + expiration_at: str | None = Field(None, alias="expirationAt") + data_expiracao: str | None = None + + +class CertificadoGetDados(BaseDados): + id: str | None = None + + +class CertificadoPutDados(CertificadoPostDados): + id: str + + +class EmpresaPostDados(Empresa): + id_usuario: int | None = 1 + + +class EmpresaGetDados(BaseDados): + cnpj: str | None = None + + +class EmpresaPostFileDados(BaseDados): + file: str + cnpj: str + + +class EmpresaPatchDados(BaseDados): + cnpj: str + certificado: str + + +class ServicoPostDados(Servico): + id_usuario: int | None = 1 + + +class ServicoGetDados(BaseDados): + id: str | None = None + + +class ServicoPutDados(Servico): + id: str + id_usuario: int | None = 1 + + +class ServicoDeleteDados(BaseDados): + id: str + + +class TomadorPostDados(Tomador): + id_usuario: int | None = 1 + + +class TomadorGetDados(BaseDados): + cpf_cnpj: str | None = None + + +class TomadorPutDados(Tomador): + cpf_cnpj: str + id_usuario: int | None = 1 + + +class TomadorDeleteDados(BaseDados): + cpf_cnpj: str + + +class RelatorioVazioDados(BaseDados): + pass + + +class RelatorioCnpjDados(BaseDados): + cnpj: str + + +class RelatorioPeriodoDados(BaseDados): + data_inicial: str | None = None + data_final: str | None = None + + +class RelatorioCnpjPeriodoDados(RelatorioPeriodoDados): + cnpj: str + + +class WebhookGetDeleteTestDados(BaseDados): + cnpj: str + + +class WebhookPostPutDados(WebhookGetDeleteTestDados): + url: str + + +# --- Envelopes --- + +class NFePostEnvelope(BaseEnvelope): + fila: Fila = Fila.NFE + metodo: NFeMetodo = NFeMetodo.POST + dados: NFePostDadosDirect | NFePostDadosWrapped + + +class NFeIdEnvelope(BaseEnvelope): + fila: Fila = Fila.NFE + metodo: NFeMetodo + dados: NFeIdDados + + +class NFeCancelarEnvelope(BaseEnvelope): + fila: Fila = Fila.NFE + metodo: NFeMetodo = NFeMetodo.CANCELAR + dados: NFeCancelarDados + + +class NFeCCEEnvelope(BaseEnvelope): + fila: Fila = Fila.NFE + metodo: NFeMetodo = NFeMetodo.CCE + dados: NFeCCEDados + + +class NFeResumoEnvelope(BaseEnvelope): + fila: Fila = Fila.NFE + metodo: NFeMetodo = NFeMetodo.RESUMO + dados: NFeResumoDados + + +class NFSePostEnvelope(BaseEnvelope): + fila: Fila = Fila.NFSE + metodo: NFSeMetodo = NFSeMetodo.POST + dados: NFSePostDados + + +class NFSeGetEnvelope(BaseEnvelope): + fila: Fila = Fila.NFSE + metodo: NFSeMetodo = NFSeMetodo.GET + dados: NFSeGetDados + + +class NFSeIdEnvelope(BaseEnvelope): + fila: Fila = Fila.NFSE + metodo: NFSeMetodo + dados: NFSeIdDados + + +class NFSeResumoEnvelope(BaseEnvelope): + fila: Fila = Fila.NFSE + metodo: NFSeMetodo = NFSeMetodo.RESUMO + dados: NFSeResumoDados + + +class NFSeHistoricoEnvelope(BaseEnvelope): + fila: Fila = Fila.NFSE + metodo: NFSeMetodo = NFSeMetodo.HISTORICO + dados: NFSeHistoricoDados + + +class CertificadoPostEnvelope(BaseEnvelope): + fila: Fila = Fila.CERTIFICADO_DIGITAL + metodo: CertificadoMetodo = CertificadoMetodo.POST + dados: CertificadoPostDados + + +class CertificadoGetEnvelope(BaseEnvelope): + fila: Fila = Fila.CERTIFICADO_DIGITAL + metodo: CertificadoMetodo = CertificadoMetodo.GET + dados: CertificadoGetDados + + +class CertificadoPutEnvelope(BaseEnvelope): + fila: Fila = Fila.CERTIFICADO_DIGITAL + metodo: CertificadoMetodo = CertificadoMetodo.PUT + dados: CertificadoPutDados + + +class EmpresaPostEnvelope(BaseEnvelope): + fila: Fila = Fila.EMPRESA + metodo: EmpresaMetodo = EmpresaMetodo.POST + dados: EmpresaPostDados + + +class EmpresaGetEnvelope(BaseEnvelope): + fila: Fila = Fila.EMPRESA + metodo: EmpresaMetodo = EmpresaMetodo.GET + dados: EmpresaGetDados + + +class EmpresaPostFileEnvelope(BaseEnvelope): + fila: Fila = Fila.EMPRESA + metodo: EmpresaMetodo = EmpresaMetodo.POST_FILE + dados: EmpresaPostFileDados + + +class EmpresaPatchEnvelope(BaseEnvelope): + fila: Fila = Fila.EMPRESA + metodo: EmpresaMetodo = EmpresaMetodo.PATCH + dados: EmpresaPatchDados + + +class ServicoPostEnvelope(BaseEnvelope): + fila: Fila = Fila.SERVICO + metodo: ServicoMetodo = ServicoMetodo.POST + dados: ServicoPostDados + + +class ServicoGetEnvelope(BaseEnvelope): + fila: Fila = Fila.SERVICO + metodo: ServicoMetodo = ServicoMetodo.GET + dados: ServicoGetDados + + +class ServicoPutEnvelope(BaseEnvelope): + fila: Fila = Fila.SERVICO + metodo: ServicoMetodo = ServicoMetodo.PUT + dados: ServicoPutDados + + +class ServicoDeleteEnvelope(BaseEnvelope): + fila: Fila = Fila.SERVICO + metodo: ServicoMetodo = ServicoMetodo.DELETE + dados: ServicoDeleteDados + + +class TomadorPostEnvelope(BaseEnvelope): + fila: Fila = Fila.TOMADOR + metodo: TomadorMetodo = TomadorMetodo.POST + dados: TomadorPostDados + + +class TomadorGetEnvelope(BaseEnvelope): + fila: Fila = Fila.TOMADOR + metodo: TomadorMetodo = TomadorMetodo.GET + dados: TomadorGetDados + + +class TomadorPutEnvelope(BaseEnvelope): + fila: Fila = Fila.TOMADOR + metodo: TomadorMetodo = TomadorMetodo.PUT + dados: TomadorPutDados + + +class TomadorDeleteEnvelope(BaseEnvelope): + fila: Fila = Fila.TOMADOR + metodo: TomadorMetodo = TomadorMetodo.DELETE + dados: TomadorDeleteDados + + +class RelatorioNFeEmitidasEnvelope(BaseEnvelope): + fila: Fila = Fila.RELATORIOS + metodo: RelatorioMetodo = RelatorioMetodo.NFE_EMITIDAS + dados: RelatorioVazioDados + + +class RelatorioNFeCnpjEnvelope(BaseEnvelope): + fila: Fila = Fila.RELATORIOS + metodo: RelatorioMetodo = RelatorioMetodo.NFE_POR_CNPJ + dados: RelatorioCnpjDados + + +class RelatorioNFSeEmitidasEnvelope(BaseEnvelope): + fila: Fila = Fila.RELATORIOS + metodo: RelatorioMetodo = RelatorioMetodo.NFSE_EMITIDAS + dados: RelatorioPeriodoDados + + +class RelatorioNFSeCnpjEnvelope(BaseEnvelope): + fila: Fila = Fila.RELATORIOS + metodo: RelatorioMetodo = RelatorioMetodo.NFSE_POR_CNPJ + dados: RelatorioCnpjPeriodoDados + + +class WebhookGetDeleteTestEnvelope(BaseEnvelope): + fila: Fila = Fila.WEBHOOKS + metodo: WebhookMetodo + dados: WebhookGetDeleteTestDados + + +class WebhookPostPutEnvelope(BaseEnvelope): + fila: Fila = Fila.WEBHOOKS + metodo: WebhookMetodo + dados: WebhookPostPutDados + + +# --- Generic Envelope for basic structural validation --- + +class GenericEnvelope(BaseEnvelope): + """Validates that minimum required fields exist before routing.""" + fila: Fila + metodo: str + dados: dict | None = None + + +# --- Payload Validation Router --- + +# Maps (fila, metodo) -> Envelope class +_ENVELOPE_MAP: dict[tuple[str, str], type[BaseEnvelope]] = { + # NFe + ("nfe", "post"): NFePostEnvelope, + ("nfe", "get"): NFeIdEnvelope, + ("nfe", "download"): NFeIdEnvelope, + ("nfe", "download_xml"): NFeIdEnvelope, + ("nfe", "cancelar"): NFeCancelarEnvelope, + ("nfe", "status_cancelamento"): NFeIdEnvelope, + ("nfe", "cce"): NFeCCEEnvelope, + ("nfe", "status_cce"): NFeIdEnvelope, + ("nfe", "resumo"): NFeResumoEnvelope, + # NFSe + ("nfse", "post"): NFSePostEnvelope, + ("nfse", "get"): NFSeGetEnvelope, + ("nfse", "download"): NFSeIdEnvelope, + ("nfse", "download_xml"): NFSeIdEnvelope, + ("nfse", "cancelar"): NFSeIdEnvelope, + ("nfse", "status_cancelamento"): NFSeIdEnvelope, + ("nfse", "resumo"): NFSeResumoEnvelope, + ("nfse", "historico"): NFSeHistoricoEnvelope, + # Certificado Digital + ("certificado_digital", "post"): CertificadoPostEnvelope, + ("certificado_digital", "get"): CertificadoGetEnvelope, + ("certificado_digital", "put"): CertificadoPutEnvelope, + # Empresa + ("empresa", "post"): EmpresaPostEnvelope, + ("empresa", "get"): EmpresaGetEnvelope, + ("empresa", "post_file"): EmpresaPostFileEnvelope, + ("empresa", "patch"): EmpresaPatchEnvelope, + # Serviço + ("servico", "post"): ServicoPostEnvelope, + ("servico", "get"): ServicoGetEnvelope, + ("servico", "put"): ServicoPutEnvelope, + ("servico", "delete"): ServicoDeleteEnvelope, + # Tomador + ("tomador", "post"): TomadorPostEnvelope, + ("tomador", "get"): TomadorGetEnvelope, + ("tomador", "put"): TomadorPutEnvelope, + ("tomador", "delete"): TomadorDeleteEnvelope, + # Relatórios + ("relatorios", "nfe_emitidas"): RelatorioNFeEmitidasEnvelope, + ("relatorios", "nfe_por_cnpj"): RelatorioNFeCnpjEnvelope, + ("relatorios", "nfse_emitidas"): RelatorioNFSeEmitidasEnvelope, + ("relatorios", "nfse_por_cnpj"): RelatorioNFSeCnpjEnvelope, + # Webhooks + ("webhooks", "get"): WebhookGetDeleteTestEnvelope, + ("webhooks", "post"): WebhookPostPutEnvelope, + ("webhooks", "delete"): WebhookGetDeleteTestEnvelope, + ("webhooks", "put"): WebhookPostPutEnvelope, + ("webhooks", "test"): WebhookGetDeleteTestEnvelope, +} + + +def validate_payload(payload_dict: dict) -> BaseEnvelope: + # Step 1: Basic structural validation + generic = GenericEnvelope(**payload_dict) + + fila = generic.fila.value + metodo = generic.metodo.lower() + + # Step 2: Find specific envelope + key = (fila, metodo) + envelope_cls = _ENVELOPE_MAP.get(key) + + if envelope_cls is None: + raise ValueError(f"Combinação fila/metodo desconhecida: {fila}/{metodo}") + + # Step 3: Normalize metodo in the dict to match enum lowercase values + payload_dict['metodo'] = metodo + + # Step 4: Full typed validation + return envelope_cls(**payload_dict) + + + diff --git a/endpoint_listener/src/Models/servico.py b/endpoint_listener/src/Models/servico.py new file mode 100644 index 0000000..afb717f --- /dev/null +++ b/endpoint_listener/src/Models/servico.py @@ -0,0 +1,150 @@ +from pydantic import BaseModel +from enum import IntEnum + +class TipoTributacao(IntEnum): + NAO_DEFINIDO = 0 + ISENTO_ISS = 1 + IMUNE = 2 + NAO_INCIDENCIA = 3 + NAO_TRIBUTAVEL = 4 + RETIDO = 5 + TRIBUTAVEL_MUNICIPIO = 6 + TRIBUTAVEL_FORA_MUNICIPIO = 7 + TRIBUTAVEL_MUNICIPIO_TOMADOR = 8 + +class TipoExigibilidade(IntEnum): + EXIGIVEL = 1 + NAO_INCIDENCIA = 2 + ISENCAO = 3 + EXPORTACAO = 4 + IMUNIDADE = 5 + SUSPENSO_ACAO_JUDICIAL = 6 + SUSPENSO_ACAO_ADMINISTRATIVA = 7 + +class ISS(BaseModel): + tipoTributacao: int #TipoTributacao + exigibilidade: int #TipoExigibilidade + retido: bool | None = None # Se o ISS foi retido na fonte + aliquota: float # Aliquota do ISS + valor: float # Valor do ISS + valorRetido: float # Valor do ISS retido + processoSuspensao: str # Numero processo de suspensão do ISS +############################################################################## +# tipoTributacao integer +# Default: 6 +# Enum: 0 1 2 3 4 5 6 7 8 +# Tipo de Tributação do Serviço, valores: +# 0 - Não definido +# 1 - Isento de ISS +# 2 - Imune +# 3 - Não Incidência no Município +# 4 - Não Tributável +# 5 - Retido +# 6 - Tributável Dentro do Município +# 7 - Tributável Fora do Município +# 8 - Tributável Dentro do Município pelo tomador +############################################################################## +# exigibilidade integer +# Default: 1 +# Enum: 1 2 3 4 5 6 7 +# Exigibilidade do ISS, valores: +# 1 - Exigível +# 2 - Não Incidência +# 3 - Isenção +# 4 - Exportação +# 5 - Imunidade +# 6 - Supenso por Ação Judicial +# 7 - Suspenso por Ação Administrativa +############################################################################## +# responsavelRetencao string +# Valores aceitos: +# 1 - Tomador +# 2 - Intermediário +############################################################################## + +class Obra(BaseModel): + art: str # Anotação de Responsabilidade Técnica (Codigo ART) + codigo: str # Código da obra + +class Valor(BaseModel): + servico: float # Valor bruto do serviço prestado. + baseCalculo: float # Valor da base de cálculo dos impostos. + deducoes: float # Valor deduções. + descontoCondicionado: float # Valor do desconto condicionado. + descontoIncondicionado: float # Valor do desconto incondicionado. + liquido: float # Valor liquido do serviço, caso você não informe esse campo, vamos calcular automáticamente. + unitario: float # Valor unitário do serviço. + valorAproximadoTributos: float # Valor aproximado dos tributos. + +class Deducao(BaseModel): + tipo: int + descricao: str # Descrição da dedução +############################################################################## +# tipo integer +# Tipo de dedução: +# 0 - Sem Deduções +# 1 - Materiais +# 2 - Subempreitada de Mão de Obra +# 3 - Serviços +# 4 - Produção Externa +# 5 - Alimentação e Bebidas +# 6 - Reembolso de Despesas +# 7 - Repasse Consorciado +# 8 - Repasse Plano de Saúde +# 99 - Outras Deduções +############################################################################## + +class RetencaoInterno(BaseModel): + aliquota: float # Aliquota da retenção + valor: float # Valor da retenção + +class Retencao(BaseModel): + pis: RetencaoInterno + cofins: RetencaoInterno + csll: RetencaoInterno + inss: RetencaoInterno + irrf: RetencaoInterno + outrasRetencoes: float # Valor de outras retenções + cpp: RetencaoInterno + +class Aliquota(BaseModel): + municipal: float # Aliquota municipal + estadual: float # Aliquota estadual + federal: float # Aliquota federal + +class Detalhado(BaseModel): + aliquota: Aliquota + +class Simplificado(BaseModel): + aliquota: float # Aliquota geral de impostos + +class IBPT(BaseModel): + simplificado: Simplificado + detalhado: Detalhado + +# Representa o detalhamento do serviço prestado na NFSe, impostos incidentes e deduções +class Servico(BaseModel): + codigo: str # Código de serviço, também conhecido como Código Item Lista Serviço, geralmente segue a LC116, podendo váriar de acordo com a prefeitura.Você pode encontrar esse dado no portal da prefeitura, em uma nota emitida, ou junto ao contador. + idIntegracao: str # Código EXTERNO de integração do serviço. Utilizado SOMENTE quando se cadastra previamente um serviço. + discriminacao: str # Descrição do serviço prestado + codigoTributacao: str # Código tributação no Município + cnae: str # O código CNAE ou, por extenso, Classificação Nacional de Atividades Econômicas, é uma forma de padronizar, em todo o território nacional, os códigos de atividades econômicas e os critérios de enquadramento usados pelos mais diversos órgãos da administração tributária do Brasil. + codigoCidadeIncidencia: str # Código IBGE da cidade de incidência do ISS. + descricaoCidadeIncidencia: str # Descrição da cidade de incidência do ISS. + unidade: str # Unidade do serviço prestado. + quantidade: int # Quantidade dos serviços prestados. + iss: ISS + obra: Obra + valor: Valor + deducao: Deducao + retencao: Retencao + tributavel: bool # Indica se o serviço está sujeito a tributação. + ibpt: IBPT + responsavelRetencao: str + +############################################################################## +# responsavelRetencao Valores aceitos: +# 1 - Tomador +# 2 - Intermediário +# Caso o responsável pela retenção seja o próprio prestador, não gerar o campo. +############################################################################## diff --git a/endpoint_listener/src/Models/tomador.py b/endpoint_listener/src/Models/tomador.py new file mode 100644 index 0000000..4e3cf5a --- /dev/null +++ b/endpoint_listener/src/Models/tomador.py @@ -0,0 +1,34 @@ +from pydantic import BaseModel + + +class Endereco(BaseModel): + tipoLogradouro: str + logradouro: str + numero: str + complemento: str + tipoBairro: str + bairro: str + codigoPais: str + descricaoPais: str + codigoCidade: str + descricaoCidade: str + estado: str + cep: str + +class Telefone(BaseModel): + ddd: str + numero: str + +# Representa o cliente (Tomador do Serviço) vinculado a uma nota fiscal de serviço +class Tomador(BaseModel): + cpfCnpj: str + inscricaoMunicipal: str + inscricaoEstadual: str + inscricaoSuframa: str + indicadorInscricaoEstadual: int + razaoSocial: str + nomeFantasia: str + endereco: Endereco + telefone: Telefone + email: str + orgaoPublico: bool diff --git a/endpoint_listener/src/NFSe/Servico/servico.py b/endpoint_listener/src/NFSe/Servico/servico.py new file mode 100644 index 0000000..4787089 --- /dev/null +++ b/endpoint_listener/src/NFSe/Servico/servico.py @@ -0,0 +1,105 @@ +from Models.servico import Servico +from utils.api import Api +from utils.banco import Banco, ServicoTable +from utils.Exchange2 import LoggerExchange2 +from utils.exchangeModels import RealmType +from sqlalchemy.exc import SQLAlchemyError +import json + +# Controller responsável pelos cadastros e busca de Serviços a serem vinculados em NFSe +class ServicoController: + def __init__(self, url_base, api_key, banco: Banco, logger: LoggerExchange2 = None): + self.api = Api(api_key) + self.url = f"{url_base}/nfse/servico" + self.banco = banco + self.logger = logger + + def _log(self, realm: RealmType, msg: str): + if self.logger: + self.logger.logIC2(realm, (msg,)) + + def cadastrar_servico(self, servico: Servico, data: dict): + try: + resposta = self.api.post(self.url, servico.model_dump()) + if 'error' in resposta: + self._log(RealmType.ERRORS, f"Erro na API ao cadastrar serviço") + return resposta + + servico_novo = ServicoTable( + descricao=servico.discriminacao, + id_tecnospeed=resposta['data']['id'], + id_usuario=data.get('id_usuario', 1), + status=True + ) + self.banco.add(servico_novo) + self._log(RealmType.OPERATIONAL, f"Serviço cadastrado com sucesso - ID: {resposta['data']['id']}") + return resposta + except SQLAlchemyError as e: + self.banco.rollback() + self._log(RealmType.ERRORS, f"Erro no banco ao cadastrar serviço: {e}") + return {"error": f"Erro ao cadastrar serviço: {str(e)}"} + except Exception as e: + return {"error": f"Erro inesperado: {str(e)}"} + + def buscar_servico(self, id_servico, data: dict = None): + try: + resposta = self.api.get(f"{self.url}/{id_servico}") + self._log(RealmType.OPERATIONAL, f"Serviço consultado - ID: {id_servico}") + return resposta + except Exception as e: + self._log(RealmType.ERRORS, f"Erro ao buscar serviço: {e}") + return {"error": f"Erro ao buscar serviço: {str(e)}"} + + def listar_servicos(self, data: dict = None): + try: + servicos = self.banco.session.query(ServicoTable.id_tecnospeed, ServicoTable.descricao, ServicoTable.status).all() + resultado = [{"id_tecnospeed": servico.id_tecnospeed, "descricao": servico.descricao, "status": servico.status} for servico in servicos] + self._log(RealmType.OPERATIONAL, f"Listagem de serviços - {len(resultado)} registros") + return resultado + except SQLAlchemyError as e: + self._log(RealmType.ERRORS, f"Erro ao listar serviços: {e}") + return {"error": f"Erro ao listar serviços: {str(e)}"} + except Exception as e: + return {"error": f"Erro inesperado: {str(e)}"} + + def atualizar_servico(self, id_servico, servico: Servico): + try: + resposta = self.api.patch(f"{self.url}/{id_servico}", servico.model_dump()) + servico_existente = self.banco.session.query(ServicoTable).filter_by(id=id_servico).first() + + if not servico_existente: + self._log(RealmType.ERRORS, f"Serviço não encontrado - ID: {id_servico}") + return {"error": "Serviço não encontrado"} + + servico_existente.descricao = servico.discriminacao + self.banco.session.commit() + + self._log(RealmType.OPERATIONAL, f"Serviço atualizado com sucesso - ID: {id_servico}") + return resposta + except SQLAlchemyError as e: + self.banco.rollback() + self._log(RealmType.ERRORS, f"Erro ao atualizar serviço: {e}") + return {"error": f"Erro ao atualizar serviço: {str(e)}"} + except Exception as e: + return {"error": f"Erro inesperado: {str(e)}"} + + def deletar_servico(self, id_servico): + try: + resposta = self.api.delete(f"{self.url}/{id_servico}") + servico_existente = self.banco.session.query(ServicoTable).filter_by(id=id_servico).first() + + if not servico_existente: + self._log(RealmType.ERRORS, f"Serviço não encontrado para deletar - ID: {id_servico}") + return {"error": "Serviço não encontrado"} + + self.banco.session.delete(servico_existente) + self.banco.session.commit() + + self._log(RealmType.OPERATIONAL, f"Serviço deletado com sucesso - ID: {id_servico}") + return resposta + except SQLAlchemyError as e: + self.banco.rollback() + self._log(RealmType.ERRORS, f"Erro ao deletar serviço: {e}") + return {"error": f"Erro ao deletar serviço: {str(e)}"} + except Exception as e: + return {"error": f"Erro inesperado: {str(e)}"} \ No newline at end of file diff --git a/endpoint_listener/src/NFSe/Tomador/tomador.py b/endpoint_listener/src/NFSe/Tomador/tomador.py new file mode 100644 index 0000000..e10cc78 --- /dev/null +++ b/endpoint_listener/src/NFSe/Tomador/tomador.py @@ -0,0 +1,91 @@ +from utils.api import Api +from utils.banco import Banco, TomadorTable +from utils.Exchange2 import LoggerExchange2 +from utils.exchangeModels import RealmType +from Models.tomador import Tomador + +import json + +# Controller para gerenciamento no banco e na API dos Tomadores (clientes finais) das notas +class TomadorController: + def __init__(self, url_base, api_key, banco: Banco, logger: LoggerExchange2 = None): + self.api = Api(api_key) + self.url = f"{url_base}/nfse/tomador" + self.banco = banco + self.logger = logger + + def _log(self, realm: RealmType, msg: str): + if self.logger: + self.logger.logIC2(realm, (msg,)) + + def cadastrar_tomador(self, tomador: Tomador, data: dict): + try: + retorno = self.api.post(self.url, tomador.model_dump()) + if 'error' in retorno: + self._log(RealmType.ERRORS, f"Erro na API ao cadastrar tomador") + return retorno + + tomador_novo = TomadorTable( + id_tecnospeed=retorno['id'], + nome=tomador.razaoSocial, + cpf_cnpj=tomador.cpfCnpj, + email=tomador.email, + telefone=f"({tomador.telefone.ddd}) {tomador.telefone.numero}", + id_usuario=data.get('id_usuario', 1), + status=True + ) + + self.banco.add(tomador_novo) + self._log(RealmType.OPERATIONAL, f"Tomador cadastrado com sucesso - CPF/CNPJ: {tomador.cpfCnpj}") + return retorno + except Exception as e: + self._log(RealmType.ERRORS, f"Erro ao cadastrar tomador: {e}") + return {"error": str(e)} + + def buscar_tomador(self, cpf_cnpj): + try: + tomador = self.api.get(f"{self.url}/{cpf_cnpj}") + self._log(RealmType.OPERATIONAL, f"Tomador consultado - CPF/CNPJ: {cpf_cnpj}") + return tomador + except Exception as e: + self._log(RealmType.ERRORS, f"Erro ao buscar tomador: {e}") + return {"error": str(e)} + + def atualizar_tomador(self, cpf_cnpj, tomador: Tomador): + try: + tomador_existente = self.banco.session.query(TomadorTable).filter_by(cpf_cnpj=cpf_cnpj).first() + if not tomador_existente: + self._log(RealmType.ERRORS, f"Tomador não encontrado - CPF/CNPJ: {cpf_cnpj}") + return {"error": "Tomador não encontrado"} + + resposta = self.api.patch(f"{self.url}/{cpf_cnpj}", tomador.model_dump()) + tomador_existente.nome = tomador.razaoSocial + tomador_existente.email = tomador.email + tomador_existente.telefone = f"({tomador.telefone.ddd}) {tomador.telefone.numero}" + + self.banco.session.commit() + self._log(RealmType.OPERATIONAL, f"Tomador atualizado com sucesso - CPF/CNPJ: {cpf_cnpj}") + return resposta + except Exception as e: + self.banco.session.rollback() + self._log(RealmType.ERRORS, f"Erro ao atualizar tomador: {e}") + return {"error": str(e)} + + def deletar_tomador(self, cpf_cnpj): + try: + tomador_existente = self.banco.session.query(TomadorTable).filter_by(cpf_cnpj=cpf_cnpj).first() + if not tomador_existente: + self._log(RealmType.ERRORS, f"Tomador não encontrado para deletar - CPF/CNPJ: {cpf_cnpj}") + return {"error": "Tomador não encontrado"} + + resposta = self.api.delete(f"{self.url}/{cpf_cnpj}") + self.banco.session.delete(tomador_existente) + self.banco.session.commit() + + self._log(RealmType.OPERATIONAL, f"Tomador deletado com sucesso - CPF/CNPJ: {cpf_cnpj}") + return resposta + except Exception as e: + self.banco.session.rollback() + self._log(RealmType.ERRORS, f"Erro ao deletar tomador: {e}") + return {"error": str(e)} + diff --git a/endpoint_listener/src/NFSe/nfse.py b/endpoint_listener/src/NFSe/nfse.py new file mode 100644 index 0000000..ae2da7d --- /dev/null +++ b/endpoint_listener/src/NFSe/nfse.py @@ -0,0 +1,167 @@ +from Models.nfse import NFSe +from utils.api import Api +from utils.banco import Banco, NFSeTable +from utils.Exchange2 import LoggerExchange2 +from utils.exchangeModels import RealmType +from utils.notificador import NotificadorInterno + +import json + +# Serviço responsável pelo controle, emissão e cancelamento das Notas Fiscais de Serviço Eletrônicas (NFSe) +class NFSeService: + def __init__(self, api_key, url_base, banco: Banco, logger: LoggerExchange2 = None): + self.api = Api(api_key) + self.url = f"{url_base}/nfse" + self.banco = banco + self.logger = logger + self.notificador = NotificadorInterno(logger=logger) if logger else None + + def _log(self, realm: RealmType, msg: str): + if self.logger: + self.logger.logIC2(realm, (msg,)) + + def cadastrar_nfse(self, nfse: NFSe, data: dict): + try: + id_empresa = data.get('id_empresa', "0") + resposta = self.api.post(self.url, [nfse.model_dump()]) + if 'error' in resposta: + self._log(RealmType.ERRORS, f"Erro na API ao cadastrar NFSe: {resposta.get('error')}") + return resposta + + # A API da Plugnotas retorna 'documents' contendo uma lista com os sucessos + documentos = resposta.get('documents', []) + if not documentos: + self._log(RealmType.ERRORS, f"Nenhum documento retornado pela API. Resposta: {resposta}") + return resposta + + id_recebido = documentos[0].get('id') + if not id_recebido: + raise Exception("ID do documento não encontrado na resposta.") + + nfse_nova = NFSeTable( + id_tecnospeed=id_recebido, + id_empresa=id_empresa, + id_usuario=data.get('id_usuario', 1), + status=True + ) + + self.banco.add(nfse_nova) + self._log(RealmType.OPERATIONAL, f"NFSe cadastrada com sucesso - ID: {id_recebido}") + + # Notifica o listener interno para sincronização imediata + if self.notificador: + self.notificador.notificar_emissao(id_tecnospeed=id_recebido, documento="nfse", cnpj=id_empresa) + + return resposta + except Exception as e: + self._log(RealmType.ERRORS, f"Erro ao cadastrar NFSe: {e}") + raise Exception(f"Erro ao cadastrar NFSe: {e}") + + def buscar_nfse(self, id_nfse): + try: + resposta = self.api.get(f"{self.url}/{id_nfse}") + self._log(RealmType.OPERATIONAL, f"NFSe consultada com sucesso - ID: {id_nfse}") + return resposta + except Exception as e: + self._log(RealmType.ERRORS, f"Erro ao buscar NFSe: {e}") + raise Exception(f"Erro ao buscar NFSe: {e}") + + def listar_nfse(self): + try: + nfse = self.banco.session.query(NFSeTable.id, NFSeTable.id_tecnospeed).all() + resultado = [{"id": n[0], "descricao": n[1]} for n in nfse] + self._log(RealmType.OPERATIONAL, f"Listagem de NFSe - {len(resultado)} registros") + return resultado + except Exception as e: + self._log(RealmType.ERRORS, f"Erro ao listar NFSe: {e}") + raise Exception(f"Erro ao listar NFSe: {e}") + + def baixar_nfse(self, id_nfse): + try: + resposta = self.api.get(f"{self.url}/pdf/{id_nfse}") + self._log(RealmType.OPERATIONAL, f"PDF NFSe baixado com sucesso - ID: {id_nfse}") + return resposta + except Exception as e: + self._log(RealmType.ERRORS, f"Erro ao baixar NFSe: {e}") + raise Exception(f"Erro ao baixar NFSe: {e}") + + def baixar_xml_nfse(self, id_nfse): + try: + resposta = self.api.get(f"{self.url}/xml/{id_nfse}") + self._log(RealmType.OPERATIONAL, f"XML NFSe baixado com sucesso - ID: {id_nfse}") + return resposta + except Exception as e: + self._log(RealmType.ERRORS, f"Erro ao baixar XML NFSe: {e}") + raise Exception(f"Erro ao baixar XML NFSe: {e}") + + def solicitar_cancelamento_nfse(self, id_nfse): + try: + resposta = self.api.post(f"{self.url}/cancelar/{id_nfse}", {}) + self._log(RealmType.OPERATIONAL, f"Cancelamento NFSe solicitado - ID: {id_nfse}") + return resposta + except Exception as e: + self._log(RealmType.ERRORS, f"Erro ao solicitar cancelamento NFSe: {e}") + raise Exception(f"Erro ao solicitar cancelamento NFSe: {e}") + + def consultar_status_cancelamento_nfse(self, id_nfse): + try: + resposta = self.api.get(f"{self.url}/cancelar/status/{id_nfse}") + self._log(RealmType.OPERATIONAL, f"Status cancelamento NFSe consultado - ID: {id_nfse}") + return resposta + except Exception as e: + self._log(RealmType.ERRORS, f"Erro ao consultar status cancelamento NFSe: {e}") + raise Exception(f"Erro ao consultar status cancelamento NFSe: {e}") + + def consultar_resumo_nfse(self, id_nfse, cnpj): + try: + resposta = self.api.get(f"{self.url}/consultar/{id_nfse}/{cnpj}") + self._log(RealmType.OPERATIONAL, f"Resumo NFSe consultado - ID: {id_nfse}") + return resposta + except Exception as e: + self._log(RealmType.ERRORS, f"Erro ao consultar resumo NFSe: {e}") + raise Exception(f"Erro ao consultar resumo NFSe: {e}") + + def baixar_histotico_nfse(self, id_nfse, cnpj = None): + try: + resposta = self.api.get(f"{self.url}/historico/{id_nfse}") if cnpj is None else self.api.get(f"{self.url}/historico/{id_nfse}/{cnpj}") + self._log(RealmType.OPERATIONAL, f"Histórico NFSe baixado - ID: {id_nfse}") + return resposta + except Exception as e: + self._log(RealmType.ERRORS, f"Erro ao baixar histórico NFSe: {e}") + raise Exception(f"Erro ao baixar histórico NFSe: {e}") + + def enviar_email_nfse(self, id_nfse, email:list[str], reenvio:bool = False): + try: + resposta = self.api.post(f"{self.url}/email/{id_nfse}", {"reenvio": reenvio,"destinatarios": email}) + self._log(RealmType.OPERATIONAL, f"Email NFSe enviado para {email} - ID: {id_nfse}") + return resposta + except Exception as e: + self._log(RealmType.ERRORS, f"Erro ao enviar email NFSe: {e}") + raise Exception(f"Erro ao enviar email NFSe: {e}") + + def resolver_nfse(self, id_nfse, identificacaoNota:str): + try: + resposta = self.api.post(f"{self.url}/resolve/{id_nfse}", {"identificacaoNota": identificacaoNota}) + self._log(RealmType.OPERATIONAL, f"NFSe resolvida com sucesso - ID: {id_nfse}") + return resposta + except Exception as e: + self._log(RealmType.ERRORS, f"Erro ao resolver NFSe: {e}") + raise Exception(f"Erro ao resolver NFSe: {e}") + + def consultar_nfse_periodo(self, data_inicio, data_fim): + try: + resposta = self.api.get_paginated(f"{self.url}/periodo/{data_inicio}/{data_fim}") + self._log(RealmType.OPERATIONAL, f"NFSe consultada por período: {data_inicio} a {data_fim}") + return resposta + except Exception as e: + self._log(RealmType.ERRORS, f"Erro ao consultar NFSe por período: {e}") + raise Exception(f"Erro ao consultar NFSe por período: {e}") + + def baixar_recibo_provisorio_servico(self, id_nfse, cnpj = None): + try: + resposta = self.api.get(f"{self.url}/rps/pdf/{id_nfse}") if cnpj is None else self.api.get(f"{self.url}/rps/pdf/{id_nfse}/{cnpj}") + self._log(RealmType.OPERATIONAL, f"RPS baixado com sucesso - ID: {id_nfse}") + return resposta + except Exception as e: + self._log(RealmType.ERRORS, f"Erro ao baixar recibo provisório de serviço: {e}") + raise Exception(f"Erro ao baixar recibo provisório de serviço: {e}") diff --git a/endpoint_listener/src/NFe/registroNFe.py b/endpoint_listener/src/NFe/registroNFe.py new file mode 100644 index 0000000..349f03d --- /dev/null +++ b/endpoint_listener/src/NFe/registroNFe.py @@ -0,0 +1,126 @@ +import json as json_lib +from utils.api import Api +from utils.banco import Banco, NFeTable +from Models.nfe import NFe +from typing import List +from utils.Exchange2 import LoggerExchange2 +from utils.exchangeModels import RealmType +from utils.notificador import NotificadorInterno + +class RegistroNFeService: + # Classe responsável por fornecer as operações de comunicação com a PlugNotas para o documento NFe + def __init__(self, api_key: str, url_base: str, banco: Banco, logger: LoggerExchange2 = None): + self.api = Api(api_key) + self.url = f"{url_base}/nfe" + self.banco = banco + self.logger = logger + self.notificador = NotificadorInterno(logger=logger) if logger else None + + def cadastrar_nfe(self, nfe_list: List[NFe], data: dict): + # Envia uma lista de NFes em lote ou unitária para emissão e registra no banco de dados + try: + id_empresa = data.get('id_empresa', "0") + payload = [nfe.model_dump() for nfe in nfe_list] + resposta = self.api.post(self.url, payload) + if 'error' in resposta: + id_usr = data.get('id_usuario', 1) + if self.logger: self.logger.logIC2(RealmType.OPERATIONAL, ('INSERT', 'nfe', id_usr, '0', 'ERRO', f"Erro na API: {resposta.get('error')}", json_lib.dumps(resposta))) + return resposta + if 'documents' in resposta: + for doc in resposta['documents']: + id_tecnospeed = doc.get('id') + if id_tecnospeed: + nfe_nova = NFeTable( + id_tecnospeed=id_tecnospeed, + id_empresa=id_empresa, + id_usuario=data.get('id_usuario', 1), + status=True + ) + self.banco.add(nfe_nova) + id_usr = data.get('id_usuario', 1) + if self.logger: self.logger.logIC2(RealmType.OPERATIONAL, ('INSERT', 'nfe', id_usr, str(nfe_nova.id), 'SUCESSO', 'NFe cadastrada com sucesso', json_lib.dumps(doc))) + + # Notifica o listener interno para sincronização imediata + if self.notificador: + self.notificador.notificar_emissao(id_tecnospeed=id_tecnospeed, documento="nfe", cnpj=id_empresa) + + return resposta + except Exception as e: + if self.logger: self.logger.logIC2(RealmType.OPERATIONAL, ('INSERT', 'nfe', data.get('id_usuario', 1), '0', 'ERRO', f"Erro ao cadastrar NFe: {e}", "{}")) + raise Exception(f"Erro ao cadastrar NFe: {e}") + + def cancelar_nfe(self, id_nota: str, justificativa: str): + # Solicita o cancelamento de uma NFe existente mediante uma justificativa + try: + resposta = self.api.post(f"{self.url}/{id_nota}/cancelamento", {"justificativa": justificativa}) + if self.logger: self.logger.logIC2(RealmType.OPERATIONAL, ('DELETE', 'nfe', 1, id_nota, 'SUCESSO', 'NFe cancelada com sucesso', json_lib.dumps(resposta))) + return resposta + except Exception as e: + if self.logger: self.logger.logIC2(RealmType.OPERATIONAL, ('DELETE', 'nfe', 1, id_nota, 'ERRO', f"Erro ao cancelar NFe: {e}", "{}")) + raise Exception(f"Erro ao cancelar NFe: {e}") + + def enviar_cce(self, id_nota: str, correcao: str): + # Envia uma Carta de Correção Eletrônica (CCe) para retificar uma NFe já emitida + try: + resposta = self.api.post(f"{self.url}/{id_nota}/cce", {"correcao": correcao}) + if self.logger: self.logger.logIC2(RealmType.OPERATIONAL, ('UPDATE', 'nfe', 1, id_nota, 'SUCESSO', 'CCe enviada com sucesso', json_lib.dumps(resposta))) + return resposta + except Exception as e: + if self.logger: self.logger.logIC2(RealmType.OPERATIONAL, ('UPDATE', 'nfe', 1, id_nota, 'ERRO', f"Erro ao enviar CCe NFe: {e}", "{}")) + raise Exception(f"Erro ao enviar CCe NFe: {e}") + + def consultar_resumo(self, id_nota: str): + # Consulta o resumo e o status atual da NFe na PlugNotas pelo seu ID + try: + resposta = self.api.get(f"{self.url}/{id_nota}/resumo") + if self.logger: self.logger.logIC2(RealmType.OPERATIONAL, ('SELECT', 'nfe', 1, id_nota, 'SUCESSO', 'Resumo de NFe consultado', json_lib.dumps(resposta))) + return resposta + except Exception as e: + if self.logger: self.logger.logIC2(RealmType.OPERATIONAL, ('SELECT', 'nfe', 1, id_nota, 'ERRO', f"Erro ao consultar resumo NFe: {e}", "{}")) + raise Exception(f"Erro ao consultar resumo NFe: {e}") + + def consultar_resumo_por_cnpj_integracao(self, cnpj: str, id_integracao: str): + try: + resposta = self.api.get(f"{self.url}/{cnpj}/{id_integracao}/resumo") + if self.logger: self.logger.logIC2(RealmType.OPERATIONAL, ('SELECT', 'nfe', id_integracao, id_integracao, 'SUCESSO', 'Resumo por CNPJ/Integração consultado', json_lib.dumps(resposta))) + return resposta + except Exception as e: + if self.logger: self.logger.logIC2(RealmType.OPERATIONAL, ('SELECT', 'nfe', id_integracao, id_integracao, 'ERRO', f"Erro ao consultar resumo NFe por CNPJ/Integração: {e}", "{}")) + raise Exception(f"Erro ao consultar resumo NFe por CNPJ/Integração: {e}") + + def consultar_status_cancelamento(self, id_nota: str): + try: + resposta = self.api.get(f"{self.url}/{id_nota}/cancelamento/status") + if self.logger: self.logger.logIC2(RealmType.OPERATIONAL, ('SELECT', 'nfe', id_nota, id_nota, 'SUCESSO', 'Status de cancelamento consultado', json_lib.dumps(resposta))) + return resposta + except Exception as e: + if self.logger: self.logger.logIC2(RealmType.OPERATIONAL, ('SELECT', 'nfe', id_nota, id_nota, 'ERRO', f"Erro ao consultar status de cancelamento NFe: {e}", "{}")) + raise Exception(f"Erro ao consultar status de cancelamento NFe: {e}") + + def consultar_status_cce(self, id_nota: str): + try: + resposta = self.api.get(f"{self.url}/{id_nota}/cce/status") + if self.logger: self.logger.logIC2(RealmType.OPERATIONAL, ('SELECT', 'nfe', id_nota, id_nota, 'SUCESSO', 'Status CCe consultado', json_lib.dumps(resposta))) + return resposta + except Exception as e: + if self.logger: self.logger.logIC2(RealmType.OPERATIONAL, ('SELECT', 'nfe', id_nota, id_nota, 'ERRO', f"Erro ao consultar status CCe NFe: {e}", "{}")) + raise Exception(f"Erro ao consultar status CCe NFe: {e}") + + def baixar_pdf(self, id_nota: str): + # Obtém o link para download ou o stream do PDF da Nota Fiscal processada + try: + resposta = self.api.get(f"{self.url}/{id_nota}/pdf") + if self.logger: self.logger.logIC2(RealmType.OPERATIONAL, ('DOWNLOAD (PDF)', 'nfe', id_nota, id_nota, 'SUCESSO', 'PDF baixado com sucesso', json_lib.dumps(resposta) if isinstance(resposta, dict) else '{}')) + return resposta + except Exception as e: + if self.logger: self.logger.logIC2(RealmType.OPERATIONAL, ('DOWNLOAD (PDF)', 'nfe', id_nota, id_nota, 'ERRO', f"Erro ao baixar PDF NFe: {e}", "{}")) + raise Exception(f"Erro ao baixar PDF NFe: {e}") + + def baixar_xml(self, id_nota: str): + try: + resposta = self.api.get(f"{self.url}/{id_nota}/xml") + if self.logger: self.logger.logIC2(RealmType.OPERATIONAL, ('DOWNLOAD (XML)', 'nfe', id_nota, id_nota, 'SUCESSO', 'XML baixado com sucesso', json_lib.dumps(resposta) if isinstance(resposta, dict) else '{}')) + return resposta + except Exception as e: + if self.logger: self.logger.logIC2(RealmType.OPERATIONAL, ('DOWNLOAD (XML)', 'nfe', id_nota, id_nota, 'ERRO', f"Erro ao baixar XML NFe: {e}", "{}")) + raise Exception(f"Erro ao baixar XML NFe: {e}") diff --git a/endpoint_listener/src/Relatorios/relatorios.py b/endpoint_listener/src/Relatorios/relatorios.py new file mode 100644 index 0000000..1155445 --- /dev/null +++ b/endpoint_listener/src/Relatorios/relatorios.py @@ -0,0 +1,70 @@ +from utils.api import Api +from utils.banco import Banco +from utils.Exchange2 import LoggerExchange2 +from utils.exchangeModels import RealmType + +from datetime import datetime, timedelta +import json + +# Extração e consumo de relatórios massivos de emissões diretamente da API e consolidações em lotes +class Relatorios: + def __init__(self, url_base, api_key, banco: Banco, logger: LoggerExchange2 = None): + self.api = Api(api_key) + self.url = f"{url_base}/" + self.banco = banco + self.logger = logger + + def _log(self, realm: RealmType, msg: str): + if self.logger: + self.logger.logIC2(realm, (msg,)) + + def nfe_emitidas(self, data: dict = None): + try: + resposta = self.api.get_paginated(f"{self.url}nfe/relatorio") + self._log(RealmType.OPERATIONAL, "Relatório de NFe emitidas consultado") + return resposta + except Exception as e: + msg_erro = f"Erro ao consultar relatório NFe emitidas: {e}" + self._log(RealmType.ERRORS, msg_erro) + return {"error": msg_erro} + + def nfe_por_cnpj(self, cnpj, data: dict = None): + try: + resposta = self.api.get_paginated(f"{self.url}nfe/{cnpj}/relatorio") + self._log(RealmType.OPERATIONAL, f"Relatório de NFe por CNPJ: {cnpj}") + return resposta + except Exception as e: + msg_erro = f"Erro ao consultar relatório NFe por CNPJ: {e}" + self._log(RealmType.ERRORS, msg_erro) + return {"error": msg_erro} + + def nfse_emitidas(self, data_inicial=None, data_final=None, data: dict = None): + try: + if not data_inicial: + data_inicial = (datetime.now() - timedelta(days=30)).strftime('%Y-%m-%d') + if not data_final: + data_final = datetime.now().strftime('%Y-%m-%d') + + resposta = self.api.get_paginated(f"{self.url}nfse/relatorio?from={data_inicial}&to={data_final}") + self._log(RealmType.OPERATIONAL, f"Relatório NFSe emitidas: {data_inicial} a {data_final}") + return resposta + except Exception as e: + msg_erro = f"Erro ao consultar relatório NFSe emitidas: {e}" + self._log(RealmType.ERRORS, msg_erro) + return {"error": msg_erro} + + def nfse_por_cnpj(self, cnpj, data_inicial=None, data_final=None, data: dict = None): + try: + if not data_inicial: + data_inicial = (datetime.now() - timedelta(days=30)).strftime('%Y-%m-%d') + if not data_final: + data_final = datetime.now().strftime('%Y-%m-%d') + + resposta = self.api.get_paginated(f"{self.url}nfse/{cnpj}/relatorio?from={data_inicial}&to={data_final}") + self._log(RealmType.OPERATIONAL, f"Relatório NFSe por CNPJ: {cnpj}") + return resposta + except Exception as e: + msg_erro = f"Erro ao consultar relatório NFSe por CNPJ: {e}" + self._log(RealmType.ERRORS, msg_erro) + return {"error": msg_erro} + diff --git a/endpoint_listener/src/certificado_digital/certificado_digital.py b/endpoint_listener/src/certificado_digital/certificado_digital.py new file mode 100644 index 0000000..cfbd627 --- /dev/null +++ b/endpoint_listener/src/certificado_digital/certificado_digital.py @@ -0,0 +1,165 @@ +from utils.api import Api +from utils.banco import Banco, CertificadoDigitalTable +from datetime import date +from utils.Exchange2 import LoggerExchange2 +from utils.exchangeModels import RealmType +import json +import base64 + +class CertificadoDigital: + # Serviço que gerencia o upload e atualização de certificados digitais A1/A3 na plataforma da PlugNotas + def __init__(self, url_base, banco: Banco, api_key: str, logger: LoggerExchange2 = None): + self.api = Api(api_key=api_key) + self.banco = banco + self.url = f"{url_base}/certificado" + self.logger = logger + + def post_form_data(self, data): + # Realiza o envio multipart/form-data do arquivo físico do certificado .pfx para a API e o salva no banco + try: + files = { + 'arquivo': ( + 'certificado.pfx', + base64.b64decode(data['arquivo']) if isinstance(data['arquivo'], str) else data['arquivo'], + 'application/x-pkcs12' + ), + 'senha': (None, data['senha']) + } + if 'email' in data: + files['email'] = (None, data['email']) + + response = self.api.post_form_data(self.url, files) + id_tecnospeed = response.get('data', {}).get('id') + + id_usr = data.get('id_usuario', 0) + id_emp = data.get('id_empresa', "0") + + if not id_tecnospeed: + if self.logger: self.logger.logIC2(RealmType.OPERATIONAL, ('INSERT', 'certificado_digital', id_usr, '0', 'ERRO', 'ID do certificado não encontrado na resposta da API', json.dumps(response))) + raise ValueError("Erro ao obter ID do certificado na resposta da API.") + + expiration_value = data.get('expirationAt') or data.get('data_expiracao') + certificado = CertificadoDigitalTable( + id_tecnospeed=id_tecnospeed, + certificado=data['arquivo'], + expirationAt=date.fromisoformat(expiration_value) if expiration_value else date.today(), + senha=data['senha'], + status=True, + id_empresa=id_emp, + id_usuario=id_usr, + email=data.get('email') + ) + + self.banco.add(certificado) + if self.logger: self.logger.logIC2(RealmType.OPERATIONAL, ('INSERT', 'certificado_digital', id_usr, id_tecnospeed, 'SUCESSO', 'Certificado digital cadastrado com sucesso', json.dumps(response))) + + return id_tecnospeed + except Exception as e: + if self.logger: self.logger.logIC2(RealmType.OPERATIONAL, ('INSERT', 'certificado_digital', data.get('id_usuario', 0), '0', 'ERRO', f"Erro ao adicionar certificado: {e}", "{}")) + raise + + def contar_vencidos(self, lista_certificados): + # Auxiliar para contar certificados ativos e vencidos na listagem + if isinstance(lista_certificados, dict) and "data" in lista_certificados: + lista = lista_certificados["data"] + elif isinstance(lista_certificados, list): + lista = lista_certificados + else: + return {"certificados_ativos": 0, "certificados_vencidos": 0} + + ativos = 0 + vencidos = 0 + hoje = date.today() + + for cert in lista: + if not isinstance(cert, dict): continue + + # Tenta encontrar o campo de expiração (vencimento ou outros nomes conhecidos) + exp_str = cert.get("vencimento") or cert.get("expirationAt") or cert.get("dataVencimento") or cert.get("data_expiracao") + + if exp_str: + try: + # Tenta converter a string de data: + # 1. Formato DD/MM/YYYY (do snipped do usuário) + # 2. Formato ISO YYYY-MM-DD + if "/" in exp_str: + partes = exp_str.split(" ")[0].split("/") + if len(partes) == 3: + data_exp = date(int(partes[2]), int(partes[1]), int(partes[0])) + else: + data_exp = date.fromisoformat(exp_str[:10]) + + if data_exp >= hoje: + ativos += 1 + else: + vencidos += 1 + except: + continue + + return { + "certificados_ativos": ativos, + "certificados_vencidos": vencidos + } + + def get(self, id=None): + # Consulta a situação de um certificado digital específico pelo ID ou lista todos vinculados + try: + if id: + resultado = self.api.get(f"{self.url}/{id}") + if self.logger: self.logger.logIC2(RealmType.OPERATIONAL, ('SELECT', 'certificado_digital', id, id, 'SUCESSO', f"Certificado consultado - ID: {id}", json.dumps(resultado))) + return resultado + else: + resposta = self.api.get(self.url) + + # Calcula totais + totais = self.contar_vencidos(resposta) + + # Organiza a resposta unificada + resultado = { + "certificados": resposta if isinstance(resposta, list) else resposta.get("data", []), + "totais": totais + } + + if self.logger: self.logger.logIC2(RealmType.OPERATIONAL, ('SELECT', 'certificado_digital', 'GERAL', '0', 'SUCESSO', 'Listagem de certificados digitais', json.dumps(resultado))) + return resultado + except Exception as e: + if self.logger: self.logger.logIC2(RealmType.OPERATIONAL, ('SELECT', 'certificado_digital', id or 'GERAL', '0', 'ERRO', f"Erro ao buscar certificado: {e}", "{}")) + raise + + def put_certificado(self, id, data): + # Atualiza o arquivo físico ou a senha de um certificado já existente + try: + certificado = self.banco.session.query(CertificadoDigitalTable).filter_by(id_tecnospeed=id).first() + id_usr = data.get('id_usuario', 0) + + if not certificado: + if self.logger: self.logger.logIC2(RealmType.OPERATIONAL, ('UPDATE', 'certificado_digital', id_usr, id, 'ERRO', f"Certificado com ID {id} não encontrado", "{}")) + raise ValueError(f"Certificado com ID {id} não encontrado no banco.") + + files = { + 'arquivo': ( + 'certificado.pfx', + base64.b64decode(data['arquivo']) if isinstance(data['arquivo'], str) else data['arquivo'], + 'application/x-pkcs12' + ), + 'senha': (None, data['senha']) + } + if 'email' in data: + files['email'] = (None, data['email']) + + self.api.put_form_data(f"{self.url}/{id}", files) + + certificado.certificado = data['arquivo'] + certificado.senha = data['senha'] + expiration_value = data.get('expirationAt') or data.get('data_expiracao') + if expiration_value: + certificado.expirationAt = date.fromisoformat(expiration_value) + certificado.updatedAt = date.today() + + self.banco.update(certificado) + if self.logger: self.logger.logIC2(RealmType.OPERATIONAL, ('UPDATE', 'certificado_digital', id_usr, id, 'SUCESSO', 'Certificado digital atualizado com sucesso', json.dumps({"id": id}))) + + return id + except Exception as e: + if self.logger: self.logger.logIC2(RealmType.OPERATIONAL, ('UPDATE', 'certificado_digital', data.get('id_usuario', 0), id, 'ERRO', f"Erro ao atualizar certificado: {e}", "{}")) + raise diff --git a/endpoint_listener/src/empresas/empresa.py b/endpoint_listener/src/empresas/empresa.py new file mode 100644 index 0000000..1b8c40c --- /dev/null +++ b/endpoint_listener/src/empresas/empresa.py @@ -0,0 +1,184 @@ +from Models.empresa import Empresa +from utils.api import Api +from utils.banco import Banco, EnterpriseTable, LogoTable +from utils.Exchange2 import LoggerExchange2 +from utils.exchangeModels import RealmType +from sqlalchemy.exc import SQLAlchemyError +import json +import string +import random +import time + +def generate_cuid(): + """Gera um ID no formato aproximado de CUID (c + timestamp + random).""" + ts = hex(int(time.time() * 1000))[2:] + rand = ''.join(random.choices(string.ascii_lowercase + string.digits, k=12)) + return f"c{ts}{rand}"[:25] + +# Gerenciador de contas de Empresas emissoras de notas e seus respectivos certificados e logos +class EmpresaController: + def __init__(self, url_base, api_key: str, banco: Banco, logger: LoggerExchange2 = None): + self.api = Api(api_key=api_key) + self.banco = banco + self.url = f"{url_base}/empresa" + self.logger = logger + + def _log(self, realm: RealmType, msg: str): + if self.logger: + self.logger.logIC2(realm, (msg,)) + + def post(self, empresa: Empresa, data: dict): + try: + dict_api = empresa.model_dump(exclude={'id', 'id_empresa', 'banco', 'email'}, exclude_none=True) + + # Injeção das configurações fixas e obrigatórias para a PlugNotas + if dict_api.get("nfe") and dict_api["nfe"].get("ativo"): + nfe_config_front = dict_api["nfe"].get("config") or {} + dict_api["nfe"]["config"] = { + "producao": False, + "email": {"envio": False}, + "dfe": {"primeiroNsu": False}, + "impressaoFcp": False, + "impressaoPartilha": False, + **nfe_config_front + } + dict_api["nfe"]["tipoContrato"] = 0 + + if dict_api.get("nfse") and dict_api["nfse"].get("ativo"): + nfse_config_front = dict_api["nfse"].get("config") or {} + dict_api["nfse"]["config"] = { + "producao": False, + "email": {"envio": False}, + "rps": {"lote": 1, "numeracao": [{"numero": 1, "serie": "1"}]}, + "prefeitura": {"login": "12345", "senha": "123"}, + **nfse_config_front + } + dict_api["nfse"]["tipoContrato"] = 0 + print("===========================") + print("ENVIANDO PARA A API:", json.dumps(dict_api, indent=2)) + print("===========================") + retorno = self.api.post(data=dict_api, url=self.url) + if 'error' in retorno: + err_data = retorno.get('response', {}).get('error', {}) + msg = err_data.get('message', 'Erro desconhecido') + detalhes = err_data.get('data', []) + if detalhes: + msg += f" Detalhes: {detalhes}" + + print(f"\n[ERRO API PLUGNOTAS] {msg}\n", flush=True) + self._log(RealmType.ERRORS, f"Erro na API ao cadastrar empresa: {msg}") + raise Exception(msg) + + # Gerar ID CUID se não vier no objeto ou se for nulo + empresa_id = empresa.id if empresa.id else generate_cuid() + + empresa_nova = EnterpriseTable( + id=empresa_id, + cnpj=empresa.cpfCnpj, + razao_social=empresa.razaoSocial, + nome_fantasia=empresa.nomeFantasia if empresa.nomeFantasia else empresa.razaoSocial, + banco=empresa.banco, + email=empresa.email, + status=True + ) + self.banco.add(empresa_nova) + self._log(RealmType.OPERATIONAL, f"Empresa cadastrada com sucesso - CNPJ: {empresa.cpfCnpj} (ID: {empresa_id})") + + if isinstance(retorno, dict) and 'data' in retorno: + retorno['data']['id'] = empresa_nova.id + retorno['data']['id_plugnotas'] = retorno['data'].get('_id') + retorno['data']['nome_fantasia'] = empresa_nova.nome_fantasia + retorno['data']['razao_social'] = empresa_nova.razao_social + retorno['data']['status'] = empresa_nova.status + retorno['data'].pop('razaoSocial', None) + retorno['data'].pop('nomeFantasia', None) + retorno['data'].pop('id_empresa', None) + + return retorno + except SQLAlchemyError as e: + self.banco.rollback() + self._log(RealmType.ERRORS, f"Erro no banco ao cadastrar empresa: {e}") + return {"error": f"Erro ao cadastrar empresa: {str(e)}"} + except Exception as e: + return {"error": f"Erro inesperado: {str(e)}"} + + def get(self, cnpj: str = None, data: dict = None): + try: + if cnpj: + resposta = self.api.get(f"{self.url}/{cnpj}") + + if isinstance(resposta, dict) and "error" not in resposta: + empresa_local = self.banco.session.query(EnterpriseTable).filter_by(cnpj=cnpj).first() + if empresa_local: + resposta["id"] = empresa_local.id + resposta["nome_fantasia"] = empresa_local.nome_fantasia + resposta["razao_social"] = empresa_local.razao_social + resposta["status"] = empresa_local.status + resposta.pop("razaoSocial", None) + resposta.pop("nomeFantasia", None) + + if "_id" in resposta: + resposta["id_plugnotas"] = resposta["_id"] + + else: + resposta = self.api.get_paginated(self.url) + # Injetar o ID do banco local em cada empresa listada + if isinstance(resposta, dict) and "data" in resposta: + for emp in resposta["data"]: + empresa_local = self.banco.session.query(EnterpriseTable).filter_by(cnpj=emp.get("cpfCnpj")).first() + if empresa_local: + emp["id"] = empresa_local.id + emp["nome_fantasia"] = empresa_local.nome_fantasia + emp["razao_social"] = empresa_local.razao_social + emp["status"] = empresa_local.status + emp.pop("razaoSocial", None) + emp.pop("nomeFantasia", None) + if "_id" in emp: + emp["id_plugnotas"] = emp["_id"] + + self._log(RealmType.OPERATIONAL, f"Empresa consultada - CNPJ: {cnpj or 'TODAS'}") + return resposta + except Exception as e: + self._log(RealmType.ERRORS, f"Erro ao buscar empresa: {e}") + return {"error": f"Erro ao buscar empresa: {str(e)}"} + + def post_file(self, file, cnpj, data=None): + try: + empresa = self.banco.session.query(EnterpriseTable).filter_by(cnpj=cnpj).first() + if not empresa: + self._log(RealmType.ERRORS, f"Empresa não encontrada para upload de logo - CNPJ: {cnpj}") + return {"error": "Empresa não encontrada"} + + logo = LogoTable( + id_empresa=empresa.id, + logo=file, + status=True + ) + self.banco.add(logo) + + files = {'arquivo': ('logo.png', file, 'image/png')} + resposta = self.api.post_file(file=files, url=f"{self.url}/{cnpj}/logotipo") + self._log(RealmType.OPERATIONAL, f"Logo enviada com sucesso - CNPJ: {cnpj}") + return resposta + + except SQLAlchemyError as e: + self.banco.rollback() + self._log(RealmType.ERRORS, f"Erro no banco ao salvar logo: {e}") + return {"error": f"Erro ao salvar logo: {str(e)}"} + except Exception as e: + self._log(RealmType.ERRORS, f"Erro inesperado ao enviar logo: {e}") + return {"error": f"Erro inesperado: {str(e)}"} + + def patch_certificado(self, cnpj: str, certificado: str, data: dict): + try: + empresa = self.banco.session.query(EnterpriseTable).filter_by(cnpj=cnpj).first() + if not empresa: + self._log(RealmType.ERRORS, f"Empresa não encontrada para atualizar certificado - CNPJ: {cnpj}") + return {"error": "Empresa não encontrada"} + resposta = self.api.patch(url=f"{self.url}/{cnpj}/certificado", data={"certificado": certificado}) + self._log(RealmType.OPERATIONAL, f"Certificado da empresa atualizado - CNPJ: {cnpj}") + return resposta + except Exception as e: + self._log(RealmType.ERRORS, f"Erro ao atualizar certificado: {e}") + return {"error": f"Erro ao atualizar certificado: {str(e)}"} + diff --git a/endpoint_listener/src/endpoint_listener.py b/endpoint_listener/src/endpoint_listener.py new file mode 100644 index 0000000..582883b --- /dev/null +++ b/endpoint_listener/src/endpoint_listener.py @@ -0,0 +1,21 @@ +import sys +import os +import uvicorn + +sys.path.append(os.path.dirname(os.path.abspath(__file__))) + +from listenerEndpoint import app + +# O EndpointListener tem a função de manter a aplicação servindo de webhooks ativa recebendo HTTP e convertendo para RabbitMQ +class EndpointListener: + def __init__(self): + self.host = "0.0.0.0" + self.port = int(os.getenv("WEBHOOK_PORT", 8000)) + + def run(self): + print(f"[STATUS] Endpoint Listener rodando na porta {self.port}...") + uvicorn.run(app, host=self.host, port=self.port) + +if __name__ == "__main__": + main = EndpointListener() + main.run() \ No newline at end of file diff --git a/endpoint_listener/src/listenerEndpoint.py b/endpoint_listener/src/listenerEndpoint.py new file mode 100644 index 0000000..c0cf903 --- /dev/null +++ b/endpoint_listener/src/listenerEndpoint.py @@ -0,0 +1,170 @@ +import sys +import os +import json +from contextlib import asynccontextmanager +from fastapi import FastAPI, Request +from fastapi.responses import JSONResponse +import uvicorn + +sys.path.append(os.path.dirname(os.path.abspath(__file__))) +from Models.config import SystemConfig +from utils.redis import RedisClient +from utils.Exchange2 import RPCExchange2, QueueExchange2, LoggerExchange2, AppIC2Builder +from utils.exchangeModels import RealmType + + +# Campos obrigatórios que o PlugNotas envia no Endpoint +CAMPOS_OBRIGATORIOS = ["id", "status", "documento"] + +# Status válidos que o PlugNotas envia +STATUS_VALIDOS = ["CONCLUIDO", "REJEITADO", "CANCELADO", "PROCESSANDO"] + +# Tipos de documento/entidades aceitos +DOCUMENTOS_VALIDOS = ["nfe", "nfse", "certificado_digital"] + +# Variáveis globais +redis_client = None +system_config = None +exchange = None +logger = None + + +@asynccontextmanager +async def ciclo_app(app: FastAPI): + # Gerencia o ciclo de vida da aplicação, inicializando conexões com Redis e RabbitMQ (Exchange) na inicialização + global redis_client, system_config, exchange + + try: + redis_client = RedisClient(host=os.getenv('REDIS_HOST', 'localhost'), port=6379) + system_config_data = redis_client.get("system_config") + + if not system_config_data: + print("Erro: system_config não encontrado no Redis.") + else: + system_config = SystemConfig(**json.loads(system_config_data)) + + builder = AppIC2Builder(service_name="tecnospeed", versionstr="2.0.1", origin=system_config.origin) + global logger + logger = LoggerExchange2(ic2_builder=builder, amqps=system_config.amqps, system="integracao", service="tecnospeed", version="2.0.1", origin=system_config.origin) + logger.logIC2(RealmType.LOGS, (f"[{system_config.origin}] Configuração lida com sucesso pelo Listener Endpoint.",)) + + exchange = QueueExchange2( + amqps=system_config.amqps, + origin=system_config.origin, + system="integracao", + service="endpoint_listener", + version="2.0.1", + client="#" + ) + servicekey = system_config.servicekey.ljust(32, '0')[:32] + exchange.setCrypto(serviceid=system_config.serviceid, servicekey=servicekey) + logger.logIC2(RealmType.LOGS, ("Exchange inicializado com sucesso para o Listener Endpoint.",)) + + except Exception as e: + if logger: + logger.logIC2(RealmType.ERRORS, (f"Erro ao inicializar o Listener Endpoint: {e}",)) + else: + print(f"Erro ao inicializar o Listener Endpoint: {e}") + + yield # Aplicação rodando + + if logger: + logger.logIC2(RealmType.LOGS, ("Listener Endpoint encerrando...",)) + logger.close() + + +app = FastAPI( + title="Listener Endpoint PlugNotas", + description="Endpoint para receber notificações Endpoint do PlugNotas", + version="2.0.1", + lifespan=ciclo_app +) + + +@app.get("/health") +async def healthy(): + # Verifica a saúde da aplicação, validando se as dependências (Redis e Exchange) estão conectadas + status = { + "status": "ok", + "redis": redis_client is not None, + "exchange": exchange is not None, + "system_config": system_config is not None + } + if not exchange: + return JSONResponse(status_code=503, content={**status, "status": "degraded", "message": "Exchange não inicializado"}) + return status + + +@app.post("/listenerPlugnotas") +@app.post("/listenerPlugnotas/{cnpj}") +async def receber_Endpoint_plugnotas(request: Request, cnpj: str = None): + # Rota principal que recebe a notificação da PlugNotas e repassa para processamento no RabbitMQ + try: + try: + payload = await request.json() + except Exception as e: + logger.logIC2(RealmType.ERRORS, (f"Payload inválido recebido (não é JSON válido): {e}",)) + return JSONResponse( + status_code=400, + content={"status": "error", "message": "Payload não é um JSON válido"} + ) + + documento = payload.get("documento", "desconhecido") + status_nota = payload.get("status", "desconhecido") + id_nota = payload.get("id", "") + id_integracao = payload.get("idIntegracao", "") + emitente = payload.get("emitente", cnpj or "") + + logger.logIC2(RealmType.LOGS, ( + f"Endpoint recebido - Documento: {documento} | Status: {status_nota} | " + f"ID: {id_nota} | IDIntegracao: {id_integracao} | Emitente: {emitente}", + )) + + campos_faltantes = [campo for campo in CAMPOS_OBRIGATORIOS if campo not in payload] + if campos_faltantes: + logger.logIC2(RealmType.ERRORS, ( + f"Endpoint com campos obrigatórios faltantes: {campos_faltantes} | " + f"Payload recebido: {json.dumps(payload)}", + )) + return {"status": "warning", "message": f"Campos faltantes: {campos_faltantes}"} + + if documento not in DOCUMENTOS_VALIDOS: + logger.logIC2(RealmType.ERRORS, ( + f"Tipo de documento desconhecido: {documento} | Payload: {json.dumps(payload)}", + )) + if status_nota not in STATUS_VALIDOS: + logger.logIC2(RealmType.ERRORS, ( + f"Status desconhecido: {status_nota} | Documento: {documento} | ID: {id_nota}", + )) + + if not exchange: + logger.logIC2(RealmType.ERRORS, ("Exchange não inicializado no momento da chegada do Endpoint.",)) + return JSONResponse( + status_code=500, + content={"status": "error", "message": "Serviço temporariamente indisponível"} + ) + + # Envia o EXATO objeto dict recebido do PlugNotas via Queue (assíncrono) + try: + exchange.sendMsg(json.dumps(payload), service="controller") + + logger.logIC2(RealmType.LOGS, ( + f"Endpoint enviado à Fila (Queue) Controller. " + f"Documento: {documento} | Status: {status_nota} | ID: {id_nota}", + )) + + return {"status": "ok", "message": "Enviado à Fila com sucesso"} + + except Exception as e: + logger.logIC2(RealmType.ERRORS, (f"Erro ao postar mensagem na Fila (Controller): {e}",)) + return JSONResponse( + status_code=500, + content={"status": "error", "message": "Falha ao enviar mensagem para processamento"} + ) + + except Exception as e: + logger.logIC2(RealmType.ERRORS, (f"Erro inesperado no Listener Endpoint: {e}",)) + return JSONResponse( + status_code=500, + content={"status": "error", "message": "Erro interno no processamento do Endpoint"} + ) diff --git a/endpoint_listener/src/logs/integracao_01 b/endpoint_listener/src/logs/integracao_01 new file mode 100644 index 0000000..ee7eea0 --- /dev/null +++ b/endpoint_listener/src/logs/integracao_01 @@ -0,0 +1,24 @@ +2026-04-01 09:09:33,082 - integracao - INFO - { 'caller': 'ciclo_app', + 'client': '', + 'logDate': '2026-04-01 09:09:33.082579-03:00', + 'msg': '[local] Configuração lida com sucesso pelo Listener Webhook.', + 'origin': '', + 'service': 'tecnospeed', + 'system': 'integracao', + 'version': '2.0.1'} +2026-04-01 09:09:33,700 - integracao - INFO - { 'caller': 'ciclo_app', + 'client': '', + 'logDate': '2026-04-01 09:09:33.700242-03:00', + 'msg': 'Exchange inicializado com sucesso para o Listener Webhook.', + 'origin': '', + 'service': 'tecnospeed', + 'system': 'integracao', + 'version': '2.0.1'} +2026-04-01 09:14:24,402 - integracao - INFO - { 'caller': 'ciclo_app', + 'client': '', + 'logDate': '2026-04-01 09:14:24.402193-03:00', + 'msg': 'Listener Webhook encerrando...', + 'origin': '', + 'service': 'tecnospeed', + 'system': 'integracao', + 'version': '2.0.1'} diff --git a/endpoint_listener/src/logs/integracao_02 b/endpoint_listener/src/logs/integracao_02 new file mode 100644 index 0000000..3fc1caa --- /dev/null +++ b/endpoint_listener/src/logs/integracao_02 @@ -0,0 +1,194 @@ +2026-04-02 09:14:16,362 - integracao - INFO - { 'caller': 'ciclo_app', + 'client': '', + 'logDate': '2026-04-02 09:14:16.362304-03:00', + 'msg': '[local] Configuração lida com sucesso pelo Listener Webhook.', + 'origin': '', + 'service': 'tecnospeed', + 'system': 'integracao', + 'version': '2.0.1'} +2026-04-02 09:14:16,932 - integracao - INFO - { 'caller': 'ciclo_app', + 'client': '', + 'logDate': '2026-04-02 09:14:16.932459-03:00', + 'msg': 'Exchange inicializado com sucesso para o Listener Webhook.', + 'origin': '', + 'service': 'tecnospeed', + 'system': 'integracao', + 'version': '2.0.1'} +2026-04-02 14:14:30,014 - integracao - INFO - { 'caller': 'ciclo_app', + 'client': '', + 'logDate': '2026-04-02 14:14:30.013914-03:00', + 'msg': '[local] Configuração lida com sucesso pelo Listener Webhook.', + 'origin': '', + 'service': 'tecnospeed', + 'system': 'integracao', + 'version': '2.0.1'} +2026-04-02 14:14:31,040 - integracao - INFO - { 'caller': 'ciclo_app', + 'client': '', + 'logDate': '2026-04-02 14:14:31.039753-03:00', + 'msg': 'Exchange inicializado com sucesso para o Listener Webhook.', + 'origin': '', + 'service': 'tecnospeed', + 'system': 'integracao', + 'version': '2.0.1'} +2026-04-02 14:22:20,024 - integracao - INFO - { 'caller': 'receber_webhook_plugnotas', + 'client': '', + 'logDate': '2026-04-02 14:22:20.024167-03:00', + 'msg': 'Webhook recebido - Documento: nfe | Status: CONCLUIDO | ID: ' + '69cab453... | IDIntegracao: | Emitente:', + 'origin': '', + 'service': 'tecnospeed', + 'system': 'integracao', + 'version': '2.0.1'} +2026-04-02 14:22:20,263 - integracao - INFO - { 'caller': 'receber_webhook_plugnotas', + 'client': '', + 'logDate': '2026-04-02 14:22:20.263204-03:00', + 'msg': 'Webhook repassado ao worker com sucesso - Documento: nfe | Status: ' + 'CONCLUIDO | ID: 69cab453...', + 'origin': '', + 'service': 'tecnospeed', + 'system': 'integracao', + 'version': '2.0.1'} +2026-04-02 14:23:45,608 - integracao - INFO - { 'caller': 'receber_webhook_plugnotas', + 'client': '', + 'logDate': '2026-04-02 14:23:45.608300-03:00', + 'msg': 'Webhook recebido - Documento: nfe | Status: CONCLUIDO | ID: ' + '69cab45329f21a7de7691961 | IDIntegracao: XXXYY99999_TESTE445 | ' + 'Emitente: 08187168000160', + 'origin': '', + 'service': 'tecnospeed', + 'system': 'integracao', + 'version': '2.0.1'} +2026-04-02 14:23:46,107 - integracao - INFO - { 'caller': 'receber_webhook_plugnotas', + 'client': '', + 'logDate': '2026-04-02 14:23:46.107170-03:00', + 'msg': 'Webhook repassado ao worker com sucesso - Documento: nfe | Status: ' + 'CONCLUIDO | ID: 69cab45329f21a7de7691961', + 'origin': '', + 'service': 'tecnospeed', + 'system': 'integracao', + 'version': '2.0.1'} +2026-04-02 14:26:07,402 - integracao - INFO - { 'caller': 'ciclo_app', + 'client': '', + 'logDate': '2026-04-02 14:26:07.401683-03:00', + 'msg': 'Listener Webhook encerrando...', + 'origin': '', + 'service': 'tecnospeed', + 'system': 'integracao', + 'version': '2.0.1'} +2026-04-02 14:37:36,191 - integracao - INFO - { 'caller': 'ciclo_app', + 'client': '', + 'logDate': '2026-04-02 14:37:36.190896-03:00', + 'msg': '[local] Configuração lida com sucesso pelo Listener Webhook.', + 'origin': '', + 'service': 'tecnospeed', + 'system': 'integracao', + 'version': '2.0.1'} +2026-04-02 14:37:36,969 - integracao - INFO - { 'caller': 'ciclo_app', + 'client': '', + 'logDate': '2026-04-02 14:37:36.969622-03:00', + 'msg': 'Exchange inicializado com sucesso para o Listener Webhook.', + 'origin': '', + 'service': 'tecnospeed', + 'system': 'integracao', + 'version': '2.0.1'} +2026-04-02 14:37:37,459 - integracao - INFO - { 'caller': 'ciclo_app', + 'client': '', + 'logDate': '2026-04-02 14:37:37.459047-03:00', + 'msg': 'Listener Webhook encerrando...', + 'origin': '', + 'service': 'tecnospeed', + 'system': 'integracao', + 'version': '2.0.1'} +2026-04-02 14:37:42,582 - integracao - INFO - { 'caller': 'ciclo_app', + 'client': '', + 'logDate': '2026-04-02 14:37:42.582792-03:00', + 'msg': '[local] Configuração lida com sucesso pelo Listener Webhook.', + 'origin': '', + 'service': 'tecnospeed', + 'system': 'integracao', + 'version': '2.0.1'} +2026-04-02 14:37:43,391 - integracao - INFO - { 'caller': 'ciclo_app', + 'client': '', + 'logDate': '2026-04-02 14:37:43.391071-03:00', + 'msg': 'Exchange inicializado com sucesso para o Listener Webhook.', + 'origin': '', + 'service': 'tecnospeed', + 'system': 'integracao', + 'version': '2.0.1'} +2026-04-02 14:37:43,782 - integracao - INFO - { 'caller': 'ciclo_app', + 'client': '', + 'logDate': '2026-04-02 14:37:43.781995-03:00', + 'msg': 'Listener Webhook encerrando...', + 'origin': '', + 'service': 'tecnospeed', + 'system': 'integracao', + 'version': '2.0.1'} +2026-04-02 16:03:48,010 - integracao - INFO - { 'caller': 'run', + 'client': '', + 'logDate': '2026-04-02 16:03:48.009993-03:00', + 'msg': 'Conectando ao RabbitMQ', + 'origin': '', + 'service': 'tecnospeed', + 'system': 'integracao', + 'version': '2.0.1'} +2026-04-02 16:03:48,217 - integracao - INFO - { 'caller': 'run', + 'client': '', + 'logDate': '2026-04-02 16:03:48.216535-03:00', + 'msg': "Configuração do sistema carregada - serviceid='local' " + "servicekey='local' origin='local' " + "amqps='amqp://guest:guest@localhost:5672/'", + 'origin': '', + 'service': 'tecnospeed', + 'system': 'integracao', + 'version': '2.0.1'} +2026-04-02 16:12:17,812 - integracao - INFO - { 'caller': 'run', + 'client': '', + 'logDate': '2026-04-02 16:12:17.812369-03:00', + 'msg': 'Conectando ao RabbitMQ', + 'origin': '', + 'service': 'tecnospeed', + 'system': 'integracao', + 'version': '2.0.1'} +2026-04-02 16:12:18,488 - integracao - INFO - { 'caller': 'run', + 'client': '', + 'logDate': '2026-04-02 16:12:18.487913-03:00', + 'msg': "Configuração do sistema carregada - serviceid='local' " + "servicekey='local' origin='local' " + "amqps='amqp://guest:guest@localhost:5672/'", + 'origin': '', + 'service': 'tecnospeed', + 'system': 'integracao', + 'version': '2.0.1'} +2026-04-02 16:12:18,864 - integracao - INFO - { 'caller': 'run', + 'client': '', + 'logDate': '2026-04-02 16:12:18.863889-03:00', + 'msg': 'Erro fatal ao conectar ao RabbitMQ - VERIFY PARAMETERS: ' + 'is_amqp_url: True\n' + ' amqps[amqp://guest:guest@localhost:5672/]\n' + ' origin[local]\n' + ' system[integracao]\n' + ' service[tecnospeed]\n' + ' version[2.0.1\n' + ' client[]', + 'origin': '', + 'service': 'tecnospeed', + 'system': 'integracao', + 'version': '2.0.1'} +2026-04-02 16:12:30,426 - integracao - INFO - { 'caller': 'run', + 'client': '', + 'logDate': '2026-04-02 16:12:30.426681-03:00', + 'msg': 'Conectando ao RabbitMQ', + 'origin': '', + 'service': 'tecnospeed', + 'system': 'integracao', + 'version': '2.0.1'} +2026-04-02 16:12:30,947 - integracao - INFO - { 'caller': 'run', + 'client': '', + 'logDate': '2026-04-02 16:12:30.947199-03:00', + 'msg': "Configuração do sistema carregada - serviceid='local' " + "servicekey='local' origin='local' " + "amqps='amqp://guest:guest@localhost:5672/'", + 'origin': '', + 'service': 'tecnospeed', + 'system': 'integracao', + 'version': '2.0.1'} diff --git a/endpoint_listener/src/logs/integracao_06 b/endpoint_listener/src/logs/integracao_06 new file mode 100644 index 0000000..d64ebb0 --- /dev/null +++ b/endpoint_listener/src/logs/integracao_06 @@ -0,0 +1,56 @@ +2026-04-06 08:25:17,847 - teste_wrapper.integracao - INFO - params: amqps: amqps://iawkzmhh:4iIVXCgLDsLy2oVyVWeV-o6shg-BZ89z@jackal.rmq.cloudamqp.com/iawkzmhh +2026-04-06 08:25:18,327 - teste_wrapper.integracao - INFO - Logger Messages topics: logs.teste_wrapper.integracao. +2026-04-06 08:25:18,361 - teste_wrapper.integracao - INFO - {'caller': 'IC2', + 'client': '', + 'hmac': '862c786853960277c4d518d1eb01143b2fe7782092dbf5403fb61b52127c7829', + 'logDate': '2026-04-06 08:25:18.361486-03:00', + 'msg': "LOGIC2 LOGS [('Testando o novo fo ...]", + 'origin': '', + 'payload': '44102feb05c76aac7b2b', + 'service': 'teste_wrapper', + 'serviceid': 'AF1699F719F1B6C81DE2BD38AB811994E3089B35C8E9314813A770AC7AE25D8D', + 'system': 'integracao', + 'target': '', + 'type': 'logger', + 'version': ''} +2026-04-06 08:25:18,368 - teste_wrapper.integracao - INFO - {'caller': 'logMsg', + 'client': '', + 'hmac': '', + 'logDate': '2026-04-06 08:25:18.367868-03:00', + 'msg': 'Testando o novo formato IC2', + 'origin': '', + 'payload': '', + 'service': 'teste_wrapper', + 'serviceid': 'AF1699F719F1B6C81DE2BD38AB811994E3089B35C8E9314813A770AC7AE25D8D', + 'system': 'integracao', + 'target': '', + 'type': 'logger', + 'version': ''} +2026-04-06 08:28:37,324 - teste_wrapper.integracao - INFO - params: amqps: amqps://iawkzmhh:4iIVXCgLDsLy2oVyVWeV-o6shg-BZ89z@jackal.rmq.cloudamqp.com/iawkzmhh +2026-04-06 08:28:37,834 - teste_wrapper.integracao - INFO - Logger Messages topics: logs.teste_wrapper.integracao. +2026-04-06 08:28:37,855 - teste_wrapper.integracao - INFO - {'caller': 'IC2', + 'client': '', + 'hmac': '841681411efce4ae99ff2602ae79da6284e6c3ca247a27da3a87dedf3771244e', + 'logDate': '2026-04-06 08:28:37.855637-03:00', + 'msg': "LOGIC2 LOGS [('Testando o formato ...]", + 'origin': '', + 'payload': '353fd214129406d78301', + 'service': 'teste_wrapper', + 'serviceid': 'AF1699F719F1B6C81DE2BD38AB811994E3089B35C8E9314813A770AC7AE25D8D', + 'system': 'integracao', + 'target': '', + 'type': 'logger', + 'version': ''} +2026-04-06 08:28:37,865 - teste_wrapper.integracao - INFO - {'caller': 'logMsg', + 'client': '', + 'hmac': '', + 'logDate': '2026-04-06 08:28:37.865799-03:00', + 'msg': 'Testando o formato IC2', + 'origin': '', + 'payload': '', + 'service': 'teste_wrapper', + 'serviceid': 'AF1699F719F1B6C81DE2BD38AB811994E3089B35C8E9314813A770AC7AE25D8D', + 'system': 'integracao', + 'target': '', + 'type': 'logger', + 'version': ''} diff --git a/endpoint_listener/src/logs/integracao_25 b/endpoint_listener/src/logs/integracao_25 new file mode 100644 index 0000000..e69de29 diff --git a/endpoint_listener/src/logs/integracao_26 b/endpoint_listener/src/logs/integracao_26 new file mode 100644 index 0000000..e69de29 diff --git a/endpoint_listener/src/logs/integracao_27 b/endpoint_listener/src/logs/integracao_27 new file mode 100644 index 0000000..f61511b --- /dev/null +++ b/endpoint_listener/src/logs/integracao_27 @@ -0,0 +1,140 @@ +2026-03-27 14:57:57,807 - integracao - INFO - { 'caller': 'run', + 'client': '', + 'logDate': '2026-03-27 14:57:57.807604-03:00', + 'msg': 'Conectando ao RabbitMQ', + 'origin': '', + 'service': 'tecnospeed', + 'system': 'integracao', + 'version': '2.0.1'} +2026-03-27 14:57:58,282 - integracao - INFO - { 'caller': 'run', + 'client': '', + 'logDate': '2026-03-27 14:57:58.282491-03:00', + 'msg': "Configuração do sistema carregada - serviceid='local' " + "servicekey='local' origin='local' " + "amqps='amqp://guest:guest@localhost:5672/'", + 'origin': '', + 'service': 'tecnospeed', + 'system': 'integracao', + 'version': '2.0.1'} +2026-03-27 14:59:07,273 - integracao - INFO - { 'caller': 'callback', + 'client': 'Test', + 'logDate': '2026-03-27 14:59:07.273291-03:00', + 'msg': "Configuração recebida - {'api_key': None, 'x_api_key': " + "'2da392a6-79d2-4304-a8b7-959572c7e44d', 'user_postgres': " + "'postgres', 'pass_postgres': 'postgres', 'host_postgres': " + "'localhost', 'port_postgres': '5432', 'database_postgres': " + "'tecnospeed'}", + 'origin': '', + 'service': 'tecnospeed', + 'system': 'integracao', + 'version': '2.0.1'} +2026-03-27 14:59:08,015 - integracao - INFO - { 'caller': 'callback', + 'client': 'Test', + 'logDate': '2026-03-27 14:59:08.014993-03:00', + 'msg': 'Configuração incompleta', + 'origin': '', + 'service': 'tecnospeed', + 'system': 'integracao', + 'version': '2.0.1'} +2026-03-27 15:00:56,891 - integracao - INFO - { 'caller': 'callback', + 'client': 'Test', + 'logDate': '2026-03-27 15:00:56.890566-03:00', + 'msg': "Configuração recebida - {'api_key': None, 'x_api_key': " + "'2da392a6-79d2-4304-a8b7-959572c7e44d', 'user_postgres': " + "'postgres', 'pass_postgres': 'postgres', 'host_postgres': " + "'localhost', 'port_postgres': '5432', 'database_postgres': " + "'sg_servico_tecnospeed'}", + 'origin': '', + 'service': 'tecnospeed', + 'system': 'integracao', + 'version': '2.0.1'} +2026-03-27 15:00:57,787 - integracao - INFO - { 'caller': 'callback', + 'client': 'Test', + 'logDate': '2026-03-27 15:00:57.787472-03:00', + 'msg': 'Configuração incompleta', + 'origin': '', + 'service': 'tecnospeed', + 'system': 'integracao', + 'version': '2.0.1'} +2026-03-27 15:02:14,102 - integracao - INFO - { 'caller': 'callback', + 'client': 'Test', + 'logDate': '2026-03-27 15:02:14.101859-03:00', + 'msg': "Configuração recebida - {'api_key': None, 'x_api_key': " + "'2da392a6-79d2-4304-a8b7-959572c7e44d', 'user_postgres': " + "'postgres', 'pass_postgres': 'postgres', 'host_postgres': " + "'localhost', 'port_postgres': '5432', 'database_postgres': " + "'tecnospeed'}", + 'origin': '', + 'service': 'tecnospeed', + 'system': 'integracao', + 'version': '2.0.1'} +2026-03-27 15:02:14,792 - integracao - INFO - { 'caller': 'callback', + 'client': 'Test', + 'logDate': '2026-03-27 15:02:14.791942-03:00', + 'msg': 'Configuração incompleta', + 'origin': '', + 'service': 'tecnospeed', + 'system': 'integracao', + 'version': '2.0.1'} +2026-03-27 15:09:33,329 - integracao - INFO - { 'caller': 'run', + 'client': '', + 'logDate': '2026-03-27 15:09:33.329870-03:00', + 'msg': 'Conectando ao RabbitMQ', + 'origin': '', + 'service': 'tecnospeed', + 'system': 'integracao', + 'version': '2.0.1'} +2026-03-27 15:09:33,698 - integracao - INFO - { 'caller': 'run', + 'client': '', + 'logDate': '2026-03-27 15:09:33.698183-03:00', + 'msg': "Configuração do sistema carregada - serviceid='local' " + "servicekey='local' origin='local' " + "amqps='amqp://guest:guest@localhost:5672/'", + 'origin': '', + 'service': 'tecnospeed', + 'system': 'integracao', + 'version': '2.0.1'} +2026-03-27 15:09:39,159 - integracao - INFO - { 'caller': 'callback', + 'client': 'Test', + 'logDate': '2026-03-27 15:09:39.158573-03:00', + 'msg': "Configuração recebida - {'api_key': None, 'x_api_key': " + "'2da392a6-79d2-4304-a8b7-959572c7e44d', 'user_postgres': " + "'postgres', 'pass_postgres': 'postgres', 'host_postgres': " + "'localhost', 'port_postgres': '5432', 'database_postgres': " + "'tecnospeed'}", + 'origin': '', + 'service': 'tecnospeed', + 'system': 'integracao', + 'version': '2.0.1'} +2026-03-27 15:09:44,975 - integracao - INFO - { 'caller': 'callback', + 'client': 'Test', + 'logDate': '2026-03-27 15:09:44.975168-03:00', + 'msg': 'Configuração completa para o cliente Test', + 'origin': '', + 'service': 'tecnospeed', + 'system': 'integracao', + 'version': '2.0.1'} +2026-03-27 15:09:45,763 - integracao - INFO - { 'caller': 'nfe', + 'client': 'Test', + 'logDate': '2026-03-27 15:09:45.763614-03:00', + 'msg': 'Iniciando serviço de NFe - Método: GET', + 'origin': '', + 'service': 'tecnospeed', + 'system': 'integracao', + 'version': '2.0.1'} +2026-03-27 15:09:46,610 - integracao - INFO - { 'caller': 'nfe', + 'client': 'Test', + 'logDate': '2026-03-27 15:09:46.610671-03:00', + 'msg': 'Consultando resumo de NFe', + 'origin': '', + 'service': 'tecnospeed', + 'system': 'integracao', + 'version': '2.0.1'} +2026-03-27 15:09:47,761 - integracao - INFO - { 'caller': 'nfe', + 'client': 'Test', + 'logDate': '2026-03-27 15:09:47.761417-03:00', + 'msg': 'Operação realizada com sucesso', + 'origin': '', + 'service': 'tecnospeed', + 'system': 'integracao', + 'version': '2.0.1'} diff --git a/endpoint_listener/src/tests/__init__.py b/endpoint_listener/src/tests/__init__.py new file mode 100644 index 0000000..65140f2 --- /dev/null +++ b/endpoint_listener/src/tests/__init__.py @@ -0,0 +1 @@ +# tests package diff --git a/endpoint_listener/src/tests/conftest.py b/endpoint_listener/src/tests/conftest.py new file mode 100644 index 0000000..db70125 --- /dev/null +++ b/endpoint_listener/src/tests/conftest.py @@ -0,0 +1,119 @@ +""" +Fixtures compartilhados para os testes E2E do servico_plugnotas. + +Cria um banco SQLite in-memory, mocks do Logger e da Api, +e uma instância pré-configurada da classe Main para cada teste. +""" +import pytest +import json +import sys +from unittest.mock import MagicMock, patch + +mock_pika = MagicMock() +sys.modules["pika"] = mock_pika + +mock_redis_module = MagicMock() +sys.modules["redis"] = mock_redis_module + +from utils.banco import Base, Banco, LogTable +from utils.api import Api + +@pytest.fixture +def banco_teste(): + """Cria um Banco real com SQLite in-memory para cada teste (isolado).""" + from sqlalchemy import create_engine + from sqlalchemy.orm import sessionmaker + + engine = create_engine("sqlite:///:memory:") + Base.metadata.create_all(engine) + Session = sessionmaker(bind=engine) + + banco = Banco.__new__(Banco) + banco.engine = engine + banco.session = Session() + + yield banco + + banco.session.close() + engine.dispose() + +@pytest.fixture +def mock_logger(): + """Logger que grava todas as chamadas de log em uma lista para inspeção.""" + logger = MagicMock() + logger.logs = [] + logger.client = "TestClient" + + def capture_log(realm, msg_tuple): + logger.logs.append({"realm": str(realm), "message": msg_tuple[0] if msg_tuple else ""}) + + logger.logIC2.side_effect = capture_log + return logger + +@pytest.fixture +def mock_api(): + """Api que retorna respostas configuráveis sem fazer chamadas HTTP reais.""" + api = MagicMock(spec=Api) + api.get.return_value = {"status": "ok"} + api.post.return_value = {"documents": [{"id": "PLUG-001"}]} + api.put.return_value = {"status": "ok"} + api.patch.return_value = {"status": "ok"} + api.delete.return_value = {"status": "ok"} + api.post_file.return_value = {"status": "ok"} + api.post_form_data.return_value = {"data": {"id": "CERT-001"}} + api.get_paginated.return_value = {"data": [], "total": 0} + return api + +@pytest.fixture +def mock_exchange(): + """Exchange que simula setPayload sem enviar nada.""" + exchange = MagicMock() + exchange.payload = "" + exchange.payloads_set = [] + + def capture_payload(p): + exchange.payloads_set.append(p) + exchange.payload = p + + exchange.setPayload.side_effect = capture_payload + return exchange + +@pytest.fixture +def main_instance(banco_teste, mock_logger, mock_exchange): + with patch("utils.Exchange2.RPCExchange2"), \ + patch("utils.Exchange2.QueueExchange2"), \ + patch("utils.Exchange2.LoggerExchange2"), \ + patch("utils.redis.RedisClient"): + + from main import Main + + with patch.object(Main, '__init__', lambda self: None): + main = Main() + + main.client = "TestClient" + main.api_key = "test-api-key-fake" + main.exchange = mock_exchange + main.banco = banco_teste + main.system_config = MagicMock() + main.system_config.serviceid = "test-id" + main.system_config.servicekey = "test-key-32-chars-long-12345678" + main.redis = MagicMock() + main.service = "tecnospeed" + main.system = "integracao" + main.version = "2.0.1-test" + main.logger = mock_logger + main.url_base = "https://api.sandbox.plugnotas.com.br" + + main.funcoes = { + "nfe": main.nfe, + "nfse": main.nfse, + "certificado_digital": main.certificado_digital, + "empresa": main.empresa, + "servico": main.servico, + "tomador": main.tomador, + "relatorios": main.relatorios, + "webhooks": main.webhooks, + } + + return main + diff --git a/endpoint_listener/src/tests/debug_payload.py b/endpoint_listener/src/tests/debug_payload.py new file mode 100644 index 0000000..611e9bb --- /dev/null +++ b/endpoint_listener/src/tests/debug_payload.py @@ -0,0 +1,67 @@ +import json +import os +import sys + +# Garante que o Python encontre os módulos do projeto +sys.path.append(os.path.dirname(os.path.dirname(os.path.abspath(__file__)))) + +from Models.payload import validate_payload + +sample_payload = { + "fila": "nfse", + "cliente": "MassiveNFSeTest", + "metodo": "POST", + "dados": { + "id_empresa": "some-uuid", + "id_usuario": 1, + "idIntegracao": "TEST_123", + "prestador": { + "cpfCnpj": "00000000000000" + }, + "tomador": { + "cpfCnpj": "08114280956", + "razaoSocial": "NFSE EMITIDA EM HOMOLOGACAO", + "email": "contato@tecnospeed.com.br", + "endereco": { + "logradouro": "AVENIDA DUQUE DE CAXIAS", + "numero": "882", + "bairro": "CENTRO", + "codigoCidade": "4115200", + "descricaoCidade": "MARINGA", + "estado": "PR", + "cep": "87020025", + "tipoLogradouro": "Avenida" + } + }, + "servico": { + "codigo": "1.01", + "codigoTributacao": "1.01", + "discriminacao": "SERVICO DE TESTE DE CARGA MASSIVA", + "cnae": "6201501", + "iss": { + "tipoTributacao": 1, + "exigibilidade": 1, + "aliquota": 2.0 + }, + "valor": { + "servico": 100.0 + } + } + }, + "config": { + "x_api_key": "2da392a6-79d2-4304-a8b7-959572c7e44d", + "user_postgres": "tecnospeed", + "pass_postgres": "u7QimG7E!SF//JC2c8", + "host_postgres": "banco", + "port_postgres": "5432", + "database_postgres": "tecnospeed" + }, + "url_base": "https://api.sandbox.plugnotas.com.br" +} + +try: + print("Tentando validar payload...") + validated = validate_payload(sample_payload) + print("Sucesso! Payload válido.") +except Exception as e: + print(f"Erro de validação: {e}") diff --git a/endpoint_listener/src/tests/debug_report_api.py b/endpoint_listener/src/tests/debug_report_api.py new file mode 100644 index 0000000..69bc364 --- /dev/null +++ b/endpoint_listener/src/tests/debug_report_api.py @@ -0,0 +1,33 @@ +import requests +import os +import json + +def debug_report(): + api_key = "2da392a6-79d2-4304-a8b7-959572c7e44d" # From src/utils/api.py comment + url = "https://api.sandbox.plugnotas.com.br/nfse/relatorio?from=2026-04-01&to=2026-04-30" + + headers = { + "x-api-key": api_key + } + + print(f"Testing URL: {url}") + response = requests.get(url, headers=headers) + + print(f"Status Code: {response.status_code}") + try: + print(f"Response Body: {json.dumps(response.json(), indent=2)}") + except: + print(f"Response Body: {response.text}") + + # Test with ISO format + url_iso = "https://api.sandbox.plugnotas.com.br/nfse/relatorio?from=2026-04-01T00:00:00&to=2026-04-30T23:59:59" + print(f"\nTesting URL (ISO): {url_iso}") + response = requests.get(url_iso, headers=headers) + print(f"Status Code: {response.status_code}") + try: + print(f"Response Body: {json.dumps(response.json(), indent=2)}") + except: + print(f"Response Body: {response.text}") + +if __name__ == "__main__": + debug_report() diff --git a/endpoint_listener/src/tests/purge_queue.py b/endpoint_listener/src/tests/purge_queue.py new file mode 100644 index 0000000..e5c00c5 --- /dev/null +++ b/endpoint_listener/src/tests/purge_queue.py @@ -0,0 +1,46 @@ +import pika +import os +import sys +import json + +# Garante que o Python encontre os módulos do projeto +sys.path.append(os.path.dirname(os.path.dirname(os.path.abspath(__file__)))) + +from utils.redis import RedisClient + +def purge_queue(): + # 1. Obter URL do RabbitMQ do Redis + redis_host = os.getenv('REDIS_HOST', 'localhost') + redis_client = RedisClient(host=redis_host, port=6379) + try: + system_config_data = redis_client.get("system_config") + if not system_config_data: + print("Erro: system_config não encontrado no Redis.") + return + system_config = json.loads(system_config_data) + amqps_url = system_config.get("amqps") + except Exception as e: + print(f"Erro ao ler configs do Redis: {e}") + return + + if not amqps_url: + print("Erro: AMQPS URL não encontrada na configuração.") + return + + # 2. Conectar e limpar a fila + queue_name = "integracao_tecnospeed" + try: + params = pika.URLParameters(amqps_url) + connection = pika.BlockingConnection(params) + channel = connection.channel() + + # Tenta realizar o purge + result = channel.queue_purge(queue=queue_name) + print(f"Sucesso! Fila [{queue_name}] limpa. Mensagens removidas: {result.method.message_count}") + + connection.close() + except Exception as e: + print(f"Erro ao limpar a fila: {e}") + +if __name__ == "__main__": + purge_queue() diff --git a/endpoint_listener/src/tests/test.py b/endpoint_listener/src/tests/test.py new file mode 100644 index 0000000..257b05d --- /dev/null +++ b/endpoint_listener/src/tests/test.py @@ -0,0 +1,134 @@ +import sys +import os +import json + +sys.path.append(os.path.dirname(os.path.abspath(__file__))) + +from Models.config import SystemConfig +from utils.redis import RedisClient +from utils.Exchange2 import RPCExchange2 + +def main(): + print("Iniciando script de testes da mensageria...") + + # Carregando configuração do Redis + redis_client = RedisClient(host=os.getenv('REDIS_HOST', 'localhost'), port=6379) + try: + system_config_data = redis_client.get("system_config") + if not system_config_data: + print("Erro: system_config não encontrado no Redis. Certifique-se de que a variável existe.") + return + + system_config = SystemConfig(**json.loads(system_config_data)) + print(f"[{system_config.origin}] Configuração do sistema lida com sucesso.") + + except Exception as e: + print(f"Erro ao ler configs do Redis: {e}") + return + + # Inicializando mensageria RPC + try: + amqps_local = system_config.amqps.replace("host.docker.internal", "localhost") + exchange = RPCExchange2( + amqps=amqps_local, + origin=system_config.origin, + system="integracao", + service="tecnospeed", + version="2.0.1", + client="#" + ) + except Exception as e: + print(f"Falha ao inicializar mensageria: {e}") + return + + # Configurando criptografia e payload + servicekey = system_config.servicekey.ljust(32, '0')[:32] + exchange.setCrypto(serviceid=system_config.serviceid, servicekey=servicekey) + + # Payload da mensageria + + payload_interno = { + "fila": "nfse", + "cliente": "Test", + "metodo": "POST", + "dados": { + "idIntegracao": "INTEGRACAO-NFSE-002", + "prestador": { + "cpfCnpj": "08187168000160" + }, + "tomador": { + "cpfCnpj": "08114280956", + "razaoSocial": "João Tomador da Silva", + "inscricaoMunicipal": "isento", + "email": "tomador@email.com", + "endereco": { + "descricaoCidade": "São Paulo", + "cep": "00000000", + "tipoLogradouro": "Rua", + "logradouro": "Dois de Ouro", + "tipoBairro": "Bairro", + "codigoCidade": "3550308", + "complemento": "Apto 13", + "estado": "SP", + "numero": "44", + "bairro": "Centro" + } + }, + "servico": { + "codigo": "1.01", + "codigoTributacao": "1.01", + "discriminacao": "Serviços prestados de tecnologia", + "cnae": "6204000", + "iss": { + "tipoTributacao": 6, + "exigibilidade": 1, + "aliquota": 2.5 + }, + "valor": { + "servico": 1500.50, + "descontoCondicionado": 0.0, + "descontoIncondicionado": 0.0 + } + } + }, + "config": { + "x_api_key": os.getenv('PLUGNOTAS_API_KEY', ''), + "user_postgres": os.getenv('POSTGRES_USER', 'tecnospeed'), + "pass_postgres": os.getenv('POSTGRES_PASSWORD', 'tecnospeed'), + "host_postgres": os.getenv('POSTGRES_HOST', 'localhost'), + "port_postgres": os.getenv('POSTGRES_PORT', '5432'), + "database_postgres": os.getenv('POSTGRES_DB', 'tecnospeed') + }, + "url_base": "https://api.sandbox.plugnotas.com.br" + } + + + # Criptografando e enviando tarefa + exchange.setPayload(json.dumps(payload_interno)) + + print("Disparando chamada RPC para o RabbitMQ...") + # Executando chamada RPC + response = exchange.callRPC("Disparo de testes executado pelo test.py") + print("\n[RAW RPC RESPONSE]:") + print(response) + + # Descriptografando a resposta + try: + resp_obj = json.loads(response) + hmac_resp = resp_obj.get('hmac') + payload_resp = resp_obj.get('payload') + + if hmac_resp and payload_resp: + if exchange.getPayload(hmac_resp, payload_resp): + print("\n[DECRYPTED RPC DATA]:") + print(json.dumps(json.loads(exchange.payload), indent=2)) + else: + print("\n[ERROR]: Failed to decrypt response.") + else: + print("\n[INFO]: Response does not contain an encrypted payload.") + + except Exception as e: + print(f"\n[ERROR]: Error processing response: {e}") + +if __name__ == "__main__": + main() diff --git a/endpoint_listener/src/tests/testArch.py b/endpoint_listener/src/tests/testArch.py new file mode 100644 index 0000000..1fe2d01 --- /dev/null +++ b/endpoint_listener/src/tests/testArch.py @@ -0,0 +1,58 @@ +import pytest +from unittest.mock import patch, MagicMock +from fastapi.testclient import TestClient +from src.listenerEndpoint import app + +client = TestClient(app) + +PAYLOAD_VALIDO = { + "id": "60a7c4f1c9d2a00015...", + "status": "CONCLUIDO", + "documento": "nfe" +} +HEADERS_VALIDOS = {"x-plugnotas-signature": "assinatura_hmac_valida"} + +class TestPlugNotasWebhookEndpoint: + + @patch("src.listenerEndpoint.exchange") + def test_deve_receber_payload_e_enfileirar_com_sucesso(self, mock_exchange): + """Critério: Caminho feliz. Valida, enfileira e responde com sucesso (200).""" + mock_exchange.sendTask.return_value = True + + response = client.post("/listenerPlugnotas", json=PAYLOAD_VALIDO, headers=HEADERS_VALIDOS) + + assert response.status_code == 200 + assert response.json() == {"status": "ok", "message": "Webhook recebido e encaminhado para processamento"} + + mock_exchange.setPayload.assert_called_once() + mock_exchange.sendTask.assert_called_once_with("Webhook PlugNotas Recebido") + + @patch("src.listenerEndpoint.exchange") + def test_deve_rejeitar_payload_invalido_antes_de_processar(self, mock_exchange): + """Critério: Integridade de Dados. Valida campos obrigatórios recebidos do Hook.""" + payload_invalido = {"status": "CONCLUIDO"} # Faltando ID e documento + + response = client.post("/listenerPlugnotas", json=payload_invalido, headers=HEADERS_VALIDOS) + + assert response.status_code == 200 + assert response.json()["status"] == "warning" + assert "Campos faltantes" in response.json()["message"] + + @patch("src.listenerEndpoint.exchange") + def test_deve_retornar_erro_se_internal_error_no_rabbitmq(self, mock_exchange): + """Critério: Resiliência. Retorna 500 em caso de erro ao enviar para fila.""" + # Simulando uma queda ao enviar task + mock_exchange.sendTask.side_effect = Exception("RabbitMQ is down") + + response = client.post("/listenerPlugnotas", json=PAYLOAD_VALIDO, headers=HEADERS_VALIDOS) + + assert response.status_code == 500 + assert response.json()["message"] == "Erro interno no processamento do webhook" + + def test_deve_retornar_erro_se_exchange_nao_inicializado(self): + """Critério: Resiliência. Retorna 500 se o exchange global for nulo.""" + # Sem patch de exchange, então pegará o exchange default (None) em client sem lifespan completo + response = client.post("/listenerPlugnotas", json=PAYLOAD_VALIDO) + + assert response.status_code == 500 + assert response.json()["message"] == "Serviço temporariamente indisponível" \ No newline at end of file diff --git a/endpoint_listener/src/tests/test_1000_empresas.py b/endpoint_listener/src/tests/test_1000_empresas.py new file mode 100644 index 0000000..c24a82a --- /dev/null +++ b/endpoint_listener/src/tests/test_1000_empresas.py @@ -0,0 +1,144 @@ +import os +import sys +import json +import time + +# Garante que o Python encontre os módulos do projeto +sys.path.append(os.path.dirname(os.path.dirname(os.path.abspath(__file__)))) + +from Models.config import SystemConfig +from utils.redis import RedisClient +from utils.Exchange2 import RPCExchange2, QueueExchange2 + +def get_empresa_payload(i): + """Gera um payload de empresa com CNPJ e Razão Social baseados no índice i""" + # Gerando um CNPJ fictício baseado no loop para evitar duplicidade no banco + # Ex: 94074986000001, 94074986000002... + cnpj = f"94074986{str(i).zfill(6)}" + + return { + "id_usuario": 1, + "cpfCnpj": cnpj, + "razaoSocial": f"Varejo de Teste {i} LTDA", + "nomeFantasia": f"Varejo {i}", + "inscricaoMunicipal": "0001", + "inscricaoEstadual": "123456789", + "simplesNacional": True, + "regimeTributario": 1, + "regimeTributarioEspecial": 1, + "incentivadorCultural": False, + "incentivoFiscal": True, + "certificado": "5af59d271f6e8f409178fbf3", + "endereco": { + "codigoPais": "1058", + "descricaoPais": "BRASIL", + "tipoLogradouro": "Avenida", + "logradouro": "Avenida de Teste", + "numero": str(i), + "complemento": "Casa", + "tipoBairro": "Bairro", + "bairro": "Centro", + "codigoCidade": "4115200", + "descricaoCidade": "Maringá", + "estado": "PR", + "cep": "87020025" + }, + "nfe": {"ativo": True, "tipoContrato": 0}, + "nfse": {"ativo": False} + } + +def run_rpc_test(exchange, config_rpc): + """Envia uma única empresa via RPC e aguarda a resposta (bom para debug)""" + print("\n--- INICIANDO TESTE RPC INDIVIDUAL ---") + payload_dados = get_empresa_payload(1) + + full_payload = { + "fila": "empresa", + "metodo": "post", + "cliente": "tray", + "url_base": "https://api.sandbox.plugnotas.com.br", + "config": config_rpc, + "dados": payload_dados + } + + print(f"Enviando empresa {payload_dados['cpfCnpj']} via RPC...") + exchange.setPayload(json.dumps(full_payload)) + resposta = exchange.callRPC(json.dumps(full_payload), timeout=30) + + print("\nRESPOSTA DO SERVIDOR:") + print(resposta) + print("--------------------------------------\n") + +def run_mass_test(exchange, config_rpc, total=1000): + """Envia N empresas via Fila (sem esperar resposta), ideal para teste de carga""" + print(f"\n--- INICIANDO CARGA MASSIVA: {total} Mensagens ---") + start_time = time.time() + + for i in range(1, total + 1): + payload_dados = get_empresa_payload(i) + + full_payload = { + "fila": "empresa", + "metodo": "post", + "cliente": "MassTest", + "url_base": "https://api.sandbox.plugnotas.com.br", + "config": config_rpc, + "dados": payload_dados + } + + exchange.setPayload(json.dumps(full_payload)) + exchange.sendMsg(json.dumps(full_payload), service="tecnospeed") + + if i % 100 == 0: + print(f"Enviadas: {i}/{total}...") + + end_time = time.time() + print(f"Carga concluída em {end_time - start_time:.2f} segundos.") + print("--------------------------------------\n") + +def main(): + # 1. Carrega configurações do Redis + redis_host = os.getenv('REDIS_HOST', 'localhost') + redis_client = RedisClient(host=redis_host, port=6379) + + try: + config_data = redis_client.get("system_config") + if not config_data: + print("Erro: system_config não encontrado no Redis.") + return + sc = SystemConfig(**json.loads(config_data)) + except Exception as e: + print(f"Erro ao carregar configs: {e}") + return + + # Configurações de Banco e API para o Worker + config_rpc = { + "x_api_key": "2da392a6-79d2-4304-a8b7-959572c7e44d", + "user_postgres": "tecnospeed", + "pass_postgres": "u7QimG7E%21SF%2F%2FJC2c8", + "host_postgres": "banco", + "port_postgres": "5432", + "database_postgres": "tecnospeed" + } + + print(f"Conectando ao RabbitMQ de {sc.origin}...") + + # 2. Escolha do Modo de Operação + # Mude para True para testar o retorno (RPC Server) ou False para carga assíncrona (Queue Worker) + MODO_DEBUG_RPC = True + + if MODO_DEBUG_RPC: + # Usa RPCExchange2 para modo Síncrono (Request-Response) + exchange = RPCExchange2(sc.amqps, sc.origin, "integracao", "tecnospeed", "2.1.0", client="") + servicekey = sc.servicekey.ljust(32, '0')[:32] + exchange.setCrypto(sc.serviceid, servicekey) + run_rpc_test(exchange, config_rpc) + else: + # Usa QueueExchange2 para modo Assíncrono (Fire and Forget) + exchange = QueueExchange2(sc.amqps, sc.origin, "integracao", "tecnospeed", "2.1.0", client="#") + servicekey = sc.servicekey.ljust(32, '0')[:32] + exchange.setCrypto(sc.serviceid, servicekey) + run_mass_test(exchange, config_rpc, total=1000) + +if __name__ == "__main__": + main() \ No newline at end of file diff --git a/endpoint_listener/src/tests/test_1000_nfe.py b/endpoint_listener/src/tests/test_1000_nfe.py new file mode 100644 index 0000000..feac5a8 --- /dev/null +++ b/endpoint_listener/src/tests/test_1000_nfe.py @@ -0,0 +1,219 @@ +import sys +import os +import json +import time + +# Adiciona o diretório atual ao path para importar as classes locais +sys.path.append(os.path.dirname(os.path.abspath(__file__))) + +from Models.config import SystemConfig +from utils.redis import RedisClient +from utils.Exchange2 import QueueExchange2 +from utils.banco import Banco, EnterpriseTable +import random + +def main(): + print("Iniciando script de teste de carga: 1000 NFes...", flush=True) + + # 1. Carregando configuração do Redis para obter dados do RabbitMQ + redis_host = os.getenv('REDIS_HOST', 'localhost') + redis_client = RedisClient(host=redis_host, port=6379) + try: + system_config_data = redis_client.get("system_config") + if not system_config_data: + print("Erro: system_config não encontrado no Redis.") + return + + system_config = SystemConfig(**json.loads(system_config_data)) + print(f"Configuração [{system_config.origin}] carregada.", flush=True) + except Exception as e: + print(f"Erro ao ler configs do Redis: {e}") + return + + # 2. Inicializando a conexão com a FILA (RabbitMQ) + try: + print("Tentando conectar ao RabbitMQ...", flush=True) + exchange = QueueExchange2( + amqps=system_config.amqps, + origin=system_config.origin, + system="integracao", + service="tecnospeed", + version="2.1.0", + client="#" + ) + except Exception as e: + print(f"Falha ao iniciar mensageria: {e}") + return + + # 3. Configurando criptografia (necessário para o worker aceitar a mensagem) + servicekey = system_config.servicekey.ljust(32, '0')[:32] + exchange.setCrypto(serviceid=system_config.serviceid, servicekey=servicekey) + + # 4. Buscando empresas válidas no banco para o teste + try: + # Configuração do banco - Força conexão interna se estiver no Docker + # O .env às vezes aponta para o IP externo que não resolve dentro do container + db_host = os.getenv('POSTGRES_HOST', 'postgres') + db_port = int(os.getenv('POSTGRES_PORT', 5432)) + + # Se estivermos DENTRO do docker, o host 'postgres' é o correto. + if db_host != 'postgres': + print(f"Aviso: POSTGRES_HOST no ambiente é {db_host}. Tentando 'postgres' primeiro por estar no Docker...") + db_host = 'postgres' + db_port = 5432 + + print(f"Conectando ao banco em {db_host}:{db_port}...") + + banco = Banco( + host=db_host, + port=db_port, + user=os.getenv('POSTGRES_USER', 'tecnospeed'), + password=os.getenv('POSTGRES_PASSWORD', 'u7QimG7E!SF//JC2c8'), + database=os.getenv('POSTGRES_DB', 'tecnospeed') + ) + print("Executando query na tabela Enterprise...", flush=True) + empresas = banco.session.query(EnterpriseTable.id, EnterpriseTable.cnpj).filter(EnterpriseTable.cnpj != None, EnterpriseTable.cnpj != "").limit(100).all() + lista_empresas = [{"id": emp.id, "cnpj": emp.cnpj} for emp in empresas] + if not lista_empresas: + print("Erro: Nenhuma empresa com CNPJ válido encontrada na tabela 'Enterprise'.", flush=True) + return + print(f"Sucesso: {len(lista_empresas)} empresas válidas encontradas para o teste.", flush=True) + except Exception as e: + print(f"Erro ao conectar ao banco de dados: {e}") + return + + # Base do payload fornecido pelo usuário (omitindo emitente fixo para preencher no loop) + payload_base = { + "nfe": { + "idIntegracao": "", # Será preenchido no loop + "presencial": True, + "consumidorFinal": True, + "natureza": "OPERAÇÃO INTERNA", + "destinatario": { + "cpfCnpj": "08114280956", + "razaoSocial": "NF-E EMITIDA EM HOMOLOGACAO", + "email": "contato@tecnospeed.com.br", + "endereco": { + "logradouro": "AVENIDA DUQUE DE CAXIAS", + "numero": "882", + "bairro": "CENTRO", + "codigoCidade": "4115200", + "descricaoCidade": "MARINGA", + "estado": "PR", + "cep": "87020025" + } + }, + "itens": [ + { + "codigo": "1", + "descricao": "PRODUTO DE TESTE", + "ncm": "06029090", + "cest": "0123456", + "cfop": "5101", + "valorUnitario": { + "comercial": 4.67, + "tributavel": 4.67 + }, + "valor": 4.67, + "tributos": { + "icms": { + "origem": "0", + "cst": "00", + "baseCalculo": { + "modalidadeDeterminacao": 0, + "valor": 0 + }, + "aliquota": 0, + "valor": 0 + }, + "pis": { + "cst": "99", + "baseCalculo": { + "valor": 0, + "quantidade": 0 + }, + "aliquota": 0, + "valor": 0 + }, + "cofins": { + "cst": "07", + "baseCalculo": { + "valor": 0 + }, + "aliquota": 0, + "valor": 0 + } + } + } + ], + "pagamentos": [ + { + "aVista": True, + "meio": "01", + "valor": 4.6 + } + ], + "responsavelTecnico": { + "cpfCnpj": "08187168000160", + "nome": "Tecnospeed", + "email": "contato@tecnospeed.com.br", + "telefone": { + "ddd": "44", + "numero": "30379500" + } + } + } + } + + # Estrutura completa exigida pelo mainQUEUE.py + def get_full_payload(id_num, id_empresa, cnpj_empresa): + # Gerando um timestamp para garantir idIntegracao único + ts = int(time.time()) + payload = json.loads(json.dumps(payload_base)) # Deep copy simples + payload["nfe"]["idIntegracao"] = f"TESTE_{ts}_{id_num}" + payload["nfe"]["emitente"] = { + "cpfCnpj": cnpj_empresa, + "inscricaoEstadual": "ISENTO" + } + + return { + "fila": "nfe", + "cliente": "LoadTest1000", + "metodo": "POST", + "dados": { + "id_empresa": id_empresa, + **payload + }, + "config": { + "x_api_key": "2da392a6-79d2-4304-a8b7-959572c7e44d", + "user_postgres": "tecnospeed", + "pass_postgres": "u7QimG7E!SF//JC2c8", + "host_postgres": "postgres", + "port_postgres": "5432", + "database_postgres": "tecnospeed" + }, + "url_base": "https://api.sandbox.plugnotas.com.br" + } + + print(f"Enviando 1000 mensagens para a fila...") + + start_time = time.time() + + for i in range(1, 1001): + empresa_sorteada = random.choice(lista_empresas) + full_payload = get_full_payload(i, id_empresa=empresa_sorteada["id"], cnpj_empresa=empresa_sorteada["cnpj"]) + # É necessário criptografar o payload antes de cada envio + exchange.setPayload(json.dumps(full_payload)) + exchange.sendMsg(json.dumps(full_payload), service="tecnospeed") + + if i % 100 == 0: + print(f"Enviadas: {i}/1000...", flush=True) + + end_time = time.time() + + print("\nConcluído!") + print(f"Tempo total de envio: {end_time - start_time:.2f} segundos.") + print("Agora você pode acompanhar o processamento nos logs do worker (tecnospeed-queue).") + +if __name__ == "__main__": + main() diff --git a/endpoint_listener/src/tests/test_1000_nfse.py b/endpoint_listener/src/tests/test_1000_nfse.py new file mode 100644 index 0000000..62b6a41 --- /dev/null +++ b/endpoint_listener/src/tests/test_1000_nfse.py @@ -0,0 +1,173 @@ +import sys +import os +import json +import time +import random + +# Adiciona o diretório atual ao path para importar as classes locais +sys.path.append(os.path.dirname(os.path.abspath(__file__))) + +# Adiciona o diretório src ao path para encontrar os Models e Utils +sys.path.append(os.path.dirname(os.path.dirname(os.path.abspath(__file__)))) + +from Models.config import SystemConfig +from utils.redis import RedisClient +from utils.Exchange2 import QueueExchange2 +from utils.banco import Banco, EnterpriseTable + +def main(): + print("Iniciando script de teste de carga: 1000 NFSe...", flush=True) + + # 1. Carregando configuração do Redis para obter dados do RabbitMQ + redis_host = os.getenv('REDIS_HOST', 'localhost') + redis_client = RedisClient(host=redis_host, port=6379) + try: + system_config_data = redis_client.get("system_config") + if not system_config_data: + print("Erro: system_config não encontrado no Redis.") + return + + system_config = SystemConfig(**json.loads(system_config_data)) + print(f"Configuração [{system_config.origin}] carregada.", flush=True) + except Exception as e: + print(f"Erro ao ler configs do Redis: {e}") + return + + # 2. Inicializando a conexão com a FILA (RabbitMQ) + try: + print("Tentando conectar ao RabbitMQ...", flush=True) + exchange = QueueExchange2( + amqps=system_config.amqps, + origin=system_config.origin, + system="integracao", + service="tecnospeed", + version="2.1.0", + client="#" + ) + except Exception as e: + print(f"Falha ao iniciar mensageria: {e}") + return + + # 3. Configurando criptografia (necessário para o worker aceitar a mensagem) + servicekey = system_config.servicekey.ljust(32, '0')[:32] + exchange.setCrypto(serviceid=system_config.serviceid, servicekey=servicekey) + + # 4. Buscando empresas válidas no banco para o teste + try: + db_host = os.getenv('POSTGRES_HOST', 'banco') + db_port = int(os.getenv('POSTGRES_PORT', 5432)) + + # Se estivermos DENTRO do docker e o host configurado for o IP externo ou porta mapeada, forçamos 'banco' e '5432' + if os.path.exists('/.dockerenv') or os.path.exists('/proc/1/cgroup'): + print(f"Aviso: Ambiente Docker detectado. Forçando host 'banco' e porta 5432.") + db_host = 'banco' + db_port = 5432 + + print(f"Conectando ao banco em {db_host}:{db_port}...") + + banco = Banco( + host=db_host, + port=db_port, + user=os.getenv('POSTGRES_USER', 'tecnospeed'), + password=os.getenv('POSTGRES_PASSWORD', 'u7QimG7E!SF//JC2c8'), + database=os.getenv('POSTGRES_DB', 'tecnospeed') + ) + print("Executando query na tabela Enterprise...", flush=True) + empresas = banco.session.query(EnterpriseTable.id, EnterpriseTable.cnpj).filter(EnterpriseTable.cnpj != None, EnterpriseTable.cnpj != "").limit(100).all() + lista_empresas = [{"id": emp.id, "cnpj": emp.cnpj} for emp in empresas] + if not lista_empresas: + print("Erro: Nenhuma empresa com CNPJ válido encontrada na tabela 'Enterprise'.", flush=True) + return + print(f"Sucesso: {len(lista_empresas)} empresas válidas encontradas para o teste.", flush=True) + except Exception as e: + print(f"Erro ao conectar ao banco de dados: {e}") + return + + # Base do payload de NFSe + payload_base = { + "nfse": { + "idIntegracao": "", # Será preenchido no loop + "prestador": { + "cpfCnpj": "" # Será preenchido no loop + }, + "tomador": { + "cpfCnpj": "08114280956", + "razaoSocial": "NFSE EMITIDA EM HOMOLOGACAO", + "email": "contato@tecnospeed.com.br", + "endereco": { + "logradouro": "AVENIDA DUQUE DE CAXIAS", + "numero": "882", + "bairro": "CENTRO", + "codigoCidade": "4115200", + "descricaoCidade": "MARINGA", + "estado": "PR", + "cep": "87020025", + "tipoLogradouro": "Avenida" + } + }, + "servico": { + "codigo": "1.01", + "codigoTributacao": "1.01", + "discriminacao": "SERVICO DE TESTE DE CARGA MASSIVA", + "cnae": "6201501", + "iss": { + "tipoTributacao": 1, + "exigibilidade": 1, + "aliquota": 2.0 + }, + "valor": { + "servico": 100.0 + } + } + } + } + + # Estrutura completa exigida pelo mainQUEUE.py + def get_full_payload(id_num, id_empresa, cnpj_empresa): + ts = int(time.time() * 1000) + payload = json.loads(json.dumps(payload_base)) # Deep copy + payload["nfse"]["idIntegracao"] = f"LOAD_NFSE_{ts}_{id_num}" + payload["nfse"]["prestador"]["cpfCnpj"] = cnpj_empresa + + return { + "fila": "nfse", + "cliente": "MassiveNFSeTest", + "metodo": "POST", + "dados": { + "id_empresa": id_empresa, + "id_usuario": 1, + **payload["nfse"] + }, + "config": { + "x_api_key": "2da392a6-79d2-4304-a8b7-959572c7e44d", + "user_postgres": "tecnospeed", + "pass_postgres": "u7QimG7E!SF//JC2c8", + "host_postgres": "banco", + "port_postgres": "5432", + "database_postgres": "tecnospeed" + }, + "url_base": "https://api.sandbox.plugnotas.com.br" + } + + print(f"Enviando 1000 NFSe para a fila...") + + start_time = time.time() + + for i in range(1, 1001): + empresa_sorteada = random.choice(lista_empresas) + full_payload = get_full_payload(i, id_empresa=empresa_sorteada["id"], cnpj_empresa=empresa_sorteada["cnpj"]) + + exchange.setPayload(json.dumps(full_payload)) + exchange.sendMsg(json.dumps(full_payload), service="tecnospeed") + + if i % 100 == 0: + print(f"Enviadas: {i}/1000...", flush=True) + + end_time = time.time() + + print("\nConcluído!") + print(f"Tempo total de envio: {end_time - start_time:.2f} segundos.") + print("Acompanhe o processamento nos logs do worker (tecnospeed-queue).") + +if __name__ == "__main__": + main() diff --git a/endpoint_listener/src/tests/test_api.py b/endpoint_listener/src/tests/test_api.py new file mode 100644 index 0000000..820ebef --- /dev/null +++ b/endpoint_listener/src/tests/test_api.py @@ -0,0 +1,29 @@ +import json +import sys +import os + +sys.path.append(os.path.dirname(os.path.abspath(__file__))) +from utils.api import Api + +api = Api("2da392a6-79d2-4304-a8b7-959572c7e44d") + +print("\n--- Consultando Certificados ---") +# GET /certificado (Lista todos) +certificados = api.get("https://api.sandbox.plugnotas.com.br/certificado") +print("GET /certificado response:") +print(json.dumps(certificados, indent=2)) + +# Se houver certificados, faz um GET específico no primeiro +if isinstance(certificados, list) and len(certificados) > 0: + first_cert_id = certificados[0].get('id') + print(f"\nConsultando certificado específico (ID: {first_cert_id}):") + spec_cert = api.get(f"https://api.sandbox.plugnotas.com.br/certificado/{first_cert_id}") + print(json.dumps(spec_cert, indent=2)) +elif isinstance(certificados, dict) and "data" in certificados: + # Caso a resposta venha com envelope "data" + data_list = certificados["data"] + if data_list: + first_cert_id = data_list[0].get('id') + print(f"\nConsultando certificado específico (ID: {first_cert_id}):") + spec_cert = api.get(f"https://api.sandbox.plugnotas.com.br/certificado/{first_cert_id}") + print(json.dumps(spec_cert, indent=2)) diff --git a/endpoint_listener/src/tests/test_callback.py b/endpoint_listener/src/tests/test_callback.py new file mode 100644 index 0000000..82531bd --- /dev/null +++ b/endpoint_listener/src/tests/test_callback.py @@ -0,0 +1,96 @@ +import json +import pytest +from unittest.mock import patch, MagicMock + + +class TestRoteamento: + """Verifica que todas as filas registradas no dict existem e são callable.""" + + def test_todas_filas_registradas(self, main_instance): + filas_esperadas = ["nfe", "nfse", "certificado_digital", "empresa", + "servico", "tomador", "relatorios", "webhooks"] + for fila in filas_esperadas: + assert fila in main_instance.funcoes, f"Fila '{fila}' não está registrada" + assert callable(main_instance.funcoes[fila]), f"Fila '{fila}' não é callable" + + def test_fila_inexistente_nao_registrada(self, main_instance): + assert "webhook_callback" not in main_instance.funcoes + assert "fila_fantasma" not in main_instance.funcoes + + +class TestCallbackErros: + """Testa que o callback lida corretamente com payloads inválidos.""" + + def test_callback_json_invalido(self, main_instance): + """Payload que não é JSON válido deve retornar erro.""" + resultado = json.loads(main_instance.callback("isso não é json")) + assert resultado["status"] == "error" + + def test_callback_sem_hmac(self, main_instance): + """Payload sem HMAC deve retornar erro.""" + resultado = json.loads(main_instance.callback(json.dumps({"dados": "algo"}))) + assert resultado["status"] == "error" + + def test_callback_com_hmac_e_payload(self, main_instance, mock_exchange): + """Payload com hmac+payload mas sem crypto configurada deve retornar erro.""" + mock_exchange.getPayload.return_value = False + + main_instance.system_config = MagicMock() + main_instance.system_config.servicekey = "testkey1234567890" + main_instance.system_config.serviceid = "test-service" + + resultado = json.loads(main_instance.callback(json.dumps({ + "hmac": "fake-hmac", + "payload": "fake-encrypted-payload" + }))) + + assert resultado["status"] == "error" + + +class TestBancoIntegracao: + """Testa que os logs são gravados no banco durante as operações.""" + + def test_log_gravado_no_banco(self, main_instance, banco_teste, mock_api): + """Ao executar uma operação, deve gravar log no banco.""" + mock_api.get.return_value = {"id": "PLUG-NFE-001", "status": "CONCLUIDO"} + + with patch("NFe.registroNFe.Api", return_value=mock_api): + main_instance.nfe("get", {"id": "PLUG-NFE-001", "id_usuario": 42}) + + from utils.banco import LogTable + logs = banco_teste.session.query(LogTable).all() + + # Deve ter pelo menos 2 logs: INICIO + SUCESSO + assert len(logs) >= 2 + acoes = [log.acao for log in logs] + assert "GET" in acoes + + # Verifica que o id_usuario foi gravado + assert any(log.id_usuario == 42 for log in logs) + + def test_log_erro_gravado(self, main_instance, banco_teste, mock_api): + """Erros também devem ser gravados no banco.""" + mock_api.get.side_effect = Exception("Timeout na API") + + with patch("NFe.registroNFe.Api", return_value=mock_api): + with pytest.raises(Exception): + main_instance.nfe("get", {"id": "PLUG-NFE-001", "id_usuario": 7}) + + from utils.banco import LogTable + logs = banco_teste.session.query(LogTable).filter_by(status="ERRO").all() + assert len(logs) >= 1 + + +class TestExchangePayload: + """Testa que o exchange.setPayload é chamado com o resultado correto.""" + + def test_payload_set_no_sucesso(self, main_instance, mock_api, mock_exchange): + """Ao concluir com sucesso, setPayload deve ser chamado com o resultado.""" + mock_api.get.return_value = {"id": "PLUG-NFE-001", "status": "CONCLUIDO"} + + with patch("NFe.registroNFe.Api", return_value=mock_api): + main_instance.nfe("get", {"id": "PLUG-NFE-001", "id_usuario": 1}) + + assert mock_exchange.setPayload.called + payload_enviado = json.loads(mock_exchange.payloads_set[-1]) + assert payload_enviado["status"] == "CONCLUIDO" diff --git a/endpoint_listener/src/tests/test_nfe.py b/endpoint_listener/src/tests/test_nfe.py new file mode 100644 index 0000000..771402d --- /dev/null +++ b/endpoint_listener/src/tests/test_nfe.py @@ -0,0 +1,179 @@ +import json +import pytest +from unittest.mock import patch, MagicMock + +class TestNFePost: + + @pytest.fixture + def dados_nfe(self): + return { + "id_usuario": 1, + "id_empresa": 10, + "nfe": { + "idIntegracao": "INT-NFE-001", + "presencial": True, + "consumidorFinal": True, + "natureza": "Venda de mercadoria", + "emitente": {"cpfCnpj": "08187168000160"}, + "destinatario": { + "cpfCnpj": "08114280956", + "razaoSocial": "Destinatário Teste", + "endereco": { + "logradouro": "Rua Teste", + "numero": "100", + "bairro": "Centro", + "codigoCidade": "5201405", + "descricaoCidade": "GOIANIA", + "estado": "GO", + "cep": "74000000" + } + }, + "itens": [{ + "codigo": "001", + "descricao": "Produto Teste", + "ncm": "61091000", + "cfop": "5102", + "valorUnitario": {"comercial": 100.0, "tributavel": 100.0}, + "valor": 100.0, + "tributos": { + "icms": { + "origem": "0", + "cst": "00", + "baseCalculo": {"modalidadeDeterminacao": 0, "valor": 100.0}, + "aliquota": 18.0, + "valor": 18.0 + }, + "pis": { + "cst": "01", + "baseCalculo": {"valor": 100.0, "quantidade": 1}, + "aliquota": 1.65, + "valor": 1.65 + }, + "cofins": { + "cst": "01", + "baseCalculo": {"valor": 100.0}, + "aliquota": 7.6, + "valor": 7.6 + } + } + }], + "pagamentos": [{"aVista": True, "meio": "01", "valor": 100.0}] + } + } + + def test_post_nfe_sucesso(self, main_instance, mock_api, dados_nfe): + mock_api.post.return_value = { + "documents": [{"id": "PLUG-NFE-001"}] + } + + with patch("NFe.registroNFe.Api", return_value=mock_api): + resultado = json.loads(main_instance.nfe("post", dados_nfe)) + + assert resultado["status"] == "ok" + assert resultado["data"]["documents"][0]["id"] == "PLUG-NFE-001" + + def test_post_nfe_erro_api(self, main_instance, mock_api, dados_nfe): + mock_api.post.return_value = {"error": "CPF/CNPJ inválido"} + + with patch("NFe.registroNFe.Api", return_value=mock_api): + with pytest.raises(Exception, match="CPF/CNPJ inválido"): + main_instance.nfe("post", dados_nfe) + +class TestNFeGet: + + def test_get_nfe_resumo(self, main_instance, mock_api): + mock_api.get.return_value = { + "id": "PLUG-NFE-001", + "status": "CONCLUIDO", + "xml": "https://..." + } + + with patch("NFe.registroNFe.Api", return_value=mock_api): + resultado = json.loads(main_instance.nfe("get", {"id": "PLUG-NFE-001", "id_usuario": 1})) + + assert resultado["status"] == "ok" + assert resultado["data"]["status"] == "CONCLUIDO" + +class TestNFeDownload: + """Testa download de PDF e XML de NFe.""" + + def test_download_pdf(self, main_instance, mock_api): + mock_api.get.return_value = {"url": "https://cdn.plugnotas.com.br/nfe/pdf/123"} + + with patch("NFe.registroNFe.Api", return_value=mock_api): + resultado = json.loads(main_instance.nfe("download", {"id": "PLUG-NFE-001", "id_usuario": 1})) + + assert resultado["status"] == "ok" + + def test_download_xml(self, main_instance, mock_api): + mock_api.get.return_value = {"url": "https://cdn.plugnotas.com.br/nfe/xml/123"} + + with patch("NFe.registroNFe.Api", return_value=mock_api): + resultado = json.loads(main_instance.nfe("download_xml", {"id": "PLUG-NFE-001", "id_usuario": 1})) + + assert resultado["status"] == "ok" + +class TestNFeCancelar: + + def test_cancelar_nfe_sucesso(self, main_instance, mock_api): + mock_api.post.return_value = {"id": "PLUG-NFE-001", "status": "CANCELAMENTO_SOLICITADO"} + + with patch("NFe.registroNFe.Api", return_value=mock_api): + resultado = json.loads(main_instance.nfe("cancelar", { + "id": "PLUG-NFE-001", + "justificativa": "Erro de emissão", + "id_usuario": 1 + })) + + assert resultado["status"] == "ok" + + def test_status_cancelamento(self, main_instance, mock_api): + mock_api.get.return_value = {"id": "PLUG-NFE-001", "status": "CANCELADO"} + + with patch("NFe.registroNFe.Api", return_value=mock_api): + resultado = json.loads(main_instance.nfe("status_cancelamento", {"id": "PLUG-NFE-001", "id_usuario": 1})) + + assert resultado["status"] == "ok" + +class TestNFeCCe: + + def test_enviar_cce(self, main_instance, mock_api): + mock_api.post.return_value = {"id": "PLUG-NFE-001", "status": "CCE_ENVIADA"} + + with patch("NFe.registroNFe.Api", return_value=mock_api): + resultado = json.loads(main_instance.nfe("cce", { + "id": "PLUG-NFE-001", + "correcao": "Corrigir endereço do destinatário", + "id_usuario": 1 + })) + + assert resultado["status"] == "ok" + + def test_status_cce(self, main_instance, mock_api): + mock_api.get.return_value = {"id": "PLUG-NFE-001", "status": "CCE_AUTORIZADA"} + + with patch("NFe.registroNFe.Api", return_value=mock_api): + resultado = json.loads(main_instance.nfe("status_cce", {"id": "PLUG-NFE-001", "id_usuario": 1})) + + assert resultado["status"] == "ok" + +class TestNFeResumo: + + def test_resumo_por_cnpj(self, main_instance, mock_api): + mock_api.get.return_value = {"id": "PLUG-NFE-001", "chave": "3524..."} + + with patch("NFe.registroNFe.Api", return_value=mock_api): + resultado = json.loads(main_instance.nfe("resumo", { + "cnpj": "08187168000160", + "id_integracao": "INT-001", + "id_usuario": 1 + })) + + assert resultado["status"] == "ok" + +class TestNFeInvalido: + + def test_metodo_invalido(self, main_instance): + resultado = json.loads(main_instance.nfe("inexistente", {"id_usuario": 1})) + assert resultado["status"] == "error" + assert "não encontrado" in resultado["message"].lower() diff --git a/endpoint_listener/src/tests/test_nfse.py b/endpoint_listener/src/tests/test_nfse.py new file mode 100644 index 0000000..be1045a --- /dev/null +++ b/endpoint_listener/src/tests/test_nfse.py @@ -0,0 +1,146 @@ +""" +Testes E2E para o serviço NFSe. +Testa: POST, GET, DOWNLOAD, DOWNLOAD_XML, CANCELAR, +STATUS_CANCELAMENTO, RESUMO, HISTORICO. +""" +import json +import pytest +from unittest.mock import patch, MagicMock + + +@pytest.fixture +def dados_nfse(): + return { + "id_usuario": 1, + "id_empresa": 10, + "idIntegracao": "INT-NFSE-001", + "prestador": {"cpfCnpj": "08187168000160"}, + "tomador": { + "cpfCnpj": "08114280956", + "razaoSocial": "Tomador Teste", + "inscricaoMunicipal": "isento", + "email": "tomador@teste.com", + "endereco": { + "descricaoCidade": "São Paulo", + "cep": "01000000", + "tipoLogradouro": "Rua", + "logradouro": "Teste", + "tipoBairro": "Bairro", + "codigoCidade": "3550308", + "complemento": "", + "estado": "SP", + "numero": "100", + "bairro": "Centro" + } + }, + "servico": { + "codigo": "1.01", + "codigoTributacao": "1.01", + "discriminacao": "Serviços de tecnologia", + "cnae": "6204000", + "iss": {"tipoTributacao": 6, "exigibilidade": 1, "aliquota": 2.5}, + "valor": {"servico": 1500.0, "descontoCondicionado": 0, "descontoIncondicionado": 0} + } + } + + +class TestNFSePost: + def test_post_nfse_sucesso(self, main_instance, mock_api, dados_nfse): + mock_api.post.return_value = {"documents": [{"id": "PLUG-NFSE-001"}]} + + with patch("NFSe.nfse.Api", return_value=mock_api): + resultado = json.loads(main_instance.nfse("post", dados_nfse)) + + assert resultado["status"] == "ok" + assert resultado["data"]["documents"][0]["id"] == "PLUG-NFSE-001" + + def test_post_nfse_erro_api(self, main_instance, mock_api, dados_nfse): + mock_api.post.return_value = {"error": "CNPJ do prestador não cadastrado"} + + with patch("NFSe.nfse.Api", return_value=mock_api): + with pytest.raises(Exception): + main_instance.nfse("post", dados_nfse) + + +class TestNFSeGet: + def test_get_nfse_por_id(self, main_instance, mock_api): + mock_api.get.return_value = {"id": "PLUG-NFSE-001", "status": "CONCLUIDO"} + + with patch("NFSe.nfse.Api", return_value=mock_api): + resultado = json.loads(main_instance.nfse("get", {"id": "PLUG-NFSE-001", "id_usuario": 1})) + + assert resultado["status"] == "ok" + + def test_listar_nfse(self, main_instance, banco_teste): + """Listar NFSe sem ID deve buscar do banco.""" + resultado = json.loads(main_instance.nfse("get", {"id_usuario": 1})) + assert resultado["status"] == "ok" + + +class TestNFSeDownload: + def test_download_pdf(self, main_instance, mock_api): + mock_api.get.return_value = {"pdf_url": "https://..."} + + with patch("NFSe.nfse.Api", return_value=mock_api): + resultado = json.loads(main_instance.nfse("download", {"id": "PLUG-NFSE-001", "id_usuario": 1})) + + assert resultado["status"] == "ok" + + def test_download_xml(self, main_instance, mock_api): + mock_api.get.return_value = {"xml_url": "https://..."} + + with patch("NFSe.nfse.Api", return_value=mock_api): + resultado = json.loads(main_instance.nfse("download_xml", {"id": "PLUG-NFSE-001", "id_usuario": 1})) + + assert resultado["status"] == "ok" + + +class TestNFSeCancelar: + def test_cancelar_nfse(self, main_instance, mock_api): + mock_api.post.return_value = {"status": "CANCELAMENTO_SOLICITADO"} + + with patch("NFSe.nfse.Api", return_value=mock_api): + resultado = json.loads(main_instance.nfse("cancelar", {"id": "PLUG-NFSE-001", "id_usuario": 1})) + + assert resultado["status"] == "ok" + + def test_status_cancelamento(self, main_instance, mock_api): + mock_api.get.return_value = {"status": "CANCELADO"} + + with patch("NFSe.nfse.Api", return_value=mock_api): + resultado = json.loads(main_instance.nfse("status_cancelamento", {"id": "PLUG-NFSE-001", "id_usuario": 1})) + + assert resultado["status"] == "ok" + + +class TestNFSeResumo: + def test_consultar_resumo(self, main_instance, mock_api): + mock_api.get.return_value = {"id": "PLUG-NFSE-001", "numero": "12345"} + + with patch("NFSe.nfse.Api", return_value=mock_api): + resultado = json.loads(main_instance.nfse("resumo", { + "id": "PLUG-NFSE-001", + "cnpj": "08187168000160", + "id_usuario": 1 + })) + + assert resultado["status"] == "ok" + + +class TestNFSeHistorico: + def test_historico_nfse(self, main_instance, mock_api): + mock_api.get.return_value = [{"data": "2026-01-01", "status": "CONCLUIDO"}] + + with patch("NFSe.nfse.Api", return_value=mock_api): + resultado = json.loads(main_instance.nfse("historico", { + "id": "PLUG-NFSE-001", + "id_usuario": 1 + })) + + assert resultado["status"] == "ok" + + +class TestNFSeInvalido: + def test_metodo_invalido(self, main_instance): + resultado = json.loads(main_instance.nfse("inexistente", {"id_usuario": 1})) + assert resultado["status"] == "error" diff --git a/endpoint_listener/src/tests/test_queue.py b/endpoint_listener/src/tests/test_queue.py new file mode 100644 index 0000000..9bc6adb --- /dev/null +++ b/endpoint_listener/src/tests/test_queue.py @@ -0,0 +1,164 @@ +import sys +import os +import json +import time + +sys.path.append(os.path.dirname(os.path.abspath(__file__))) + +from Models.config import SystemConfig +from utils.redis import RedisClient +from utils.Exchange2 import QueueExchange2 + +def main(): + print("Iniciando script de testes da FILA (Queue Exchange)...") + + # Carregando configuração do Redis + redis_host = os.getenv('REDIS_HOST', 'localhost') + redis_client = RedisClient(host=redis_host, port=6379) + try: + system_config_data = redis_client.get("system_config") + if not system_config_data: + print("Erro: system_config não encontrado no Redis.") + return + + system_config = SystemConfig(**json.loads(system_config_data)) + print(f"[{system_config.origin}] Configuração carregada.") + except Exception as e: + print(f"Erro ao ler configs: {e}") + return + + # Inicializando mensageria de FILA + try: + # Nota: O QueueExchange2 envia para 'system_service_client' + exchange = QueueExchange2( + amqps=system_config.amqps, + origin=system_config.origin, + system="integracao", + service="tecnospeed", + version="2.1.0", + client="#" # '#' para bater com a fila padrão do worker (evita erro de validação length) + ) + except Exception as e: + print(f"Falha ao iniciar mensageria: {e}") + return + + # Configurando criptografia + servicekey = system_config.servicekey.ljust(32, '0')[:32] + exchange.setCrypto(serviceid=system_config.serviceid, servicekey=servicekey) + + # Payload (mesmo do teste RPC) + payload_interno = { + "fila": "empresa", + "cliente": "TestQueue", + "metodo": "POST", + "dados": { + "endereco" : { + "codigoPais": "1058", + "descricaoPais": "Brasil", + "tipoLogradouro": "Rua", + "logradouro": "T55", + "numero": "882", + "complemento": "Quadra99 Lote 11/14 Sala 1507", + "tipoBairro": "Zona", + "bairro": "Setor Bueno", + "codigoCidade": "5208707", + "descricaoCidade": "Goiania", + "estado": "GO", + "cep": "74215170" + }, + "nfse" : { + "config" : { + "rps" : { + "lote" : 12, + "numeracao" : [ + { + "numero" : 2, + "serie" : "CTBZ", + "numeracaoAtual" : 2 + } + ] + }, + "email" : { + "envio" : False + }, + "producao" : True, + "envioLimitado" : False, + "integracoes" : [] + }, + "ativo" : True, + "tipoContrato" : 0 + }, + "nfe" : { + "config" : { + "dfe" : { + "primeiroNsu" : True + }, + "email" : { + "envio" : False + }, + "integracoes" : { + "plugStorage" : { + "ativo" : False + } + }, + "producao" : False, + "impressaoFcp" : False, + "impressaoPartilha" : True, + "versaoManual" : "6.0", + "versaoEsquema" : "pl_009", + "numeracao" : [] + }, + "ativo" : False + }, + "nfce" : { + "config" : { + "email" : { + "envio" : False + }, + "integracoes" : { + "plugStorage" : { + "ativo" : False + } + }, + "producao" : False, + "versaoManual" : "6.0", + "versaoEsquema" : "pl_009", + "numeracao" : [] + }, + "ativo" : False + }, + "incentivoFiscal" : False, + "incentivadorCultural" : False, + "cpfCnpj" : "14358405000100", + "inscricaoMunicipal" : "1085328365", + "certificado" : "668698592698d7831c758d52", + "razaoSocial" : "Teste da SIlva do teste", + "simplesNacional" : True, + "regimeTributario" : 1, + "regimeTributarioEspecial" : 6 + }, + "config": { + "x_api_key": "2da392a6-79d2-4304-a8b7-959572c7e44d", + "user_postgres": "tecnospeed", + "pass_postgres": "u7QimG7E!SF//JC2c8", + "host_postgres": "postgres", + "port_postgres": "5432", + "database_postgres": "tecnospeed" + }, + "url_base": "https://api.sandbox.plugnotas.com.br" + } + + # Criptografando e enviando + exchange.setPayload(json.dumps(payload_interno)) + + print("\n--- DEBUG ENVIO ---") + print(f"Alvo (targetQueue): integracao_tecnospeed") + print(f"Config Enviada: {payload_interno['config']['host_postgres']}:{payload_interno['config']['port_postgres']}") + print("-------------------\n") + + print("Disparando mensagem para a FILA...") + exchange.sendMsg(json.dumps(payload_interno), service="tecnospeed") + print("Mensagem enviada com sucesso. Verifique os logs do worker mainQUEUE.py") + +if __name__ == "__main__": + main() diff --git a/endpoint_listener/src/tests/test_servicos.py b/endpoint_listener/src/tests/test_servicos.py new file mode 100644 index 0000000..1228a2a --- /dev/null +++ b/endpoint_listener/src/tests/test_servicos.py @@ -0,0 +1,328 @@ +import json +import pytest +from unittest.mock import patch, MagicMock +from utils.banco import EmpresaTable, ServicoTable, TomadorTable + +class TestEmpresaPost: + @pytest.fixture + def dados_empresa(self): + return { + "id_usuario": 1, + "endereco": { + "codigoPais": "1058", "descricaoPais": "BRASIL", + "tipoLogradouro": "Rua", "logradouro": "Teste", "numero": "123", + "complemento": "", "tipoBairro": "Bairro", "bairro": "Centro", + "codigoCidade": "5201405", "descricaoCidade": "GOIANIA", + "estado": "GO", "cep": "74000000" + }, + "nfse": { + "config": {"rps": {"lote": 1, "numeracao": [1]}, "email": {"envio": True}, + "producao": False, "envioLimitado": False, "integracoes": []}, + "ativo": True, "tipoContrato": 0 + }, + "nfe": { + "config": {"dfe": {"primeiroNsu": False}, "email": {"envio": False}, + "integracoes": {}, "producao": False, "impressaoFcp": False, + "impressaoPartilha": False, "versaoManual": "6.0", + "versaoEsquema": "PL_009_V4", "numeracao": [1]}, + "ativo": False + }, + "nfce": { + "config": {"email": {"envio": False}, "integracoes": {}, "producao": False, + "versaoManual": "6.0", "versaoEsquema": "PL_009_V4", "numeracao": [1]}, + "ativo": False + }, + "incentivoFiscal": False, "incentivadorCultural": False, + "cpfCnpj": "17956143000174", "inscricaoMunicipal": "12345", + "certificado": "CERT-UUID", "razaoSocial": "Empresa Teste SA", + "simplesNacional": True, "regimeTributario": 1, "regimeTributarioEspecial": 0 + } + + def test_post_empresa_sucesso(self, main_instance, mock_api, dados_empresa): + mock_api.post.return_value = {"_id": "PLUG-EMP-001", "cpfCnpj": "17956143000174"} + + with patch("empresas.empresa.Api", return_value=mock_api): + resultado = json.loads(main_instance.empresa("post", dados_empresa)) + + assert resultado["status"] == "ok" + + def test_post_empresa_erro(self, main_instance, mock_api, dados_empresa): + mock_api.post.return_value = { + "error": "Erro", + "response": {"error": {"message": "CNPJ já cadastrado", "data": []}} + } + + with patch("empresas.empresa.Api", return_value=mock_api): + resultado = json.loads(main_instance.empresa("post", dados_empresa)) + + assert resultado["status"] == "error" + + +class TestEmpresaGet: + def test_get_empresa_por_cnpj(self, main_instance, mock_api): + mock_api.get.return_value = {"_id": "PLUG-EMP-001", "cpfCnpj": "17956143000174", "razaoSocial": "Teste SA"} + + with patch("empresas.empresa.Api", return_value=mock_api): + resultado = json.loads(main_instance.empresa("get", {"cnpj": "17956143000174", "id_usuario": 1})) + + assert resultado["status"] == "ok" + + def test_listar_empresas(self, main_instance, mock_api): + mock_api.get_paginated.return_value = {"data": [{"cpfCnpj": "111", "razaoSocial": "Emp A"}], "total": 1} + + with patch("empresas.empresa.Api", return_value=mock_api): + resultado = json.loads(main_instance.empresa("get", {"id_usuario": 1})) + + assert resultado["status"] == "ok" + + +class TestEmpresaInvalido: + def test_metodo_invalido(self, main_instance): + resultado = json.loads(main_instance.empresa("inexistente", {"id_usuario": 1})) + assert resultado["status"] == "error" + + +class TestCertificadoPost: + @pytest.fixture + def dados_certificado(self): + return { + "id_usuario": 1, + "id_empresa": 10, + "arquivo": "CONTEUDO_PFX_BASE64", + "senha": "12345678", + "expirationAt": "2027-12-31" + } + + def test_post_certificado_sucesso(self, main_instance, mock_api, dados_certificado): + mock_api.post_form_data.return_value = {"data": {"id": "CERT-001"}} + + with patch("certificado_digital.certificado_digital.Api", return_value=mock_api): + resultado = json.loads(main_instance.certificado_digital("post", dados_certificado)) + + assert resultado["status"] == "ok" + assert resultado["data"] == "CERT-001" + + +class TestCertificadoGet: + def test_get_certificado_por_id(self, main_instance, mock_api): + mock_api.get.return_value = {"id": "CERT-001", "status": "VALIDO"} + + with patch("certificado_digital.certificado_digital.Api", return_value=mock_api): + resultado = json.loads(main_instance.certificado_digital("get", {"id": "CERT-001", "id_usuario": 1})) + + assert resultado["status"] == "ok" + + def test_listar_certificados(self, main_instance, mock_api): + mock_api.get.return_value = [{"id": "CERT-001"}, {"id": "CERT-002"}] + + with patch("certificado_digital.certificado_digital.Api", return_value=mock_api): + resultado = json.loads(main_instance.certificado_digital("get", {"id_usuario": 1})) + + assert resultado["status"] == "ok" + + +class TestCertificadoInvalido: + def test_metodo_invalido(self, main_instance): + resultado = json.loads(main_instance.certificado_digital("inexistente", {"id_usuario": 1})) + assert resultado["status"] == "error" + +class TestServicoPost: + """Testa criação de Serviço na API + banco.""" + + @pytest.fixture + def dados_servico_completo(self): + """Payload completo do Model Servico com todos campos obrigatórios.""" + return { + "id_usuario": 1, + "codigo": "1.01", + "idIntegracao": "SERV-INT-001", + "discriminacao": "Consultoria em TI", + "codigoTributacao": "1.01", + "cnae": "6204000", + "codigoCidadeIncidencia": "3550308", + "descricaoCidadeIncidencia": "São Paulo", + "unidade": "UN", + "quantidade": 1, + "iss": { + "tipoTributacao": 6, "exigibilidade": 1, + "retido": False, "aliquota": 2.5, "valor": 37.5, + "valorRetido": 0, "processoSuspensao": "" + }, + "obra": {"art": "", "codigo": ""}, + "valor": { + "servico": 1500.0, "baseCalculo": 1500.0, "deducoes": 0, + "descontoCondicionado": 0, "descontoIncondicionado": 0, + "liquido": 1500.0, "unitario": 1500.0, "valorAproximadoTributos": 0 + }, + "deducao": {"tipo": 0, "descricao": ""}, + "retencao": { + "pis": {"aliquota": 0, "valor": 0}, + "cofins": {"aliquota": 0, "valor": 0}, + "csll": {"aliquota": 0, "valor": 0}, + "inss": {"aliquota": 0, "valor": 0}, + "irrf": {"aliquota": 0, "valor": 0}, + "outrasRetencoes": 0, + "cpp": {"aliquota": 0, "valor": 0} + }, + "tributavel": True, + "ibpt": { + "simplificado": {"aliquota": 8.65}, + "detalhado": {"aliquota": {"municipal": 2.5, "estadual": 0, "federal": 6.15}} + }, + "responsavelRetencao": "" + } + + def test_post_servico_sucesso(self, main_instance, mock_api, dados_servico_completo): + mock_api.post.return_value = {"data": {"id": "SERV-001"}} + + with patch("NFSe.Servico.servico.Api", return_value=mock_api): + resultado = json.loads(main_instance.servico("post", dados_servico_completo)) + + assert resultado["status"] == "ok" + + +class TestServicoGet: + def test_listar_servicos(self, main_instance, banco_teste): + """Deve retornar lista vazia quando não há serviços.""" + resultado = json.loads(main_instance.servico("get", {"id_usuario": 1})) + assert resultado["status"] == "ok" + + def test_buscar_servico_por_id(self, main_instance, mock_api): + mock_api.get.return_value = {"id": "SERV-001", "discriminacao": "Consultoria"} + + with patch("NFSe.Servico.servico.Api", return_value=mock_api): + resultado = json.loads(main_instance.servico("get", {"id": "SERV-001", "id_usuario": 1})) + + assert resultado["status"] == "ok" + + +class TestServicoInvalido: + def test_metodo_invalido(self, main_instance): + resultado = json.loads(main_instance.servico("inexistente", {"id_usuario": 1})) + assert resultado["status"] == "error" + +class TestTomadorPost: + @pytest.fixture + def dados_tomador(self): + return { + "id_usuario": 1, + "cpfCnpj": "08114280956", + "inscricaoMunicipal": "isento", + "inscricaoEstadual": "", + "inscricaoSuframa": "", + "indicadorInscricaoEstadual": 9, + "razaoSocial": "Tomador Teste LTDA", + "nomeFantasia": "Tomador Teste", + "endereco": { + "tipoLogradouro": "Rua", "logradouro": "Teste", "numero": "100", + "complemento": "", "tipoBairro": "Bairro", "bairro": "Centro", + "codigoPais": "1058", "descricaoPais": "BRASIL", + "codigoCidade": "3550308", "descricaoCidade": "São Paulo", + "estado": "SP", "cep": "01000000" + }, + "telefone": {"ddd": "11", "numero": "999999999"}, + "email": "tomador@teste.com", + "orgaoPublico": False + } + + def test_post_tomador_sucesso(self, main_instance, mock_api, dados_tomador): + mock_api.post.return_value = {"id": "TOM-001", "cpfCnpj": "08114280956"} + + with patch("NFSe.Tomador.tomador.Api", return_value=mock_api): + resultado = json.loads(main_instance.tomador("post", dados_tomador)) + + assert resultado["status"] == "ok" + + +class TestTomadorGet: + def test_buscar_tomador(self, main_instance, mock_api): + mock_api.get.return_value = {"cpfCnpj": "08114280956", "razaoSocial": "Tomador Teste"} + + with patch("NFSe.Tomador.tomador.Api", return_value=mock_api): + resultado = json.loads(main_instance.tomador("get", {"cpf_cnpj": "08114280956", "id_usuario": 1})) + + assert resultado["status"] == "ok" + + +class TestTomadorInvalido: + def test_metodo_invalido(self, main_instance): + resultado = json.loads(main_instance.tomador("inexistente", {"id_usuario": 1})) + assert resultado["status"] == "error" + +class TestRelatorios: + def test_nfe_emitidas(self, main_instance, mock_api): + mock_api.get_paginated.return_value = {"data": [{"id": "NFE-1"}], "total": 1} + + with patch("Relatorios.relatorios.Api", return_value=mock_api): + resultado = json.loads(main_instance.relatorios("nfe_emitidas", {"id_usuario": 1})) + + assert resultado["status"] == "ok" + + def test_nfe_por_cnpj(self, main_instance, mock_api): + mock_api.get_paginated.return_value = {"data": [], "total": 0} + + with patch("Relatorios.relatorios.Api", return_value=mock_api): + resultado = json.loads(main_instance.relatorios("nfe_por_cnpj", {"cnpj": "08187168000160", "id_usuario": 1})) + + assert resultado["status"] == "ok" + + def test_nfse_emitidas(self, main_instance, mock_api): + mock_api.get_paginated.return_value = {"data": [], "total": 0} + + with patch("Relatorios.relatorios.Api", return_value=mock_api): + resultado = json.loads(main_instance.relatorios("nfse_emitidas", { + "data_inicial": "2026-01-01", "data_final": "2026-12-31", "id_usuario": 1 + })) + + assert resultado["status"] == "ok" + + def test_nfse_por_cnpj(self, main_instance, mock_api): + mock_api.get_paginated.return_value = {"data": [], "total": 0} + + with patch("Relatorios.relatorios.Api", return_value=mock_api): + resultado = json.loads(main_instance.relatorios("nfse_por_cnpj", { + "cnpj": "08187168000160", + "data_inicial": "2026-01-01", "data_final": "2026-12-31", + "id_usuario": 1 + })) + + assert resultado["status"] == "ok" + + def test_metodo_invalido(self, main_instance): + resultado = json.loads(main_instance.relatorios("inexistente", {"id_usuario": 1})) + assert resultado["status"] == "error" + +class TestWebhooks: + def test_consultar_webhooks(self, main_instance, mock_api): + mock_api.get.return_value = {"url": " ", "method": "POST"} + + with patch("webhooks.webhooks.Api", return_value=mock_api): + main_instance.webhooks("get", {"cnpj": "08187168000160", "id_usuario": 1}) + + def test_criar_webhook(self, main_instance, mock_api): + mock_api.post.return_value = {"status": "ok"} + + with patch("webhooks.webhooks.Api", return_value=mock_api): + main_instance.webhooks("post", {"cnpj": "08187168000160", "url": " ", "id_usuario": 1}) + + def test_deletar_webhook(self, main_instance, mock_api): + mock_api.delete.return_value = {"status": "ok"} + + with patch("webhooks.webhooks.Api", return_value=mock_api): + main_instance.webhooks("delete", {"cnpj": "08187168000160", "id_usuario": 1}) + + def test_atualizar_webhook(self, main_instance, mock_api): + mock_api.put.return_value = {"status": "ok"} + + with patch("webhooks.webhooks.Api", return_value=mock_api): + main_instance.webhooks("put", {"cnpj": "08187168000160", "url": " ", "id_usuario": 1}) + + def test_testar_webhook(self, main_instance, mock_api): + mock_api.post.return_value = {"status": "ok", "message": "Webhook testado"} + + with patch("webhooks.webhooks.Api", return_value=mock_api): + main_instance.webhooks("test", {"cnpj": "08187168000160", "id_usuario": 1}) + + def test_metodo_invalido(self, main_instance): + resultado = json.loads(main_instance.webhooks("inexistente", {"id_usuario": 1})) + assert resultado["status"] == "error" diff --git a/endpoint_listener/src/tests/test_simplified_payload.py b/endpoint_listener/src/tests/test_simplified_payload.py new file mode 100644 index 0000000..d40c494 --- /dev/null +++ b/endpoint_listener/src/tests/test_simplified_payload.py @@ -0,0 +1,80 @@ +import os +import sys +import json +import time + +sys.path.append(os.path.dirname(os.path.dirname(os.path.abspath(__file__)))) + +from Models.config import SystemConfig +from utils.redis import RedisClient +from utils.Exchange2 import RPCExchange2 + +def run_rpc_test(): + redis_host = os.getenv('REDIS_HOST', 'localhost') + redis_client = RedisClient(host=redis_host, port=6379) + try: + config_data = redis_client.get("system_config") + sc = SystemConfig(**json.loads(config_data)) + except Exception as e: + print(f"Erro ao carregar configs: {e}") + return + + config_rpc = { + "x_api_key": "2da392a6-79d2-4304-a8b7-959572c7e44d", + "user_postgres": "tecnospeed", + "pass_postgres": "u7QimG7E%21SF%2F%2FJC2c8", + "host_postgres": "banco", + "port_postgres": "5432", + "database_postgres": "tecnospeed" + } + + exchange = RPCExchange2(sc.amqps, sc.origin, "integracao", "tecnospeed", "2.1.0", client="#") + servicekey = sc.servicekey.ljust(32, '0')[:32] + exchange.setCrypto(sc.serviceid, servicekey) + + # Payload "simplificado" do front-end + full_payload = { + "fila": "empresa", + "metodo": "POST", + "cliente": "tray", + "url_base": "https://api.sandbox.plugnotas.com.br", + "config": config_rpc, + "dados": { + "cpfCnpj": "94074986000198", + "razaoSocial": "Tech nux", + "nomeFantasia": "Tech nux ltda", + "inscricaoMunicipal": "0001", + "simplesNacional": True, + "regimeTributario": 1, + "regimeTributarioEspecial": 1, + "incentivoCultural": False, + "incentivoFiscal": True, + "certificado": "cert-mock-01", + "endereco": { + "codigoPais": "1058", + "descricaoPais": "BRASIL", + "tipoLogradouro": "Avenida", + "logradouro": "Avenida", + "numero": "123", + "complemento": "Casa", + "tipoBairro": "Bairro", + "bairro": "Centro", + "codigoCidade": "5200175", + "descricaoCidade": "ÁGUA FRIA DE GOIÁS", + "estado": "GO", + "cep": "68907-460" + }, + "nfe": { "ativo": True }, + "nfse": { "ativo": False } + } + } + + print("Enviando payload via RPC...") + exchange.setPayload(json.dumps(full_payload)) + resposta = exchange.callRPC(json.dumps(full_payload), timeout=30) + + print("\nRESPOSTA DO SERVIDOR (RPC):") + print(resposta) + +if __name__ == "__main__": + run_rpc_test() diff --git a/endpoint_listener/src/tests/test_upload_certificado.py b/endpoint_listener/src/tests/test_upload_certificado.py new file mode 100644 index 0000000..c3c7b9d --- /dev/null +++ b/endpoint_listener/src/tests/test_upload_certificado.py @@ -0,0 +1,88 @@ +import sys +import os +import json +import time +from dotenv import load_dotenv + +# Carrega variáveis de ambiente +load_dotenv() + +# Adiciona o diretório atual ao path para importar as classes locais +sys.path.append(os.path.dirname(os.path.abspath(__file__))) + +from Models.config import SystemConfig +from utils.redis import RedisClient +from utils.Exchange2 import QueueExchange2 + +# Dados do Certificado (Preenchidos dinamicamente) +PFX_BASE64 = " " +PFX_PASSWORD = str(sys.argv[1]) if len(sys.argv) > 1 else " " + +def main(): + print(f"Iniciando teste de upload de certificado para senha: {PFX_PASSWORD}") + + # 1. Carregando configuração do Redis + redis_host = os.getenv('REDIS_HOST', 'localhost') + redis_client = RedisClient(host=redis_host, port=6379) + try: + system_config_data = redis_client.get("system_config") + if not system_config_data: + print("Erro: system_config não encontrado no Redis.") + return + + system_config = SystemConfig(**json.loads(system_config_data)) + except Exception as e: + print(f"Erro ao ler configs do Redis: {e}") + return + + # 2. Inicializando QueueExchange2 + try: + exchange = QueueExchange2( + amqps=system_config.amqps, + origin=system_config.origin, + system="integracao", + service="tecnospeed", + version="1.0.0", + client="#" + ) + + servicekey = system_config.servicekey.ljust(32, '0')[:32] + exchange.setCrypto(serviceid=system_config.serviceid, servicekey=servicekey) + except Exception as e: + print(f"Falha ao iniciar mensageria: {e}") + return + + # 3. Montando o payload interno + internal_payload = { + "fila": "certificado_digital", + "cliente": "UploadTest", + "metodo": "POST", + "dados": { + "arquivo": PFX_BASE64, + "senha": PFX_PASSWORD, + "id_empresa": "c19d874447b1kxlapg1xv783", + "id_usuario": 1, + "expirationAt": "2027-04-13" + }, + "config": { + "x_api_key": os.getenv("PLUGNOTAS_API_KEY"), + "user_postgres": os.getenv("POSTGRES_USER", "tecnospeed"), + "pass_postgres": os.getenv("POSTGRES_PASSWORD", "u7QimG7E!SF//JC2c8"), + "host_postgres": "postgres", + "port_postgres": "5432", + "database_postgres": os.getenv("POSTGRES_DB", "tecnospeed") + }, + "url_base": "https://api.sandbox.plugnotas.com.br" + } + + # 4. Enviando com criptografia (hmac/payload) + try: + msg_json = json.dumps(internal_payload) + exchange.setPayload(msg_json) + exchange.sendMsg(msg_json, service="tecnospeed") + print("Mensagem enviada com sucesso (criptografada)!") + except Exception as e: + print(f"Erro ao enviar mensagem: {e}") + +if __name__ == "__main__": + main() diff --git a/endpoint_listener/src/utils/Exchange2.py b/endpoint_listener/src/utils/Exchange2.py new file mode 100644 index 0000000..d6af010 --- /dev/null +++ b/endpoint_listener/src/utils/Exchange2.py @@ -0,0 +1,1041 @@ +import json, time, sys, inspect, os +import hmac, hashlib, uuid, re + +import pika, pprint +from datetime import datetime +from pytz import timezone +from cryptography.hazmat.primitives.ciphers import Cipher, algorithms, modes +from cryptography.hazmat.primitives import padding +from cryptography.hazmat.backends import default_backend + +from queue import Queue, Full, Empty +from threading import Thread, Event +from abc import ABC, abstractmethod +from typing import Sequence, Any +import logging + +from utils.exchangeModels import * + +versao = "v1.0.0 14/12/2023 - Baltazar - Prefetch count" +versao = "v1.1.0 06/02/2024 - MJ. - Detecta Serviço rabbitmq ausente no inicio" +versao = "v1.2.0 20/02/2024 - MJ. - Possibilita uma taksqueue por serviço" +versao = "v1.3.0 06/03/2024 - MJ. - Verboso apenas nos erros" +versao = "v1.5.0 07/03/2024 - MJ. - External Rabbitmq Connection" +versao = "v1.6.0 26/06/2024 - MJ. - Conversao dos parametros host, service e client para lowercase" +versao = "v1.7.0 26/07/2024 - MJ. - Prepacao para utilizar Cloudamqp ou Rabbitmq local amqps/system" +versao = "v2.0.0 16/08/2024 - MJ. - Nova classe RPC e restruturação de Exchange2 e Tasks" +versao = "v2.1.0 21/11/2024 - MJ. - Clareza no log e tratamento de erros" +versao = "v2.2.0 31/01/2025 - MJ. - Remoção de alguns logs e melhorias de performance" +versao = "v2.3.0 12/02/2025 - MJ. - Melhorias na conexão com o Rabbitmq" +versao = "v2.4.0 14/02/2025 - MJ. - Check amqp url parameters" +versao = "v2.4.1 19/02/2025 - MJ. - Correcao prefetch_count=1" +versao = "v2.5.1 13/05/2025 - MJ. - Payload até 128kb - ajuste documentação e visual" +versao = "v2.5.2 01/10/2025 - MJ. - Evita especificar um client vazio" +versao = "v3" +versaostr = f"{versao}0.0 16/01/2026 S.D.G. - SG_IC2 LoggerExchange" + +def is_amqp_url(url): + pattern = r"^amqps?:\/\/([a-zA-Z0-9_\-\.]+):([^@]+)@([a-zA-Z0-9_\-\.]+)(:[0-9]+)?(\/[a-zA-Z0-9_\-\.]*)?$" + + return bool(re.match(pattern, url)) + +# QUEUE EXCHANGE - Apoia no comunicação inter-serviços de um sistema +# EXCHANGE TIPO DIRECT +# NAO DURAVEL - EXCLUSIVO - AUTODELETE +# SE NAO HOUVER QUEM RECEBA A MSG A MSG SERÁ PERDIDA -> O ENVIO FUNCIONA MESMO QUE NINGUEM EXISTA PARA RECEBER +class QueueExchange2: + def __init__( self, + amqps:str, + origin:str, + system:str, + service:str, + version:str, + client:str = '#', + callback: object = None, + delivery_mode = 1): + + paramOK = len(service) and len(system) and len(amqps) and len(origin) and len(version) and len(client) + if not (paramOK) or not is_amqp_url(amqps): + if not is_amqp_url(amqps): + msg = f"NOT A VALID AMQP URL: {amqps}" + else: + msg = f"VERIFY PARAMETERS: is_amqp_url: {is_amqp_url(amqps)}\n amqps[{amqps}]\n origin[{origin}]\n system[{system}]\n service[{service}]\n version[{version}\n client[{client}]" + raise Exception(msg) + + self.delivery = delivery_mode + self.payload = "" + self.hmac = "" + self.version = version + self.serviceid = "" + self.type = "msg" + self.origin = origin.lower() + self.client = client.lower() + self.service = service.lower() + self.system = system.lower() + self.client = client + self.params = pika.URLParameters(amqps) + self.callback = callback + self.queue = f"{self.system}_{service}_{self.client}" if client != '#' else f"{self.system}_{service}" + if callback is not None: + try: + self.connection = pika.BlockingConnection(self.params) + self.channelIn = self.connection.channel() # start main channel + self.channelIn.exchange_declare (exchange = f"{self.system}.direct", exchange_type = 'direct') + self.channelIn.queue_declare (queue = self.queue, durable = True) + self.channelIn.queue_bind (queue = self.queue, exchange = f"{self.system}.direct", routing_key= self.queue) + self.channelIn.basic_qos (prefetch_count=1) + self.consumerTag = self.channelIn.basic_consume (queue = self.queue, + auto_ack = False, + on_message_callback = self.callback) + except Exception as error: + raise(error) + else: + conexao = pika.BlockingConnection(self.params) #testa conexao + conexao.close() + + def cancel(self): + self.channelIn.basic_cancel(self.consumerTag) + + # METODOS INTERNOS _ + # MODELO MSG QUE TRAFEGA ENTRE SERVIÇOS E CONTROLLER + """ + MSG JSON CONTROLADOR + data: + { + "salesContractId": , + "client": "bambui", + "service": "SALES_CONTRACT_CREATED" + } + + message: + { ..., + "serviceid": , + "payload": aes-256 , + "hmac": hmac , + "type": "work", + ... + } + """ + def _setMsg(self, msg:str, caller:str): + now = datetime.now(timezone('America/Sao_Paulo')) + message =msg.strip('"') + + self.Msg = { + "system": self.system, + "target": self.target, + "service": self.service, + "serviceid": self.serviceid, + "payload": self.payload, + "hmac": self.hmac, + "type": self.type, + "version": self.version, + "origin": self.origin, + "client": self.client, + "caller": caller, + "logDate": f"{now}", + "msg": f"{message}" + } + + # Envia uma mensagem para um serviço associado ao sistema + # controller é o nome padrão do serviço, caso nao seja informado + def sendMsg(self, message: str, service:str = 'controller'): + trials = 5 + waitTime = 12 + messageSent = False + caller = inspect.stack()[1].function + if caller == '': caller = 'main' + + self.target = service + self._setMsg(message, caller) + targetQueue = f"{self.system}_{self.target}_{self.client}" if self.client != '#' else f"{self.system}_{self.target}" + while not messageSent and trials: + try: + connection = pika.BlockingConnection(self.params) + channelOut = connection.channel() # start a channel + #channelOut.exchange_declare(exchange = f"{self.system}.direct", exchange_type = 'direct') + #channelOut.queue_declare (queue= targetQueue, durable= True) + #channelOut.queue_bind (queue= targetQueue, exchange= f"{self.system}.direct", routing_key= targetQueue) + channelOut.basic_publish( + exchange = f"{self.system}.direct", + routing_key = targetQueue, + body = json.dumps(self.Msg), + properties = pika.BasicProperties(content_type = 'text/plain', delivery_mode= self.delivery)) + messageSent = True + connection.close() + except Exception as ex: + print(f"EXCH2: TROUBLE CNX RABBITMQ REMAIN ATTEMPT: {trials}") + print(ex, flush=True) + sys.stdout.flush() + time.sleep(waitTime) + trials -= 1 + if messageSent: + self.payload = "" + self.hmac = "" + else: + print(f"EXCH2: ERROR MESSAGE NOT SENT TO [{targetQueue}]", flush=True) + + def start_consuming(self): + if self.callback is not None: + self.consumerTag = self.channelIn.start_consuming() + + def setCrypto(self, serviceid:str, servicekey:str): + self.serviceid = serviceid + self.servicekey = servicekey + self.Crypto = Payload(serviceid, servicekey) + + def setPayload(self, data:str): + try: + self.hmac = "" + self.payload = "" + self.hmac, self.payload = self.Crypto.setPayload(data) + except Exception as ex: + print(f"EXCH2: CRYPTO ERROR: {ex}", flush=True) + + def getPayload(self, hmac:str, safeData:str): + try: + self.hmac = "" + self.payload = "" + self.hmac, self.payload = self.Crypto.getPayload(safeData) + except Exception as ex: + print(ex) + return False + + return hmac == self.hmac + +# CRIPTOGRAFIA DO PAYLOAD +class Payload: + def __init__(self, serviceid:str, servicekey:str): + self.serviceid = serviceid + self.servicekey = servicekey + + + def setPayload(self, data:str): + data = data.encode('utf-8') + tamanho = len(data) + if tamanho > 1024 * 1024 * 10 or tamanho <= 0: + raise Exception("EXCHANGE2 Payload setPayload: Invalid Payload size") + + hmacData= hmac.new(self.serviceid.encode('utf-8'), data, 'sha256').hexdigest() + iv = os.urandom(16) + padder = padding.PKCS7(128).padder() + datarw = padder.update(data) + padder.finalize() + cipher = Cipher(algorithms.AES(self.servicekey.encode('utf-8')), modes.CBC(iv)) + encrypt = cipher.encryptor() + safeData = encrypt.update(datarw) + encrypt.finalize() + payload = (iv + safeData) + + return (hmacData, payload.hex()) + + def getPayload(self, safeData:str): + safeData = bytes.fromhex(safeData) + iv = safeData[:16] + cipher = Cipher(algorithms.AES(self.servicekey.encode('utf-8')), modes.CBC(iv)) + decrypt = cipher.decryptor() + rawdata = decrypt.update(safeData[16:]) + decrypt.finalize() + if len(rawdata)%16 != 0: + raise Exception("EXCHANGE2 Payload getPayload: Invalid Payload size block {0}".format(len(rawdata))) + + unpadder = padding.PKCS7(128).unpadder() + data = unpadder.update(rawdata) + unpadder.finalize() + hmacData = hmac.new(self.serviceid.encode('utf-8'), data, 'sha256').hexdigest() + + return (hmacData, data.decode('utf-8')) + +# TASK EXCHANGE +# PERMITE CRIAR UM DISTRIBUIDOR DE TAREFAS +# O DISTRIBUIDOR DEVE RECEBER UMA FUNCAO DE CALLBACK +class TaskExchange2: + def __init__(self, + amqps:str, + origin:str, + system:str, + service:str, + version:str, + client:str = '#', callback: object = None, delivery_mode:int = 2): + paramOK = len(service) and len(system) and len(amqps) and len(origin) and len(version) + if not (paramOK) or not is_amqp_url(amqps): + if not is_amqp_url(amqps): + msg = f"NOT A VALID AMQP URL {amqps}" + else: + msg = f"VERIFY PARAMETERS: is_amqp_url: {is_amqp_url(amqps)}\n amqps[{amqps}]\n origin[{origin}]\n system[{system}]\n service[{service}]\n version[{version}]" + raise Exception(msg) + + self.delivery = delivery_mode + self.payload = "" + self.hmac = "" + self.version = version + self.serviceid = "" + self.type = "task" + self.origin = origin.lower() + self.service = service.lower() + self.system = system.lower() + self.client = client.lower() + self.params = pika.URLParameters(amqps) + self.callback = callback + self.queue = f'{self.system}_{self.service}_tasks_{client}' if client != '#' else f'{self.system}_{self.service}_tasks' + print(f"EXCH2: TASK EXCHANGE: [{self.queue}] [{amqps}]", flush=True) + if callback is not None: + self.connection = pika.BlockingConnection(self.params) + self.channelIn = self.connection.channel() # start main channel + self.channelIn.exchange_declare (exchange = f"{self.system}.direct", exchange_type = 'direct') + self.channelIn.queue_declare (queue = self.queue, durable = True) + self.channelIn.queue_bind (queue = self.queue, exchange = f"{self.system}.direct", routing_key= self.queue) + self.channelIn.basic_qos (prefetch_count=1) + self.consumerTag = self.channelIn.basic_consume (queue = self.queue, + auto_ack = False, + on_message_callback = self.callback) + else: + conexao = pika.BlockingConnection(self.params) #testa conexao + conexao.close() + def _setTask(self, msg:str, caller:str): + now = datetime.now(timezone('America/Sao_Paulo')) + message =msg.strip('"') + + self.Msg = { + "system": self.system, + "target": self.target, + "service": self.service, + "serviceid": self.serviceid, + "payload": self.payload, + "hmac": self.hmac, + "type": self.type, + "version": self.version, + "origin": self.origin, + "client": self.client, + "caller": caller, + "logDate": f"{now}", + "msg": f"{message}" + } + + def cancel(self): + self.channelIn.basic_cancel(self.consumerTag) + + def sendTask(self, message:str): + trials = 5 + waitTime = 12 + messageSent = False + caller = 'TaskExchange2' + self.target = 'workers' + self._setTask(message, caller) + + while not messageSent and trials: + try: + connection = pika.BlockingConnection(self.params) + channelOut = connection.channel() # start a channel + channelOut.exchange_declare(exchange = f"{self.system}.direct", exchange_type = 'direct') + channelOut.queue_declare (queue = self.queue, durable = True) + channelOut.queue_bind (queue = self.queue, exchange = f"{self.system}.direct", routing_key= self.queue) + + channelOut.basic_publish (exchange = f"{self.system}.direct", + routing_key = self.queue, + body = json.dumps(self.Msg), + properties = pika.BasicProperties(content_type = 'text/plain', + delivery_mode= self.delivery)) + messageSent = True + connection.close() + except Exception as ex: + print(f"EXCH2: TROUBLE CNX RABBITMQ REMAIN ATTEMPT: {trials}") + print(ex, flush=True) + time.sleep(waitTime) + trials -= 1 + if not messageSent: + print(f"EXCH2: ERROR TASK NOT SENT TO: {self.queue}", flush=True) + + def start_consuming(self): + if self.callback is not None: + print(f"EXCH2: SEND TASKS TO: [{self.queue}]", flush=True) + self.consumerTag = self.channelIn.start_consuming() + + def setCrypto(self, serviceid:str, servicekey:str): + self.serviceid = serviceid + self.servicekey = servicekey + self.Crypto = Payload(serviceid, servicekey) + + def setPayload(self, data:str): + try: + self.hmac = "" + self.payload = "" + self.hmac, self.payload = self.Crypto.setPayload(data) + except Exception as ex: + print(f"EXCH2: CRYPTO ERROR: {ex}", flush=True) + + def getPayload(self, hmac:str, safeData:str): + try: + self.hmac = "" + self.payload = "" + self.hmac, self.payload = self.Crypto.getPayload(safeData) + except Exception as ex: + print(f"EXCH2: CRYPTO ERROR: {ex}", flush=True) + return False + + return hmac == self.hmac + +# RPC - QUANDO FOR UTILIZAR COMO ENDPOINT +# FORNECER FUNCAO DE TRABALHO NA INICIALIZACAO +# COMO RPC Server: FILA ONDE RECEBERA AS TAREFAS => SISTEM_SERVICE_RPC ou SISTEM_SERVICE_RPC_CLIENT +# COMO RPC Client: UTILIZA callRPC +class RPCExchange2: + def __init__(self, + amqps:str, # ENDERECO URL RABBITMQ - CLOUDAMQP + origin:str, # NOME DO HOST ONDE O SCRIPT ESTÁ RODANDO + system:str, # NOME DO SISTEMA QUE O SERVIÇO FAZ PARTE + service:str, # NOME DO SERVIÇO DENTRO DO SISTEMA + version:str, # VERSÃO DO SERVIÇO + client:str = '#', # CASO ESPECIFICO A UM CLIENTE, COMO NO CASO DO SG_ETL + workfunction=None, # FUNCAO DE TRABALHO PARA O USO COMO RCP Server, i.e. para substituir um endpoint fastapi + delivery_mode=2 # VAI PERSISTIR AS MENSAGENS NO MODO 2 + ): + paramOK = len(service) and len(system) and len(amqps) and len(origin) and len(version) + + if not (paramOK) or not is_amqp_url(amqps): + if not is_amqp_url(amqps): + msg = f"RPCExchange2: NOT A VALID AMQP URL: {amqps}" + else: + msg = f"RPCExchange2: VERIFY PARAMETERS: is_amqp_url: {is_amqp_url(amqps)}\n amqps[{amqps}]\n origin[{origin}]\n system[{system}]\n service[{service}]\n version[{version}]" + raise Exception(msg) + + self.callback = workfunction + self.delivery = delivery_mode + self.payload = "" + self.hmac = "" + self.version = version + self.serviceid = "" + self.servicekey = "" + self.correlation_id = "" + self.consumerTag = None + self.type = "rpc" + self.origin = origin.lower() + self.service = service.lower() + self.system = system.lower() + self.client = client.lower() + self.params = pika.URLParameters(amqps) + # Server ouve nessa fila - Cliente publica nesta fila + self.queue = f'{self.system}_{self.service}_rpc_{client}' if client != '#' else f'{self.system}_{self.service}_rpc' + self.connection = pika.BlockingConnection(self.params) + self.channel = self.connection.channel() # start main channel + self.channel.exchange_declare (exchange = f"{self.system}.direct", exchange_type = 'direct') + + if (workfunction is not None): # FUNCIONAMENTO COMO RPC SERVER + self.target = 'rpcclt' # SINALIZA O DESTINO DA MSG SER UM RPC CLIENT + self.channel.queue_declare (queue = self.queue, exclusive = False, durable=True, auto_delete = True) + self.channel.queue_bind (exchange = f"{self.system}.direct", queue = self.queue, routing_key = self.queue) + self.channel.basic_qos (prefetch_count = 1) + self.consumerTag = self.channel.basic_consume(queue = self.queue, + auto_ack = True, + on_message_callback = self._RPCResp) # de lá chamamos self.callback + else: # FUNCIONAMENTO COMO RPC CLIENT + self.target = 'rpcsrv' # SINALIZA O DESTINO DA MSG SER UM RPC SERVER + + # PREPARA QUEUE DE RETORNO + result = self.channel.queue_declare(queue='', exclusive=True, auto_delete=True) + self.callback_queue = result.method.queue + self.channel.queue_bind(exchange = f"{self.system}.direct", + queue = self.callback_queue, + routing_key = self.callback_queue) + + self.channel.basic_consume( + queue = self.callback_queue, + on_message_callback = self._on_response, + auto_ack = True + ) + #print("EXCH2: RPC CLT - QUEUE:", self.callback_queue, flush=True) + + # RPC - QUANDO FOR UTILIZAR COMO ENDPOINT + def _RPCResp(self, ch, method, props, body): + #ch.basic_ack(delivery_tag = method.delivery_tag) + message = body.decode("utf-8") + reply_to = getattr(props, 'reply_to', None) + correlation_id = getattr(props, 'correlation_id', None) + resposta = "{}" # SEM WORKFUNCTION + # ENVIA A MENSAGEM AO CALLBACK + self.payload = "" + self.hmac = "" + try: + if self.callback is not None: + resposta = self.callback(message) + rspObj = json.loads(resposta) + # para enviar um payload criptografado na resposta ao caller + # retorne um objeto contendo serviceid, servicekey e payload + # essas chaves serao retiradas do objeto e o restante da informacao + # irá no chave msg + if 'payload' in rspObj and 'serviceid' in rspObj and 'servicekey' in rspObj: + self.setCrypto(rspObj['serviceid'], rspObj['servicekey']) + self.setPayload(json.dumps(rspObj['payload'])) + rspObj.pop('payload', None) + rspObj.pop('serviceid', None) + rspObj.pop('servicekey', None) + resposta = json.dumps(rspObj) + + + except Exception as ex: + erro = f"ERROR CALLBACK: {str(ex)}" + print(erro, flush=True) + resposta = json.dumps({"status": "error", "message": erro}) + + # ENVIA RESPOSTA AO CALLER + msg = json.loads(message) + msg = msg['msg'] + #print(f"RPC <== {msg}", flush=True) + #print(f"RPC ==> {resposta}", flush=True) + try: + if reply_to: + self.target = "rpcclt" + self._setJSON(resposta, 'RPCResp') + + ch.basic_publish(exchange = f"{self.system}.direct", + routing_key = reply_to, + body = json.dumps(self.Msg), + properties = pika.BasicProperties(content_type = 'application/json', + delivery_mode = self.delivery, + correlation_id = correlation_id)) + else: + print(f"RPCExchange2: NO TARGET FOUND: {resposta}", props, flush=True) + + except Exception as ex: + print(ex) + resposta = f"RPCExchange2: ERRO MSG CALLER: {str(ex)}" + raise Exception(resposta) + + + def _setJSON(self, msg:str, caller:str): + now = datetime.now(timezone('America/Sao_Paulo')) + message =msg.strip('"') + + self.Msg = { + "system": self.system, + "target": self.target, + "service": self.service, + "serviceid": self.serviceid, + "payload": self.payload, + "hmac": self.hmac, + "type": self.type, + "correlation_id": self.correlation_id, + "version": self.version, + "origin": self.origin, + "client": self.client, + "caller": caller, + "logDate": f"{now}", + "msg": f"{message}" + } + self.hmac = "" + self.payload = "" + + + def _on_response(self, ch, method, properties, body): + try: + if self.correlation_id == properties.correlation_id: + self.response = body.decode("utf-8") + except Exception as ex: + print(f"RPCExchange2: RPC ERROR: {ex}", flush=True) + sys.exit(0) + + return self.response + + # ENVIA A MENSAGEM AO CALLBACK + # RPC CLIENT CHAMA O RPC SERVER + def callRPC(self, message:str, timeout:int=10): + # ENVIA A MENSAGEM + self.response = None + self.correlation_id = str(uuid.uuid4()) + self._setJSON(message, 'callRPC') # COLOCA A message DENTRO DA CHAVE msg em self.Msg + + self.channel.exchange_declare(exchange = f"{self.system}.direct", exchange_type = 'direct') + self.channel.queue_declare (queue= self.queue, durable=True, exclusive=False, auto_delete=True) + self.channel.queue_bind (queue= self.queue, exchange= f"{self.system}.direct", routing_key= self.queue) + + self.channel.basic_publish (exchange = f"{self.system}.direct", + routing_key = self.queue, + body = json.dumps(self.Msg), + properties = pika.BasicProperties(content_type = 'application/json', + delivery_mode = self.delivery, + reply_to = self.callback_queue, + correlation_id= self.correlation_id)) + # AGUARDA O RETORNO + while self.response is None: + self.connection.process_data_events(time_limit=timeout) + return self.response + + def cancel(self): + print(f"RPCExchange2: CONSUMER TAG CANCELED: {self.consumerTag}", flush=True) + self.channel.basic_cancel(self.consumerTag) + + def start_consuming(self): + if self.callback is not None: + print(f"RPCExchange2: {self.service.upper()} RPC SRV QUEUE: [{self.queue}]", flush=True) + self.channel.start_consuming() + + def setCrypto(self, serviceid:str, servicekey:str): + self.serviceid = serviceid + self.servicekey = servicekey + self.Crypto = Payload(serviceid, servicekey) + + def setPayload(self, data:str): + print(f"RPCExchange2: SET PAYLOAD", flush=True) + try: + self.hmac = "" + self.payload = "" + self.hmac, self.payload = self.Crypto.setPayload(data) + except Exception as ex: + print(ex) + + def getPayload(self, hmac:str, safeData:str): + print(f"RPCExchange2: GET PAYLOAD", flush=True) + try: + self.hmac = "" + self.payload = "" + self.hmac, self.payload = self.Crypto.getPayload(safeData) + except Exception as ex: + print(ex) + return False + + return hmac == self.hmac + +# CADA APLICAÇÃO CRIA UMA CLASSE QUE ESTENDE IC2Builder IMPLEMENTANDO +# OS METODOS ABSTRATOS DE CADA REALMTYPE +class IC2Builder(ABC): + @abstractmethod + def _setLogs(self, message: tuple) -> InfoConnector: ... + @abstractmethod + def _setErrors(self, message: tuple) -> InfoConnector: ... + @abstractmethod + def _setOperational(self, message: tuple) -> InfoConnector: ... + @abstractmethod + def _setStatus(self, message: tuple) -> InfoConnector: ... + @abstractmethod + def _setMetrics(self, message: tuple) -> InfoConnector: ... + + def build(self, realm: RealmType, message: Sequence[Any]) -> InfoConnector: + handlers = { + RealmType.LOGS: self._setLogs, + RealmType.ERRORS: self._setErrors, + RealmType.OPERATIONAL: self._setOperational, + RealmType.STATUS: self._setStatus, + RealmType.METRICS: self._setMetrics, + } + + try: + return handlers[realm](message) + except KeyError: + raise ValueError(f"RealmType não suportado: {realm}") + +class LoggerExchange2: + def __enter__(self): return self + def __exit__(self, exc_type, exc, tb): self.close() + + def __init__(self, + ic2_builder: IC2Builder, + amqps : str, + system : str, + service : str, + version : str, + origin : str = "", + client : str = "#", + logType : str = 'logs', + logLocal : bool = False, + callback : object = None): + self.callback = callback + self.ic2_builder = ic2_builder + self.system = system.lower() + self.target = "" + self.service = service.lower() + self.serviceid = "AF1699F719F1B6C81DE2BD38AB811994E3089B35C8E9314813A770AC7AE25D8D" + self.servicekey = "KoG704lAafBA7Yafj8jyg5lAHfarrQ12" + self.payload = "" + self.hmac = "" + self.type = "logger" + self.version = version + self.origin = origin + self.client = client.lower() + self.params = pika.URLParameters(amqps) + self.params.socket_timeout = 15 + self.params.heartbeat = 60 + self.logType = logType.lower() + self.logTypeSvd = logType.lower() + self.logLocal = logLocal + self.pp = pprint.PrettyPrinter() + self.diaMes = datetime.now().strftime("%d") # PARA SABER SE ATUALIZA OU NÃO O ARQUIVO DE LOG LOCAL + self.flush = True + self.base_routeKey = f"{self.service}.{self.system}" + self.connection = None + self.channel = None + self.logging = logging.getLogger(self.base_routeKey) + self.Crypto = Payload(self.serviceid, self.servicekey) + self.exchange_name = 'info_connector.topic' + + try: + self._connect() + except Exception as err: + self.logging.error(f"ERROR CONNECTION {err}") + raise + + if self.logLocal: self.setLogLocalName() + + # --- async logging --- + self.queue_size = 1000 + self.queue = Queue(maxsize=self.queue_size) + self.stop_event = Event() + self.worker_thread = Thread( + target = self._worker, + name = "logger-worker", + daemon = True + ) + self.worker_thread.start() + + self.logging.info(f"params: {self.params} amqps: {amqps}") + if self.callback is not None: + try: + self.queueIC2 = "info_connector_collector" + self.channelIn = self.connection.channel() + self.channelIn.queue_declare (queue = self.queueIC2, exclusive=True, durable=True) + self.channelIn.queue_bind (queue = self.queueIC2, exchange = self.exchange_name, routing_key = "#") + self.channelIn.basic_qos (prefetch_count = 1) + self.consumerTag = self.channelIn.basic_consume (queue = self.queueIC2, + auto_ack = False, + on_message_callback = self.callback) + self.logging.info("COLLECTING TOPIC MESSAGES VHOST info_connector_host EXCHANGE info_connector.topic KEY #") + except Exception as error: + raise(error) + else: + conexao = pika.BlockingConnection(self.params) #testa conexao + conexao.close() + self.logging.info(f"Logger Messages topics: {self.logType}.{self.base_routeKey}.{self.client}") + + + # PERSISTENCIA LOGS MESSGERIA + def _worker(self): + while not self.stop_event.is_set() or not self.queue.empty(): + try: + payload, client = self.queue.get(timeout=0.5) + except Empty: + continue + + try: + self.sendMsgWork(payload, client) + except Exception as e: + # última linha de defesa + self.logging.error(f"ASYNC LOGGER FAILURE: {e}") + if self.logLocal: + self.logging.info(payload) + finally: + self.queue.task_done() + + # PERSISTENCIA CONEXAO AMQPS + def _connect(self): + """Conecta ou reconecta ao RabbitMQ.""" + if self.connection and self.connection.is_open: + return + try: + if self.connection: + self.connection.close() + except Exception: + pass + try: + self.connection = pika.BlockingConnection(self.params) + self.channel = self.connection.channel() + self.channel.exchange_declare(exchange = self.exchange_name, + exchange_type = 'topic', + auto_delete = False) + + self.logging.info("Conexão ao RabbitMQ estabelecida.") + except pika.exceptions.AMQPConnectionError as e: + self.logging.error(f"Falha na conexão: {e}") + raise + + def _setMsg(self, msg:str, caller:str): + now = datetime.now(timezone('America/Sao_Paulo')) # melhoria: acrescentar timezone info no system_config + return { + "system" : self.system, + "target" : self.target, + "service" : self.service, + "serviceid": self.serviceid, + "payload" : self.payload, + "hmac" : self.hmac, + "type" : self.type, + "version" : self.version, + "origin" : self.origin, + "client" : self.client, + "caller" : caller, + "logDate" : f"{now}", + "msg" : msg + } + + def _sendMsg(self, msg: str, caller: str, *args): + # Avalia arquivo de logs + if self.logLocal: + current_dia = datetime.now().strftime("%d") + if current_dia != self.diaMes: + self.diaMes = current_dia + self.setLogLocalName() + + message = msg.strip() + + try: + if args and isinstance(args[0], (list, tuple)): + for item in args[0]: + msgArg = pprint.pformat(item).replace("'", "").strip() + message = f"{message}, {msgArg}" + + Msg = self._setMsg(message, caller) + payload = json.dumps(Msg) + + # console / docker logs continuam síncronos + self.pp.pprint(f"{Msg['logDate'][0:21]} {Msg['msg']}") + + # log local imediato (garantia mínima) + if self.logLocal: + Msg['payload'] = Msg['payload'][:20] + self.logging.info(self.pp.pformat(Msg)) + + # envio assíncrono + try: + self.queue.put_nowait((payload, self.client)) + except Full: + # fila cheia → degrada com dignidade + self.logging.warning("LOGGER QUEUE FULL — DROPPING MESSAGE") + + if self.flush: + sys.stdout.flush() + + except Exception as e: + erro = f"LOGGER ERROR: {e}" + self.logging.error(erro) + print(erro, flush=True) + + + def _sendMsgLocal(self, msg: str, caller: str, *args): + # Avalia arquivo de logs + if self.logLocal: + current_dia = datetime.now().strftime("%d") + if current_dia != self.diaMes: + self.diaMes = current_dia + self.setLogLocalName() + + message = msg.strip() + + try: + if args and isinstance(args[0], (list, tuple)): + for item in args[0]: + msgArg = pprint.pformat(item).replace("'", "").strip() + message = f"{message}, {msgArg}" + + Msg = self._setMsg(message, caller) + payload = json.dumps(Msg) + + # console / docker logs continuam síncronos + self.pp.pprint(f"{Msg['logDate'][0:21]} {Msg['msg']}") + + # log local imediato (garantia mínima) + if self.logLocal: + Msg['payload'] = Msg['payload'][:20] + self.logging.info(self.pp.pformat(Msg)) + + except Exception as e: + erro = f"LOGGER ERROR: {e}" + self.logging.error(erro) + print(erro, flush=True) + + def start_consuming(self): + if self.callback is not None: + self.consumerTag = self.channelIn.start_consuming() + + def close(self): + # sinaliza parada + self.stop_event.set() + + # aguarda flush da fila (timeout defensivo) + try: + self.queue.join() + except Exception: + pass + + if self.worker_thread.is_alive(): + self.worker_thread.join(timeout=2) + + if self.channel: + self.channel.close() + if self.connection: + self.connection.close() + + def setLogLocalName(self): + self.logLocalName = 'logs/'+ self.system.split()[0] + '_' + self.diaMes + formatter = logging.Formatter("%(asctime)s - %(name)s - %(levelname)s - %(message)s") + fhandler = logging.FileHandler(self.logLocalName) + fhandler.setLevel(logging.INFO) + fhandler.setFormatter(formatter) + self.logging.propagate = False + self.logging.setLevel(logging.INFO) + if not any(isinstance(h, logging.FileHandler) for h in self.logging.handlers): + self.logging.addHandler(fhandler) + + # Funcionamento: caso nao seja possivel enviar a msg, tentar nova conexao + # e envio da msg original mais uma vez + def sendMsgWork(self, message:str, client:str = "#"): + messageSent = False + trials = 5 + waitTime = 5 + while not messageSent and trials > 0: + self._connect() + # Cliente informado - MAIOR PRECEDENCIA + if client != "#": + routeKey = self.base_routeKey + f".{client.lower()}" + elif self.client != "#": + routeKey = self.base_routeKey + f".{self.client.lower()}" + else: + routeKey = self.base_routeKey + + try: + routing_key = f"{self.logType}.{routeKey}" + self.channel.basic_publish( + exchange = "info_connector.topic", + routing_key = routing_key, + body = message, + properties = pika.BasicProperties( + content_type = 'text/plain', + delivery_mode= pika.DeliveryMode.Transient)) + messageSent = True + except (pika.exceptions.ConnectionClosed, pika.exceptions.ChannelClosed, pika.exceptions.AMQPConnectionError) as ex: + self.logging.warning(f"TROUBLE CNX LOGGER REMAIN ATTEMPT: {trials} {ex}") + self._connect() + except Exception as ex: + self.logging.error(f"LOGGER ERROR: {ex}") + self.logging.warning(str(self.params)) + finally: + if not messageSent: + trials -= 1 + time.sleep(waitTime) + waitTime *= 2 + + if not messageSent: + self.logging.error(f"LOGGER ERROR: {message}") + + # Para quando desejar trocar o header do topico + # Salva o tópico padrão especificado no momento da inicialização + def setLogType(self, logType:str): + if not logType or len(logType) > 25: + return + self.logTypeSvd = self.logType + self.logType = logType.lower() + + # Volta ao header topico padrao + def clrLogType(self): + self.logType = self.logTypeSvd + + def logIC2(self, realm: RealmType, msg: tuple = ()): + ic_obj = self.ic2_builder.build(realm, msg) + payload = ic_obj.model_dump_json() + self.setPayload(data=payload) + self.setLogType(logType=realm.value) + self._sendMsg(f"LOGIC2 {realm.value.upper()} [{str(msg)[:20]} ...]", caller="IC2") + self.clrLogType() + self.clrPayload() + + def logMsgError(self, msg:str, *args): + self.setLogType('error.logs') + caller = inspect.stack()[1].function + if caller == '': caller = 'main' + + self._sendMsgLocal(msg, caller, args) + self.clrLogType() + + # LOGS GERAIS + # INFORM logs...# + # Log geral de acordo com o valor logType + def logMsg (self, msg: str, *args): + caller = inspect.stack()[1].function + if caller == '': caller = 'main' + Msg = f"{msg}" + self._sendMsgLocal(Msg, caller, args) + + # FUNÇÕES PARA CRYPTOGRAFIA + def clrPayload(self): + self.hmac = "" + self.payload = "" + + def setPayload(self, data:str): + try: + self.hmac = "" + self.payload = "" + self.hmac, self.payload = self.Crypto.setPayload(data) + except Exception as ex: + print(f"EXCH2: CRYPTO ERROR: {ex}", flush=True) + + def getPayload(self, hmac:str, safeData:str): + try: + self.hmac = "" + self.payload = "" + self.hmac, self.payload = self.Crypto.getPayload(safeData) + except Exception as ex: + print(f"EXCH2: CRYPTO ERROR: {ex}", flush=True) + return False + + return hmac == self.hmac + +class AppIC2Builder(IC2Builder): + def __init__(self, service_name: str, versionstr: str, origin: str): + self.service_name = service_name + self.versionstr = versionstr + self.origin = origin + + def _get_common_fields(self): + now = datetime.now().strftime("%Y-%m-%d %H:%M:%S") + return { + "log_date": now, + "service_date": now, + "service": self.service_name, + "versionstr": self.versionstr, + "origin": self.origin + } + + def _setLogs(self, message: tuple) -> InfoConnector: + msg_str = str(message[0]) if len(message) > 0 else "" + ent_id = str(message[1]) if len(message) > 1 else "0" + ent_name = str(message[2]) if len(message) > 2 else "Unknown" + tenant = str(message[3]) if len(message) > 3 else "#" + + ic_msg = ICLogs(**self._get_common_fields(), enterprise_id=ent_id, enterprise_name=ent_name, message=msg_str, tenant=tenant) + return InfoConnector(realm=RealmType.LOGS, messageIC=ic_msg) + + def _setErrors(self, message: tuple) -> InfoConnector: + msg_str = str(message[0]) if len(message) > 0 else "" + ent_id = str(message[1]) if len(message) > 1 else "0" + ent_name = str(message[2]) if len(message) > 2 else "Unknown" + tenant = str(message[3]) if len(message) > 3 else "#" + + ic_msg = ICErrors(**self._get_common_fields(), enterprise_id=ent_id, enterprise_name=ent_name, message=msg_str, tenant=tenant) + return InfoConnector(realm=RealmType.ERRORS, messageIC=ic_msg) + + def _setOperational(self, message: tuple) -> InfoConnector: + acao = str(message[0]) if len(message) > 0 else "" + tabela = str(message[1]) if len(message) > 1 else "" + id_usuario = str(message[2]) if len(message) > 2 else "0" + id_registro = str(message[3]) if len(message) > 3 else "0" + status_str = str(message[4]).upper() if len(message) > 4 else "INICIO" + mensagem = str(message[5]) if len(message) > 5 else "" + json_data = str(message[6]) if len(message) > 6 else "{}" + + if status_str == 'SUCESSO': op_status = OpStatus.OK + elif status_str == 'ERRO': op_status = OpStatus.ERROR + else: op_status = OpStatus.ACTIVE + + common = self._get_common_fields() + ic_msg = ICOperational( + log_date=common["log_date"], service_date=common["service_date"], service_name=self.service_name, + versionstr=self.versionstr, origin=self.origin, enterprise_id=id_usuario, enterprise_name=f"User {id_usuario}", + status=op_status, step_status=acao, message=f"{mensagem} | Tabela: {tabela} | ID_Reg: {id_registro} | Data: {json_data}" + ) + return InfoConnector(realm=RealmType.OPERATIONAL, messageIC=ic_msg) + + def _setStatus(self, message: tuple) -> InfoConnector: + common = self._get_common_fields() + ic_msg = ICStatus( service=self.service_name, versionstr=self.versionstr, system_name="integracao", + startup_date=common["log_date"], access_date=common["log_date"], origin=self.origin ) + return InfoConnector(realm=RealmType.STATUS, messageIC=ic_msg) + + def _setMetrics(self, message: tuple) -> InfoConnector: + metric_name = str(message[0]) if len(message) > 0 else "" + value = str(message[1]) if len(message) > 1 else "0" + unit = str(message[2]) if len(message) > 2 else "" + + now = datetime.now().strftime("%Y-%m-%d %H:%M:%S") + ic_msg = ICMetrics( metric_date=now, metric=metric_name, metric_name=metric_name, value=value, unit=unit, tenant="#" ) + return InfoConnector(realm=RealmType.METRICS, messageIC=ic_msg) diff --git a/endpoint_listener/src/utils/api.py b/endpoint_listener/src/utils/api.py new file mode 100644 index 0000000..e7c48ce --- /dev/null +++ b/endpoint_listener/src/utils/api.py @@ -0,0 +1,158 @@ +import requests + +# Wrapper base para realizar requests (GET/POST/PUT/DELETE) à API da PlugNotas de forma padronizada +class Api: + def __init__(self, api_key): + self.api_key = api_key + + def get(self, url): + try: + headers = { + "x-api-key": self.api_key + } + response = requests.get(url, headers=headers, timeout=30) + response.raise_for_status() + return response.json() + except requests.exceptions.RequestException as e: + resp_data = {} + if 'response' in locals() and response is not None: + try: + resp_data = response.json() + except ValueError: + resp_data = response.text + return {"error": str(e), "response": resp_data} + + def get_paginated(self, url, max_pages=100): + try: + headers = { + "x-api-key": self.api_key + } + all_data = [] + current_url = url + page = 0 + + while current_url and page < max_pages: + separator = "&" if "?" in current_url else "?" + request_url = current_url if page == 0 else current_url + + response = requests.get(request_url, headers=headers, timeout=30) + response.raise_for_status() + result = response.json() + + if isinstance(result, list): + all_data.extend(result) + break + elif isinstance(result, dict): + page_data = result.get("data", []) + if isinstance(page_data, list): + all_data.extend(page_data) + elif page_data: + all_data.append(page_data) + + hash_proxima = result.get("hashProximaPagina") + if hash_proxima: + separator = "&" if "?" in url else "?" + current_url = f"{url}{separator}hashProximaPagina={hash_proxima}" + page += 1 + else: + break + else: + break + + return {"data": all_data, "total_paginas": page + 1, "total_registros": len(all_data)} + except requests.exceptions.RequestException as e: + resp_data = {} + if 'response' in locals() and response is not None: + try: + resp_data = response.json() + except ValueError: + resp_data = response.text + return {"error": str(e), "response": resp_data} + + def post(self, url, data): + try: + headers = { + "Content-Type": "application/json", + "x-api-key": self.api_key + } + response = requests.post(url, headers=headers, json=data, timeout=30) + response.raise_for_status() + return response.json() + except requests.exceptions.RequestException as e: + resp_data = {} + if 'response' in locals() and response is not None: + try: + resp_data = response.json() + except ValueError: + resp_data = response.text + return {"error": str(e), "response": resp_data} + + def put(self, url, data): + try: + headers = { + "x-api-key": self.api_key + } + response = requests.put(url, headers=headers, json=data, timeout=30) + response.raise_for_status() + return response.json() + except requests.exceptions.RequestException as e: + return {"error": str(e)} + + def patch(self, url, data): + try: + headers = { + "x-api-key": self.api_key + } + response = requests.patch(url, headers=headers, json=data, timeout=30) + response.raise_for_status() + return response.json() + except requests.exceptions.RequestException as e: + return {"error": str(e)} + + def delete(self, url): + try: + headers = { + "x-api-key": self.api_key + } + response = requests.delete(url, headers=headers, timeout=30) + response.raise_for_status() + return response.json() + except requests.exceptions.RequestException as e: + return {"error": str(e)} + + def post_file(self, url, file): + try: + headers = { + "x-api-key": self.api_key + } + response = requests.post(url, headers=headers, files=file, timeout=60) + response.raise_for_status() + return response.json() + except requests.exceptions.RequestException as e: + return {"error": str(e)} + + def post_form_data(self, url, data): + try: + headers = { + "x-api-key": self.api_key + } + response = requests.post(url, headers=headers, files=data, timeout=60) + response.raise_for_status() + return response.json() + except requests.exceptions.RequestException as e: + return {"error": str(e)} + + def put_form_data(self, url, data): + try: + headers = { + "x-api-key": self.api_key + } + response = requests.put(url, headers=headers, files=data, timeout=60) + response.raise_for_status() + return response.json() + except requests.exceptions.RequestException as e: + return {"error": str(e)} + +# api = Api("2da392a6-79d2-4304-a8b7-959572c7e44d") +# response = api.get("https://api.sandbox.plugnotas.com.br/certificado") +# print(response) diff --git a/endpoint_listener/src/utils/banco.py b/endpoint_listener/src/utils/banco.py new file mode 100644 index 0000000..24131b4 --- /dev/null +++ b/endpoint_listener/src/utils/banco.py @@ -0,0 +1,242 @@ +from datetime import datetime +import uuid +from sqlalchemy import Column, Integer, String, DateTime, Text, create_engine, Boolean +from sqlalchemy.orm import declarative_base, sessionmaker +from sqlalchemy.exc import SQLAlchemyError + +Base = declarative_base() + +class CertificadoDigitalTable(Base): + __tablename__ = 'certificado_digital' + id = Column(Integer, primary_key=True, autoincrement=True) + id_tecnospeed = Column(String(50), nullable=False) + certificado = Column(Text, nullable=False) + createdAt = Column(DateTime, default=datetime.now) + updatedAt = Column(DateTime, default=datetime.now, onupdate=datetime.now) + deletedAt = Column(DateTime, nullable=True) + expirationAt = Column(DateTime, nullable=False) + senha = Column(String(50), nullable=False) + status = Column(Boolean, nullable=False) + id_empresa = Column(String(50), nullable=False) + id_usuario = Column(Integer, nullable=False) + email = Column(String(255), nullable=True) + +class EnterpriseTable(Base): + __tablename__ = 'Enterprise' + id = Column(String(50), primary_key=True) + nome_fantasia = Column(String(255), nullable=True) + razao_social = Column(String(255), nullable=True) + cnpj = Column(String(50), nullable=False) + banco = Column(Text, nullable=True) + email = Column(String(255), nullable=True) + createdAt = Column(DateTime, default=datetime.now) + updatedAt = Column(DateTime, default=datetime.now, onupdate=datetime.now) + deletedAt = Column(DateTime, nullable=True) + status = Column(Boolean, nullable=False, default=True) + +class LogoTable(Base): + __tablename__ = 'logo' + id = Column(Integer, primary_key=True, autoincrement=True) + logo = Column(Text, nullable=False) + id_empresa = Column(String(50), nullable=False) + createdAt = Column(DateTime, default=datetime.now) + updatedAt = Column(DateTime, default=datetime.now, onupdate=datetime.now) + deletedAt = Column(DateTime, nullable=True) + status = Column(Boolean, nullable=False) + +class LogTable(Base): + __tablename__ = 'log' + id = Column(Integer, primary_key=True, autoincrement=True) + id_usuario = Column(Integer, nullable=False) + createdAt = Column(DateTime, default=datetime.now) + updatedAt = Column(DateTime, default=datetime.now, onupdate=datetime.now) + deletedAt = Column(DateTime, nullable=True) + acao = Column(String(50), nullable=False) + tabela = Column(String(50), nullable=False) + id_registro = Column(String(50), nullable=False) + json = Column(Text, nullable=False) + status = Column(String(20), nullable=False, default='INICIO') + mensagem = Column(Text, nullable=False, default='') + cliente = Column(String(100), nullable=True) + correlation_id = Column(String(50), nullable=True) + +class ServicoTable(Base): + __tablename__ = 'servico' + id = Column(Integer, primary_key=True, autoincrement=True) + id_tecnospeed = Column(String(50), nullable=False) + descricao = Column(Text, nullable=False) + id_usuario = Column(Integer, nullable=False) + createdAt = Column(DateTime, default=datetime.now) + updatedAt = Column(DateTime, default=datetime.now, onupdate=datetime.now) + deletedAt = Column(DateTime, nullable=True) + status = Column(Boolean, nullable=False) + +class TomadorTable(Base): + __tablename__ = 'tomador' + id = Column(Integer, primary_key=True, autoincrement=True) + id_tecnospeed = Column(String(50), nullable=False) + nome = Column(String(50), nullable=False) + cpf_cnpj = Column(String(50), nullable=False) + email = Column(String(50), nullable=False) + telefone = Column(String(50), nullable=False) + id_usuario = Column(Integer, nullable=False) + createdAt = Column(DateTime, default=datetime.now) + updatedAt = Column(DateTime, default=datetime.now, onupdate=datetime.now) + deletedAt = Column(DateTime, nullable=True) + status = Column(Boolean, nullable=False) + +class NFSeTable(Base): + __tablename__ = 'nfse' + id = Column(Integer, primary_key=True, autoincrement=True) + id_tecnospeed = Column(String(50), nullable=False) + id_empresa = Column(String(50), nullable=False) + id_usuario = Column(Integer, nullable=False) + createdAt = Column(DateTime, default=datetime.now) + updatedAt = Column(DateTime, default=datetime.now, onupdate=datetime.now) + deletedAt = Column(DateTime, nullable=True) + status = Column(Boolean, nullable=False) + +class NFeTable(Base): + __tablename__ = 'nfe' + id = Column(Integer, primary_key=True, autoincrement=True) + id_tecnospeed = Column(String(50), nullable=False) + id_empresa = Column(String(50), nullable=False) + id_usuario = Column(Integer, nullable=False) + createdAt = Column(DateTime, default=datetime.now) + updatedAt = Column(DateTime, default=datetime.now, onupdate=datetime.now) + deletedAt = Column(DateTime, nullable=True) + status = Column(Boolean, nullable=False) + +# Cache de motores criados pelo SQLAlchemy por URL de conexao +_engines = {} + +# Abstração de conexão base e de persistência de instâncias de metadados relacionais usando PostgreSQL +class Banco: + def __init__(self, host, port, user, password, database): + conn_str = f'postgresql://{user}:{password}@{host}:{port}/{database}' + if conn_str not in _engines: + _engines[conn_str] = create_engine( + conn_str, + pool_pre_ping=True, + pool_recycle=3600 + ) + self.engine = _engines[conn_str] + self.Session = sessionmaker(bind=self.engine) + self.session = self.Session() + + def close(self): + """Encerra a sessão SQLAlchemy atual liberando do pooler.""" + if hasattr(self, 'session') and self.session is not None: + self.session.close() + + def create_tables(self): + """Cria as tabelas no banco de dados.""" + try: + Base.metadata.create_all(self.engine) + except SQLAlchemyError as e: + print(f"Erro ao criar tabelas (SQLAlchemy): {e}") + except Exception as e: + print(f"Erro ao criar tabelas: {e}") + + def add(self, obj): + """Adiciona um novo registro no banco.""" + try: + self.session.add(obj) + self.session.commit() + return obj + except SQLAlchemyError as e: + self.session.rollback() + print(f"Erro ao adicionar registro (SQLAlchemy): {e}") + raise e + except Exception as e: + self.session.rollback() + print(f"Erro ao adicionar registro: {e}") + raise e + + def get(self, model, obj_id): + """Busca um registro pelo ID.""" + try: + return self.session.query(model).get(obj_id) + except SQLAlchemyError as e: + self.session.rollback() + print(f"Erro ao buscar registro (SQLAlchemy): {e}") + raise e + except Exception as e: + self.session.rollback() + print(f"Erro ao buscar registro: {e}") + raise e + + def get_all(self, model): + """Retorna todos os registros de um modelo.""" + try: + return self.session.query(model).all() + except SQLAlchemyError as e: + print(f"Erro ao buscar registros (SQLAlchemy): {e}") + raise e + except Exception as e: + print(f"Erro ao buscar registros: {e}") + raise e + + def update(self, obj): + """Atualiza um registro no banco.""" + try: + self.session.commit() + return obj + except SQLAlchemyError as e: + self.session.rollback() + print(f"Erro ao atualizar registro (SQLAlchemy): {e}") + raise e + except Exception as e: + self.session.rollback() + print(f"Erro ao atualizar registro: {e}") + raise e + + def delete(self, obj): + """Deleta um registro do banco.""" + try: + self.session.delete(obj) + self.session.commit() + return True + except SQLAlchemyError as e: + self.session.rollback() + print(f"Erro ao deletar registro (SQLAlchemy): {e}") + raise e + except Exception as e: + self.session.rollback() + print(f"Erro ao deletar registro: {e}") + raise e + + def rollback(self): + """Desfaz as alterações pendentes.""" + try: + self.session.rollback() + except SQLAlchemyError as e: + print(f"Erro ao desfazer alterações (SQLAlchemy): {e}") + except Exception as e: + print(f"Erro ao desfazer alterações: {e}") + + def logar_sistema(self, id_usuario, acao, tabela, id_registro=0, json="{}", status="INICIO", mensagem="", cliente=None, correlation_id=None): + """Registra uma ação no log do sistema.""" + try: + log = LogTable( + id_usuario=id_usuario, + acao=acao, + tabela=tabela, + id_registro=id_registro, + json=json, + status=status, + mensagem=mensagem, + cliente=cliente, + correlation_id=correlation_id + ) + self.add(log) + except SQLAlchemyError as e: + print(f"Erro ao logar ação (SQLAlchemy): {e}") + except Exception as e: + print(f"Erro ao logar ação: {e}") + + @staticmethod + def gerar_correlation_id(): + """Gera um ID único para rastrear toda a cadeia de uma operação.""" + return str(uuid.uuid4())[:8] + diff --git a/endpoint_listener/src/utils/enviaListerner.py b/endpoint_listener/src/utils/enviaListerner.py new file mode 100644 index 0000000..6db4635 --- /dev/null +++ b/endpoint_listener/src/utils/enviaListerner.py @@ -0,0 +1,35 @@ +import requests +import json +import os + +class enviaListerner: + + def __init__(self, logger=None): + self.url = os.getenv("LISTENER_INTERNAL_URL", "http://endpoint_listener:8000/listenerPlugnotas") + self.logger = logger + + def notificar_emissao(self, id_tecnospeed: str, documento: str, cnpj: str, status: str = "PROCESSANDO"): + """ + Envia um POST para o listener local para sincronização imediata. + """ + payload = { + "id": id_tecnospeed, + "status": status, + "documento": documento, + "emitente": cnpj + } + + try: + response = requests.post(self.url, json=payload, timeout=5) + + if self.logger: + if response.status_code in [200, 201]: + self.logger.log_logs(f"Notificação interna enviada com sucesso - ID: {id_tecnospeed} | Status: {status}") + else: + self.logger.log_errors(f"Falha na notificação interna - Status: {response.status_code} | Resposta: {response.text}") + + return response.status_code in [200, 201] + except Exception as e: + if self.logger: + self.logger.log_errors(f"Erro ao tentar notificar listener interno: {e}") + return False diff --git a/endpoint_listener/src/utils/exchangeModels.py b/endpoint_listener/src/utils/exchangeModels.py new file mode 100644 index 0000000..1e19116 --- /dev/null +++ b/endpoint_listener/src/utils/exchangeModels.py @@ -0,0 +1,127 @@ + +__version__ = "1.0.0 - 18/02/2025" # VERSAO PARA TODOS OS SISTEMAS TRANSMITINDO PARA O SERVIÇO LOGGER_CONTROLLER DO SG_IC2 +__version__ = "1.0.1 - 15/01/2026 - S.D.G. - Ajustes" +__version__ = "1.0.2 - 23/01/2026 - S.D.G. - ForwardCompatibleModel & SerializeAsAny" +from pydantic import BaseModel, field_validator, ValidationInfo, Field, ConfigDict, SerializeAsAny +from enum import StrEnum +from typing import Union, Any, Optional + +class ForwardCompatibleModel(BaseModel): + """ + Base para todos os modelos que permite campos extras. + O Pydantic V2 gerencia automaticamente o dicionário 'model_extra'. + """ + model_config = ConfigDict(extra='allow') + +class RealmType(StrEnum): + LOGS = "LOGS" + METRICS = "METRICS" + STATUS = "STATUS" + ERRORS = "ERRORS" + OPERATIONAL = "OPERATIONAL" + +class OpStatus(StrEnum): + ACTIVE = "ACTIVE" + OK = "OK" + ERROR = "ERROR" + +class ICLogs(ForwardCompatibleModel): + log_date : str + service_date : str + service : str + versionstr : str + origin : str + enterprise_id : str + enterprise_name : str + message : str + tenant : Optional[str] = Field(default='#', description="Tenant ou '#' para global") + +class ICErrors(ForwardCompatibleModel): + log_date : str + service_date : str + service : str + versionstr : str + origin : str + enterprise_id : str + enterprise_name : str + message : str + tenant : Optional[str] = Field(default='#', description="Tenant ou '#' para global") + +class ICMetrics(ForwardCompatibleModel): + metric_date : str + metric : str + metric_name : str + value : str + unit : str + tenant : Optional[str] = Field(default='#', description="Tenant ou '#' para global") + +class ICOperational(ForwardCompatibleModel): + log_date : str # YYYY-MM-DD HH:MM:S + service_date : str # YYYY-MM-DD HH:MM:S + service_name : str + versionstr : str + origin : str + enterprise_id : str + enterprise_name : str + status : OpStatus + step_status : str + message : str + +class ICStatus(ForwardCompatibleModel): + service : str + versionstr : str + system_name : str + startup_date : str + access_date : str + origin : str + sid : Optional[str] = "" + task_count : Optional[int] = 0 + cpu : Optional[float] = 0.0 + memory : Optional[float] = 0.0 + disk : Optional[float] = 0.0 + network : Optional[float] = 0.0 + +# Modelo polimórfico gerador de metadados JSON para o Exchange de Logs do Sistema Inteirador +class InfoConnector(ForwardCompatibleModel): + realm: RealmType + + # SerializeAsAny garante que se 'messageIC' for uma subclasse (como 'Operation'), + # os campos extras da subclasse (como 'reserva_id') aparecerão no JSON final. + messageIC: SerializeAsAny[Union[ICLogs, ICErrors, ICMetrics, ICStatus, ICOperational]] + + @field_validator('messageIC', mode='before') + @classmethod + def validate_schema(cls, v: Any, info: ValidationInfo) -> Any: + realm = info.data.get("realm") + if realm is None: + # Se realm não existe, o próprio Pydantic lançará erro de validação do campo 'realm' + return v + + # Mapeamento de Realm para Classe Esperada + target_map = { + RealmType.LOGS : ICLogs, + RealmType.ERRORS : ICErrors, + RealmType.METRICS : ICMetrics, + RealmType.STATUS : ICStatus, + RealmType.OPERATIONAL: ICOperational, + } + + expected_class = target_map.get(realm) + if not expected_class: + raise ValueError(f"Invalid realm: {realm}") + + # Se for um dicionário (vindo de um JSON), validamos contra a classe mapeada + if isinstance(v, dict): + return expected_class.model_validate(v) + + # Se já for um objeto instanciado (como no seu Builder), verificamos a herança + if not isinstance(v, expected_class): + raise ValueError(f"Expected {expected_class.__name__} for realm {realm}, got {type(v).__name__}") + + return v + + model_config = ConfigDict( + extra="allow", + frozen=False, + str_strip_whitespace=True + ) \ No newline at end of file diff --git a/endpoint_listener/src/utils/redis.py b/endpoint_listener/src/utils/redis.py new file mode 100644 index 0000000..a712eb1 --- /dev/null +++ b/endpoint_listener/src/utils/redis.py @@ -0,0 +1,107 @@ +import redis + +# Fornece mecanismos de Caching e Persistência Volátil/PubSub na aplicação usando Redis +class RedisClient: + def __init__(self, host='localhost', port=6379, db=0): + """ + Inicializa o cliente Redis com as configurações fornecidas. + """ + self.client = redis.Redis(host=host, port=port, db=db, decode_responses=True) + + def set(self, key, value): + """ + Define um valor para uma chave no Redis. + """ + self.client.set(key, value) + + def setex(self, key, value, ex): + """ + Define um valor para uma chave no Redis com um tempo de expiração em segundos. + """ + self.client.set(key, value, ex) + + def get(self, key): + """ + Obtém o valor de uma chave no Redis. + """ + return self.client.get(key) + + def get_keys(self, pattern="*"): + """ + Obtém todas as chaves que correspondem a um padrão. + """ + return self.client.keys(pattern) + + def get_all(self): + """ + Obtém todas as chaves e seus valores no Redis. + """ + keys = self.client.keys('*') + return {key: self.client.get(key) for key in keys} + + def delete(self, key): + """ + Remove uma chave do Redis. + """ + self.client.delete(key) + + def exists(self, key): + """ + Verifica se uma chave existe no Redis. + Retorna True se a chave existir, caso contrário, False. + """ + return self.client.exists(key) > 0 + + def close(self): + """ + Fecha a conexão com o Redis. + """ + self.client.close() + + def set_expiration(self, key, value, seconds): + """ + Define um valor para uma chave e configura um tempo de expiração em segundos. + """ + self.client.set(key, value, ex=seconds) + + def increment(self, key, amount=1): + """ + Incrementa o valor de uma chave numérica no Redis. + """ + return self.client.incr(key, amount) + + def decrement(self, key, amount=1): + """ + Decrementa o valor de uma chave numérica no Redis. + """ + return self.client.decr(key, amount) + + def hset(self, name, key, value): + """ + Define um campo em um hash no Redis. + """ + self.client.hset(name, key, value) + + def hget(self, name, key): + """ + Obtém o valor de um campo em um hash no Redis. + """ + return self.client.hget(name, key) + + def hgetall(self, name): + """ + Obtém todos os campos e valores de um hash no Redis. + """ + return self.client.hgetall(name) + + def expire(self, key, seconds): + """ + Define um tempo de expiração para uma chave existente. + """ + self.client.expire(key, seconds) + + def ttl(self, key): + """ + Obtém o tempo restante de vida (TTL) de uma chave. + """ + return self.client.ttl(key) \ No newline at end of file diff --git a/endpoint_listener/src/webhooks/webhooks.py b/endpoint_listener/src/webhooks/webhooks.py new file mode 100644 index 0000000..4106a1c --- /dev/null +++ b/endpoint_listener/src/webhooks/webhooks.py @@ -0,0 +1,64 @@ +from utils.api import Api +from utils.banco import Banco +from utils.Exchange2 import LoggerExchange2 +from utils.exchangeModels import RealmType +import json + +# Serviço responsável por vincular rotas (webhooks) por CNPJ onde a PlugNotas enviará notificações +class Webhooks: + def __init__(self, url_base, api_key, banco:Banco, logger: LoggerExchange2 = None): + self.api = Api(api_key) + self.url = f"{url_base}/empresa/" + self.banco = banco + self.logger = logger + + def _log(self, realm: RealmType, msg: str): + if self.logger: + self.logger.logIC2(realm, (msg,)) + + def consultar_webhooks(self, cnpj): + try: + resposta = self.api.get(f"{self.url}{cnpj}/webhook") + self._log(RealmType.OPERATIONAL, f"Webhook consultado - CNPJ: {cnpj}") + return resposta + except Exception as e: + self._log(RealmType.ERRORS, f"Erro ao consultar webhook: {e}") + return {"error": str(e)} + + def criar_webhook(self, cnpj, url): + try: + resposta = self.api.post(f"{self.url}{cnpj}/webhook", {"url": url}) + self._log(RealmType.OPERATIONAL, f"Webhook criado - CNPJ: {cnpj}") + return resposta + except Exception as e: + self._log(RealmType.ERRORS, f"Erro ao criar webhook: {e}") + return {"error": str(e)} + + def deletar_webhook(self, cnpj): + try: + resposta = self.api.delete(f"{self.url}{cnpj}/webhook") + self._log(RealmType.OPERATIONAL, f"Webhook deletado - CNPJ: {cnpj}") + return resposta + except Exception as e: + self._log(RealmType.ERRORS, f"Erro ao deletar webhook: {e}") + return {"error": str(e)} + + def atualizar_webhook(self, cnpj, url): + try: + resposta = self.api.put(f"{self.url}{cnpj}/webhook", {"url": url,"method": "POST"}) + self._log(RealmType.OPERATIONAL, f"Webhook atualizado - CNPJ: {cnpj}") + return resposta + except Exception as e: + self._log(RealmType.ERRORS, f"Erro ao atualizar webhook: {e}") + return {"error": str(e)} + + def testar_webhook(self, cnpj): + try: + resposta = self.api.post(f"{self.url}{cnpj}/webhook/verify", {}) + self._log(RealmType.OPERATIONAL, f"Webhook testado - CNPJ: {cnpj}") + return resposta + except Exception as e: + self._log(RealmType.ERRORS, f"Erro ao testar webhook: {e}") + return {"error": str(e)} + + \ No newline at end of file diff --git a/logs/integracao_02 b/logs/integracao_02 new file mode 100644 index 0000000..7725be4 --- /dev/null +++ b/logs/integracao_02 @@ -0,0 +1,280 @@ +2026-04-02 09:19:18,247 - integracao - INFO - { 'caller': 'receber_webhook_plugnotas', + 'client': '', + 'logDate': '2026-04-02 09:19:18.247402-03:00', + 'msg': 'Webhook recebido - Documento: nfe | Status: CONCLUIDO | ID: ' + '60a7c4f1c9d2a00015... | IDIntegracao: | Emitente:', + 'origin': '', + 'service': 'tecnospeed', + 'system': 'integracao', + 'version': '2.0.1'} +2026-04-02 09:19:18,723 - integracao - INFO - { 'caller': 'receber_webhook_plugnotas', + 'client': '', + 'logDate': '2026-04-02 09:19:18.723252-03:00', + 'msg': 'Webhook repassado ao worker com sucesso - Documento: nfe | Status: ' + 'CONCLUIDO | ID: 60a7c4f1c9d2a00015...', + 'origin': '', + 'service': 'tecnospeed', + 'system': 'integracao', + 'version': '2.0.1'} +2026-04-02 09:19:19,229 - integracao - INFO - { 'caller': 'receber_webhook_plugnotas', + 'client': '', + 'logDate': '2026-04-02 09:19:19.229234-03:00', + 'msg': 'Webhook recebido - Documento: desconhecido | Status: CONCLUIDO | ' + 'ID: | IDIntegracao: | Emitente:', + 'origin': '', + 'service': 'tecnospeed', + 'system': 'integracao', + 'version': '2.0.1'} +2026-04-02 09:19:19,720 - integracao - INFO - { 'caller': 'receber_webhook_plugnotas', + 'client': '', + 'logDate': '2026-04-02 09:19:19.719953-03:00', + 'msg': "Webhook com campos obrigatórios faltantes: ['id', 'documento'] | " + 'Payload recebido: {"status": "CONCLUIDO"}', + 'origin': '', + 'service': 'tecnospeed', + 'system': 'integracao', + 'version': '2.0.1'} +2026-04-02 09:19:20,269 - integracao - INFO - { 'caller': 'receber_webhook_plugnotas', + 'client': '', + 'logDate': '2026-04-02 09:19:20.269551-03:00', + 'msg': 'Webhook recebido - Documento: nfe | Status: CONCLUIDO | ID: ' + '60a7c4f1c9d2a00015... | IDIntegracao: | Emitente:', + 'origin': '', + 'service': 'tecnospeed', + 'system': 'integracao', + 'version': '2.0.1'} +2026-04-02 09:19:20,789 - integracao - INFO - { 'caller': 'receber_webhook_plugnotas', + 'client': '', + 'logDate': '2026-04-02 09:19:20.789712-03:00', + 'msg': 'Erro inesperado no Listener Webhook: RabbitMQ is down', + 'origin': '', + 'service': 'tecnospeed', + 'system': 'integracao', + 'version': '2.0.1'} +2026-04-02 09:19:21,302 - integracao - INFO - { 'caller': 'receber_webhook_plugnotas', + 'client': '', + 'logDate': '2026-04-02 09:19:21.302544-03:00', + 'msg': 'Webhook recebido - Documento: nfe | Status: CONCLUIDO | ID: ' + '60a7c4f1c9d2a00015... | IDIntegracao: | Emitente:', + 'origin': '', + 'service': 'tecnospeed', + 'system': 'integracao', + 'version': '2.0.1'} +2026-04-02 09:19:21,823 - integracao - INFO - { 'caller': 'receber_webhook_plugnotas', + 'client': '', + 'logDate': '2026-04-02 09:19:21.822978-03:00', + 'msg': 'Exchange não inicializado no momento da chegada do webhook.', + 'origin': '', + 'service': 'tecnospeed', + 'system': 'integracao', + 'version': '2.0.1'} +2026-04-02 09:22:41,246 - integracao - INFO - { 'caller': 'receber_webhook_plugnotas', + 'client': '', + 'logDate': '2026-04-02 09:22:41.246418-03:00', + 'msg': 'Webhook recebido - Documento: nfe | Status: CONCLUIDO | ID: ' + '60a7c4f1c9d2a00015... | IDIntegracao: | Emitente:', + 'origin': '', + 'service': 'tecnospeed', + 'system': 'integracao', + 'version': '2.0.1'} +2026-04-02 09:22:42,016 - integracao - INFO - { 'caller': 'receber_webhook_plugnotas', + 'client': '', + 'logDate': '2026-04-02 09:22:42.015935-03:00', + 'msg': 'Webhook repassado ao worker com sucesso - Documento: nfe | Status: ' + 'CONCLUIDO | ID: 60a7c4f1c9d2a00015...', + 'origin': '', + 'service': 'tecnospeed', + 'system': 'integracao', + 'version': '2.0.1'} +2026-04-02 09:22:42,465 - integracao - INFO - { 'caller': 'receber_webhook_plugnotas', + 'client': '', + 'logDate': '2026-04-02 09:22:42.465115-03:00', + 'msg': 'Webhook recebido - Documento: desconhecido | Status: CONCLUIDO | ' + 'ID: | IDIntegracao: | Emitente:', + 'origin': '', + 'service': 'tecnospeed', + 'system': 'integracao', + 'version': '2.0.1'} +2026-04-02 09:22:42,917 - integracao - INFO - { 'caller': 'receber_webhook_plugnotas', + 'client': '', + 'logDate': '2026-04-02 09:22:42.917074-03:00', + 'msg': "Webhook com campos obrigatórios faltantes: ['id', 'documento'] | " + 'Payload recebido: {"status": "CONCLUIDO"}', + 'origin': '', + 'service': 'tecnospeed', + 'system': 'integracao', + 'version': '2.0.1'} +2026-04-02 09:22:43,418 - integracao - INFO - { 'caller': 'receber_webhook_plugnotas', + 'client': '', + 'logDate': '2026-04-02 09:22:43.417823-03:00', + 'msg': 'Webhook recebido - Documento: nfe | Status: CONCLUIDO | ID: ' + '60a7c4f1c9d2a00015... | IDIntegracao: | Emitente:', + 'origin': '', + 'service': 'tecnospeed', + 'system': 'integracao', + 'version': '2.0.1'} +2026-04-02 09:22:43,878 - integracao - INFO - { 'caller': 'receber_webhook_plugnotas', + 'client': '', + 'logDate': '2026-04-02 09:22:43.877863-03:00', + 'msg': 'Erro inesperado no Listener Webhook: RabbitMQ is down', + 'origin': '', + 'service': 'tecnospeed', + 'system': 'integracao', + 'version': '2.0.1'} +2026-04-02 09:22:44,370 - integracao - INFO - { 'caller': 'receber_webhook_plugnotas', + 'client': '', + 'logDate': '2026-04-02 09:22:44.369949-03:00', + 'msg': 'Webhook recebido - Documento: nfe | Status: CONCLUIDO | ID: ' + '60a7c4f1c9d2a00015... | IDIntegracao: | Emitente:', + 'origin': '', + 'service': 'tecnospeed', + 'system': 'integracao', + 'version': '2.0.1'} +2026-04-02 09:22:44,856 - integracao - INFO - { 'caller': 'receber_webhook_plugnotas', + 'client': '', + 'logDate': '2026-04-02 09:22:44.855959-03:00', + 'msg': 'Exchange não inicializado no momento da chegada do webhook.', + 'origin': '', + 'service': 'tecnospeed', + 'system': 'integracao', + 'version': '2.0.1'} +2026-04-02 09:25:27,414 - integracao - INFO - { 'caller': 'receber_webhook_plugnotas', + 'client': '', + 'logDate': '2026-04-02 09:25:27.414587-03:00', + 'msg': 'Webhook recebido - Documento: nfe | Status: CONCLUIDO | ID: ' + '60a7c4f1c9d2a00015... | IDIntegracao: | Emitente:', + 'origin': '', + 'service': 'tecnospeed', + 'system': 'integracao', + 'version': '2.0.1'} +2026-04-02 09:25:27,924 - integracao - INFO - { 'caller': 'receber_webhook_plugnotas', + 'client': '', + 'logDate': '2026-04-02 09:25:27.924363-03:00', + 'msg': 'Webhook repassado ao worker com sucesso - Documento: nfe | Status: ' + 'CONCLUIDO | ID: 60a7c4f1c9d2a00015...', + 'origin': '', + 'service': 'tecnospeed', + 'system': 'integracao', + 'version': '2.0.1'} +2026-04-02 09:25:28,392 - integracao - INFO - { 'caller': 'receber_webhook_plugnotas', + 'client': '', + 'logDate': '2026-04-02 09:25:28.392554-03:00', + 'msg': 'Webhook recebido - Documento: desconhecido | Status: CONCLUIDO | ' + 'ID: | IDIntegracao: | Emitente:', + 'origin': '', + 'service': 'tecnospeed', + 'system': 'integracao', + 'version': '2.0.1'} +2026-04-02 09:25:28,888 - integracao - INFO - { 'caller': 'receber_webhook_plugnotas', + 'client': '', + 'logDate': '2026-04-02 09:25:28.888143-03:00', + 'msg': "Webhook com campos obrigatórios faltantes: ['id', 'documento'] | " + 'Payload recebido: {"status": "CONCLUIDO"}', + 'origin': '', + 'service': 'tecnospeed', + 'system': 'integracao', + 'version': '2.0.1'} +2026-04-02 09:25:29,399 - integracao - INFO - { 'caller': 'receber_webhook_plugnotas', + 'client': '', + 'logDate': '2026-04-02 09:25:29.399008-03:00', + 'msg': 'Webhook recebido - Documento: nfe | Status: CONCLUIDO | ID: ' + '60a7c4f1c9d2a00015... | IDIntegracao: | Emitente:', + 'origin': '', + 'service': 'tecnospeed', + 'system': 'integracao', + 'version': '2.0.1'} +2026-04-02 09:25:29,886 - integracao - INFO - { 'caller': 'receber_webhook_plugnotas', + 'client': '', + 'logDate': '2026-04-02 09:25:29.886368-03:00', + 'msg': 'Erro inesperado no Listener Webhook: RabbitMQ is down', + 'origin': '', + 'service': 'tecnospeed', + 'system': 'integracao', + 'version': '2.0.1'} +2026-04-02 09:25:30,390 - integracao - INFO - { 'caller': 'receber_webhook_plugnotas', + 'client': '', + 'logDate': '2026-04-02 09:25:30.390314-03:00', + 'msg': 'Webhook recebido - Documento: nfe | Status: CONCLUIDO | ID: ' + '60a7c4f1c9d2a00015... | IDIntegracao: | Emitente:', + 'origin': '', + 'service': 'tecnospeed', + 'system': 'integracao', + 'version': '2.0.1'} +2026-04-02 09:25:30,900 - integracao - INFO - { 'caller': 'receber_webhook_plugnotas', + 'client': '', + 'logDate': '2026-04-02 09:25:30.899973-03:00', + 'msg': 'Exchange não inicializado no momento da chegada do webhook.', + 'origin': '', + 'service': 'tecnospeed', + 'system': 'integracao', + 'version': '2.0.1'} +2026-04-02 09:48:15,931 - integracao - INFO - { 'caller': 'receber_webhook_plugnotas', + 'client': '', + 'logDate': '2026-04-02 09:48:15.931280-03:00', + 'msg': 'Webhook recebido - Documento: nfe | Status: CONCLUIDO | ID: ' + '60a7c4f1c9d2a00015... | IDIntegracao: | Emitente:', + 'origin': '', + 'service': 'tecnospeed', + 'system': 'integracao', + 'version': '2.0.1'} +2026-04-02 09:48:19,998 - integracao - INFO - { 'caller': 'receber_webhook_plugnotas', + 'client': '', + 'logDate': '2026-04-02 09:48:19.997985-03:00', + 'msg': 'Webhook repassado ao worker com sucesso - Documento: nfe | Status: ' + 'CONCLUIDO | ID: 60a7c4f1c9d2a00015...', + 'origin': '', + 'service': 'tecnospeed', + 'system': 'integracao', + 'version': '2.0.1'} +2026-04-02 09:48:21,965 - integracao - INFO - { 'caller': 'receber_webhook_plugnotas', + 'client': '', + 'logDate': '2026-04-02 09:48:21.965548-03:00', + 'msg': 'Webhook recebido - Documento: desconhecido | Status: CONCLUIDO | ' + 'ID: | IDIntegracao: | Emitente:', + 'origin': '', + 'service': 'tecnospeed', + 'system': 'integracao', + 'version': '2.0.1'} +2026-04-02 09:48:23,021 - integracao - INFO - { 'caller': 'receber_webhook_plugnotas', + 'client': '', + 'logDate': '2026-04-02 09:48:23.020872-03:00', + 'msg': "Webhook com campos obrigatórios faltantes: ['id', 'documento'] | " + 'Payload recebido: {"status": "CONCLUIDO"}', + 'origin': '', + 'service': 'tecnospeed', + 'system': 'integracao', + 'version': '2.0.1'} +2026-04-02 09:48:23,473 - integracao - INFO - { 'caller': 'receber_webhook_plugnotas', + 'client': '', + 'logDate': '2026-04-02 09:48:23.472858-03:00', + 'msg': 'Webhook recebido - Documento: nfe | Status: CONCLUIDO | ID: ' + '60a7c4f1c9d2a00015... | IDIntegracao: | Emitente:', + 'origin': '', + 'service': 'tecnospeed', + 'system': 'integracao', + 'version': '2.0.1'} +2026-04-02 09:48:23,993 - integracao - INFO - { 'caller': 'receber_webhook_plugnotas', + 'client': '', + 'logDate': '2026-04-02 09:48:23.993062-03:00', + 'msg': 'Erro inesperado no Listener Webhook: RabbitMQ is down', + 'origin': '', + 'service': 'tecnospeed', + 'system': 'integracao', + 'version': '2.0.1'} +2026-04-02 09:48:25,464 - integracao - INFO - { 'caller': 'receber_webhook_plugnotas', + 'client': '', + 'logDate': '2026-04-02 09:48:25.463810-03:00', + 'msg': 'Webhook recebido - Documento: nfe | Status: CONCLUIDO | ID: ' + '60a7c4f1c9d2a00015... | IDIntegracao: | Emitente:', + 'origin': '', + 'service': 'tecnospeed', + 'system': 'integracao', + 'version': '2.0.1'} +2026-04-02 09:48:25,893 - integracao - INFO - { 'caller': 'receber_webhook_plugnotas', + 'client': '', + 'logDate': '2026-04-02 09:48:25.893646-03:00', + 'msg': 'Exchange não inicializado no momento da chegada do webhook.', + 'origin': '', + 'service': 'tecnospeed', + 'system': 'integracao', + 'version': '2.0.1'} diff --git a/logs/integracao_27 b/logs/integracao_27 new file mode 100644 index 0000000..e69de29 diff --git a/pytest.ini b/pytest.ini new file mode 100644 index 0000000..e0d8d21 --- /dev/null +++ b/pytest.ini @@ -0,0 +1,3 @@ +[pytest] +testpaths = src/tests +pythonpath = src diff --git a/tecnospeed_queue/Dockerfile b/tecnospeed_queue/Dockerfile new file mode 100644 index 0000000..f3f2031 --- /dev/null +++ b/tecnospeed_queue/Dockerfile @@ -0,0 +1,28 @@ +FROM python:3.12-slim +ARG INSTALL_GCC + +ENV LANG=C.UTF-8 \ + LC_ALL=C.UTF-8 + +RUN apt-get update && apt-get install -y --no-install-recommends \ + locales \ + vim \ + iputils-ping \ + && locale-gen en_US.UTF-8 \ + && if [ "$INSTALL_GCC" = "true" ]; then \ + apt-get install -y --no-install-recommends build-essential curl; \ + fi \ + && apt-get clean && apt-get autoremove -y \ + && rm -rf /var/lib/apt/lists/* + +RUN pip install --no-cache-dir unidecode + +# PARTE ESPECIFICA DA IMAGEM +WORKDIR /app +RUN mkdir /app/logs +COPY requirements.txt ./ +RUN pip install --no-cache-dir -r requirements.txt + +COPY src . + +CMD [ "python", "tecnospeed_queue.py"] diff --git a/tecnospeed_queue/requirements.txt b/tecnospeed_queue/requirements.txt new file mode 100644 index 0000000..cd6db41 --- /dev/null +++ b/tecnospeed_queue/requirements.txt @@ -0,0 +1,13 @@ +sqlalchemy +pydantic +requests +datetime +redis +pytz +pika +cryptography +psycopg2-binary +fastapi +uvicorn[standard] +pytest +pytest-cov \ No newline at end of file diff --git a/tecnospeed_queue/src/Models/config.py b/tecnospeed_queue/src/Models/config.py new file mode 100644 index 0000000..76500ee --- /dev/null +++ b/tecnospeed_queue/src/Models/config.py @@ -0,0 +1,18 @@ +from pydantic import BaseModel + +# Dados de conexão ao banco de dados isolado do cliente e da API PlugNotas +class Config(BaseModel): + api_key: str | None = None + x_api_key: str | None = None + user_postgres: str + pass_postgres: str + host_postgres: str + port_postgres: str + database_postgres: str + +# Informações de mensageria para a troca de arquivos (AMQP) via subagent +class SystemConfig(BaseModel): + serviceid: str + servicekey: str + origin: str + amqps: str \ No newline at end of file diff --git a/tecnospeed_queue/src/Models/empresa.py b/tecnospeed_queue/src/Models/empresa.py new file mode 100644 index 0000000..0aad758 --- /dev/null +++ b/tecnospeed_queue/src/Models/empresa.py @@ -0,0 +1,97 @@ +from pydantic import BaseModel + + +################### +## POST /empresas # +################### +class Endereco(BaseModel): + codigoPais: str + descricaoPais: str + tipoLogradouro: str | None = None + logradouro: str + numero: str + complemento: str | None = None + tipoBairro: str | None = None + bairro: str + codigoCidade: str + descricaoCidade: str + estado: str + cep: str + +class Rps(BaseModel): + lote: int + numeracao: list + +class Email(BaseModel): + envio: bool + +class Config(BaseModel): + rps: Rps | None = None + email: Email | None = None + producao: bool | None = None + envioLimitado: bool | None = None + integracoes: list | None = None + prefeitura: dict | str | int | None = None + +class Nfse(BaseModel): + config: Config | None = None + ativo: bool = False + tipoContrato: int | None = 0 + +class Dfe(BaseModel): + primeiroNsu: bool + +class ConfigNfe(BaseModel): + dfe: Dfe | None = None + email: Email | None = None + integracoes: dict | None = None + producao: bool | None = None + impressaoFcp: bool | None = None + impressaoPartilha: bool | None = None + versaoManual: str | None = None + versaoEsquema: str | None = None + numeracao: list | None = None + calculoAutomaticoIbpt: bool | None = True + +class Nfe(BaseModel): + config: ConfigNfe | None = None + ativo: bool = False + tipoContrato: int | None = 0 + +class ConfigNfce(BaseModel): + email: Email | None = None + integracoes: dict | None = None + producao: bool | None = None + versaoManual: str | None = None + versaoEsquema: str | None = None + numeracao: list | None = None + +class Nfce(BaseModel): + config: ConfigNfce | None = None + ativo: bool = False + tipoContrato: int | None = 0 + + +# Entidade Empresa que cadastra a credencial emissora no sistema da PlugNotas +class Empresa(BaseModel): + endereco: Endereco + nfse: Nfse | None = None + nfe: Nfe | None = None + nfce: Nfce | None = None + incentivoFiscal: bool + incentivadorCultural: bool + cpfCnpj: str + inscricaoMunicipal: str + inscricaoEstadual: str | None = None + certificado: str + razaoSocial: str + nomeFantasia: str | None = None + simplesNacional: bool + regimeTributario: int + regimeTributarioEspecial: int + id: str | None = None + banco: str | None = None + email: str | None = None + id_empresa: int | None = None + + diff --git a/tecnospeed_queue/src/Models/nfe.py b/tecnospeed_queue/src/Models/nfe.py new file mode 100644 index 0000000..b6e186a --- /dev/null +++ b/tecnospeed_queue/src/Models/nfe.py @@ -0,0 +1,115 @@ +from pydantic import BaseModel +from typing import List, Optional, Literal + +EstadoEnum = Literal[ + "AC", "AL", "AM", "AP", "BA", "CE", "DF", "ES", "GO", "MA", "MG", "MS", "MT", + "PA", "PB", "PE", "PI", "PR", "RJ", "RN", "RO", "RR", "RS", "SC", "SE", "SP", "TO" +] + +TipoLogradouroEnum = Literal[ + "Alameda", "Avenida", "Chácara", "Colônia", "Condomínio", "Estância", "Estrada", + "Fazenda", "Praça", "Prolongamento", "Rodovia", "Rua", "Sítio", "Travessa", "Vicinal", "Eqnp" +] + +class Emitente(BaseModel): + cpfCnpj: str + inscricaoEstadual: Optional[str] = None + +class Endereco(BaseModel): + logradouro: str + numero: str + bairro: str + codigoCidade: str + estado: EstadoEnum + cep: str + tipoLogradouro: TipoLogradouroEnum + codigoPais: Optional[str] = "1058" + complemento: Optional[str] = None + descricaoCidade: Optional[str] = None + descricaoPais: Optional[str] = "Brasil" + +class Destinatario(BaseModel): + cpfCnpj: str + razaoSocial: str + nomeFantasia: Optional[str] = None + email: Optional[str] = None + inscricaoEstadual: Optional[str] = None + inscricaoMunicipal: Optional[str] = None + inscricaoSuframa: Optional[str] = None + orgaoPublico: Optional[bool] = False + endereco: Endereco + +class ValorTributavel(BaseModel): + comercial: float + tributavel: float + +class BaseCalculoIcms(BaseModel): + modalidadeDeterminacao: int + valor: float + +class Icms(BaseModel): + origem: str + cst: str + baseCalculo: BaseCalculoIcms + aliquota: float + valor: float + +class BaseCalculoPis(BaseModel): + valor: float + quantidade: int + +class Pis(BaseModel): + cst: str + baseCalculo: BaseCalculoPis + aliquota: float + valor: float + +class BaseCalculoCofins(BaseModel): + valor: float + +class Cofins(BaseModel): + cst: str + baseCalculo: BaseCalculoCofins + aliquota: float + valor: float + +class Tributos(BaseModel): + icms: Optional[Icms] = None + pis: Optional[Pis] = None + cofins: Optional[Cofins] = None + +class ItemNFe(BaseModel): + codigo: str + descricao: str + ncm: str + cest: Optional[str] = None + cfop: str + valorUnitario: ValorTributavel + valor: float + tributos: Tributos + +class Pagamento(BaseModel): + aVista: Optional[bool] = True + meio: Optional[str] = "01" + valor: float + +class Telefone(BaseModel): + ddd: str + numero: str + +class ResponsavelTecnico(BaseModel): + cpfCnpj: str + nome: str + email: str + telefone: Telefone + +class NFe(BaseModel): + idIntegracao: str + presencial: bool + consumidorFinal: bool + natureza: str + emitente: Emitente + destinatario: Destinatario + itens: List[ItemNFe] + pagamentos: List[Pagamento] + responsavelTecnico: Optional[ResponsavelTecnico] = None diff --git a/tecnospeed_queue/src/Models/nfse.py b/tecnospeed_queue/src/Models/nfse.py new file mode 100644 index 0000000..857ad97 --- /dev/null +++ b/tecnospeed_queue/src/Models/nfse.py @@ -0,0 +1,49 @@ +from pydantic import BaseModel +from typing import Optional + +class Prestador(BaseModel): + cpfCnpj: str + +class Endereco(BaseModel): + descricaoCidade: str + cep: str + tipoLogradouro: str + logradouro: str + tipoBairro: Optional[str] = None + codigoCidade: str + complemento: Optional[str] = None + estado: str + numero: str + bairro: str + +class Tomador(BaseModel): + cpfCnpj: str + razaoSocial: str + inscricaoMunicipal: Optional[str] = None + email: str + endereco: dict + +class ISS(BaseModel): + tipoTributacao: int + exigibilidade: int + aliquota: float + +class Valor(BaseModel): + servico: float + descontoCondicionado: Optional[float] = 0 + descontoIncondicionado: Optional[float] = 0 + +class Servico(BaseModel): + codigo: str + codigoTributacao: str + discriminacao: str + cnae: str + iss: ISS + valor: Valor + +# Estrutura padrão de envio de uma NFSe contendo Prestador, Tomador e Serviço prestado +class NFSe(BaseModel): + idIntegracao: str + prestador: Prestador + tomador: Tomador + servico: Servico diff --git a/tecnospeed_queue/src/Models/payload.py b/tecnospeed_queue/src/Models/payload.py new file mode 100644 index 0000000..841551b --- /dev/null +++ b/tecnospeed_queue/src/Models/payload.py @@ -0,0 +1,518 @@ +from enum import Enum +from pydantic import BaseModel, ConfigDict, Field + +from Models.config import Config +from Models.empresa import Empresa +from Models.nfe import NFe +from Models.nfse import NFSe +from Models.servico import Servico +from Models.tomador import Tomador + + +class Fila(str, Enum): + NFE = "nfe" + NFSE = "nfse" + CERTIFICADO_DIGITAL = "certificado_digital" + EMPRESA = "empresa" + SERVICO = "servico" + TOMADOR = "tomador" + RELATORIOS = "relatorios" + WEBHOOKS = "webhooks" + + +class NFeMetodo(str, Enum): + POST = "post" + GET = "get" + DOWNLOAD = "download" + DOWNLOAD_XML = "download_xml" + CANCELAR = "cancelar" + STATUS_CANCELAMENTO = "status_cancelamento" + CCE = "cce" + STATUS_CCE = "status_cce" + RESUMO = "resumo" + + +class NFSeMetodo(str, Enum): + POST = "post" + GET = "get" + DOWNLOAD = "download" + DOWNLOAD_XML = "download_xml" + CANCELAR = "cancelar" + STATUS_CANCELAMENTO = "status_cancelamento" + RESUMO = "resumo" + HISTORICO = "historico" + + +class CertificadoMetodo(str, Enum): + POST = "post" + GET = "get" + PUT = "put" + + +class EmpresaMetodo(str, Enum): + POST = "post" + GET = "get" + POST_FILE = "post_file" + PATCH = "patch" + + +class ServicoMetodo(str, Enum): + POST = "post" + GET = "get" + PUT = "put" + DELETE = "delete" + + +class TomadorMetodo(str, Enum): + POST = "post" + GET = "get" + PUT = "put" + DELETE = "delete" + + +class RelatorioMetodo(str, Enum): + NFE_EMITIDAS = "nfe_emitidas" + NFE_POR_CNPJ = "nfe_por_cnpj" + NFSE_EMITIDAS = "nfse_emitidas" + NFSE_POR_CNPJ = "nfse_por_cnpj" + + +class WebhookMetodo(str, Enum): + GET = "get" + POST = "post" + DELETE = "delete" + PUT = "put" + TEST = "test" + + +class BaseEnvelope(BaseModel): + cliente: str | None = None + config: Config + url_base: str | None = None + + +# --- Base Data Models --- + +class BaseDados(BaseModel): + model_config = ConfigDict(populate_by_name=True) + id_usuario: int | None = 1 + + +class NFePostDadosWrapped(BaseDados): + nfe: NFe + id_empresa: str | None = None + + +class NFePostDadosDirect(NFe): + id_usuario: int | None = 1 + id_empresa: str | None = None + + +class NFeIdDados(BaseDados): + id: str + + +class NFeCancelarDados(NFeIdDados): + justificativa: str | None = None + + +class NFeCCEDados(NFeIdDados): + correcao: str + + +class NFeResumoDados(BaseDados): + cnpj: str + id_integracao: str | None = None + id: str | None = None + + +class NFSePostDados(NFSe): + id_usuario: int | None = 1 + id_empresa: str | None = None + + +class NFSeIdDados(BaseDados): + id: str + + +class NFSeGetDados(BaseDados): + id: str | None = None + + +class NFSeResumoDados(NFSeIdDados): + cnpj: str + + +class NFSeHistoricoDados(NFSeIdDados): + cnpj: str | None = None + + +class CertificadoPostDados(BaseDados): + arquivo: str + senha: str + id_empresa: str | None = None + email: str | None = None + expiration_at: str | None = Field(None, alias="expirationAt") + data_expiracao: str | None = None + + +class CertificadoGetDados(BaseDados): + id: str | None = None + + +class CertificadoPutDados(CertificadoPostDados): + id: str + + +class EmpresaPostDados(Empresa): + id_usuario: int | None = 1 + + +class EmpresaGetDados(BaseDados): + cnpj: str | None = None + + +class EmpresaPostFileDados(BaseDados): + file: str + cnpj: str + + +class EmpresaPatchDados(BaseDados): + cnpj: str + certificado: str + + +class ServicoPostDados(Servico): + id_usuario: int | None = 1 + + +class ServicoGetDados(BaseDados): + id: str | None = None + + +class ServicoPutDados(Servico): + id: str + id_usuario: int | None = 1 + + +class ServicoDeleteDados(BaseDados): + id: str + + +class TomadorPostDados(Tomador): + id_usuario: int | None = 1 + + +class TomadorGetDados(BaseDados): + cpf_cnpj: str | None = None + + +class TomadorPutDados(Tomador): + cpf_cnpj: str + id_usuario: int | None = 1 + + +class TomadorDeleteDados(BaseDados): + cpf_cnpj: str + + +class RelatorioVazioDados(BaseDados): + pass + + +class RelatorioCnpjDados(BaseDados): + cnpj: str + + +class RelatorioPeriodoDados(BaseDados): + data_inicial: str | None = None + data_final: str | None = None + + +class RelatorioCnpjPeriodoDados(RelatorioPeriodoDados): + cnpj: str + + +class WebhookGetDeleteTestDados(BaseDados): + cnpj: str + + +class WebhookPostPutDados(WebhookGetDeleteTestDados): + url: str + + +# --- Envelopes --- + +class NFePostEnvelope(BaseEnvelope): + fila: Fila = Fila.NFE + metodo: NFeMetodo = NFeMetodo.POST + dados: NFePostDadosDirect | NFePostDadosWrapped + + +class NFeIdEnvelope(BaseEnvelope): + fila: Fila = Fila.NFE + metodo: NFeMetodo + dados: NFeIdDados + + +class NFeCancelarEnvelope(BaseEnvelope): + fila: Fila = Fila.NFE + metodo: NFeMetodo = NFeMetodo.CANCELAR + dados: NFeCancelarDados + + +class NFeCCEEnvelope(BaseEnvelope): + fila: Fila = Fila.NFE + metodo: NFeMetodo = NFeMetodo.CCE + dados: NFeCCEDados + + +class NFeResumoEnvelope(BaseEnvelope): + fila: Fila = Fila.NFE + metodo: NFeMetodo = NFeMetodo.RESUMO + dados: NFeResumoDados + + +class NFSePostEnvelope(BaseEnvelope): + fila: Fila = Fila.NFSE + metodo: NFSeMetodo = NFSeMetodo.POST + dados: NFSePostDados + + +class NFSeGetEnvelope(BaseEnvelope): + fila: Fila = Fila.NFSE + metodo: NFSeMetodo = NFSeMetodo.GET + dados: NFSeGetDados + + +class NFSeIdEnvelope(BaseEnvelope): + fila: Fila = Fila.NFSE + metodo: NFSeMetodo + dados: NFSeIdDados + + +class NFSeResumoEnvelope(BaseEnvelope): + fila: Fila = Fila.NFSE + metodo: NFSeMetodo = NFSeMetodo.RESUMO + dados: NFSeResumoDados + + +class NFSeHistoricoEnvelope(BaseEnvelope): + fila: Fila = Fila.NFSE + metodo: NFSeMetodo = NFSeMetodo.HISTORICO + dados: NFSeHistoricoDados + + +class CertificadoPostEnvelope(BaseEnvelope): + fila: Fila = Fila.CERTIFICADO_DIGITAL + metodo: CertificadoMetodo = CertificadoMetodo.POST + dados: CertificadoPostDados + + +class CertificadoGetEnvelope(BaseEnvelope): + fila: Fila = Fila.CERTIFICADO_DIGITAL + metodo: CertificadoMetodo = CertificadoMetodo.GET + dados: CertificadoGetDados + + +class CertificadoPutEnvelope(BaseEnvelope): + fila: Fila = Fila.CERTIFICADO_DIGITAL + metodo: CertificadoMetodo = CertificadoMetodo.PUT + dados: CertificadoPutDados + + +class EmpresaPostEnvelope(BaseEnvelope): + fila: Fila = Fila.EMPRESA + metodo: EmpresaMetodo = EmpresaMetodo.POST + dados: EmpresaPostDados + + +class EmpresaGetEnvelope(BaseEnvelope): + fila: Fila = Fila.EMPRESA + metodo: EmpresaMetodo = EmpresaMetodo.GET + dados: EmpresaGetDados + + +class EmpresaPostFileEnvelope(BaseEnvelope): + fila: Fila = Fila.EMPRESA + metodo: EmpresaMetodo = EmpresaMetodo.POST_FILE + dados: EmpresaPostFileDados + + +class EmpresaPatchEnvelope(BaseEnvelope): + fila: Fila = Fila.EMPRESA + metodo: EmpresaMetodo = EmpresaMetodo.PATCH + dados: EmpresaPatchDados + + +class ServicoPostEnvelope(BaseEnvelope): + fila: Fila = Fila.SERVICO + metodo: ServicoMetodo = ServicoMetodo.POST + dados: ServicoPostDados + + +class ServicoGetEnvelope(BaseEnvelope): + fila: Fila = Fila.SERVICO + metodo: ServicoMetodo = ServicoMetodo.GET + dados: ServicoGetDados + + +class ServicoPutEnvelope(BaseEnvelope): + fila: Fila = Fila.SERVICO + metodo: ServicoMetodo = ServicoMetodo.PUT + dados: ServicoPutDados + + +class ServicoDeleteEnvelope(BaseEnvelope): + fila: Fila = Fila.SERVICO + metodo: ServicoMetodo = ServicoMetodo.DELETE + dados: ServicoDeleteDados + + +class TomadorPostEnvelope(BaseEnvelope): + fila: Fila = Fila.TOMADOR + metodo: TomadorMetodo = TomadorMetodo.POST + dados: TomadorPostDados + + +class TomadorGetEnvelope(BaseEnvelope): + fila: Fila = Fila.TOMADOR + metodo: TomadorMetodo = TomadorMetodo.GET + dados: TomadorGetDados + + +class TomadorPutEnvelope(BaseEnvelope): + fila: Fila = Fila.TOMADOR + metodo: TomadorMetodo = TomadorMetodo.PUT + dados: TomadorPutDados + + +class TomadorDeleteEnvelope(BaseEnvelope): + fila: Fila = Fila.TOMADOR + metodo: TomadorMetodo = TomadorMetodo.DELETE + dados: TomadorDeleteDados + + +class RelatorioNFeEmitidasEnvelope(BaseEnvelope): + fila: Fila = Fila.RELATORIOS + metodo: RelatorioMetodo = RelatorioMetodo.NFE_EMITIDAS + dados: RelatorioVazioDados + + +class RelatorioNFeCnpjEnvelope(BaseEnvelope): + fila: Fila = Fila.RELATORIOS + metodo: RelatorioMetodo = RelatorioMetodo.NFE_POR_CNPJ + dados: RelatorioCnpjDados + + +class RelatorioNFSeEmitidasEnvelope(BaseEnvelope): + fila: Fila = Fila.RELATORIOS + metodo: RelatorioMetodo = RelatorioMetodo.NFSE_EMITIDAS + dados: RelatorioPeriodoDados + + +class RelatorioNFSeCnpjEnvelope(BaseEnvelope): + fila: Fila = Fila.RELATORIOS + metodo: RelatorioMetodo = RelatorioMetodo.NFSE_POR_CNPJ + dados: RelatorioCnpjPeriodoDados + + +class WebhookGetDeleteTestEnvelope(BaseEnvelope): + fila: Fila = Fila.WEBHOOKS + metodo: WebhookMetodo + dados: WebhookGetDeleteTestDados + + +class WebhookPostPutEnvelope(BaseEnvelope): + fila: Fila = Fila.WEBHOOKS + metodo: WebhookMetodo + dados: WebhookPostPutDados + + +# --- Generic Envelope for basic structural validation --- + +class GenericEnvelope(BaseEnvelope): + """Validates that minimum required fields exist before routing.""" + fila: Fila + metodo: str + dados: dict | None = None + + +# --- Payload Validation Router --- + +# Maps (fila, metodo) -> Envelope class +_ENVELOPE_MAP: dict[tuple[str, str], type[BaseEnvelope]] = { + # NFe + ("nfe", "post"): NFePostEnvelope, + ("nfe", "get"): NFeIdEnvelope, + ("nfe", "download"): NFeIdEnvelope, + ("nfe", "download_xml"): NFeIdEnvelope, + ("nfe", "cancelar"): NFeCancelarEnvelope, + ("nfe", "status_cancelamento"): NFeIdEnvelope, + ("nfe", "cce"): NFeCCEEnvelope, + ("nfe", "status_cce"): NFeIdEnvelope, + ("nfe", "resumo"): NFeResumoEnvelope, + # NFSe + ("nfse", "post"): NFSePostEnvelope, + ("nfse", "get"): NFSeGetEnvelope, + ("nfse", "download"): NFSeIdEnvelope, + ("nfse", "download_xml"): NFSeIdEnvelope, + ("nfse", "cancelar"): NFSeIdEnvelope, + ("nfse", "status_cancelamento"): NFSeIdEnvelope, + ("nfse", "resumo"): NFSeResumoEnvelope, + ("nfse", "historico"): NFSeHistoricoEnvelope, + # Certificado Digital + ("certificado_digital", "post"): CertificadoPostEnvelope, + ("certificado_digital", "get"): CertificadoGetEnvelope, + ("certificado_digital", "put"): CertificadoPutEnvelope, + # Empresa + ("empresa", "post"): EmpresaPostEnvelope, + ("empresa", "get"): EmpresaGetEnvelope, + ("empresa", "post_file"): EmpresaPostFileEnvelope, + ("empresa", "patch"): EmpresaPatchEnvelope, + # Serviço + ("servico", "post"): ServicoPostEnvelope, + ("servico", "get"): ServicoGetEnvelope, + ("servico", "put"): ServicoPutEnvelope, + ("servico", "delete"): ServicoDeleteEnvelope, + # Tomador + ("tomador", "post"): TomadorPostEnvelope, + ("tomador", "get"): TomadorGetEnvelope, + ("tomador", "put"): TomadorPutEnvelope, + ("tomador", "delete"): TomadorDeleteEnvelope, + # Relatórios + ("relatorios", "nfe_emitidas"): RelatorioNFeEmitidasEnvelope, + ("relatorios", "nfe_por_cnpj"): RelatorioNFeCnpjEnvelope, + ("relatorios", "nfse_emitidas"): RelatorioNFSeEmitidasEnvelope, + ("relatorios", "nfse_por_cnpj"): RelatorioNFSeCnpjEnvelope, + # Webhooks + ("webhooks", "get"): WebhookGetDeleteTestEnvelope, + ("webhooks", "post"): WebhookPostPutEnvelope, + ("webhooks", "delete"): WebhookGetDeleteTestEnvelope, + ("webhooks", "put"): WebhookPostPutEnvelope, + ("webhooks", "test"): WebhookGetDeleteTestEnvelope, +} + + +def validate_payload(payload_dict: dict) -> BaseEnvelope: + # Step 1: Basic structural validation + generic = GenericEnvelope(**payload_dict) + + fila = generic.fila.value + metodo = generic.metodo.lower() + + # Step 2: Find specific envelope + key = (fila, metodo) + envelope_cls = _ENVELOPE_MAP.get(key) + + if envelope_cls is None: + raise ValueError(f"Combinação fila/metodo desconhecida: {fila}/{metodo}") + + # Step 3: Normalize metodo in the dict to match enum lowercase values + payload_dict['metodo'] = metodo + + # Step 4: Full typed validation + return envelope_cls(**payload_dict) + + + diff --git a/tecnospeed_queue/src/Models/servico.py b/tecnospeed_queue/src/Models/servico.py new file mode 100644 index 0000000..afb717f --- /dev/null +++ b/tecnospeed_queue/src/Models/servico.py @@ -0,0 +1,150 @@ +from pydantic import BaseModel +from enum import IntEnum + +class TipoTributacao(IntEnum): + NAO_DEFINIDO = 0 + ISENTO_ISS = 1 + IMUNE = 2 + NAO_INCIDENCIA = 3 + NAO_TRIBUTAVEL = 4 + RETIDO = 5 + TRIBUTAVEL_MUNICIPIO = 6 + TRIBUTAVEL_FORA_MUNICIPIO = 7 + TRIBUTAVEL_MUNICIPIO_TOMADOR = 8 + +class TipoExigibilidade(IntEnum): + EXIGIVEL = 1 + NAO_INCIDENCIA = 2 + ISENCAO = 3 + EXPORTACAO = 4 + IMUNIDADE = 5 + SUSPENSO_ACAO_JUDICIAL = 6 + SUSPENSO_ACAO_ADMINISTRATIVA = 7 + +class ISS(BaseModel): + tipoTributacao: int #TipoTributacao + exigibilidade: int #TipoExigibilidade + retido: bool | None = None # Se o ISS foi retido na fonte + aliquota: float # Aliquota do ISS + valor: float # Valor do ISS + valorRetido: float # Valor do ISS retido + processoSuspensao: str # Numero processo de suspensão do ISS +############################################################################## +# tipoTributacao integer +# Default: 6 +# Enum: 0 1 2 3 4 5 6 7 8 +# Tipo de Tributação do Serviço, valores: +# 0 - Não definido +# 1 - Isento de ISS +# 2 - Imune +# 3 - Não Incidência no Município +# 4 - Não Tributável +# 5 - Retido +# 6 - Tributável Dentro do Município +# 7 - Tributável Fora do Município +# 8 - Tributável Dentro do Município pelo tomador +############################################################################## +# exigibilidade integer +# Default: 1 +# Enum: 1 2 3 4 5 6 7 +# Exigibilidade do ISS, valores: +# 1 - Exigível +# 2 - Não Incidência +# 3 - Isenção +# 4 - Exportação +# 5 - Imunidade +# 6 - Supenso por Ação Judicial +# 7 - Suspenso por Ação Administrativa +############################################################################## +# responsavelRetencao string +# Valores aceitos: +# 1 - Tomador +# 2 - Intermediário +############################################################################## + +class Obra(BaseModel): + art: str # Anotação de Responsabilidade Técnica (Codigo ART) + codigo: str # Código da obra + +class Valor(BaseModel): + servico: float # Valor bruto do serviço prestado. + baseCalculo: float # Valor da base de cálculo dos impostos. + deducoes: float # Valor deduções. + descontoCondicionado: float # Valor do desconto condicionado. + descontoIncondicionado: float # Valor do desconto incondicionado. + liquido: float # Valor liquido do serviço, caso você não informe esse campo, vamos calcular automáticamente. + unitario: float # Valor unitário do serviço. + valorAproximadoTributos: float # Valor aproximado dos tributos. + +class Deducao(BaseModel): + tipo: int + descricao: str # Descrição da dedução +############################################################################## +# tipo integer +# Tipo de dedução: +# 0 - Sem Deduções +# 1 - Materiais +# 2 - Subempreitada de Mão de Obra +# 3 - Serviços +# 4 - Produção Externa +# 5 - Alimentação e Bebidas +# 6 - Reembolso de Despesas +# 7 - Repasse Consorciado +# 8 - Repasse Plano de Saúde +# 99 - Outras Deduções +############################################################################## + +class RetencaoInterno(BaseModel): + aliquota: float # Aliquota da retenção + valor: float # Valor da retenção + +class Retencao(BaseModel): + pis: RetencaoInterno + cofins: RetencaoInterno + csll: RetencaoInterno + inss: RetencaoInterno + irrf: RetencaoInterno + outrasRetencoes: float # Valor de outras retenções + cpp: RetencaoInterno + +class Aliquota(BaseModel): + municipal: float # Aliquota municipal + estadual: float # Aliquota estadual + federal: float # Aliquota federal + +class Detalhado(BaseModel): + aliquota: Aliquota + +class Simplificado(BaseModel): + aliquota: float # Aliquota geral de impostos + +class IBPT(BaseModel): + simplificado: Simplificado + detalhado: Detalhado + +# Representa o detalhamento do serviço prestado na NFSe, impostos incidentes e deduções +class Servico(BaseModel): + codigo: str # Código de serviço, também conhecido como Código Item Lista Serviço, geralmente segue a LC116, podendo váriar de acordo com a prefeitura.Você pode encontrar esse dado no portal da prefeitura, em uma nota emitida, ou junto ao contador. + idIntegracao: str # Código EXTERNO de integração do serviço. Utilizado SOMENTE quando se cadastra previamente um serviço. + discriminacao: str # Descrição do serviço prestado + codigoTributacao: str # Código tributação no Município + cnae: str # O código CNAE ou, por extenso, Classificação Nacional de Atividades Econômicas, é uma forma de padronizar, em todo o território nacional, os códigos de atividades econômicas e os critérios de enquadramento usados pelos mais diversos órgãos da administração tributária do Brasil. + codigoCidadeIncidencia: str # Código IBGE da cidade de incidência do ISS. + descricaoCidadeIncidencia: str # Descrição da cidade de incidência do ISS. + unidade: str # Unidade do serviço prestado. + quantidade: int # Quantidade dos serviços prestados. + iss: ISS + obra: Obra + valor: Valor + deducao: Deducao + retencao: Retencao + tributavel: bool # Indica se o serviço está sujeito a tributação. + ibpt: IBPT + responsavelRetencao: str + +############################################################################## +# responsavelRetencao Valores aceitos: +# 1 - Tomador +# 2 - Intermediário +# Caso o responsável pela retenção seja o próprio prestador, não gerar o campo. +############################################################################## diff --git a/tecnospeed_queue/src/Models/tomador.py b/tecnospeed_queue/src/Models/tomador.py new file mode 100644 index 0000000..4e3cf5a --- /dev/null +++ b/tecnospeed_queue/src/Models/tomador.py @@ -0,0 +1,34 @@ +from pydantic import BaseModel + + +class Endereco(BaseModel): + tipoLogradouro: str + logradouro: str + numero: str + complemento: str + tipoBairro: str + bairro: str + codigoPais: str + descricaoPais: str + codigoCidade: str + descricaoCidade: str + estado: str + cep: str + +class Telefone(BaseModel): + ddd: str + numero: str + +# Representa o cliente (Tomador do Serviço) vinculado a uma nota fiscal de serviço +class Tomador(BaseModel): + cpfCnpj: str + inscricaoMunicipal: str + inscricaoEstadual: str + inscricaoSuframa: str + indicadorInscricaoEstadual: int + razaoSocial: str + nomeFantasia: str + endereco: Endereco + telefone: Telefone + email: str + orgaoPublico: bool diff --git a/tecnospeed_queue/src/NFSe/Servico/servico.py b/tecnospeed_queue/src/NFSe/Servico/servico.py new file mode 100644 index 0000000..4787089 --- /dev/null +++ b/tecnospeed_queue/src/NFSe/Servico/servico.py @@ -0,0 +1,105 @@ +from Models.servico import Servico +from utils.api import Api +from utils.banco import Banco, ServicoTable +from utils.Exchange2 import LoggerExchange2 +from utils.exchangeModels import RealmType +from sqlalchemy.exc import SQLAlchemyError +import json + +# Controller responsável pelos cadastros e busca de Serviços a serem vinculados em NFSe +class ServicoController: + def __init__(self, url_base, api_key, banco: Banco, logger: LoggerExchange2 = None): + self.api = Api(api_key) + self.url = f"{url_base}/nfse/servico" + self.banco = banco + self.logger = logger + + def _log(self, realm: RealmType, msg: str): + if self.logger: + self.logger.logIC2(realm, (msg,)) + + def cadastrar_servico(self, servico: Servico, data: dict): + try: + resposta = self.api.post(self.url, servico.model_dump()) + if 'error' in resposta: + self._log(RealmType.ERRORS, f"Erro na API ao cadastrar serviço") + return resposta + + servico_novo = ServicoTable( + descricao=servico.discriminacao, + id_tecnospeed=resposta['data']['id'], + id_usuario=data.get('id_usuario', 1), + status=True + ) + self.banco.add(servico_novo) + self._log(RealmType.OPERATIONAL, f"Serviço cadastrado com sucesso - ID: {resposta['data']['id']}") + return resposta + except SQLAlchemyError as e: + self.banco.rollback() + self._log(RealmType.ERRORS, f"Erro no banco ao cadastrar serviço: {e}") + return {"error": f"Erro ao cadastrar serviço: {str(e)}"} + except Exception as e: + return {"error": f"Erro inesperado: {str(e)}"} + + def buscar_servico(self, id_servico, data: dict = None): + try: + resposta = self.api.get(f"{self.url}/{id_servico}") + self._log(RealmType.OPERATIONAL, f"Serviço consultado - ID: {id_servico}") + return resposta + except Exception as e: + self._log(RealmType.ERRORS, f"Erro ao buscar serviço: {e}") + return {"error": f"Erro ao buscar serviço: {str(e)}"} + + def listar_servicos(self, data: dict = None): + try: + servicos = self.banco.session.query(ServicoTable.id_tecnospeed, ServicoTable.descricao, ServicoTable.status).all() + resultado = [{"id_tecnospeed": servico.id_tecnospeed, "descricao": servico.descricao, "status": servico.status} for servico in servicos] + self._log(RealmType.OPERATIONAL, f"Listagem de serviços - {len(resultado)} registros") + return resultado + except SQLAlchemyError as e: + self._log(RealmType.ERRORS, f"Erro ao listar serviços: {e}") + return {"error": f"Erro ao listar serviços: {str(e)}"} + except Exception as e: + return {"error": f"Erro inesperado: {str(e)}"} + + def atualizar_servico(self, id_servico, servico: Servico): + try: + resposta = self.api.patch(f"{self.url}/{id_servico}", servico.model_dump()) + servico_existente = self.banco.session.query(ServicoTable).filter_by(id=id_servico).first() + + if not servico_existente: + self._log(RealmType.ERRORS, f"Serviço não encontrado - ID: {id_servico}") + return {"error": "Serviço não encontrado"} + + servico_existente.descricao = servico.discriminacao + self.banco.session.commit() + + self._log(RealmType.OPERATIONAL, f"Serviço atualizado com sucesso - ID: {id_servico}") + return resposta + except SQLAlchemyError as e: + self.banco.rollback() + self._log(RealmType.ERRORS, f"Erro ao atualizar serviço: {e}") + return {"error": f"Erro ao atualizar serviço: {str(e)}"} + except Exception as e: + return {"error": f"Erro inesperado: {str(e)}"} + + def deletar_servico(self, id_servico): + try: + resposta = self.api.delete(f"{self.url}/{id_servico}") + servico_existente = self.banco.session.query(ServicoTable).filter_by(id=id_servico).first() + + if not servico_existente: + self._log(RealmType.ERRORS, f"Serviço não encontrado para deletar - ID: {id_servico}") + return {"error": "Serviço não encontrado"} + + self.banco.session.delete(servico_existente) + self.banco.session.commit() + + self._log(RealmType.OPERATIONAL, f"Serviço deletado com sucesso - ID: {id_servico}") + return resposta + except SQLAlchemyError as e: + self.banco.rollback() + self._log(RealmType.ERRORS, f"Erro ao deletar serviço: {e}") + return {"error": f"Erro ao deletar serviço: {str(e)}"} + except Exception as e: + return {"error": f"Erro inesperado: {str(e)}"} \ No newline at end of file diff --git a/tecnospeed_queue/src/NFSe/Tomador/tomador.py b/tecnospeed_queue/src/NFSe/Tomador/tomador.py new file mode 100644 index 0000000..e10cc78 --- /dev/null +++ b/tecnospeed_queue/src/NFSe/Tomador/tomador.py @@ -0,0 +1,91 @@ +from utils.api import Api +from utils.banco import Banco, TomadorTable +from utils.Exchange2 import LoggerExchange2 +from utils.exchangeModels import RealmType +from Models.tomador import Tomador + +import json + +# Controller para gerenciamento no banco e na API dos Tomadores (clientes finais) das notas +class TomadorController: + def __init__(self, url_base, api_key, banco: Banco, logger: LoggerExchange2 = None): + self.api = Api(api_key) + self.url = f"{url_base}/nfse/tomador" + self.banco = banco + self.logger = logger + + def _log(self, realm: RealmType, msg: str): + if self.logger: + self.logger.logIC2(realm, (msg,)) + + def cadastrar_tomador(self, tomador: Tomador, data: dict): + try: + retorno = self.api.post(self.url, tomador.model_dump()) + if 'error' in retorno: + self._log(RealmType.ERRORS, f"Erro na API ao cadastrar tomador") + return retorno + + tomador_novo = TomadorTable( + id_tecnospeed=retorno['id'], + nome=tomador.razaoSocial, + cpf_cnpj=tomador.cpfCnpj, + email=tomador.email, + telefone=f"({tomador.telefone.ddd}) {tomador.telefone.numero}", + id_usuario=data.get('id_usuario', 1), + status=True + ) + + self.banco.add(tomador_novo) + self._log(RealmType.OPERATIONAL, f"Tomador cadastrado com sucesso - CPF/CNPJ: {tomador.cpfCnpj}") + return retorno + except Exception as e: + self._log(RealmType.ERRORS, f"Erro ao cadastrar tomador: {e}") + return {"error": str(e)} + + def buscar_tomador(self, cpf_cnpj): + try: + tomador = self.api.get(f"{self.url}/{cpf_cnpj}") + self._log(RealmType.OPERATIONAL, f"Tomador consultado - CPF/CNPJ: {cpf_cnpj}") + return tomador + except Exception as e: + self._log(RealmType.ERRORS, f"Erro ao buscar tomador: {e}") + return {"error": str(e)} + + def atualizar_tomador(self, cpf_cnpj, tomador: Tomador): + try: + tomador_existente = self.banco.session.query(TomadorTable).filter_by(cpf_cnpj=cpf_cnpj).first() + if not tomador_existente: + self._log(RealmType.ERRORS, f"Tomador não encontrado - CPF/CNPJ: {cpf_cnpj}") + return {"error": "Tomador não encontrado"} + + resposta = self.api.patch(f"{self.url}/{cpf_cnpj}", tomador.model_dump()) + tomador_existente.nome = tomador.razaoSocial + tomador_existente.email = tomador.email + tomador_existente.telefone = f"({tomador.telefone.ddd}) {tomador.telefone.numero}" + + self.banco.session.commit() + self._log(RealmType.OPERATIONAL, f"Tomador atualizado com sucesso - CPF/CNPJ: {cpf_cnpj}") + return resposta + except Exception as e: + self.banco.session.rollback() + self._log(RealmType.ERRORS, f"Erro ao atualizar tomador: {e}") + return {"error": str(e)} + + def deletar_tomador(self, cpf_cnpj): + try: + tomador_existente = self.banco.session.query(TomadorTable).filter_by(cpf_cnpj=cpf_cnpj).first() + if not tomador_existente: + self._log(RealmType.ERRORS, f"Tomador não encontrado para deletar - CPF/CNPJ: {cpf_cnpj}") + return {"error": "Tomador não encontrado"} + + resposta = self.api.delete(f"{self.url}/{cpf_cnpj}") + self.banco.session.delete(tomador_existente) + self.banco.session.commit() + + self._log(RealmType.OPERATIONAL, f"Tomador deletado com sucesso - CPF/CNPJ: {cpf_cnpj}") + return resposta + except Exception as e: + self.banco.session.rollback() + self._log(RealmType.ERRORS, f"Erro ao deletar tomador: {e}") + return {"error": str(e)} + diff --git a/tecnospeed_queue/src/NFSe/nfse.py b/tecnospeed_queue/src/NFSe/nfse.py new file mode 100644 index 0000000..cd432e7 --- /dev/null +++ b/tecnospeed_queue/src/NFSe/nfse.py @@ -0,0 +1,167 @@ +from Models.nfse import NFSe +from utils.api import Api +from utils.banco import Banco, NFSeTable +from utils.Exchange2 import LoggerExchange2 +from utils.exchangeModels import RealmType +from utils.enviaListerner import enviaListerner + +import json + +# Serviço responsável pelo controle, emissão e cancelamento das Notas Fiscais de Serviço Eletrônicas (NFSe) +class NFSeService: + def __init__(self, api_key, url_base, banco: Banco, logger: LoggerExchange2 = None): + self.api = Api(api_key) + self.url = f"{url_base}/nfse" + self.banco = banco + self.logger = logger + self.notificador = enviaListerner(logger=logger) if logger else None + + def _log(self, realm: RealmType, msg: str): + if self.logger: + self.logger.logIC2(realm, (msg,)) + + def cadastrar_nfse(self, nfse: NFSe, data: dict): + try: + id_empresa = data.get('id_empresa', "0") + resposta = self.api.post(self.url, [nfse.model_dump()]) + if 'error' in resposta: + self._log(RealmType.ERRORS, f"Erro na API ao cadastrar NFSe: {resposta.get('error')}") + return resposta + + # A API da Plugnotas retorna 'documents' contendo uma lista com os sucessos + documentos = resposta.get('documents', []) + if not documentos: + self._log(RealmType.ERRORS, f"Nenhum documento retornado pela API. Resposta: {resposta}") + return resposta + + id_recebido = documentos[0].get('id') + if not id_recebido: + raise Exception("ID do documento não encontrado na resposta.") + + nfse_nova = NFSeTable( + id_tecnospeed=id_recebido, + id_empresa=id_empresa, + id_usuario=data.get('id_usuario', 1), + status=True + ) + + self.banco.add(nfse_nova) + self._log(RealmType.OPERATIONAL, f"NFSe cadastrada com sucesso - ID: {id_recebido}") + + # Notifica o listener interno para sincronização imediata + if self.notificador: + self.notificador.notificar_emissao(id_tecnospeed=id_recebido, documento="nfse", cnpj=id_empresa) + + return resposta + except Exception as e: + self._log(RealmType.ERRORS, f"Erro ao cadastrar NFSe: {e}") + raise Exception(f"Erro ao cadastrar NFSe: {e}") + + def buscar_nfse(self, id_nfse): + try: + resposta = self.api.get(f"{self.url}/{id_nfse}") + self._log(RealmType.OPERATIONAL, f"NFSe consultada com sucesso - ID: {id_nfse}") + return resposta + except Exception as e: + self._log(RealmType.ERRORS, f"Erro ao buscar NFSe: {e}") + raise Exception(f"Erro ao buscar NFSe: {e}") + + def listar_nfse(self): + try: + nfse = self.banco.session.query(NFSeTable.id, NFSeTable.id_tecnospeed).all() + resultado = [{"id": n[0], "descricao": n[1]} for n in nfse] + self._log(RealmType.OPERATIONAL, f"Listagem de NFSe - {len(resultado)} registros") + return resultado + except Exception as e: + self._log(RealmType.ERRORS, f"Erro ao listar NFSe: {e}") + raise Exception(f"Erro ao listar NFSe: {e}") + + def baixar_nfse(self, id_nfse): + try: + resposta = self.api.get(f"{self.url}/pdf/{id_nfse}") + self._log(RealmType.OPERATIONAL, f"PDF NFSe baixado com sucesso - ID: {id_nfse}") + return resposta + except Exception as e: + self._log(RealmType.ERRORS, f"Erro ao baixar NFSe: {e}") + raise Exception(f"Erro ao baixar NFSe: {e}") + + def baixar_xml_nfse(self, id_nfse): + try: + resposta = self.api.get(f"{self.url}/xml/{id_nfse}") + self._log(RealmType.OPERATIONAL, f"XML NFSe baixado com sucesso - ID: {id_nfse}") + return resposta + except Exception as e: + self._log(RealmType.ERRORS, f"Erro ao baixar XML NFSe: {e}") + raise Exception(f"Erro ao baixar XML NFSe: {e}") + + def solicitar_cancelamento_nfse(self, id_nfse): + try: + resposta = self.api.post(f"{self.url}/cancelar/{id_nfse}", {}) + self._log(RealmType.OPERATIONAL, f"Cancelamento NFSe solicitado - ID: {id_nfse}") + return resposta + except Exception as e: + self._log(RealmType.ERRORS, f"Erro ao solicitar cancelamento NFSe: {e}") + raise Exception(f"Erro ao solicitar cancelamento NFSe: {e}") + + def consultar_status_cancelamento_nfse(self, id_nfse): + try: + resposta = self.api.get(f"{self.url}/cancelar/status/{id_nfse}") + self._log(RealmType.OPERATIONAL, f"Status cancelamento NFSe consultado - ID: {id_nfse}") + return resposta + except Exception as e: + self._log(RealmType.ERRORS, f"Erro ao consultar status cancelamento NFSe: {e}") + raise Exception(f"Erro ao consultar status cancelamento NFSe: {e}") + + def consultar_resumo_nfse(self, id_nfse, cnpj): + try: + resposta = self.api.get(f"{self.url}/consultar/{id_nfse}/{cnpj}") + self._log(RealmType.OPERATIONAL, f"Resumo NFSe consultado - ID: {id_nfse}") + return resposta + except Exception as e: + self._log(RealmType.ERRORS, f"Erro ao consultar resumo NFSe: {e}") + raise Exception(f"Erro ao consultar resumo NFSe: {e}") + + def baixar_histotico_nfse(self, id_nfse, cnpj = None): + try: + resposta = self.api.get(f"{self.url}/historico/{id_nfse}") if cnpj is None else self.api.get(f"{self.url}/historico/{id_nfse}/{cnpj}") + self._log(RealmType.OPERATIONAL, f"Histórico NFSe baixado - ID: {id_nfse}") + return resposta + except Exception as e: + self._log(RealmType.ERRORS, f"Erro ao baixar histórico NFSe: {e}") + raise Exception(f"Erro ao baixar histórico NFSe: {e}") + + def enviar_email_nfse(self, id_nfse, email:list[str], reenvio:bool = False): + try: + resposta = self.api.post(f"{self.url}/email/{id_nfse}", {"reenvio": reenvio,"destinatarios": email}) + self._log(RealmType.OPERATIONAL, f"Email NFSe enviado para {email} - ID: {id_nfse}") + return resposta + except Exception as e: + self._log(RealmType.ERRORS, f"Erro ao enviar email NFSe: {e}") + raise Exception(f"Erro ao enviar email NFSe: {e}") + + def resolver_nfse(self, id_nfse, identificacaoNota:str): + try: + resposta = self.api.post(f"{self.url}/resolve/{id_nfse}", {"identificacaoNota": identificacaoNota}) + self._log(RealmType.OPERATIONAL, f"NFSe resolvida com sucesso - ID: {id_nfse}") + return resposta + except Exception as e: + self._log(RealmType.ERRORS, f"Erro ao resolver NFSe: {e}") + raise Exception(f"Erro ao resolver NFSe: {e}") + + def consultar_nfse_periodo(self, data_inicio, data_fim): + try: + resposta = self.api.get_paginated(f"{self.url}/periodo/{data_inicio}/{data_fim}") + self._log(RealmType.OPERATIONAL, f"NFSe consultada por período: {data_inicio} a {data_fim}") + return resposta + except Exception as e: + self._log(RealmType.ERRORS, f"Erro ao consultar NFSe por período: {e}") + raise Exception(f"Erro ao consultar NFSe por período: {e}") + + def baixar_recibo_provisorio_servico(self, id_nfse, cnpj = None): + try: + resposta = self.api.get(f"{self.url}/rps/pdf/{id_nfse}") if cnpj is None else self.api.get(f"{self.url}/rps/pdf/{id_nfse}/{cnpj}") + self._log(RealmType.OPERATIONAL, f"RPS baixado com sucesso - ID: {id_nfse}") + return resposta + except Exception as e: + self._log(RealmType.ERRORS, f"Erro ao baixar recibo provisório de serviço: {e}") + raise Exception(f"Erro ao baixar recibo provisório de serviço: {e}") diff --git a/tecnospeed_queue/src/NFe/registroNFe.py b/tecnospeed_queue/src/NFe/registroNFe.py new file mode 100644 index 0000000..96c54ff --- /dev/null +++ b/tecnospeed_queue/src/NFe/registroNFe.py @@ -0,0 +1,126 @@ +import json as json_lib +from utils.api import Api +from utils.banco import Banco, NFeTable +from Models.nfe import NFe +from typing import List +from utils.Exchange2 import LoggerExchange2 +from utils.exchangeModels import RealmType +from utils.enviaListerner import enviaListerner + +class RegistroNFeService: + # Classe responsável por fornecer as operações de comunicação com a PlugNotas para o documento NFe + def __init__(self, api_key: str, url_base: str, banco: Banco, logger: LoggerExchange2 = None): + self.api = Api(api_key) + self.url = f"{url_base}/nfe" + self.banco = banco + self.logger = logger + self.notificador = enviaListerner(logger=logger) if logger else None + + def cadastrar_nfe(self, nfe_list: List[NFe], data: dict): + # Envia uma lista de NFes em lote ou unitária para emissão e registra no banco de dados + try: + id_empresa = data.get('id_empresa', "0") + payload = [nfe.model_dump() for nfe in nfe_list] + resposta = self.api.post(self.url, payload) + if 'error' in resposta: + id_usr = data.get('id_usuario', 1) + if self.logger: self.logger.logIC2(RealmType.OPERATIONAL, ('INSERT', 'nfe', id_usr, '0', 'ERRO', f"Erro na API: {resposta.get('error')}", json_lib.dumps(resposta))) + return resposta + if 'documents' in resposta: + for doc in resposta['documents']: + id_tecnospeed = doc.get('id') + if id_tecnospeed: + nfe_nova = NFeTable( + id_tecnospeed=id_tecnospeed, + id_empresa=id_empresa, + id_usuario=data.get('id_usuario', 1), + status=True + ) + self.banco.add(nfe_nova) + id_usr = data.get('id_usuario', 1) + if self.logger: self.logger.logIC2(RealmType.OPERATIONAL, ('INSERT', 'nfe', id_usr, str(nfe_nova.id), 'SUCESSO', 'NFe cadastrada com sucesso', json_lib.dumps(doc))) + + # Notifica o listener interno para sincronização imediata + if self.notificador: + self.notificador.notificar_emissao(id_tecnospeed=id_tecnospeed, documento="nfe", cnpj=id_empresa) + + return resposta + except Exception as e: + if self.logger: self.logger.logIC2(RealmType.OPERATIONAL, ('INSERT', 'nfe', data.get('id_usuario', 1), '0', 'ERRO', f"Erro ao cadastrar NFe: {e}", "{}")) + raise Exception(f"Erro ao cadastrar NFe: {e}") + + def cancelar_nfe(self, id_nota: str, justificativa: str): + # Solicita o cancelamento de uma NFe existente mediante uma justificativa + try: + resposta = self.api.post(f"{self.url}/{id_nota}/cancelamento", {"justificativa": justificativa}) + if self.logger: self.logger.logIC2(RealmType.OPERATIONAL, ('DELETE', 'nfe', 1, id_nota, 'SUCESSO', 'NFe cancelada com sucesso', json_lib.dumps(resposta))) + return resposta + except Exception as e: + if self.logger: self.logger.logIC2(RealmType.OPERATIONAL, ('DELETE', 'nfe', 1, id_nota, 'ERRO', f"Erro ao cancelar NFe: {e}", "{}")) + raise Exception(f"Erro ao cancelar NFe: {e}") + + def enviar_cce(self, id_nota: str, correcao: str): + # Envia uma Carta de Correção Eletrônica (CCe) para retificar uma NFe já emitida + try: + resposta = self.api.post(f"{self.url}/{id_nota}/cce", {"correcao": correcao}) + if self.logger: self.logger.logIC2(RealmType.OPERATIONAL, ('UPDATE', 'nfe', 1, id_nota, 'SUCESSO', 'CCe enviada com sucesso', json_lib.dumps(resposta))) + return resposta + except Exception as e: + if self.logger: self.logger.logIC2(RealmType.OPERATIONAL, ('UPDATE', 'nfe', 1, id_nota, 'ERRO', f"Erro ao enviar CCe NFe: {e}", "{}")) + raise Exception(f"Erro ao enviar CCe NFe: {e}") + + def consultar_resumo(self, id_nota: str): + # Consulta o resumo e o status atual da NFe na PlugNotas pelo seu ID + try: + resposta = self.api.get(f"{self.url}/{id_nota}/resumo") + if self.logger: self.logger.logIC2(RealmType.OPERATIONAL, ('SELECT', 'nfe', 1, id_nota, 'SUCESSO', 'Resumo de NFe consultado', json_lib.dumps(resposta))) + return resposta + except Exception as e: + if self.logger: self.logger.logIC2(RealmType.OPERATIONAL, ('SELECT', 'nfe', 1, id_nota, 'ERRO', f"Erro ao consultar resumo NFe: {e}", "{}")) + raise Exception(f"Erro ao consultar resumo NFe: {e}") + + def consultar_resumo_por_cnpj_integracao(self, cnpj: str, id_integracao: str): + try: + resposta = self.api.get(f"{self.url}/{cnpj}/{id_integracao}/resumo") + if self.logger: self.logger.logIC2(RealmType.OPERATIONAL, ('SELECT', 'nfe', id_integracao, id_integracao, 'SUCESSO', 'Resumo por CNPJ/Integração consultado', json_lib.dumps(resposta))) + return resposta + except Exception as e: + if self.logger: self.logger.logIC2(RealmType.OPERATIONAL, ('SELECT', 'nfe', id_integracao, id_integracao, 'ERRO', f"Erro ao consultar resumo NFe por CNPJ/Integração: {e}", "{}")) + raise Exception(f"Erro ao consultar resumo NFe por CNPJ/Integração: {e}") + + def consultar_status_cancelamento(self, id_nota: str): + try: + resposta = self.api.get(f"{self.url}/{id_nota}/cancelamento/status") + if self.logger: self.logger.logIC2(RealmType.OPERATIONAL, ('SELECT', 'nfe', id_nota, id_nota, 'SUCESSO', 'Status de cancelamento consultado', json_lib.dumps(resposta))) + return resposta + except Exception as e: + if self.logger: self.logger.logIC2(RealmType.OPERATIONAL, ('SELECT', 'nfe', id_nota, id_nota, 'ERRO', f"Erro ao consultar status de cancelamento NFe: {e}", "{}")) + raise Exception(f"Erro ao consultar status de cancelamento NFe: {e}") + + def consultar_status_cce(self, id_nota: str): + try: + resposta = self.api.get(f"{self.url}/{id_nota}/cce/status") + if self.logger: self.logger.logIC2(RealmType.OPERATIONAL, ('SELECT', 'nfe', id_nota, id_nota, 'SUCESSO', 'Status CCe consultado', json_lib.dumps(resposta))) + return resposta + except Exception as e: + if self.logger: self.logger.logIC2(RealmType.OPERATIONAL, ('SELECT', 'nfe', id_nota, id_nota, 'ERRO', f"Erro ao consultar status CCe NFe: {e}", "{}")) + raise Exception(f"Erro ao consultar status CCe NFe: {e}") + + def baixar_pdf(self, id_nota: str): + # Obtém o link para download ou o stream do PDF da Nota Fiscal processada + try: + resposta = self.api.get(f"{self.url}/{id_nota}/pdf") + if self.logger: self.logger.logIC2(RealmType.OPERATIONAL, ('DOWNLOAD (PDF)', 'nfe', id_nota, id_nota, 'SUCESSO', 'PDF baixado com sucesso', json_lib.dumps(resposta) if isinstance(resposta, dict) else '{}')) + return resposta + except Exception as e: + if self.logger: self.logger.logIC2(RealmType.OPERATIONAL, ('DOWNLOAD (PDF)', 'nfe', id_nota, id_nota, 'ERRO', f"Erro ao baixar PDF NFe: {e}", "{}")) + raise Exception(f"Erro ao baixar PDF NFe: {e}") + + def baixar_xml(self, id_nota: str): + try: + resposta = self.api.get(f"{self.url}/{id_nota}/xml") + if self.logger: self.logger.logIC2(RealmType.OPERATIONAL, ('DOWNLOAD (XML)', 'nfe', id_nota, id_nota, 'SUCESSO', 'XML baixado com sucesso', json_lib.dumps(resposta) if isinstance(resposta, dict) else '{}')) + return resposta + except Exception as e: + if self.logger: self.logger.logIC2(RealmType.OPERATIONAL, ('DOWNLOAD (XML)', 'nfe', id_nota, id_nota, 'ERRO', f"Erro ao baixar XML NFe: {e}", "{}")) + raise Exception(f"Erro ao baixar XML NFe: {e}") diff --git a/tecnospeed_queue/src/Relatorios/relatorios.py b/tecnospeed_queue/src/Relatorios/relatorios.py new file mode 100644 index 0000000..1155445 --- /dev/null +++ b/tecnospeed_queue/src/Relatorios/relatorios.py @@ -0,0 +1,70 @@ +from utils.api import Api +from utils.banco import Banco +from utils.Exchange2 import LoggerExchange2 +from utils.exchangeModels import RealmType + +from datetime import datetime, timedelta +import json + +# Extração e consumo de relatórios massivos de emissões diretamente da API e consolidações em lotes +class Relatorios: + def __init__(self, url_base, api_key, banco: Banco, logger: LoggerExchange2 = None): + self.api = Api(api_key) + self.url = f"{url_base}/" + self.banco = banco + self.logger = logger + + def _log(self, realm: RealmType, msg: str): + if self.logger: + self.logger.logIC2(realm, (msg,)) + + def nfe_emitidas(self, data: dict = None): + try: + resposta = self.api.get_paginated(f"{self.url}nfe/relatorio") + self._log(RealmType.OPERATIONAL, "Relatório de NFe emitidas consultado") + return resposta + except Exception as e: + msg_erro = f"Erro ao consultar relatório NFe emitidas: {e}" + self._log(RealmType.ERRORS, msg_erro) + return {"error": msg_erro} + + def nfe_por_cnpj(self, cnpj, data: dict = None): + try: + resposta = self.api.get_paginated(f"{self.url}nfe/{cnpj}/relatorio") + self._log(RealmType.OPERATIONAL, f"Relatório de NFe por CNPJ: {cnpj}") + return resposta + except Exception as e: + msg_erro = f"Erro ao consultar relatório NFe por CNPJ: {e}" + self._log(RealmType.ERRORS, msg_erro) + return {"error": msg_erro} + + def nfse_emitidas(self, data_inicial=None, data_final=None, data: dict = None): + try: + if not data_inicial: + data_inicial = (datetime.now() - timedelta(days=30)).strftime('%Y-%m-%d') + if not data_final: + data_final = datetime.now().strftime('%Y-%m-%d') + + resposta = self.api.get_paginated(f"{self.url}nfse/relatorio?from={data_inicial}&to={data_final}") + self._log(RealmType.OPERATIONAL, f"Relatório NFSe emitidas: {data_inicial} a {data_final}") + return resposta + except Exception as e: + msg_erro = f"Erro ao consultar relatório NFSe emitidas: {e}" + self._log(RealmType.ERRORS, msg_erro) + return {"error": msg_erro} + + def nfse_por_cnpj(self, cnpj, data_inicial=None, data_final=None, data: dict = None): + try: + if not data_inicial: + data_inicial = (datetime.now() - timedelta(days=30)).strftime('%Y-%m-%d') + if not data_final: + data_final = datetime.now().strftime('%Y-%m-%d') + + resposta = self.api.get_paginated(f"{self.url}nfse/{cnpj}/relatorio?from={data_inicial}&to={data_final}") + self._log(RealmType.OPERATIONAL, f"Relatório NFSe por CNPJ: {cnpj}") + return resposta + except Exception as e: + msg_erro = f"Erro ao consultar relatório NFSe por CNPJ: {e}" + self._log(RealmType.ERRORS, msg_erro) + return {"error": msg_erro} + diff --git a/tecnospeed_queue/src/certificado_digital/certificado_digital.py b/tecnospeed_queue/src/certificado_digital/certificado_digital.py new file mode 100644 index 0000000..51720b0 --- /dev/null +++ b/tecnospeed_queue/src/certificado_digital/certificado_digital.py @@ -0,0 +1,194 @@ +from utils.api import Api +from utils.banco import Banco, CertificadoDigitalTable, EnterpriseTable +from datetime import date +from utils.Exchange2 import LoggerExchange2 +from utils.exchangeModels import RealmType +import json +import base64 + +class CertificadoDigital: + # Serviço que gerencia o upload e atualização de certificados digitais A1/A3 na plataforma da PlugNotas + def __init__(self, url_base, banco: Banco, api_key: str, logger: LoggerExchange2 = None): + self.api = Api(api_key=api_key) + self.banco = banco + self.url = f"{url_base}/certificado" + self.logger = logger + + def post_form_data(self, data): + # Realiza o envio multipart/form-data do arquivo físico do certificado .pfx para a API e o salva no banco + try: + files = { + 'arquivo': ( + 'certificado.pfx', + base64.b64decode(data['arquivo']) if isinstance(data['arquivo'], str) else data['arquivo'], + 'application/x-pkcs12' + ), + 'senha': (None, data['senha']) + } + if 'email' in data: + files['email'] = (None, data['email']) + + cnpj = data.get('cnpj') + if not cnpj: + raise ValueError("CNPJ é obrigatório para cadastrar o certificado.") + + # 1. Validar existência local (Banco de Dados) + empresa_local = self.banco.session.query(EnterpriseTable).filter_by(cnpj=cnpj).first() + if not empresa_local: + msg = f"Empresa com CNPJ {cnpj} não encontrada na base de dados local." + if self.logger: self.logger.logIC2(RealmType.OPERATIONAL, ('INSERT', 'certificado_digital', cnpj, '0', 'ERRO', msg, "{}")) + raise ValueError(msg) + + # 2. Validar existência na API (PlugNotas) + try: + # O endpoint da API costuma ser /empresa/{cnpj} + self.api.get(f"{self.url.replace('/certificado', '/empresa')}/{cnpj}") + except Exception: + msg = f"Empresa com CNPJ {cnpj} não encontrada na API da PlugNotas." + if self.logger: self.logger.logIC2(RealmType.OPERATIONAL, ('INSERT', 'certificado_digital', cnpj, '0', 'ERRO', msg, "{}")) + raise ValueError(msg) + + # 3. Enviar o certificado para a PlugNotas + response = self.api.post_form_data(self.url, files) + id_tecnospeed = response.get('data', {}).get('id') + + if not id_tecnospeed: + if self.logger: self.logger.logIC2(RealmType.OPERATIONAL, ('INSERT', 'certificado_digital', cnpj, '0', 'ERRO', 'ID do certificado não encontrado na resposta da API', json.dumps(response))) + raise ValueError("Erro ao obter ID do certificado na resposta da API.") + + # 4. Vincular o certificado à empresa (PATCH) + try: + # Endpoint: PATCH /empresa/{cnpj}/certificado + url_patch = f"{self.url.replace('/certificado', '/empresa')}/{cnpj}/certificado" + self.api.patch(url_patch, data={"certificado": id_tecnospeed}) + except Exception as e: + msg = f"Erro ao vincular certificado {id_tecnospeed} à empresa {cnpj}: {e}" + if self.logger: self.logger.logIC2(RealmType.OPERATIONAL, ('UPDATE', 'empresa', cnpj, id_tecnospeed, 'ERRO', msg, "{}")) + # Opcional: decidir se deve falhar aqui ou apenas logar. Como é vínculo obrigatório, vamos subir o erro. + raise Exception(msg) + + expiration_value = data.get('expirationAt') or data.get('data_expiracao') + certificado = CertificadoDigitalTable( + id_tecnospeed=id_tecnospeed, + certificado=data['arquivo'], + expirationAt=date.fromisoformat(expiration_value) if expiration_value else date.today(), + senha=data['senha'], + status=True, + cnpj=cnpj, + email=data.get('email') + ) + + self.banco.add(certificado) + if self.logger: self.logger.logIC2(RealmType.OPERATIONAL, ('INSERT', 'certificado_digital', cnpj, id_tecnospeed, 'SUCESSO', 'Certificado digital cadastrado e vinculado com sucesso', json.dumps(response))) + + return response + except Exception as e: + if self.logger: self.logger.logIC2(RealmType.OPERATIONAL, ('INSERT', 'certificado_digital', data.get('cnpj', '0'), '0', 'ERRO', f"Erro ao adicionar certificado: {e}", "{}")) + raise + + def contar_vencidos(self, lista_certificados): + # Auxiliar para contar certificados ativos e vencidos na listagem + if isinstance(lista_certificados, dict) and "data" in lista_certificados: + lista = lista_certificados["data"] + elif isinstance(lista_certificados, list): + lista = lista_certificados + else: + return {"certificados_ativos": 0, "certificados_vencidos": 0} + + ativos = 0 + vencidos = 0 + hoje = date.today() + + for cert in lista: + if not isinstance(cert, dict): continue + + # Tenta encontrar o campo de expiração (vencimento ou outros nomes conhecidos) + exp_str = cert.get("vencimento") or cert.get("expirationAt") or cert.get("dataVencimento") or cert.get("data_expiracao") + + if exp_str: + try: + # Tenta converter a string de data: + # 1. Formato DD/MM/YYYY (do snipped do usuário) + # 2. Formato ISO YYYY-MM-DD + if "/" in exp_str: + partes = exp_str.split(" ")[0].split("/") + if len(partes) == 3: + data_exp = date(int(partes[2]), int(partes[1]), int(partes[0])) + else: + data_exp = date.fromisoformat(exp_str[:10]) + + if data_exp >= hoje: + ativos += 1 + else: + vencidos += 1 + except: + continue + + return { + "certificados_ativos": ativos, + "certificados_vencidos": vencidos + } + + def get(self, id=None): + # Consulta a situação de um certificado digital específico pelo ID ou lista todos vinculados + try: + if id: + resultado = self.api.get(f"{self.url}/{id}") + if self.logger: self.logger.logIC2(RealmType.OPERATIONAL, ('SELECT', 'certificado_digital', id, id, 'SUCESSO', f"Certificado consultado - ID: {id}", json.dumps(resultado))) + return resultado + else: + resposta = self.api.get(self.url) + + # Calcula totais + totais = self.contar_vencidos(resposta) + + # Organiza a resposta unificada + resultado = { + "certificados": resposta if isinstance(resposta, list) else resposta.get("data", []), + "totais": totais + } + + if self.logger: self.logger.logIC2(RealmType.OPERATIONAL, ('SELECT', 'certificado_digital', 'GERAL', '0', 'SUCESSO', 'Listagem de certificados digitais', json.dumps(resultado))) + return resultado + except Exception as e: + if self.logger: self.logger.logIC2(RealmType.OPERATIONAL, ('SELECT', 'certificado_digital', id or 'GERAL', '0', 'ERRO', f"Erro ao buscar certificado: {e}", "{}")) + raise + + def put_certificado(self, id, data): + # Atualiza o arquivo físico ou a senha de um certificado já existente + try: + certificado = self.banco.session.query(CertificadoDigitalTable).filter_by(id_tecnospeed=id).first() + id_usr = data.get('id_usuario', 0) + + if not certificado: + if self.logger: self.logger.logIC2(RealmType.OPERATIONAL, ('UPDATE', 'certificado_digital', id_usr, id, 'ERRO', f"Certificado com ID {id} não encontrado", "{}")) + raise ValueError(f"Certificado com ID {id} não encontrado no banco.") + + files = { + 'arquivo': ( + 'certificado.pfx', + base64.b64decode(data['arquivo']) if isinstance(data['arquivo'], str) else data['arquivo'], + 'application/x-pkcs12' + ), + 'senha': (None, data['senha']) + } + if 'email' in data: + files['email'] = (None, data['email']) + + self.api.put_form_data(f"{self.url}/{id}", files) + response = {"id": id, "status": "atualizado", "message": "Certificado atualizado localmente e na API"} + + certificado.certificado = data['arquivo'] + certificado.senha = data['senha'] + expiration_value = data.get('expirationAt') or data.get('data_expiracao') + if expiration_value: + certificado.expirationAt = date.fromisoformat(expiration_value) + certificado.updatedAt = date.today() + + self.banco.update(certificado) + if self.logger: self.logger.logIC2(RealmType.OPERATIONAL, ('UPDATE', 'certificado_digital', id_usr, id, 'SUCESSO', 'Certificado digital atualizado com sucesso', json.dumps({"id": id}))) + + return response + except Exception as e: + if self.logger: self.logger.logIC2(RealmType.OPERATIONAL, ('UPDATE', 'certificado_digital', data.get('id_usuario', 0), id, 'ERRO', f"Erro ao atualizar certificado: {e}", "{}")) + raise diff --git a/tecnospeed_queue/src/empresas/empresa.py b/tecnospeed_queue/src/empresas/empresa.py new file mode 100644 index 0000000..1b8c40c --- /dev/null +++ b/tecnospeed_queue/src/empresas/empresa.py @@ -0,0 +1,184 @@ +from Models.empresa import Empresa +from utils.api import Api +from utils.banco import Banco, EnterpriseTable, LogoTable +from utils.Exchange2 import LoggerExchange2 +from utils.exchangeModels import RealmType +from sqlalchemy.exc import SQLAlchemyError +import json +import string +import random +import time + +def generate_cuid(): + """Gera um ID no formato aproximado de CUID (c + timestamp + random).""" + ts = hex(int(time.time() * 1000))[2:] + rand = ''.join(random.choices(string.ascii_lowercase + string.digits, k=12)) + return f"c{ts}{rand}"[:25] + +# Gerenciador de contas de Empresas emissoras de notas e seus respectivos certificados e logos +class EmpresaController: + def __init__(self, url_base, api_key: str, banco: Banco, logger: LoggerExchange2 = None): + self.api = Api(api_key=api_key) + self.banco = banco + self.url = f"{url_base}/empresa" + self.logger = logger + + def _log(self, realm: RealmType, msg: str): + if self.logger: + self.logger.logIC2(realm, (msg,)) + + def post(self, empresa: Empresa, data: dict): + try: + dict_api = empresa.model_dump(exclude={'id', 'id_empresa', 'banco', 'email'}, exclude_none=True) + + # Injeção das configurações fixas e obrigatórias para a PlugNotas + if dict_api.get("nfe") and dict_api["nfe"].get("ativo"): + nfe_config_front = dict_api["nfe"].get("config") or {} + dict_api["nfe"]["config"] = { + "producao": False, + "email": {"envio": False}, + "dfe": {"primeiroNsu": False}, + "impressaoFcp": False, + "impressaoPartilha": False, + **nfe_config_front + } + dict_api["nfe"]["tipoContrato"] = 0 + + if dict_api.get("nfse") and dict_api["nfse"].get("ativo"): + nfse_config_front = dict_api["nfse"].get("config") or {} + dict_api["nfse"]["config"] = { + "producao": False, + "email": {"envio": False}, + "rps": {"lote": 1, "numeracao": [{"numero": 1, "serie": "1"}]}, + "prefeitura": {"login": "12345", "senha": "123"}, + **nfse_config_front + } + dict_api["nfse"]["tipoContrato"] = 0 + print("===========================") + print("ENVIANDO PARA A API:", json.dumps(dict_api, indent=2)) + print("===========================") + retorno = self.api.post(data=dict_api, url=self.url) + if 'error' in retorno: + err_data = retorno.get('response', {}).get('error', {}) + msg = err_data.get('message', 'Erro desconhecido') + detalhes = err_data.get('data', []) + if detalhes: + msg += f" Detalhes: {detalhes}" + + print(f"\n[ERRO API PLUGNOTAS] {msg}\n", flush=True) + self._log(RealmType.ERRORS, f"Erro na API ao cadastrar empresa: {msg}") + raise Exception(msg) + + # Gerar ID CUID se não vier no objeto ou se for nulo + empresa_id = empresa.id if empresa.id else generate_cuid() + + empresa_nova = EnterpriseTable( + id=empresa_id, + cnpj=empresa.cpfCnpj, + razao_social=empresa.razaoSocial, + nome_fantasia=empresa.nomeFantasia if empresa.nomeFantasia else empresa.razaoSocial, + banco=empresa.banco, + email=empresa.email, + status=True + ) + self.banco.add(empresa_nova) + self._log(RealmType.OPERATIONAL, f"Empresa cadastrada com sucesso - CNPJ: {empresa.cpfCnpj} (ID: {empresa_id})") + + if isinstance(retorno, dict) and 'data' in retorno: + retorno['data']['id'] = empresa_nova.id + retorno['data']['id_plugnotas'] = retorno['data'].get('_id') + retorno['data']['nome_fantasia'] = empresa_nova.nome_fantasia + retorno['data']['razao_social'] = empresa_nova.razao_social + retorno['data']['status'] = empresa_nova.status + retorno['data'].pop('razaoSocial', None) + retorno['data'].pop('nomeFantasia', None) + retorno['data'].pop('id_empresa', None) + + return retorno + except SQLAlchemyError as e: + self.banco.rollback() + self._log(RealmType.ERRORS, f"Erro no banco ao cadastrar empresa: {e}") + return {"error": f"Erro ao cadastrar empresa: {str(e)}"} + except Exception as e: + return {"error": f"Erro inesperado: {str(e)}"} + + def get(self, cnpj: str = None, data: dict = None): + try: + if cnpj: + resposta = self.api.get(f"{self.url}/{cnpj}") + + if isinstance(resposta, dict) and "error" not in resposta: + empresa_local = self.banco.session.query(EnterpriseTable).filter_by(cnpj=cnpj).first() + if empresa_local: + resposta["id"] = empresa_local.id + resposta["nome_fantasia"] = empresa_local.nome_fantasia + resposta["razao_social"] = empresa_local.razao_social + resposta["status"] = empresa_local.status + resposta.pop("razaoSocial", None) + resposta.pop("nomeFantasia", None) + + if "_id" in resposta: + resposta["id_plugnotas"] = resposta["_id"] + + else: + resposta = self.api.get_paginated(self.url) + # Injetar o ID do banco local em cada empresa listada + if isinstance(resposta, dict) and "data" in resposta: + for emp in resposta["data"]: + empresa_local = self.banco.session.query(EnterpriseTable).filter_by(cnpj=emp.get("cpfCnpj")).first() + if empresa_local: + emp["id"] = empresa_local.id + emp["nome_fantasia"] = empresa_local.nome_fantasia + emp["razao_social"] = empresa_local.razao_social + emp["status"] = empresa_local.status + emp.pop("razaoSocial", None) + emp.pop("nomeFantasia", None) + if "_id" in emp: + emp["id_plugnotas"] = emp["_id"] + + self._log(RealmType.OPERATIONAL, f"Empresa consultada - CNPJ: {cnpj or 'TODAS'}") + return resposta + except Exception as e: + self._log(RealmType.ERRORS, f"Erro ao buscar empresa: {e}") + return {"error": f"Erro ao buscar empresa: {str(e)}"} + + def post_file(self, file, cnpj, data=None): + try: + empresa = self.banco.session.query(EnterpriseTable).filter_by(cnpj=cnpj).first() + if not empresa: + self._log(RealmType.ERRORS, f"Empresa não encontrada para upload de logo - CNPJ: {cnpj}") + return {"error": "Empresa não encontrada"} + + logo = LogoTable( + id_empresa=empresa.id, + logo=file, + status=True + ) + self.banco.add(logo) + + files = {'arquivo': ('logo.png', file, 'image/png')} + resposta = self.api.post_file(file=files, url=f"{self.url}/{cnpj}/logotipo") + self._log(RealmType.OPERATIONAL, f"Logo enviada com sucesso - CNPJ: {cnpj}") + return resposta + + except SQLAlchemyError as e: + self.banco.rollback() + self._log(RealmType.ERRORS, f"Erro no banco ao salvar logo: {e}") + return {"error": f"Erro ao salvar logo: {str(e)}"} + except Exception as e: + self._log(RealmType.ERRORS, f"Erro inesperado ao enviar logo: {e}") + return {"error": f"Erro inesperado: {str(e)}"} + + def patch_certificado(self, cnpj: str, certificado: str, data: dict): + try: + empresa = self.banco.session.query(EnterpriseTable).filter_by(cnpj=cnpj).first() + if not empresa: + self._log(RealmType.ERRORS, f"Empresa não encontrada para atualizar certificado - CNPJ: {cnpj}") + return {"error": "Empresa não encontrada"} + resposta = self.api.patch(url=f"{self.url}/{cnpj}/certificado", data={"certificado": certificado}) + self._log(RealmType.OPERATIONAL, f"Certificado da empresa atualizado - CNPJ: {cnpj}") + return resposta + except Exception as e: + self._log(RealmType.ERRORS, f"Erro ao atualizar certificado: {e}") + return {"error": f"Erro ao atualizar certificado: {str(e)}"} + diff --git a/tecnospeed_queue/src/logs/integracao_01 b/tecnospeed_queue/src/logs/integracao_01 new file mode 100644 index 0000000..ee7eea0 --- /dev/null +++ b/tecnospeed_queue/src/logs/integracao_01 @@ -0,0 +1,24 @@ +2026-04-01 09:09:33,082 - integracao - INFO - { 'caller': 'ciclo_app', + 'client': '', + 'logDate': '2026-04-01 09:09:33.082579-03:00', + 'msg': '[local] Configuração lida com sucesso pelo Listener Webhook.', + 'origin': '', + 'service': 'tecnospeed', + 'system': 'integracao', + 'version': '2.0.1'} +2026-04-01 09:09:33,700 - integracao - INFO - { 'caller': 'ciclo_app', + 'client': '', + 'logDate': '2026-04-01 09:09:33.700242-03:00', + 'msg': 'Exchange inicializado com sucesso para o Listener Webhook.', + 'origin': '', + 'service': 'tecnospeed', + 'system': 'integracao', + 'version': '2.0.1'} +2026-04-01 09:14:24,402 - integracao - INFO - { 'caller': 'ciclo_app', + 'client': '', + 'logDate': '2026-04-01 09:14:24.402193-03:00', + 'msg': 'Listener Webhook encerrando...', + 'origin': '', + 'service': 'tecnospeed', + 'system': 'integracao', + 'version': '2.0.1'} diff --git a/tecnospeed_queue/src/logs/integracao_02 b/tecnospeed_queue/src/logs/integracao_02 new file mode 100644 index 0000000..3fc1caa --- /dev/null +++ b/tecnospeed_queue/src/logs/integracao_02 @@ -0,0 +1,194 @@ +2026-04-02 09:14:16,362 - integracao - INFO - { 'caller': 'ciclo_app', + 'client': '', + 'logDate': '2026-04-02 09:14:16.362304-03:00', + 'msg': '[local] Configuração lida com sucesso pelo Listener Webhook.', + 'origin': '', + 'service': 'tecnospeed', + 'system': 'integracao', + 'version': '2.0.1'} +2026-04-02 09:14:16,932 - integracao - INFO - { 'caller': 'ciclo_app', + 'client': '', + 'logDate': '2026-04-02 09:14:16.932459-03:00', + 'msg': 'Exchange inicializado com sucesso para o Listener Webhook.', + 'origin': '', + 'service': 'tecnospeed', + 'system': 'integracao', + 'version': '2.0.1'} +2026-04-02 14:14:30,014 - integracao - INFO - { 'caller': 'ciclo_app', + 'client': '', + 'logDate': '2026-04-02 14:14:30.013914-03:00', + 'msg': '[local] Configuração lida com sucesso pelo Listener Webhook.', + 'origin': '', + 'service': 'tecnospeed', + 'system': 'integracao', + 'version': '2.0.1'} +2026-04-02 14:14:31,040 - integracao - INFO - { 'caller': 'ciclo_app', + 'client': '', + 'logDate': '2026-04-02 14:14:31.039753-03:00', + 'msg': 'Exchange inicializado com sucesso para o Listener Webhook.', + 'origin': '', + 'service': 'tecnospeed', + 'system': 'integracao', + 'version': '2.0.1'} +2026-04-02 14:22:20,024 - integracao - INFO - { 'caller': 'receber_webhook_plugnotas', + 'client': '', + 'logDate': '2026-04-02 14:22:20.024167-03:00', + 'msg': 'Webhook recebido - Documento: nfe | Status: CONCLUIDO | ID: ' + '69cab453... | IDIntegracao: | Emitente:', + 'origin': '', + 'service': 'tecnospeed', + 'system': 'integracao', + 'version': '2.0.1'} +2026-04-02 14:22:20,263 - integracao - INFO - { 'caller': 'receber_webhook_plugnotas', + 'client': '', + 'logDate': '2026-04-02 14:22:20.263204-03:00', + 'msg': 'Webhook repassado ao worker com sucesso - Documento: nfe | Status: ' + 'CONCLUIDO | ID: 69cab453...', + 'origin': '', + 'service': 'tecnospeed', + 'system': 'integracao', + 'version': '2.0.1'} +2026-04-02 14:23:45,608 - integracao - INFO - { 'caller': 'receber_webhook_plugnotas', + 'client': '', + 'logDate': '2026-04-02 14:23:45.608300-03:00', + 'msg': 'Webhook recebido - Documento: nfe | Status: CONCLUIDO | ID: ' + '69cab45329f21a7de7691961 | IDIntegracao: XXXYY99999_TESTE445 | ' + 'Emitente: 08187168000160', + 'origin': '', + 'service': 'tecnospeed', + 'system': 'integracao', + 'version': '2.0.1'} +2026-04-02 14:23:46,107 - integracao - INFO - { 'caller': 'receber_webhook_plugnotas', + 'client': '', + 'logDate': '2026-04-02 14:23:46.107170-03:00', + 'msg': 'Webhook repassado ao worker com sucesso - Documento: nfe | Status: ' + 'CONCLUIDO | ID: 69cab45329f21a7de7691961', + 'origin': '', + 'service': 'tecnospeed', + 'system': 'integracao', + 'version': '2.0.1'} +2026-04-02 14:26:07,402 - integracao - INFO - { 'caller': 'ciclo_app', + 'client': '', + 'logDate': '2026-04-02 14:26:07.401683-03:00', + 'msg': 'Listener Webhook encerrando...', + 'origin': '', + 'service': 'tecnospeed', + 'system': 'integracao', + 'version': '2.0.1'} +2026-04-02 14:37:36,191 - integracao - INFO - { 'caller': 'ciclo_app', + 'client': '', + 'logDate': '2026-04-02 14:37:36.190896-03:00', + 'msg': '[local] Configuração lida com sucesso pelo Listener Webhook.', + 'origin': '', + 'service': 'tecnospeed', + 'system': 'integracao', + 'version': '2.0.1'} +2026-04-02 14:37:36,969 - integracao - INFO - { 'caller': 'ciclo_app', + 'client': '', + 'logDate': '2026-04-02 14:37:36.969622-03:00', + 'msg': 'Exchange inicializado com sucesso para o Listener Webhook.', + 'origin': '', + 'service': 'tecnospeed', + 'system': 'integracao', + 'version': '2.0.1'} +2026-04-02 14:37:37,459 - integracao - INFO - { 'caller': 'ciclo_app', + 'client': '', + 'logDate': '2026-04-02 14:37:37.459047-03:00', + 'msg': 'Listener Webhook encerrando...', + 'origin': '', + 'service': 'tecnospeed', + 'system': 'integracao', + 'version': '2.0.1'} +2026-04-02 14:37:42,582 - integracao - INFO - { 'caller': 'ciclo_app', + 'client': '', + 'logDate': '2026-04-02 14:37:42.582792-03:00', + 'msg': '[local] Configuração lida com sucesso pelo Listener Webhook.', + 'origin': '', + 'service': 'tecnospeed', + 'system': 'integracao', + 'version': '2.0.1'} +2026-04-02 14:37:43,391 - integracao - INFO - { 'caller': 'ciclo_app', + 'client': '', + 'logDate': '2026-04-02 14:37:43.391071-03:00', + 'msg': 'Exchange inicializado com sucesso para o Listener Webhook.', + 'origin': '', + 'service': 'tecnospeed', + 'system': 'integracao', + 'version': '2.0.1'} +2026-04-02 14:37:43,782 - integracao - INFO - { 'caller': 'ciclo_app', + 'client': '', + 'logDate': '2026-04-02 14:37:43.781995-03:00', + 'msg': 'Listener Webhook encerrando...', + 'origin': '', + 'service': 'tecnospeed', + 'system': 'integracao', + 'version': '2.0.1'} +2026-04-02 16:03:48,010 - integracao - INFO - { 'caller': 'run', + 'client': '', + 'logDate': '2026-04-02 16:03:48.009993-03:00', + 'msg': 'Conectando ao RabbitMQ', + 'origin': '', + 'service': 'tecnospeed', + 'system': 'integracao', + 'version': '2.0.1'} +2026-04-02 16:03:48,217 - integracao - INFO - { 'caller': 'run', + 'client': '', + 'logDate': '2026-04-02 16:03:48.216535-03:00', + 'msg': "Configuração do sistema carregada - serviceid='local' " + "servicekey='local' origin='local' " + "amqps='amqp://guest:guest@localhost:5672/'", + 'origin': '', + 'service': 'tecnospeed', + 'system': 'integracao', + 'version': '2.0.1'} +2026-04-02 16:12:17,812 - integracao - INFO - { 'caller': 'run', + 'client': '', + 'logDate': '2026-04-02 16:12:17.812369-03:00', + 'msg': 'Conectando ao RabbitMQ', + 'origin': '', + 'service': 'tecnospeed', + 'system': 'integracao', + 'version': '2.0.1'} +2026-04-02 16:12:18,488 - integracao - INFO - { 'caller': 'run', + 'client': '', + 'logDate': '2026-04-02 16:12:18.487913-03:00', + 'msg': "Configuração do sistema carregada - serviceid='local' " + "servicekey='local' origin='local' " + "amqps='amqp://guest:guest@localhost:5672/'", + 'origin': '', + 'service': 'tecnospeed', + 'system': 'integracao', + 'version': '2.0.1'} +2026-04-02 16:12:18,864 - integracao - INFO - { 'caller': 'run', + 'client': '', + 'logDate': '2026-04-02 16:12:18.863889-03:00', + 'msg': 'Erro fatal ao conectar ao RabbitMQ - VERIFY PARAMETERS: ' + 'is_amqp_url: True\n' + ' amqps[amqp://guest:guest@localhost:5672/]\n' + ' origin[local]\n' + ' system[integracao]\n' + ' service[tecnospeed]\n' + ' version[2.0.1\n' + ' client[]', + 'origin': '', + 'service': 'tecnospeed', + 'system': 'integracao', + 'version': '2.0.1'} +2026-04-02 16:12:30,426 - integracao - INFO - { 'caller': 'run', + 'client': '', + 'logDate': '2026-04-02 16:12:30.426681-03:00', + 'msg': 'Conectando ao RabbitMQ', + 'origin': '', + 'service': 'tecnospeed', + 'system': 'integracao', + 'version': '2.0.1'} +2026-04-02 16:12:30,947 - integracao - INFO - { 'caller': 'run', + 'client': '', + 'logDate': '2026-04-02 16:12:30.947199-03:00', + 'msg': "Configuração do sistema carregada - serviceid='local' " + "servicekey='local' origin='local' " + "amqps='amqp://guest:guest@localhost:5672/'", + 'origin': '', + 'service': 'tecnospeed', + 'system': 'integracao', + 'version': '2.0.1'} diff --git a/tecnospeed_queue/src/logs/integracao_06 b/tecnospeed_queue/src/logs/integracao_06 new file mode 100644 index 0000000..d64ebb0 --- /dev/null +++ b/tecnospeed_queue/src/logs/integracao_06 @@ -0,0 +1,56 @@ +2026-04-06 08:25:17,847 - teste_wrapper.integracao - INFO - params: amqps: amqps://iawkzmhh:4iIVXCgLDsLy2oVyVWeV-o6shg-BZ89z@jackal.rmq.cloudamqp.com/iawkzmhh +2026-04-06 08:25:18,327 - teste_wrapper.integracao - INFO - Logger Messages topics: logs.teste_wrapper.integracao. +2026-04-06 08:25:18,361 - teste_wrapper.integracao - INFO - {'caller': 'IC2', + 'client': '', + 'hmac': '862c786853960277c4d518d1eb01143b2fe7782092dbf5403fb61b52127c7829', + 'logDate': '2026-04-06 08:25:18.361486-03:00', + 'msg': "LOGIC2 LOGS [('Testando o novo fo ...]", + 'origin': '', + 'payload': '44102feb05c76aac7b2b', + 'service': 'teste_wrapper', + 'serviceid': 'AF1699F719F1B6C81DE2BD38AB811994E3089B35C8E9314813A770AC7AE25D8D', + 'system': 'integracao', + 'target': '', + 'type': 'logger', + 'version': ''} +2026-04-06 08:25:18,368 - teste_wrapper.integracao - INFO - {'caller': 'logMsg', + 'client': '', + 'hmac': '', + 'logDate': '2026-04-06 08:25:18.367868-03:00', + 'msg': 'Testando o novo formato IC2', + 'origin': '', + 'payload': '', + 'service': 'teste_wrapper', + 'serviceid': 'AF1699F719F1B6C81DE2BD38AB811994E3089B35C8E9314813A770AC7AE25D8D', + 'system': 'integracao', + 'target': '', + 'type': 'logger', + 'version': ''} +2026-04-06 08:28:37,324 - teste_wrapper.integracao - INFO - params: amqps: amqps://iawkzmhh:4iIVXCgLDsLy2oVyVWeV-o6shg-BZ89z@jackal.rmq.cloudamqp.com/iawkzmhh +2026-04-06 08:28:37,834 - teste_wrapper.integracao - INFO - Logger Messages topics: logs.teste_wrapper.integracao. +2026-04-06 08:28:37,855 - teste_wrapper.integracao - INFO - {'caller': 'IC2', + 'client': '', + 'hmac': '841681411efce4ae99ff2602ae79da6284e6c3ca247a27da3a87dedf3771244e', + 'logDate': '2026-04-06 08:28:37.855637-03:00', + 'msg': "LOGIC2 LOGS [('Testando o formato ...]", + 'origin': '', + 'payload': '353fd214129406d78301', + 'service': 'teste_wrapper', + 'serviceid': 'AF1699F719F1B6C81DE2BD38AB811994E3089B35C8E9314813A770AC7AE25D8D', + 'system': 'integracao', + 'target': '', + 'type': 'logger', + 'version': ''} +2026-04-06 08:28:37,865 - teste_wrapper.integracao - INFO - {'caller': 'logMsg', + 'client': '', + 'hmac': '', + 'logDate': '2026-04-06 08:28:37.865799-03:00', + 'msg': 'Testando o formato IC2', + 'origin': '', + 'payload': '', + 'service': 'teste_wrapper', + 'serviceid': 'AF1699F719F1B6C81DE2BD38AB811994E3089B35C8E9314813A770AC7AE25D8D', + 'system': 'integracao', + 'target': '', + 'type': 'logger', + 'version': ''} diff --git a/tecnospeed_queue/src/logs/integracao_25 b/tecnospeed_queue/src/logs/integracao_25 new file mode 100644 index 0000000..e69de29 diff --git a/tecnospeed_queue/src/logs/integracao_26 b/tecnospeed_queue/src/logs/integracao_26 new file mode 100644 index 0000000..e69de29 diff --git a/tecnospeed_queue/src/logs/integracao_27 b/tecnospeed_queue/src/logs/integracao_27 new file mode 100644 index 0000000..f61511b --- /dev/null +++ b/tecnospeed_queue/src/logs/integracao_27 @@ -0,0 +1,140 @@ +2026-03-27 14:57:57,807 - integracao - INFO - { 'caller': 'run', + 'client': '', + 'logDate': '2026-03-27 14:57:57.807604-03:00', + 'msg': 'Conectando ao RabbitMQ', + 'origin': '', + 'service': 'tecnospeed', + 'system': 'integracao', + 'version': '2.0.1'} +2026-03-27 14:57:58,282 - integracao - INFO - { 'caller': 'run', + 'client': '', + 'logDate': '2026-03-27 14:57:58.282491-03:00', + 'msg': "Configuração do sistema carregada - serviceid='local' " + "servicekey='local' origin='local' " + "amqps='amqp://guest:guest@localhost:5672/'", + 'origin': '', + 'service': 'tecnospeed', + 'system': 'integracao', + 'version': '2.0.1'} +2026-03-27 14:59:07,273 - integracao - INFO - { 'caller': 'callback', + 'client': 'Test', + 'logDate': '2026-03-27 14:59:07.273291-03:00', + 'msg': "Configuração recebida - {'api_key': None, 'x_api_key': " + "'2da392a6-79d2-4304-a8b7-959572c7e44d', 'user_postgres': " + "'postgres', 'pass_postgres': 'postgres', 'host_postgres': " + "'localhost', 'port_postgres': '5432', 'database_postgres': " + "'tecnospeed'}", + 'origin': '', + 'service': 'tecnospeed', + 'system': 'integracao', + 'version': '2.0.1'} +2026-03-27 14:59:08,015 - integracao - INFO - { 'caller': 'callback', + 'client': 'Test', + 'logDate': '2026-03-27 14:59:08.014993-03:00', + 'msg': 'Configuração incompleta', + 'origin': '', + 'service': 'tecnospeed', + 'system': 'integracao', + 'version': '2.0.1'} +2026-03-27 15:00:56,891 - integracao - INFO - { 'caller': 'callback', + 'client': 'Test', + 'logDate': '2026-03-27 15:00:56.890566-03:00', + 'msg': "Configuração recebida - {'api_key': None, 'x_api_key': " + "'2da392a6-79d2-4304-a8b7-959572c7e44d', 'user_postgres': " + "'postgres', 'pass_postgres': 'postgres', 'host_postgres': " + "'localhost', 'port_postgres': '5432', 'database_postgres': " + "'sg_servico_tecnospeed'}", + 'origin': '', + 'service': 'tecnospeed', + 'system': 'integracao', + 'version': '2.0.1'} +2026-03-27 15:00:57,787 - integracao - INFO - { 'caller': 'callback', + 'client': 'Test', + 'logDate': '2026-03-27 15:00:57.787472-03:00', + 'msg': 'Configuração incompleta', + 'origin': '', + 'service': 'tecnospeed', + 'system': 'integracao', + 'version': '2.0.1'} +2026-03-27 15:02:14,102 - integracao - INFO - { 'caller': 'callback', + 'client': 'Test', + 'logDate': '2026-03-27 15:02:14.101859-03:00', + 'msg': "Configuração recebida - {'api_key': None, 'x_api_key': " + "'2da392a6-79d2-4304-a8b7-959572c7e44d', 'user_postgres': " + "'postgres', 'pass_postgres': 'postgres', 'host_postgres': " + "'localhost', 'port_postgres': '5432', 'database_postgres': " + "'tecnospeed'}", + 'origin': '', + 'service': 'tecnospeed', + 'system': 'integracao', + 'version': '2.0.1'} +2026-03-27 15:02:14,792 - integracao - INFO - { 'caller': 'callback', + 'client': 'Test', + 'logDate': '2026-03-27 15:02:14.791942-03:00', + 'msg': 'Configuração incompleta', + 'origin': '', + 'service': 'tecnospeed', + 'system': 'integracao', + 'version': '2.0.1'} +2026-03-27 15:09:33,329 - integracao - INFO - { 'caller': 'run', + 'client': '', + 'logDate': '2026-03-27 15:09:33.329870-03:00', + 'msg': 'Conectando ao RabbitMQ', + 'origin': '', + 'service': 'tecnospeed', + 'system': 'integracao', + 'version': '2.0.1'} +2026-03-27 15:09:33,698 - integracao - INFO - { 'caller': 'run', + 'client': '', + 'logDate': '2026-03-27 15:09:33.698183-03:00', + 'msg': "Configuração do sistema carregada - serviceid='local' " + "servicekey='local' origin='local' " + "amqps='amqp://guest:guest@localhost:5672/'", + 'origin': '', + 'service': 'tecnospeed', + 'system': 'integracao', + 'version': '2.0.1'} +2026-03-27 15:09:39,159 - integracao - INFO - { 'caller': 'callback', + 'client': 'Test', + 'logDate': '2026-03-27 15:09:39.158573-03:00', + 'msg': "Configuração recebida - {'api_key': None, 'x_api_key': " + "'2da392a6-79d2-4304-a8b7-959572c7e44d', 'user_postgres': " + "'postgres', 'pass_postgres': 'postgres', 'host_postgres': " + "'localhost', 'port_postgres': '5432', 'database_postgres': " + "'tecnospeed'}", + 'origin': '', + 'service': 'tecnospeed', + 'system': 'integracao', + 'version': '2.0.1'} +2026-03-27 15:09:44,975 - integracao - INFO - { 'caller': 'callback', + 'client': 'Test', + 'logDate': '2026-03-27 15:09:44.975168-03:00', + 'msg': 'Configuração completa para o cliente Test', + 'origin': '', + 'service': 'tecnospeed', + 'system': 'integracao', + 'version': '2.0.1'} +2026-03-27 15:09:45,763 - integracao - INFO - { 'caller': 'nfe', + 'client': 'Test', + 'logDate': '2026-03-27 15:09:45.763614-03:00', + 'msg': 'Iniciando serviço de NFe - Método: GET', + 'origin': '', + 'service': 'tecnospeed', + 'system': 'integracao', + 'version': '2.0.1'} +2026-03-27 15:09:46,610 - integracao - INFO - { 'caller': 'nfe', + 'client': 'Test', + 'logDate': '2026-03-27 15:09:46.610671-03:00', + 'msg': 'Consultando resumo de NFe', + 'origin': '', + 'service': 'tecnospeed', + 'system': 'integracao', + 'version': '2.0.1'} +2026-03-27 15:09:47,761 - integracao - INFO - { 'caller': 'nfe', + 'client': 'Test', + 'logDate': '2026-03-27 15:09:47.761417-03:00', + 'msg': 'Operação realizada com sucesso', + 'origin': '', + 'service': 'tecnospeed', + 'system': 'integracao', + 'version': '2.0.1'} diff --git a/tecnospeed_queue/src/tecnospeed_queue.py b/tecnospeed_queue/src/tecnospeed_queue.py new file mode 100644 index 0000000..fe5bc29 --- /dev/null +++ b/tecnospeed_queue/src/tecnospeed_queue.py @@ -0,0 +1,685 @@ +# IMPORT DAS CLASSES DE TRABALHO +from certificado_digital.certificado_digital import CertificadoDigital +from empresas.empresa import EmpresaController +from NFSe.nfse import NFSeService +from NFSe.Servico.servico import ServicoController +from NFSe.Tomador.tomador import TomadorController +from Relatorios.relatorios import Relatorios +from webhooks.webhooks import Webhooks +from NFe.registroNFe import RegistroNFeService + +# IMPORTS DE MODELS +from Models.config import Config, SystemConfig +from Models.empresa import Empresa +from Models.nfse import NFSe +from Models.servico import Servico +from Models.nfe import NFe +from Models.tomador import Tomador +from Models.payload import validate_payload + +# IMPORTS DE BIBLIOTECAS +from pydantic import ValidationError +import json, sys, time, random, os, uuid + +# IMPORTS DE CLASSES AUXILIARES +from utils.Exchange2 import QueueExchange2, LoggerExchange2, AppIC2Builder +from utils.exchangeModels import RealmType +from utils.banco import Banco + +from utils.redis import RedisClient + +# VERSÃO DO SISTEMA +version = "1.0.0" # Versão inicial do sistema +version = "1.0.1" # Implementando ORM +version = "1.0.2" # Implementando RPC +version = "1.0.3" # Implementando configuração via Redis +version = "2.0.0" # NFe e refatoração da Main +version = "2.0.1" # Refatoração para QueueExchange + + +class Main: + def __init__(self): + self.client = "#" + self.api_key = "" + self.exchange = None + self.banco = None + self.system_config: SystemConfig = None + self.redis = RedisClient(host=os.getenv('REDIS_HOST', 'localhost'), port=6379) + self.service = "tecnospeed" + self.system = "integracao" + self.version = version + self.logger = None + self.url_base = os.getenv('PLUGNOTAS_URL', 'https://api.sandbox.plugnotas.com.br') + self.funcoes = { + "nfe": self.nfe, + "nfse": self.nfse, + "certificado_digital": self.certificado_digital, + "empresa": self.empresa, + "servico": self.servico, + "tomador": self.tomador, + "relatorios": self.relatorios, + "webhooks": self.webhooks + } + + # Métodos de serviços orientado a eventos. + + def nfe(self, metodo, dados=None, correlation_id=None): + # Roteador de emissões, baixas e cancelamentos via NFe + id_usr = dados.get('id_usuario', 1) if dados else 1 + try: + self.logger.logIC2(RealmType.LOGS, (f"Iniciando serviço de NFe - Método: {metodo.upper()}",)) + + nfe_service = RegistroNFeService(api_key=self.api_key, + url_base=self.url_base, + banco=self.banco, + logger=self.logger) + + metodo = metodo.lower() + resultado = None + + match metodo: + case "post": + self.logger.logIC2(RealmType.LOGS, ("Cadastrando NFe",)) + nfe = NFe(**dados.get('nfe', dados)) + resultado = nfe_service.cadastrar_nfe(nfe_list=[nfe], data=dados) + + case "get": + self.logger.logIC2(RealmType.LOGS, ("Consultando resumo de NFe",)) + resultado = nfe_service.consultar_resumo(id_nota=dados.get('id')) + + case "download": + self.logger.logIC2(RealmType.LOGS, ("Baixando PDF NFe",)) + resultado = nfe_service.baixar_pdf(id_nota=dados.get('id')) + + case "download_xml": + self.logger.logIC2(RealmType.LOGS, ("Baixando XML NFe",)) + resultado = nfe_service.baixar_xml(id_nota=dados.get('id')) + + case "cancelar": + self.logger.logIC2(RealmType.LOGS, ("Cancelando NFe",)) + resultado = nfe_service.cancelar_nfe(id_nota=dados.get('id'), justificativa=dados.get('justificativa', 'Cancelamento padrão')) + + case "status_cancelamento": + self.logger.logIC2(RealmType.LOGS, ("Consultando status de cancelamento",)) + resultado = nfe_service.consultar_status_cancelamento(id_nota=dados.get('id')) + + case "cce": + self.logger.logIC2(RealmType.LOGS, ("Enviando Carta de Correção NFe",)) + resultado = nfe_service.enviar_cce(id_nota=dados.get('id'), correcao=dados.get('correcao')) + + case "status_cce": + self.logger.logIC2(RealmType.LOGS, ("Consultando status CCe NFe",)) + resultado = nfe_service.consultar_status_cce(id_nota=dados.get('id')) + + case "resumo": + self.logger.logIC2(RealmType.LOGS, ("Consultando resumo de NFe por CNPJ de integracao",)) + resultado = nfe_service.consultar_resumo_por_cnpj_integracao(cnpj=dados.get('cnpj'), id_integracao=dados.get('id_integracao', dados.get('id'))) + + case _: + self.logger.logIC2(RealmType.ERRORS, ("Método não encontrado",)) + self.banco.logar_sistema(id_usuario=id_usr, acao=metodo.upper(), tabela='nfe', status='ERRO', mensagem='Método não encontrado', cliente=self.client, correlation_id=correlation_id) + return json.dumps({"status": "error", "message": "Método não encontrado"}) + + if isinstance(resultado, dict) and "error" in resultado: + msg_erro = resultado["error"] + if "response" in resultado and resultado["response"]: + msg_erro += f" - Detalhes: {json.dumps(resultado['response'])}" + raise Exception(msg_erro) + + self.exchange.setPayload(json.dumps(resultado)) + self.logger.logIC2(RealmType.LOGS, ("Operação realizada com sucesso",)) + self.banco.logar_sistema(id_usuario=id_usr, acao=metodo.upper(), tabela='nfe', status='SUCESSO', mensagem='Operação concluída', json=json.dumps(resultado) if resultado else '{}', cliente=self.client, correlation_id=correlation_id) + return json.dumps({"status": "ok", "data": resultado}) + except Exception as e: + msg_completa = str(e) + self.logger.logIC2(RealmType.ERRORS, (f"Erro ao executar método {metodo} - {msg_completa}",)) + self.banco.logar_sistema(id_usuario=id_usr, acao=metodo.upper() if isinstance(metodo, str) else 'NFE', tabela='nfe', status='ERRO', mensagem=msg_completa, cliente=self.client, correlation_id=correlation_id) + raise e + + def nfse(self, metodo, dados=None, correlation_id=None): + # Roteador de Notas Fiscais de Roteiro/Serviço (NFSe) + id_usr = dados.get('id_usuario', 1) if dados else 1 + try: + self.logger.logIC2(RealmType.LOGS, (f"Iniciando serviço de NFSe - Método: {metodo.upper()}",)) + + nfse_service = NFSeService(api_key=self.api_key, + banco=self.banco, + url_base=self.url_base, + logger=self.logger) + + metodo = metodo.lower() + resultado = None + + match metodo: + case "post": + self.logger.logIC2(RealmType.LOGS, ("Cadastrando NFSe",)) + nfse = NFSe(**dados.get('nfse', dados)) + resultado = nfse_service.cadastrar_nfse(nfse=nfse, data=dados) + + case "get": + self.logger.logIC2(RealmType.LOGS, ("Listando NFSe",)) + resultado = nfse_service.buscar_nfse(id_nfse=dados.get('id')) if dados and 'id' in dados else nfse_service.listar_nfse() + + case "download": + self.logger.logIC2(RealmType.LOGS, ("Baixando NFSe",)) + resultado = nfse_service.baixar_nfse(id_nfse=dados.get('id')) + + case "download_xml": + self.logger.logIC2(RealmType.LOGS, ("Baixando XML NFSe",)) + resultado = nfse_service.baixar_xml_nfse(id_nfse=dados.get('id')) + + case "cancelar": + self.logger.logIC2(RealmType.LOGS, ("Cancelando NFSe",)) + resultado = nfse_service.solicitar_cancelamento_nfse(id_nfse=dados.get('id')) + + case "status_cancelamento": + self.logger.logIC2(RealmType.LOGS, ("Consultando status de cancelamento",)) + resultado = nfse_service.consultar_status_cancelamento_nfse(id_nfse=dados.get('id')) + + case "resumo": + self.logger.logIC2(RealmType.LOGS, ("Consultando resumo de NFSe",)) + resultado = nfse_service.consultar_resumo_nfse(id_nfse=dados.get('id'), cnpj=dados.get('cnpj')) + + case "historico": + self.logger.logIC2(RealmType.LOGS, ("Baixando histórico de NFSe",)) + resultado = nfse_service.baixar_histotico_nfse(id_nfse=dados.get('id'), cnpj=dados.get('cnpj')) + + case _: + self.logger.logIC2(RealmType.ERRORS, ("Método não encontrado",)) + self.banco.logar_sistema(id_usuario=id_usr, acao=metodo.upper(), tabela='nfse', status='ERRO', mensagem='Método não encontrado', cliente=self.client, correlation_id=correlation_id) + return json.dumps({"status": "error", "message": "Método não encontrado"}) + + if isinstance(resultado, dict) and "error" in resultado: + raise Exception(json.dumps(resultado)) + + self.exchange.setPayload(json.dumps(resultado)) + self.logger.logIC2(RealmType.LOGS, ("Operação realizada com sucesso",)) + self.banco.logar_sistema(id_usuario=id_usr, acao=metodo.upper(), tabela='nfse', status='SUCESSO', mensagem='Operação concluída', json=json.dumps(resultado) if resultado else '{}', cliente=self.client, correlation_id=correlation_id) + return json.dumps({"status": "ok", "data": resultado}) + except Exception as e: + self.logger.logIC2(RealmType.ERRORS, (f"Erro ao executar método {metodo} - {e}",)) + self.banco.logar_sistema(id_usuario=id_usr, acao=metodo.upper() if isinstance(metodo, str) else 'NFSE', tabela='nfse', status='ERRO', mensagem=str(e), cliente=self.client, correlation_id=correlation_id) + raise e + + def certificado_digital(self, metodo, dados=None, correlation_id=None): + # Gerenciamento de envio dos certificados A3/A1 + id_usr = dados.get('id_usuario', 1) if dados else 1 + try: + self.logger.logIC2(RealmType.LOGS, (f"Iniciando serviço de Certificado Digital - Método: {metodo.upper()}",)) + + certificado_digital = CertificadoDigital(api_key=self.api_key, banco=self.banco, url_base=self.url_base, logger=self.logger) + + metodo = metodo.lower() + resultado = None + + match metodo: + case "post": + self.logger.logIC2(RealmType.LOGS, ("Cadastrando certificado digital",)) + resultado = certificado_digital.post_form_data(data=dados) + case "get": + self.logger.logIC2(RealmType.LOGS, ("Listando certificados digitais",)) + resultado = certificado_digital.get(id=dados.get('id')) if dados and 'id' in dados else certificado_digital.get() + case "put": + self.logger.logIC2(RealmType.LOGS, ("Atualizando certificado digital",)) + resultado = certificado_digital.put_certificado(id=dados.get('id'), data=dados) + case _: + self.logger.logIC2(RealmType.ERRORS, ("Método não encontrado",)) + self.banco.logar_sistema(id_usuario=id_usr, acao=metodo.upper(), tabela='certificado_digital', status='ERRO', mensagem='Método não encontrado', cliente=self.client, correlation_id=correlation_id) + return json.dumps({"status": "error", "message": "Método não encontrado"}) + + if isinstance(resultado, dict) and "error" in resultado: + raise Exception(resultado["error"]) + + self.exchange.setPayload(json.dumps(resultado)) + self.logger.logIC2(RealmType.LOGS, ("Operação realizada com sucesso",)) + self.banco.logar_sistema(id_usuario=id_usr, acao=metodo.upper(), tabela='certificado_digital', status='SUCESSO', mensagem='Operação concluída', cliente=self.client, correlation_id=correlation_id) + return json.dumps({"status": "ok", "data": resultado}) + + except Exception as e: + self.logger.logIC2(RealmType.ERRORS, (f"Erro ao executar método {metodo} - {e}",)) + self.banco.logar_sistema(id_usuario=id_usr, acao=metodo.upper() if isinstance(metodo, str) else 'CERT', tabela='certificado_digital', status='ERRO', mensagem=str(e), cliente=self.client, correlation_id=correlation_id) + raise e + + def empresa(self, metodo, dados=None, correlation_id=None): + # Intermediador do cadastro e listagem das Empresas emissoras + id_usr = dados.get('id_usuario', 1) if dados else 1 + try: + self.logger.logIC2(RealmType.LOGS, (f"Iniciando serviço de Enterprise - Método: {metodo.upper()}",)) + + empresa_controller = EmpresaController( + api_key=self.api_key, + banco=self.banco, + url_base=self.url_base, + logger=self.logger + ) + + metodo = metodo.lower() + resultado = None + + match metodo: + case "post": + self.logger.logIC2(RealmType.LOGS, ("Cadastrando empresa",)) + empresa = Empresa(**dados) + resultado = empresa_controller.post(empresa=empresa, data=dados) + + case "get": + self.logger.logIC2(RealmType.LOGS, ("Listando empresas",)) + resultado = empresa_controller.get(cnpj=dados.get('cnpj'), data=dados) if dados and 'cnpj' in dados else empresa_controller.get(data=dados) + + case "post_file": + self.logger.logIC2(RealmType.LOGS, ("Cadastrando empresa via arquivo",)) + if not dados or 'file' not in dados or 'cnpj' not in dados: + raise ValueError("Parâmetros 'file' e 'cnpj' são obrigatórios para 'post_file'") + resultado = empresa_controller.post_file(file=dados['file'], cnpj=dados['cnpj']) + + case _: + self.logger.logIC2(RealmType.ERRORS, ("Método não encontrado",)) + self.banco.logar_sistema(id_usuario=id_usr, acao=metodo.upper(), tabela='Enterprise', status='ERRO', mensagem='Método não encontrado', cliente=self.client, correlation_id=correlation_id) + return json.dumps({"status": "error", "message": "Método não encontrado"}) + + if isinstance(resultado, dict) and "error" in resultado: + raise Exception(resultado["error"]) + + self.exchange.setPayload(json.dumps(resultado)) + self.logger.logIC2(RealmType.LOGS, ("Operação realizada com sucesso",)) + self.banco.logar_sistema(id_usuario=id_usr, acao=metodo.upper(), tabela='Enterprise', status='SUCESSO', mensagem='Operação concluída', json=json.dumps(resultado) if resultado else '{}', cliente=self.client, correlation_id=correlation_id) + return json.dumps({"status": "ok", "data": resultado}) + + except Exception as e: + print(f"\n[ERRO CRITICAL] {metodo.upper()} -> {str(e)}\n", flush=True); self.logger.logIC2(RealmType.ERRORS, (f"Erro ao executar método {metodo.upper()} - {str(e)}",)) + self.banco.logar_sistema(id_usuario=id_usr, acao=metodo.upper() if isinstance(metodo, str) else 'ENTERPRISE', tabela='Enterprise', status='ERRO', mensagem=str(e), cliente=self.client, correlation_id=correlation_id) + return json.dumps({"status": "error", "message": str(e)}) + + def servico(self, metodo, dados=None, correlation_id=None): + # Intermediador do CRUD de Serviços com configurações de impostos + id_usr = dados.get('id_usuario', 1) if dados else 1 + try: + self.logger.logIC2(RealmType.LOGS, (f"Iniciando serviço de Serviço - Método: {metodo.upper()}",)) + + servico_controller = ServicoController( + api_key=self.api_key, + banco=self.banco, + url_base=self.url_base, + logger=self.logger + ) + + metodo = metodo.lower() + resultado = None + + match metodo: + case "post": + self.logger.logIC2(RealmType.LOGS, ("Cadastrando serviço",)) + try: + servico = Servico(**dados) + except ValidationError as e: + self.logger.logIC2(RealmType.ERRORS, (f"Erro ao validar dados do serviço: {e}",)) + self.banco.logar_sistema(id_usuario=id_usr, acao='POST', tabela='servico', status='ERRO', mensagem=f'Validação: {e}', cliente=self.client, correlation_id=correlation_id) + return json.dumps({"status": "error", "message": str(e)}) + resultado = servico_controller.cadastrar_servico(servico=servico, data=dados) + + case "get": + self.logger.logIC2(RealmType.LOGS, ("Listando serviços",)) + if dados and "id" in dados: + resultado = servico_controller.buscar_servico(id_servico=dados.get('id'), data=dados) + else: + resultado = servico_controller.listar_servicos(data=dados) + + case "put": + self.logger.logIC2(RealmType.LOGS, ("Atualizando serviço",)) + resultado = servico_controller.atualizar_servico(id_servico=dados.get('id'), servico=dados) + + case "delete": + self.logger.logIC2(RealmType.LOGS, ("Deletando serviço",)) + resultado = servico_controller.deletar_servico(id_servico=dados.get('id')) + + case _: + self.logger.logIC2(RealmType.ERRORS, ("Método não encontrado",)) + self.banco.logar_sistema(id_usuario=id_usr, acao=metodo.upper(), tabela='servico', status='ERRO', mensagem='Método não encontrado', cliente=self.client, correlation_id=correlation_id) + return json.dumps({"status": "error", "message": "Método não encontrado"}) + + if isinstance(resultado, dict) and "error" in resultado: + raise Exception(resultado["error"]) + + self.exchange.setPayload(json.dumps(resultado)) + self.logger.logIC2(RealmType.LOGS, ("Operação realizada com sucesso",)) + self.banco.logar_sistema(id_usuario=id_usr, acao=metodo.upper(), tabela='servico', status='SUCESSO', mensagem='Operação concluída', cliente=self.client, correlation_id=correlation_id) + return json.dumps({"status": "ok", "data": resultado}) + + except Exception as e: + self.logger.logIC2(RealmType.ERRORS, (f"Erro ao executar método {metodo} - {e}",)) + self.banco.logar_sistema(id_usuario=id_usr, acao=metodo.upper() if isinstance(metodo, str) else 'SERVICO', tabela='servico', status='ERRO', mensagem=str(e), cliente=self.client, correlation_id=correlation_id) + raise e + + def tomador(self, metodo, dados=None, correlation_id=None): + # Gerencia clientes/clientes fiscais envolvidos nas transações (Tomadores) + id_usr = dados.get('id_usuario', 1) if dados else 1 + try: + self.logger.logIC2(RealmType.LOGS, (f"Iniciando serviço de Tomador - Método: {metodo.upper()}",)) + + tomador_controller = TomadorController( + api_key=self.api_key, + banco=self.banco, + url_base=self.url_base, + logger=self.logger + ) + + metodo = metodo.lower() + resultado = None + + match metodo: + case "post": + self.logger.logIC2(RealmType.LOGS, ("Cadastrando tomador",)) + tomador_model = Tomador(**dados) + resultado = tomador_controller.cadastrar_tomador(tomador=tomador_model, data=dados) + + case "get": + self.logger.logIC2(RealmType.LOGS, ("Listando tomadores",)) + resultado = tomador_controller.buscar_tomador(cpf_cnpj=dados.get('cpf_cnpj')) if dados and 'cpf_cnpj' in dados else tomador_controller.buscar_tomador() + + case "put": + self.logger.logIC2(RealmType.LOGS, ("Atualizando tomador",)) + tomador_model = Tomador(**dados) + resultado = tomador_controller.atualizar_tomador(cpf_cnpj=dados.get('cpf_cnpj'), tomador=tomador_model) + + case "delete": + self.logger.logIC2(RealmType.LOGS, ("Deletando tomador",)) + resultado = tomador_controller.deletar_tomador(cpf_cnpj=dados.get('cpf_cnpj')) + + case _: + self.logger.logIC2(RealmType.ERRORS, ("Método não encontrado",)) + self.banco.logar_sistema(id_usuario=id_usr, acao=metodo.upper(), tabela='tomador', status='ERRO', mensagem='Método não encontrado', cliente=self.client, correlation_id=correlation_id) + return json.dumps({"status": "error", "message": "Método não encontrado"}) + + if isinstance(resultado, dict) and "error" in resultado: + raise Exception(resultado["error"]) + + self.exchange.setPayload(json.dumps(resultado)) + self.logger.logIC2(RealmType.LOGS, ("Operação realizada com sucesso",)) + self.banco.logar_sistema(id_usuario=id_usr, acao=metodo.upper(), tabela='tomador', status='SUCESSO', mensagem='Operação concluída', cliente=self.client, correlation_id=correlation_id) + return json.dumps({"status": "ok", "data": resultado}) + except Exception as e: + self.logger.logIC2(RealmType.ERRORS, (f"Erro ao executar método {metodo} - {e}",)) + self.banco.logar_sistema(id_usuario=id_usr, acao=metodo.upper() if isinstance(metodo, str) else 'TOMADOR', tabela='tomador', status='ERRO', mensagem=str(e), cliente=self.client, correlation_id=correlation_id) + raise e + + def relatorios(self, metodo, dados=None, correlation_id=None): + # Extrai planilhas mensais/períodicas e relatórios aglomerados + id_usr = dados.get('id_usuario', 1) if dados else 1 + try: + self.logger.logIC2(RealmType.LOGS, (f"Iniciando serviço de Relatórios - Método: {metodo.upper()}",)) + + relatorios = Relatorios( + url_base=self.url_base, + api_key=self.api_key, + banco=self.banco, + logger=self.logger + ) + metodo = metodo.lower() + resultado = None + + match metodo: + case "nfe_emitidas": + self.logger.logIC2(RealmType.LOGS, ("Relatório de NF-e emitidas",)) + resultado = relatorios.nfe_emitidas() + + case "nfe_por_cnpj": + self.logger.logIC2(RealmType.LOGS, ("Relatório de NF-e por CNPJ",)) + resultado = relatorios.nfe_por_cnpj(cnpj=dados.get('cnpj')) + + case "nfse_emitidas": + self.logger.logIC2(RealmType.LOGS, ("Relatório de NFS-e emitidas",)) + resultado = relatorios.nfse_emitidas(data_inicial=dados.get('data_inicial'), data_final=dados.get('data_final')) + + case "nfse_por_cnpj": + self.logger.logIC2(RealmType.LOGS, ("Relatório de NFS-e por CNPJ",)) + resultado = relatorios.nfse_por_cnpj(cnpj=dados.get('cnpj'), data_inicial=dados.get('data_inicial'), data_final=dados.get('data_final')) + + case _: + self.logger.logIC2(RealmType.ERRORS, ("Método não encontrado",)) + self.banco.logar_sistema(id_usuario=id_usr, acao=metodo.upper(), tabela='relatorios', status='ERRO', mensagem='Método não encontrado', cliente=self.client, correlation_id=correlation_id) + return json.dumps({"status": "error", "message": "Método não encontrado"}) + + if isinstance(resultado, dict) and "error" in resultado: + raise Exception(resultado["error"]) + + self.exchange.setPayload(json.dumps(resultado)) + self.logger.logIC2(RealmType.LOGS, ("Operação realizada com sucesso",)) + self.banco.logar_sistema(id_usuario=id_usr, acao=metodo.upper(), tabela='relatorios', status='SUCESSO', mensagem='Operação concluída', cliente=self.client, correlation_id=correlation_id) + return json.dumps({"status": "ok", "data": resultado}) + + except Exception as e: + self.logger.logIC2(RealmType.ERRORS, (f"Erro ao executar método {metodo} - {e}",)) + self.banco.logar_sistema(id_usuario=id_usr, acao=metodo.upper() if isinstance(metodo, str) else 'RELATORIOS', tabela='relatorios', status='ERRO', mensagem=str(e), cliente=self.client, correlation_id=correlation_id) + raise e + + def webhooks(self, metodo, dados=None, correlation_id=None): + # Gerencia as rotas externas para qual a PlugNotas envia atualização + id_usr = dados.get('id_usuario', 1) if dados else 1 + try: + self.logger.logIC2(RealmType.LOGS, (f"Iniciando serviço de Webhooks - Método: {metodo.upper()}",)) + + webhooks = Webhooks( + url_base=self.url_base, + api_key=self.api_key, + banco=self.banco, + logger=self.logger + ) + + metodo = metodo.lower() + resultado = None + + match metodo: + case "get": + self.logger.logIC2(RealmType.LOGS, ("Consultando webhooks",)) + resultado = webhooks.consultar_webhooks(cnpj=dados.get('cnpj')) + + case "post": + self.logger.logIC2(RealmType.LOGS, ("Criando webhook",)) + resultado = webhooks.criar_webhook(cnpj=dados.get('cnpj'), url=dados.get('url')) + + case "delete": + self.logger.logIC2(RealmType.LOGS, ("Deletando webhook",)) + resultado = webhooks.deletar_webhook(cnpj=dados.get('cnpj')) + + case "put": + self.logger.logIC2(RealmType.LOGS, ("Atualizando webhook",)) + resultado = webhooks.atualizar_webhook(cnpj=dados.get('cnpj'), url=dados.get('url')) + + case "test": + self.logger.logIC2(RealmType.LOGS, ("Testando webhook",)) + resultado = webhooks.testar_webhook(cnpj=dados.get('cnpj')) + + case _: + self.logger.logIC2(RealmType.ERRORS, ("Método não encontrado",)) + self.banco.logar_sistema(id_usuario=id_usr, acao=metodo.upper(), tabela='webhooks', status='ERRO', mensagem='Método não encontrado', cliente=self.client, correlation_id=correlation_id) + return json.dumps({"status": "error", "message": "Método não encontrado"}) + + if isinstance(resultado, dict) and "error" in resultado: + raise Exception(resultado["error"]) + + self.exchange.setPayload(json.dumps(resultado)) + self.logger.logIC2(RealmType.LOGS, ("Operação realizada com sucesso",)) + self.banco.logar_sistema(id_usuario=id_usr, acao=metodo.upper(), tabela='webhooks', status='SUCESSO', mensagem='Operação concluída', cliente=self.client, correlation_id=correlation_id) + + except Exception as e: + self.logger.logIC2(RealmType.ERRORS, (f"Erro ao executar método {metodo} - {e}",)) + self.banco.logar_sistema(id_usuario=id_usr, acao=metodo.upper() if isinstance(metodo, str) else 'WEBHOOKS', tabela='webhooks', status='ERRO', mensagem=str(e), cliente=self.client, correlation_id=correlation_id) + raise e + + def callback(self, ch, method, properties, body): + #Descriptografa a msg, e repassa pra função correta (ex: nfe) + try: + message = body.decode("utf-8") + dado = json.loads(message) + + hmac = dado.get('hmac') + payload = dado.get('payload') + + if hmac and payload: + time.sleep(random.uniform(1,5)) + servicekey = self.system_config.servicekey.ljust(32, '0')[:32] + self.exchange.setCrypto(serviceid=self.system_config.serviceid, + servicekey=servicekey) + + if self.exchange.getPayload(hmac, payload): + payload_dict = json.loads(self.exchange.payload) + + self.client = payload_dict.get('cliente') + self.logger.client = self.client + + # Lógica de Configuração Persistente (Redis) + redis_key = f"config:{self.client}" + stored_config_str = self.redis.get(redis_key) + stored_config = json.loads(stored_config_str) if stored_config_str else {} + + incoming_config = payload_dict.get('config', {}) + + if incoming_config: + # Mescla: o que vem no payload sobrescreve o que está no Redis + final_config_dict = {**stored_config, **incoming_config} + # Salva a configuração atualizada no Redis + self.redis.set(redis_key, json.dumps(final_config_dict)) + payload_dict['config'] = final_config_dict + self.logger.logIC2(RealmType.LOGS, (f"Configuração atualizada e salva para o cliente {self.client}",)) + elif stored_config: + # Usa a configuração persistida + payload_dict['config'] = stored_config + self.logger.logIC2(RealmType.LOGS, (f"Usando configuração persistida do Redis para o cliente {self.client}",)) + + # Validação do payload contra os modelos tipados + try: + if isinstance(payload_dict.get('dados'), dict) and 'incentivoCultural' in payload_dict['dados']: + payload_dict['dados']['incentivadorCultural'] = payload_dict['dados'].pop('incentivoCultural') + + validate_payload(payload_dict) + self.logger.logIC2(RealmType.LOGS, (f"Payload validado: {payload_dict.get('fila')}/{payload_dict.get('metodo')}",)) + except (ValidationError, ValueError) as ve: + self.logger.logIC2(RealmType.ERRORS, (f"Payload inválido: {str(ve)}",)) + ch.basic_nack(delivery_tag=method.delivery_tag, requeue=False) + return json.dumps({"status": "error", "message": f"Payload inválido: {str(ve)}"}) + + if not payload_dict.get('config'): + self.logger.logIC2(RealmType.ERRORS, (f"Configuração não encontrada para o cliente {self.client}",)) + ch.basic_nack(delivery_tag=method.delivery_tag, requeue=False) + return json.dumps({"status": "error", "message": "Configuração incompleta e não encontrada no sistema"}) + + config = Config(**payload_dict['config']) + self.logger.logIC2(RealmType.LOGS, (f"Configuração validada para o cliente {self.client}",)) + + api_key_extraida = config.api_key or config.x_api_key + expected_pg_user = os.getenv("POSTGRES_USER", "tecnospeed") + if config.user_postgres != expected_pg_user: + self.logger.logIC2( + RealmType.ERRORS, + (f"Usuário PostgreSQL inválido: {config.user_postgres}. Permitido: {expected_pg_user}",) + ) + ch.basic_nack(delivery_tag=method.delivery_tag, requeue=False) + return json.dumps({"status": "error", "message": f"Apenas o usuário PostgreSQL '{expected_pg_user}' é permitido"}) + + if api_key_extraida and config.user_postgres and config.pass_postgres and config.host_postgres and config.port_postgres and config.database_postgres: + + self.api_key = api_key_extraida + self.banco = Banco(user=config.user_postgres, + password=config.pass_postgres, + host=config.host_postgres, + port=config.port_postgres, + database=config.database_postgres) + + self.banco.create_tables() + + self.logger.logIC2(RealmType.LOGS, (f"Configuração completa para o cliente {self.client}",)) + if 'url_base' in payload_dict: + self.url_base = payload_dict['url_base'] + + target_fila = str(payload_dict.get('fila', '')).lower() + if target_fila not in self.funcoes: + self.logger.logIC2(RealmType.ERRORS, (f"Fila '{target_fila}' não encontrada",)) + ch.basic_nack(delivery_tag=method.delivery_tag, requeue=False) + return json.dumps({"status":"error","message":"Fila não encontrada"}) + + corr_id = self.banco.gerar_correlation_id() + self.logger.logIC2(RealmType.LOGS, (f"[{corr_id}] Processando {target_fila}/{payload_dict['metodo']} para cliente {self.client}",)) + retorno = self.funcoes[target_fila](payload_dict['metodo'], payload_dict['dados'], correlation_id=corr_id) + ch.basic_ack(delivery_tag=method.delivery_tag) + return retorno + + else: + self.logger.logIC2(RealmType.ERRORS, ("Configuração incompleta",)) + ch.basic_nack(delivery_tag=method.delivery_tag, requeue=False) + return json.dumps({"status":"error","message":"Configuração incompleta"}) + else: + self.logger.logIC2(RealmType.ERRORS, (f"Erro ao decodificar payload - {payload}",)) + ch.basic_nack(delivery_tag=method.delivery_tag, requeue=False) + return json.dumps({"status":"error","message":"Erro ao decodificar payload"}) + + else: + if self.logger: + self.logger.logIC2(RealmType.ERRORS, (f"Erro ao receber dado (sem hmac/payload) - {dado}",)) + ch.basic_nack(delivery_tag=method.delivery_tag, requeue=False) + return json.dumps({"status":"error","message":"Erro ao receber dado"}) + + except Exception as e: + if self.logger: + self.logger.logIC2(RealmType.ERRORS, (f"Erro ao executar callback - {e}",)) + ch.basic_nack(delivery_tag=method.delivery_tag, requeue=False) + return json.dumps({"status":"error","message":str(e)}) + finally: + if hasattr(self, 'banco') and self.banco: + self.banco.close() + + def run(self): + # Ponto de Partida. Conecta no Redis, constrói os parâmetros AMQP (URL) e inicia o loop RabbitMQ + try: + print("[STATUS] Conectando ao RabbitMQ") + + try: + system_config = self.redis.get("system_config") + if system_config: + system_config = json.loads(system_config) + self.system_config = SystemConfig(**system_config) + print(f"[INFO] Configuração do sistema carregada - {self.system_config}") + else: + msg = "Configuração do sistema não encontrada no Redis" + print(f"[ERRO] {msg}") + raise Exception(msg) + except Exception as e: + msg = f"Erro ao conectar ao Redis - {e}" + print(f"[ERRO] {msg}") + raise + + # Inicializa LoggerExchange2 após obter a URL AMQP do Redis + builder = AppIC2Builder(service_name=self.service, versionstr=self.version, origin=self.system_config.origin) + self.logger = LoggerExchange2( + ic2_builder=builder, + amqps=self.system_config.amqps, + system=self.system, + service=self.service, + version=self.version, + origin=self.system_config.origin + ) + self.logger.logIC2(RealmType.LOGS, ("LoggerExchange2 inicializado com sucesso",)) + + self.exchange = QueueExchange2( + amqps=self.system_config.amqps, + origin=self.system_config.origin, + system=self.system, + client=self.client, + service=self.service, + version=self.version, + callback=self.callback, + delivery_mode = 1) + self.logger.logIC2(RealmType.LOGS, (f"Consumindo da fila {self.exchange.queue}",)) + self.exchange.start_consuming() + + except Exception as e: + print(f"[ERRO FATAL (CONSOLE)] Erro ao conectar ao RabbitMQ - {e}") + if self.logger: + self.logger.logIC2(RealmType.ERRORS, (f"Erro fatal ao conectar ao RabbitMQ - {e}",)) + else: + print(f"[ERRO] Erro ao conectar ao RabbitMQ - {e}") + time.sleep(5) + sys.exit(1) + +if __name__ == "__main__": + main = Main() + main.run() diff --git a/tecnospeed_queue/src/tests/__init__.py b/tecnospeed_queue/src/tests/__init__.py new file mode 100644 index 0000000..65140f2 --- /dev/null +++ b/tecnospeed_queue/src/tests/__init__.py @@ -0,0 +1 @@ +# tests package diff --git a/tecnospeed_queue/src/tests/conftest.py b/tecnospeed_queue/src/tests/conftest.py new file mode 100644 index 0000000..db70125 --- /dev/null +++ b/tecnospeed_queue/src/tests/conftest.py @@ -0,0 +1,119 @@ +""" +Fixtures compartilhados para os testes E2E do servico_plugnotas. + +Cria um banco SQLite in-memory, mocks do Logger e da Api, +e uma instância pré-configurada da classe Main para cada teste. +""" +import pytest +import json +import sys +from unittest.mock import MagicMock, patch + +mock_pika = MagicMock() +sys.modules["pika"] = mock_pika + +mock_redis_module = MagicMock() +sys.modules["redis"] = mock_redis_module + +from utils.banco import Base, Banco, LogTable +from utils.api import Api + +@pytest.fixture +def banco_teste(): + """Cria um Banco real com SQLite in-memory para cada teste (isolado).""" + from sqlalchemy import create_engine + from sqlalchemy.orm import sessionmaker + + engine = create_engine("sqlite:///:memory:") + Base.metadata.create_all(engine) + Session = sessionmaker(bind=engine) + + banco = Banco.__new__(Banco) + banco.engine = engine + banco.session = Session() + + yield banco + + banco.session.close() + engine.dispose() + +@pytest.fixture +def mock_logger(): + """Logger que grava todas as chamadas de log em uma lista para inspeção.""" + logger = MagicMock() + logger.logs = [] + logger.client = "TestClient" + + def capture_log(realm, msg_tuple): + logger.logs.append({"realm": str(realm), "message": msg_tuple[0] if msg_tuple else ""}) + + logger.logIC2.side_effect = capture_log + return logger + +@pytest.fixture +def mock_api(): + """Api que retorna respostas configuráveis sem fazer chamadas HTTP reais.""" + api = MagicMock(spec=Api) + api.get.return_value = {"status": "ok"} + api.post.return_value = {"documents": [{"id": "PLUG-001"}]} + api.put.return_value = {"status": "ok"} + api.patch.return_value = {"status": "ok"} + api.delete.return_value = {"status": "ok"} + api.post_file.return_value = {"status": "ok"} + api.post_form_data.return_value = {"data": {"id": "CERT-001"}} + api.get_paginated.return_value = {"data": [], "total": 0} + return api + +@pytest.fixture +def mock_exchange(): + """Exchange que simula setPayload sem enviar nada.""" + exchange = MagicMock() + exchange.payload = "" + exchange.payloads_set = [] + + def capture_payload(p): + exchange.payloads_set.append(p) + exchange.payload = p + + exchange.setPayload.side_effect = capture_payload + return exchange + +@pytest.fixture +def main_instance(banco_teste, mock_logger, mock_exchange): + with patch("utils.Exchange2.RPCExchange2"), \ + patch("utils.Exchange2.QueueExchange2"), \ + patch("utils.Exchange2.LoggerExchange2"), \ + patch("utils.redis.RedisClient"): + + from main import Main + + with patch.object(Main, '__init__', lambda self: None): + main = Main() + + main.client = "TestClient" + main.api_key = "test-api-key-fake" + main.exchange = mock_exchange + main.banco = banco_teste + main.system_config = MagicMock() + main.system_config.serviceid = "test-id" + main.system_config.servicekey = "test-key-32-chars-long-12345678" + main.redis = MagicMock() + main.service = "tecnospeed" + main.system = "integracao" + main.version = "2.0.1-test" + main.logger = mock_logger + main.url_base = "https://api.sandbox.plugnotas.com.br" + + main.funcoes = { + "nfe": main.nfe, + "nfse": main.nfse, + "certificado_digital": main.certificado_digital, + "empresa": main.empresa, + "servico": main.servico, + "tomador": main.tomador, + "relatorios": main.relatorios, + "webhooks": main.webhooks, + } + + return main + diff --git a/tecnospeed_queue/src/tests/debug_payload.py b/tecnospeed_queue/src/tests/debug_payload.py new file mode 100644 index 0000000..611e9bb --- /dev/null +++ b/tecnospeed_queue/src/tests/debug_payload.py @@ -0,0 +1,67 @@ +import json +import os +import sys + +# Garante que o Python encontre os módulos do projeto +sys.path.append(os.path.dirname(os.path.dirname(os.path.abspath(__file__)))) + +from Models.payload import validate_payload + +sample_payload = { + "fila": "nfse", + "cliente": "MassiveNFSeTest", + "metodo": "POST", + "dados": { + "id_empresa": "some-uuid", + "id_usuario": 1, + "idIntegracao": "TEST_123", + "prestador": { + "cpfCnpj": "00000000000000" + }, + "tomador": { + "cpfCnpj": "08114280956", + "razaoSocial": "NFSE EMITIDA EM HOMOLOGACAO", + "email": "contato@tecnospeed.com.br", + "endereco": { + "logradouro": "AVENIDA DUQUE DE CAXIAS", + "numero": "882", + "bairro": "CENTRO", + "codigoCidade": "4115200", + "descricaoCidade": "MARINGA", + "estado": "PR", + "cep": "87020025", + "tipoLogradouro": "Avenida" + } + }, + "servico": { + "codigo": "1.01", + "codigoTributacao": "1.01", + "discriminacao": "SERVICO DE TESTE DE CARGA MASSIVA", + "cnae": "6201501", + "iss": { + "tipoTributacao": 1, + "exigibilidade": 1, + "aliquota": 2.0 + }, + "valor": { + "servico": 100.0 + } + } + }, + "config": { + "x_api_key": "2da392a6-79d2-4304-a8b7-959572c7e44d", + "user_postgres": "tecnospeed", + "pass_postgres": "u7QimG7E!SF//JC2c8", + "host_postgres": "banco", + "port_postgres": "5432", + "database_postgres": "tecnospeed" + }, + "url_base": "https://api.sandbox.plugnotas.com.br" +} + +try: + print("Tentando validar payload...") + validated = validate_payload(sample_payload) + print("Sucesso! Payload válido.") +except Exception as e: + print(f"Erro de validação: {e}") diff --git a/tecnospeed_queue/src/tests/debug_report_api.py b/tecnospeed_queue/src/tests/debug_report_api.py new file mode 100644 index 0000000..69bc364 --- /dev/null +++ b/tecnospeed_queue/src/tests/debug_report_api.py @@ -0,0 +1,33 @@ +import requests +import os +import json + +def debug_report(): + api_key = "2da392a6-79d2-4304-a8b7-959572c7e44d" # From src/utils/api.py comment + url = "https://api.sandbox.plugnotas.com.br/nfse/relatorio?from=2026-04-01&to=2026-04-30" + + headers = { + "x-api-key": api_key + } + + print(f"Testing URL: {url}") + response = requests.get(url, headers=headers) + + print(f"Status Code: {response.status_code}") + try: + print(f"Response Body: {json.dumps(response.json(), indent=2)}") + except: + print(f"Response Body: {response.text}") + + # Test with ISO format + url_iso = "https://api.sandbox.plugnotas.com.br/nfse/relatorio?from=2026-04-01T00:00:00&to=2026-04-30T23:59:59" + print(f"\nTesting URL (ISO): {url_iso}") + response = requests.get(url_iso, headers=headers) + print(f"Status Code: {response.status_code}") + try: + print(f"Response Body: {json.dumps(response.json(), indent=2)}") + except: + print(f"Response Body: {response.text}") + +if __name__ == "__main__": + debug_report() diff --git a/tecnospeed_queue/src/tests/purge_queue.py b/tecnospeed_queue/src/tests/purge_queue.py new file mode 100644 index 0000000..e5c00c5 --- /dev/null +++ b/tecnospeed_queue/src/tests/purge_queue.py @@ -0,0 +1,46 @@ +import pika +import os +import sys +import json + +# Garante que o Python encontre os módulos do projeto +sys.path.append(os.path.dirname(os.path.dirname(os.path.abspath(__file__)))) + +from utils.redis import RedisClient + +def purge_queue(): + # 1. Obter URL do RabbitMQ do Redis + redis_host = os.getenv('REDIS_HOST', 'localhost') + redis_client = RedisClient(host=redis_host, port=6379) + try: + system_config_data = redis_client.get("system_config") + if not system_config_data: + print("Erro: system_config não encontrado no Redis.") + return + system_config = json.loads(system_config_data) + amqps_url = system_config.get("amqps") + except Exception as e: + print(f"Erro ao ler configs do Redis: {e}") + return + + if not amqps_url: + print("Erro: AMQPS URL não encontrada na configuração.") + return + + # 2. Conectar e limpar a fila + queue_name = "integracao_tecnospeed" + try: + params = pika.URLParameters(amqps_url) + connection = pika.BlockingConnection(params) + channel = connection.channel() + + # Tenta realizar o purge + result = channel.queue_purge(queue=queue_name) + print(f"Sucesso! Fila [{queue_name}] limpa. Mensagens removidas: {result.method.message_count}") + + connection.close() + except Exception as e: + print(f"Erro ao limpar a fila: {e}") + +if __name__ == "__main__": + purge_queue() diff --git a/tecnospeed_queue/src/tests/test.py b/tecnospeed_queue/src/tests/test.py new file mode 100644 index 0000000..257b05d --- /dev/null +++ b/tecnospeed_queue/src/tests/test.py @@ -0,0 +1,134 @@ +import sys +import os +import json + +sys.path.append(os.path.dirname(os.path.abspath(__file__))) + +from Models.config import SystemConfig +from utils.redis import RedisClient +from utils.Exchange2 import RPCExchange2 + +def main(): + print("Iniciando script de testes da mensageria...") + + # Carregando configuração do Redis + redis_client = RedisClient(host=os.getenv('REDIS_HOST', 'localhost'), port=6379) + try: + system_config_data = redis_client.get("system_config") + if not system_config_data: + print("Erro: system_config não encontrado no Redis. Certifique-se de que a variável existe.") + return + + system_config = SystemConfig(**json.loads(system_config_data)) + print(f"[{system_config.origin}] Configuração do sistema lida com sucesso.") + + except Exception as e: + print(f"Erro ao ler configs do Redis: {e}") + return + + # Inicializando mensageria RPC + try: + amqps_local = system_config.amqps.replace("host.docker.internal", "localhost") + exchange = RPCExchange2( + amqps=amqps_local, + origin=system_config.origin, + system="integracao", + service="tecnospeed", + version="2.0.1", + client="#" + ) + except Exception as e: + print(f"Falha ao inicializar mensageria: {e}") + return + + # Configurando criptografia e payload + servicekey = system_config.servicekey.ljust(32, '0')[:32] + exchange.setCrypto(serviceid=system_config.serviceid, servicekey=servicekey) + + # Payload da mensageria + + payload_interno = { + "fila": "nfse", + "cliente": "Test", + "metodo": "POST", + "dados": { + "idIntegracao": "INTEGRACAO-NFSE-002", + "prestador": { + "cpfCnpj": "08187168000160" + }, + "tomador": { + "cpfCnpj": "08114280956", + "razaoSocial": "João Tomador da Silva", + "inscricaoMunicipal": "isento", + "email": "tomador@email.com", + "endereco": { + "descricaoCidade": "São Paulo", + "cep": "00000000", + "tipoLogradouro": "Rua", + "logradouro": "Dois de Ouro", + "tipoBairro": "Bairro", + "codigoCidade": "3550308", + "complemento": "Apto 13", + "estado": "SP", + "numero": "44", + "bairro": "Centro" + } + }, + "servico": { + "codigo": "1.01", + "codigoTributacao": "1.01", + "discriminacao": "Serviços prestados de tecnologia", + "cnae": "6204000", + "iss": { + "tipoTributacao": 6, + "exigibilidade": 1, + "aliquota": 2.5 + }, + "valor": { + "servico": 1500.50, + "descontoCondicionado": 0.0, + "descontoIncondicionado": 0.0 + } + } + }, + "config": { + "x_api_key": os.getenv('PLUGNOTAS_API_KEY', ''), + "user_postgres": os.getenv('POSTGRES_USER', 'tecnospeed'), + "pass_postgres": os.getenv('POSTGRES_PASSWORD', 'tecnospeed'), + "host_postgres": os.getenv('POSTGRES_HOST', 'localhost'), + "port_postgres": os.getenv('POSTGRES_PORT', '5432'), + "database_postgres": os.getenv('POSTGRES_DB', 'tecnospeed') + }, + "url_base": "https://api.sandbox.plugnotas.com.br" + } + + + # Criptografando e enviando tarefa + exchange.setPayload(json.dumps(payload_interno)) + + print("Disparando chamada RPC para o RabbitMQ...") + # Executando chamada RPC + response = exchange.callRPC("Disparo de testes executado pelo test.py") + print("\n[RAW RPC RESPONSE]:") + print(response) + + # Descriptografando a resposta + try: + resp_obj = json.loads(response) + hmac_resp = resp_obj.get('hmac') + payload_resp = resp_obj.get('payload') + + if hmac_resp and payload_resp: + if exchange.getPayload(hmac_resp, payload_resp): + print("\n[DECRYPTED RPC DATA]:") + print(json.dumps(json.loads(exchange.payload), indent=2)) + else: + print("\n[ERROR]: Failed to decrypt response.") + else: + print("\n[INFO]: Response does not contain an encrypted payload.") + + except Exception as e: + print(f"\n[ERROR]: Error processing response: {e}") + +if __name__ == "__main__": + main() diff --git a/tecnospeed_queue/src/tests/testArch.py b/tecnospeed_queue/src/tests/testArch.py new file mode 100644 index 0000000..1fe2d01 --- /dev/null +++ b/tecnospeed_queue/src/tests/testArch.py @@ -0,0 +1,58 @@ +import pytest +from unittest.mock import patch, MagicMock +from fastapi.testclient import TestClient +from src.listenerEndpoint import app + +client = TestClient(app) + +PAYLOAD_VALIDO = { + "id": "60a7c4f1c9d2a00015...", + "status": "CONCLUIDO", + "documento": "nfe" +} +HEADERS_VALIDOS = {"x-plugnotas-signature": "assinatura_hmac_valida"} + +class TestPlugNotasWebhookEndpoint: + + @patch("src.listenerEndpoint.exchange") + def test_deve_receber_payload_e_enfileirar_com_sucesso(self, mock_exchange): + """Critério: Caminho feliz. Valida, enfileira e responde com sucesso (200).""" + mock_exchange.sendTask.return_value = True + + response = client.post("/listenerPlugnotas", json=PAYLOAD_VALIDO, headers=HEADERS_VALIDOS) + + assert response.status_code == 200 + assert response.json() == {"status": "ok", "message": "Webhook recebido e encaminhado para processamento"} + + mock_exchange.setPayload.assert_called_once() + mock_exchange.sendTask.assert_called_once_with("Webhook PlugNotas Recebido") + + @patch("src.listenerEndpoint.exchange") + def test_deve_rejeitar_payload_invalido_antes_de_processar(self, mock_exchange): + """Critério: Integridade de Dados. Valida campos obrigatórios recebidos do Hook.""" + payload_invalido = {"status": "CONCLUIDO"} # Faltando ID e documento + + response = client.post("/listenerPlugnotas", json=payload_invalido, headers=HEADERS_VALIDOS) + + assert response.status_code == 200 + assert response.json()["status"] == "warning" + assert "Campos faltantes" in response.json()["message"] + + @patch("src.listenerEndpoint.exchange") + def test_deve_retornar_erro_se_internal_error_no_rabbitmq(self, mock_exchange): + """Critério: Resiliência. Retorna 500 em caso de erro ao enviar para fila.""" + # Simulando uma queda ao enviar task + mock_exchange.sendTask.side_effect = Exception("RabbitMQ is down") + + response = client.post("/listenerPlugnotas", json=PAYLOAD_VALIDO, headers=HEADERS_VALIDOS) + + assert response.status_code == 500 + assert response.json()["message"] == "Erro interno no processamento do webhook" + + def test_deve_retornar_erro_se_exchange_nao_inicializado(self): + """Critério: Resiliência. Retorna 500 se o exchange global for nulo.""" + # Sem patch de exchange, então pegará o exchange default (None) em client sem lifespan completo + response = client.post("/listenerPlugnotas", json=PAYLOAD_VALIDO) + + assert response.status_code == 500 + assert response.json()["message"] == "Serviço temporariamente indisponível" \ No newline at end of file diff --git a/tecnospeed_queue/src/tests/test_1000_empresas.py b/tecnospeed_queue/src/tests/test_1000_empresas.py new file mode 100644 index 0000000..c24a82a --- /dev/null +++ b/tecnospeed_queue/src/tests/test_1000_empresas.py @@ -0,0 +1,144 @@ +import os +import sys +import json +import time + +# Garante que o Python encontre os módulos do projeto +sys.path.append(os.path.dirname(os.path.dirname(os.path.abspath(__file__)))) + +from Models.config import SystemConfig +from utils.redis import RedisClient +from utils.Exchange2 import RPCExchange2, QueueExchange2 + +def get_empresa_payload(i): + """Gera um payload de empresa com CNPJ e Razão Social baseados no índice i""" + # Gerando um CNPJ fictício baseado no loop para evitar duplicidade no banco + # Ex: 94074986000001, 94074986000002... + cnpj = f"94074986{str(i).zfill(6)}" + + return { + "id_usuario": 1, + "cpfCnpj": cnpj, + "razaoSocial": f"Varejo de Teste {i} LTDA", + "nomeFantasia": f"Varejo {i}", + "inscricaoMunicipal": "0001", + "inscricaoEstadual": "123456789", + "simplesNacional": True, + "regimeTributario": 1, + "regimeTributarioEspecial": 1, + "incentivadorCultural": False, + "incentivoFiscal": True, + "certificado": "5af59d271f6e8f409178fbf3", + "endereco": { + "codigoPais": "1058", + "descricaoPais": "BRASIL", + "tipoLogradouro": "Avenida", + "logradouro": "Avenida de Teste", + "numero": str(i), + "complemento": "Casa", + "tipoBairro": "Bairro", + "bairro": "Centro", + "codigoCidade": "4115200", + "descricaoCidade": "Maringá", + "estado": "PR", + "cep": "87020025" + }, + "nfe": {"ativo": True, "tipoContrato": 0}, + "nfse": {"ativo": False} + } + +def run_rpc_test(exchange, config_rpc): + """Envia uma única empresa via RPC e aguarda a resposta (bom para debug)""" + print("\n--- INICIANDO TESTE RPC INDIVIDUAL ---") + payload_dados = get_empresa_payload(1) + + full_payload = { + "fila": "empresa", + "metodo": "post", + "cliente": "tray", + "url_base": "https://api.sandbox.plugnotas.com.br", + "config": config_rpc, + "dados": payload_dados + } + + print(f"Enviando empresa {payload_dados['cpfCnpj']} via RPC...") + exchange.setPayload(json.dumps(full_payload)) + resposta = exchange.callRPC(json.dumps(full_payload), timeout=30) + + print("\nRESPOSTA DO SERVIDOR:") + print(resposta) + print("--------------------------------------\n") + +def run_mass_test(exchange, config_rpc, total=1000): + """Envia N empresas via Fila (sem esperar resposta), ideal para teste de carga""" + print(f"\n--- INICIANDO CARGA MASSIVA: {total} Mensagens ---") + start_time = time.time() + + for i in range(1, total + 1): + payload_dados = get_empresa_payload(i) + + full_payload = { + "fila": "empresa", + "metodo": "post", + "cliente": "MassTest", + "url_base": "https://api.sandbox.plugnotas.com.br", + "config": config_rpc, + "dados": payload_dados + } + + exchange.setPayload(json.dumps(full_payload)) + exchange.sendMsg(json.dumps(full_payload), service="tecnospeed") + + if i % 100 == 0: + print(f"Enviadas: {i}/{total}...") + + end_time = time.time() + print(f"Carga concluída em {end_time - start_time:.2f} segundos.") + print("--------------------------------------\n") + +def main(): + # 1. Carrega configurações do Redis + redis_host = os.getenv('REDIS_HOST', 'localhost') + redis_client = RedisClient(host=redis_host, port=6379) + + try: + config_data = redis_client.get("system_config") + if not config_data: + print("Erro: system_config não encontrado no Redis.") + return + sc = SystemConfig(**json.loads(config_data)) + except Exception as e: + print(f"Erro ao carregar configs: {e}") + return + + # Configurações de Banco e API para o Worker + config_rpc = { + "x_api_key": "2da392a6-79d2-4304-a8b7-959572c7e44d", + "user_postgres": "tecnospeed", + "pass_postgres": "u7QimG7E%21SF%2F%2FJC2c8", + "host_postgres": "banco", + "port_postgres": "5432", + "database_postgres": "tecnospeed" + } + + print(f"Conectando ao RabbitMQ de {sc.origin}...") + + # 2. Escolha do Modo de Operação + # Mude para True para testar o retorno (RPC Server) ou False para carga assíncrona (Queue Worker) + MODO_DEBUG_RPC = True + + if MODO_DEBUG_RPC: + # Usa RPCExchange2 para modo Síncrono (Request-Response) + exchange = RPCExchange2(sc.amqps, sc.origin, "integracao", "tecnospeed", "2.1.0", client="") + servicekey = sc.servicekey.ljust(32, '0')[:32] + exchange.setCrypto(sc.serviceid, servicekey) + run_rpc_test(exchange, config_rpc) + else: + # Usa QueueExchange2 para modo Assíncrono (Fire and Forget) + exchange = QueueExchange2(sc.amqps, sc.origin, "integracao", "tecnospeed", "2.1.0", client="#") + servicekey = sc.servicekey.ljust(32, '0')[:32] + exchange.setCrypto(sc.serviceid, servicekey) + run_mass_test(exchange, config_rpc, total=1000) + +if __name__ == "__main__": + main() \ No newline at end of file diff --git a/tecnospeed_queue/src/tests/test_1000_nfe.py b/tecnospeed_queue/src/tests/test_1000_nfe.py new file mode 100644 index 0000000..d72a10a --- /dev/null +++ b/tecnospeed_queue/src/tests/test_1000_nfe.py @@ -0,0 +1,221 @@ +import sys +import os +import json +import time + +# Adiciona o diretório atual ao path para importar as classes locais +sys.path.append(os.path.dirname(os.path.abspath(__file__))) + +# Adiciona o diretório src ao path para encontrar os Models e Utils +sys.path.append(os.path.dirname(os.path.dirname(os.path.abspath(__file__)))) + +from Models.config import SystemConfig +from utils.redis import RedisClient +from utils.Exchange2 import QueueExchange2 +from utils.banco import Banco, EnterpriseTable +import random + +def main(): + print("Iniciando script de teste de carga: 1000 NFes...", flush=True) + + # 1. Carregando configuração do Redis para obter dados do RabbitMQ + redis_host = os.getenv('REDIS_HOST', 'localhost') + redis_client = RedisClient(host=redis_host, port=6379) + try: + system_config_data = redis_client.get("system_config") + if not system_config_data: + print("Erro: system_config não encontrado no Redis.") + return + + system_config = SystemConfig(**json.loads(system_config_data)) + print(f"Configuração [{system_config.origin}] carregada.", flush=True) + except Exception as e: + print(f"Erro ao ler configs do Redis: {e}") + return + + # 2. Inicializando a conexão com a FILA (RabbitMQ) + try: + print("Tentando conectar ao RabbitMQ...", flush=True) + exchange = QueueExchange2( + amqps=system_config.amqps, + origin=system_config.origin, + system="integracao", + service="tecnospeed", + version="2.1.0", + client="#" + ) + except Exception as e: + print(f"Falha ao iniciar mensageria: {e}") + return + + # 3. Configurando criptografia (necessário para o worker aceitar a mensagem) + servicekey = system_config.servicekey.ljust(32, '0')[:32] + exchange.setCrypto(serviceid=system_config.serviceid, servicekey=servicekey) + + # 4. Buscando empresas válidas no banco para o teste + try: + # Configuração do banco - Força conexão interna se estiver no Docker + db_host = os.getenv('POSTGRES_HOST', 'banco') + db_port = int(os.getenv('POSTGRES_PORT', 5432)) + + # Se estivermos DENTRO do docker, o host 'banco' é o correto. + if db_host != 'banco': + print(f"Aviso: POSTGRES_HOST no ambiente é {db_host}. Tentando 'banco' primeiro por estar no Docker...") + db_host = 'banco' + db_port = 5432 + + print(f"Conectando ao banco em {db_host}:{db_port}...") + + banco = Banco( + host=db_host, + port=db_port, + user=os.getenv('POSTGRES_USER', 'tecnospeed'), + password=os.getenv('POSTGRES_PASSWORD', 'u7QimG7E!SF//JC2c8'), + database=os.getenv('POSTGRES_DB', 'tecnospeed') + ) + print("Executando query na tabela Enterprise...", flush=True) + empresas = banco.session.query(EnterpriseTable.id, EnterpriseTable.cnpj).filter(EnterpriseTable.cnpj != None, EnterpriseTable.cnpj != "").limit(100).all() + lista_empresas = [{"id": emp.id, "cnpj": emp.cnpj} for emp in empresas] + if not lista_empresas: + print("Erro: Nenhuma empresa com CNPJ válido encontrada na tabela 'Enterprise'.", flush=True) + return + print(f"Sucesso: {len(lista_empresas)} empresas válidas encontradas para o teste.", flush=True) + except Exception as e: + print(f"Erro ao conectar ao banco de dados: {e}") + return + + # Base do payload fornecido pelo usuário (omitindo emitente fixo para preencher no loop) + payload_base = { + "nfe": { + "idIntegracao": "", # Será preenchido no loop + "presencial": True, + "consumidorFinal": True, + "natureza": "OPERAÇÃO INTERNA", + "destinatario": { + "cpfCnpj": "08114280956", + "razaoSocial": "NF-E EMITIDA EM HOMOLOGACAO", + "email": "contato@tecnospeed.com.br", + "endereco": { + "logradouro": "AVENIDA DUQUE DE CAXIAS", + "numero": "882", + "bairro": "CENTRO", + "codigoCidade": "4115200", + "descricaoCidade": "MARINGA", + "estado": "PR", + "cep": "87020025" + } + }, + "itens": [ + { + "codigo": "1", + "descricao": "PRODUTO DE TESTE", + "ncm": "06029090", + "cest": "0123456", + "cfop": "5101", + "valorUnitario": { + "comercial": 4.67, + "tributavel": 4.67 + }, + "valor": 4.67, + "tributos": { + "icms": { + "origem": "0", + "cst": "00", + "baseCalculo": { + "modalidadeDeterminacao": 0, + "valor": 0 + }, + "aliquota": 0, + "valor": 0 + }, + "pis": { + "cst": "99", + "baseCalculo": { + "valor": 0, + "quantidade": 0 + }, + "aliquota": 0, + "valor": 0 + }, + "cofins": { + "cst": "07", + "baseCalculo": { + "valor": 0 + }, + "aliquota": 0, + "valor": 0 + } + } + } + ], + "pagamentos": [ + { + "aVista": True, + "meio": "01", + "valor": 4.6 + } + ], + "responsavelTecnico": { + "cpfCnpj": "08187168000160", + "nome": "Tecnospeed", + "email": "contato@tecnospeed.com.br", + "telefone": { + "ddd": "44", + "numero": "30379500" + } + } + } + } + + # Estrutura completa exigida pelo mainQUEUE.py + def get_full_payload(id_num, id_empresa, cnpj_empresa): + # Gerando um timestamp para garantir idIntegracao único + ts = int(time.time()) + payload = json.loads(json.dumps(payload_base)) # Deep copy simples + payload["nfe"]["idIntegracao"] = f"TESTE_{ts}_{id_num}" + payload["nfe"]["emitente"] = { + "cpfCnpj": cnpj_empresa, + "inscricaoEstadual": "ISENTO" + } + + return { + "fila": "nfe", + "cliente": "LoadTest1000", + "metodo": "POST", + "dados": { + "id_empresa": id_empresa, + **payload + }, + "config": { + "x_api_key": "2da392a6-79d2-4304-a8b7-959572c7e44d", + "user_postgres": "tecnospeed", + "pass_postgres": "u7QimG7E!SF//JC2c8", + "host_postgres": "banco", + "port_postgres": "5432", + "database_postgres": "tecnospeed" + }, + "url_base": "https://api.sandbox.plugnotas.com.br" + } + + print(f"Enviando 1000 mensagens para a fila...") + + start_time = time.time() + + for i in range(1, 1001): + empresa_sorteada = random.choice(lista_empresas) + full_payload = get_full_payload(i, id_empresa=empresa_sorteada["id"], cnpj_empresa=empresa_sorteada["cnpj"]) + # É necessário criptografar o payload antes de cada envio + exchange.setPayload(json.dumps(full_payload)) + exchange.sendMsg(json.dumps(full_payload), service="tecnospeed") + + if i % 100 == 0: + print(f"Enviadas: {i}/1000...", flush=True) + + end_time = time.time() + + print("\nConcluído!") + print(f"Tempo total de envio: {end_time - start_time:.2f} segundos.") + print("Agora você pode acompanhar o processamento nos logs do worker (tecnospeed-queue).") + +if __name__ == "__main__": + main() diff --git a/tecnospeed_queue/src/tests/test_1000_nfse.py b/tecnospeed_queue/src/tests/test_1000_nfse.py new file mode 100644 index 0000000..1021795 --- /dev/null +++ b/tecnospeed_queue/src/tests/test_1000_nfse.py @@ -0,0 +1,173 @@ +import sys +import os +import json +import time +import random + +# Adiciona o diretório atual ao path para importar as classes locais +sys.path.append(os.path.dirname(os.path.abspath(__file__))) + +# Adiciona o diretório src ao path para encontrar os Models e Utils +sys.path.append(os.path.dirname(os.path.dirname(os.path.abspath(__file__)))) + +from Models.config import SystemConfig +from utils.redis import RedisClient +from utils.Exchange2 import QueueExchange2 +from utils.banco import Banco, EnterpriseTable + +def main(): + print("Iniciando script de teste de carga: 1000 NFSe...", flush=True) + + # 1. Carregando configuração do Redis para obter dados do RabbitMQ + redis_host = os.getenv('REDIS_HOST', 'localhost') + redis_client = RedisClient(host=redis_host, port=6379) + try: + system_config_data = redis_client.get("system_config") + if not system_config_data: + print("Erro: system_config não encontrado no Redis.") + return + + system_config = SystemConfig(**json.loads(system_config_data)) + print(f"Configuração [{system_config.origin}] carregada.", flush=True) + except Exception as e: + print(f"Erro ao ler configs do Redis: {e}") + return + + # 2. Inicializando a conexão com a FILA (RabbitMQ) + try: + print("Tentando conectar ao RabbitMQ...", flush=True) + exchange = QueueExchange2( + amqps=system_config.amqps, + origin=system_config.origin, + system="integracao", + service="tecnospeed", + version="2.1.0", + client="#" + ) + except Exception as e: + print(f"Falha ao iniciar mensageria: {e}") + return + + # 3. Configurando criptografia (necessário para o worker aceitar a mensagem) + servicekey = system_config.servicekey.ljust(32, '0')[:32] + exchange.setCrypto(serviceid=system_config.serviceid, servicekey=servicekey) + + # 4. Buscando empresas válidas no banco para o teste + try: + db_host = os.getenv('POSTGRES_HOST', 'banco') + db_port = int(os.getenv('POSTGRES_PORT', 5432)) + + # Se estivermos DENTRO do docker e o host configurado for o IP externo ou porta mapeada, forçamos 'banco' e '5432' + if os.path.exists('/.dockerenv') or os.path.exists('/proc/1/cgroup'): + print(f"Aviso: Ambiente Docker detectado. Forçando host 'banco' e porta 5432.") + db_host = 'banco' + db_port = 5432 + + print(f"Conectando ao banco em {db_host}:{db_port}...") + + banco = Banco( + host=db_host, + port=db_port, + user=os.getenv('POSTGRES_USER', 'tecnospeed'), + password=os.getenv('POSTGRES_PASSWORD', 'u7QimG7E!SF//JC2c8'), + database=os.getenv('POSTGRES_DB', 'tecnospeed') + ) + print("Executando query na tabela Enterprise...", flush=True) + empresas = banco.session.query(EnterpriseTable.id, EnterpriseTable.cnpj).filter(EnterpriseTable.cnpj != None, EnterpriseTable.cnpj != "").limit(100).all() + lista_empresas = [{"id": emp.id, "cnpj": emp.cnpj} for emp in empresas] + if not lista_empresas: + print("Erro: Nenhuma empresa com CNPJ válido encontrada na tabela 'Enterprise'.", flush=True) + return + print(f"Sucesso: {len(lista_empresas)} empresas válidas encontradas para o teste.", flush=True) + except Exception as e: + print(f"Erro ao conectar ao banco de dados: {e}") + return + + # Base do payload de NFSe + payload_base = { + "nfse": { + "idIntegracao": "", # Será preenchido no loop + "prestador": { + "cpfCnpj": "" # Será preenchido no loop + }, + "tomador": { + "cpfCnpj": "08114280956", + "razaoSocial": "NFSE EMITIDA EM HOMOLOGACAO", + "email": "contato@tecnospeed.com.br", + "endereco": { + "logradouro": "AVENIDA DUQUE DE CAXIAS", + "numero": "882", + "bairro": "CENTRO", + "codigoCidade": "4115200", + "descricaoCidade": "MARINGA", + "estado": "PR", + "cep": "87020025", + "tipoLogradouro": "Avenida" + } + }, + "servico": { + "codigo": "1.01", + "codigoTributacao": "1.01", + "discriminacao": "SERVICO DE TESTE DE CARGA MASSIVA", + "cnae": "6201501", + "iss": { + "tipoTributacao": 1, + "exigibilidade": 1, + "aliquota": 2.0 + }, + "valor": { + "servico": 100.0 + } + } + } + } + + # Estrutura completa exigida pelo mainQUEUE.py + def get_full_payload(id_num, id_empresa, cnpj_empresa): + ts = int(time.time() * 1000) + payload = json.loads(json.dumps(payload_base)) + payload["nfse"]["idIntegracao"] = f"LOAD_NFSE_{ts}_{id_num}" + payload["nfse"]["prestador"]["cpfCnpj"] = cnpj_empresa + + return { + "fila": "nfse", + "cliente": "MassiveNFSeTest", + "metodo": "POST", + "dados": { + "id_empresa": id_empresa, + "id_usuario": 1, + **payload["nfse"] + }, + "config": { + "x_api_key": "2da392a6-79d2-4304-a8b7-959572c7e44d", + "user_postgres": "tecnospeed", + "pass_postgres": "u7QimG7E!SF//JC2c8", + "host_postgres": "banco", + "port_postgres": "5432", + "database_postgres": "tecnospeed" + }, + "url_base": "https://api.sandbox.plugnotas.com.br" + } + + print(f"Enviando 1000 NFSe para a fila...") + + start_time = time.time() + + for i in range(1, 1001): + empresa_sorteada = random.choice(lista_empresas) + full_payload = get_full_payload(i, id_empresa=empresa_sorteada["id"], cnpj_empresa=empresa_sorteada["cnpj"]) + + exchange.setPayload(json.dumps(full_payload)) + exchange.sendMsg(json.dumps(full_payload), service="tecnospeed") + + if i % 100 == 0: + print(f"Enviadas: {i}/1000...", flush=True) + + end_time = time.time() + + print("\nConcluído!") + print(f"Tempo total de envio: {end_time - start_time:.2f} segundos.") + print("Acompanhe o processamento nos logs do worker (tecnospeed-queue).") + +if __name__ == "__main__": + main() diff --git a/tecnospeed_queue/src/tests/test_api.py b/tecnospeed_queue/src/tests/test_api.py new file mode 100644 index 0000000..820ebef --- /dev/null +++ b/tecnospeed_queue/src/tests/test_api.py @@ -0,0 +1,29 @@ +import json +import sys +import os + +sys.path.append(os.path.dirname(os.path.abspath(__file__))) +from utils.api import Api + +api = Api("2da392a6-79d2-4304-a8b7-959572c7e44d") + +print("\n--- Consultando Certificados ---") +# GET /certificado (Lista todos) +certificados = api.get("https://api.sandbox.plugnotas.com.br/certificado") +print("GET /certificado response:") +print(json.dumps(certificados, indent=2)) + +# Se houver certificados, faz um GET específico no primeiro +if isinstance(certificados, list) and len(certificados) > 0: + first_cert_id = certificados[0].get('id') + print(f"\nConsultando certificado específico (ID: {first_cert_id}):") + spec_cert = api.get(f"https://api.sandbox.plugnotas.com.br/certificado/{first_cert_id}") + print(json.dumps(spec_cert, indent=2)) +elif isinstance(certificados, dict) and "data" in certificados: + # Caso a resposta venha com envelope "data" + data_list = certificados["data"] + if data_list: + first_cert_id = data_list[0].get('id') + print(f"\nConsultando certificado específico (ID: {first_cert_id}):") + spec_cert = api.get(f"https://api.sandbox.plugnotas.com.br/certificado/{first_cert_id}") + print(json.dumps(spec_cert, indent=2)) diff --git a/tecnospeed_queue/src/tests/test_callback.py b/tecnospeed_queue/src/tests/test_callback.py new file mode 100644 index 0000000..82531bd --- /dev/null +++ b/tecnospeed_queue/src/tests/test_callback.py @@ -0,0 +1,96 @@ +import json +import pytest +from unittest.mock import patch, MagicMock + + +class TestRoteamento: + """Verifica que todas as filas registradas no dict existem e são callable.""" + + def test_todas_filas_registradas(self, main_instance): + filas_esperadas = ["nfe", "nfse", "certificado_digital", "empresa", + "servico", "tomador", "relatorios", "webhooks"] + for fila in filas_esperadas: + assert fila in main_instance.funcoes, f"Fila '{fila}' não está registrada" + assert callable(main_instance.funcoes[fila]), f"Fila '{fila}' não é callable" + + def test_fila_inexistente_nao_registrada(self, main_instance): + assert "webhook_callback" not in main_instance.funcoes + assert "fila_fantasma" not in main_instance.funcoes + + +class TestCallbackErros: + """Testa que o callback lida corretamente com payloads inválidos.""" + + def test_callback_json_invalido(self, main_instance): + """Payload que não é JSON válido deve retornar erro.""" + resultado = json.loads(main_instance.callback("isso não é json")) + assert resultado["status"] == "error" + + def test_callback_sem_hmac(self, main_instance): + """Payload sem HMAC deve retornar erro.""" + resultado = json.loads(main_instance.callback(json.dumps({"dados": "algo"}))) + assert resultado["status"] == "error" + + def test_callback_com_hmac_e_payload(self, main_instance, mock_exchange): + """Payload com hmac+payload mas sem crypto configurada deve retornar erro.""" + mock_exchange.getPayload.return_value = False + + main_instance.system_config = MagicMock() + main_instance.system_config.servicekey = "testkey1234567890" + main_instance.system_config.serviceid = "test-service" + + resultado = json.loads(main_instance.callback(json.dumps({ + "hmac": "fake-hmac", + "payload": "fake-encrypted-payload" + }))) + + assert resultado["status"] == "error" + + +class TestBancoIntegracao: + """Testa que os logs são gravados no banco durante as operações.""" + + def test_log_gravado_no_banco(self, main_instance, banco_teste, mock_api): + """Ao executar uma operação, deve gravar log no banco.""" + mock_api.get.return_value = {"id": "PLUG-NFE-001", "status": "CONCLUIDO"} + + with patch("NFe.registroNFe.Api", return_value=mock_api): + main_instance.nfe("get", {"id": "PLUG-NFE-001", "id_usuario": 42}) + + from utils.banco import LogTable + logs = banco_teste.session.query(LogTable).all() + + # Deve ter pelo menos 2 logs: INICIO + SUCESSO + assert len(logs) >= 2 + acoes = [log.acao for log in logs] + assert "GET" in acoes + + # Verifica que o id_usuario foi gravado + assert any(log.id_usuario == 42 for log in logs) + + def test_log_erro_gravado(self, main_instance, banco_teste, mock_api): + """Erros também devem ser gravados no banco.""" + mock_api.get.side_effect = Exception("Timeout na API") + + with patch("NFe.registroNFe.Api", return_value=mock_api): + with pytest.raises(Exception): + main_instance.nfe("get", {"id": "PLUG-NFE-001", "id_usuario": 7}) + + from utils.banco import LogTable + logs = banco_teste.session.query(LogTable).filter_by(status="ERRO").all() + assert len(logs) >= 1 + + +class TestExchangePayload: + """Testa que o exchange.setPayload é chamado com o resultado correto.""" + + def test_payload_set_no_sucesso(self, main_instance, mock_api, mock_exchange): + """Ao concluir com sucesso, setPayload deve ser chamado com o resultado.""" + mock_api.get.return_value = {"id": "PLUG-NFE-001", "status": "CONCLUIDO"} + + with patch("NFe.registroNFe.Api", return_value=mock_api): + main_instance.nfe("get", {"id": "PLUG-NFE-001", "id_usuario": 1}) + + assert mock_exchange.setPayload.called + payload_enviado = json.loads(mock_exchange.payloads_set[-1]) + assert payload_enviado["status"] == "CONCLUIDO" diff --git a/tecnospeed_queue/src/tests/test_nfe.py b/tecnospeed_queue/src/tests/test_nfe.py new file mode 100644 index 0000000..771402d --- /dev/null +++ b/tecnospeed_queue/src/tests/test_nfe.py @@ -0,0 +1,179 @@ +import json +import pytest +from unittest.mock import patch, MagicMock + +class TestNFePost: + + @pytest.fixture + def dados_nfe(self): + return { + "id_usuario": 1, + "id_empresa": 10, + "nfe": { + "idIntegracao": "INT-NFE-001", + "presencial": True, + "consumidorFinal": True, + "natureza": "Venda de mercadoria", + "emitente": {"cpfCnpj": "08187168000160"}, + "destinatario": { + "cpfCnpj": "08114280956", + "razaoSocial": "Destinatário Teste", + "endereco": { + "logradouro": "Rua Teste", + "numero": "100", + "bairro": "Centro", + "codigoCidade": "5201405", + "descricaoCidade": "GOIANIA", + "estado": "GO", + "cep": "74000000" + } + }, + "itens": [{ + "codigo": "001", + "descricao": "Produto Teste", + "ncm": "61091000", + "cfop": "5102", + "valorUnitario": {"comercial": 100.0, "tributavel": 100.0}, + "valor": 100.0, + "tributos": { + "icms": { + "origem": "0", + "cst": "00", + "baseCalculo": {"modalidadeDeterminacao": 0, "valor": 100.0}, + "aliquota": 18.0, + "valor": 18.0 + }, + "pis": { + "cst": "01", + "baseCalculo": {"valor": 100.0, "quantidade": 1}, + "aliquota": 1.65, + "valor": 1.65 + }, + "cofins": { + "cst": "01", + "baseCalculo": {"valor": 100.0}, + "aliquota": 7.6, + "valor": 7.6 + } + } + }], + "pagamentos": [{"aVista": True, "meio": "01", "valor": 100.0}] + } + } + + def test_post_nfe_sucesso(self, main_instance, mock_api, dados_nfe): + mock_api.post.return_value = { + "documents": [{"id": "PLUG-NFE-001"}] + } + + with patch("NFe.registroNFe.Api", return_value=mock_api): + resultado = json.loads(main_instance.nfe("post", dados_nfe)) + + assert resultado["status"] == "ok" + assert resultado["data"]["documents"][0]["id"] == "PLUG-NFE-001" + + def test_post_nfe_erro_api(self, main_instance, mock_api, dados_nfe): + mock_api.post.return_value = {"error": "CPF/CNPJ inválido"} + + with patch("NFe.registroNFe.Api", return_value=mock_api): + with pytest.raises(Exception, match="CPF/CNPJ inválido"): + main_instance.nfe("post", dados_nfe) + +class TestNFeGet: + + def test_get_nfe_resumo(self, main_instance, mock_api): + mock_api.get.return_value = { + "id": "PLUG-NFE-001", + "status": "CONCLUIDO", + "xml": "https://..." + } + + with patch("NFe.registroNFe.Api", return_value=mock_api): + resultado = json.loads(main_instance.nfe("get", {"id": "PLUG-NFE-001", "id_usuario": 1})) + + assert resultado["status"] == "ok" + assert resultado["data"]["status"] == "CONCLUIDO" + +class TestNFeDownload: + """Testa download de PDF e XML de NFe.""" + + def test_download_pdf(self, main_instance, mock_api): + mock_api.get.return_value = {"url": "https://cdn.plugnotas.com.br/nfe/pdf/123"} + + with patch("NFe.registroNFe.Api", return_value=mock_api): + resultado = json.loads(main_instance.nfe("download", {"id": "PLUG-NFE-001", "id_usuario": 1})) + + assert resultado["status"] == "ok" + + def test_download_xml(self, main_instance, mock_api): + mock_api.get.return_value = {"url": "https://cdn.plugnotas.com.br/nfe/xml/123"} + + with patch("NFe.registroNFe.Api", return_value=mock_api): + resultado = json.loads(main_instance.nfe("download_xml", {"id": "PLUG-NFE-001", "id_usuario": 1})) + + assert resultado["status"] == "ok" + +class TestNFeCancelar: + + def test_cancelar_nfe_sucesso(self, main_instance, mock_api): + mock_api.post.return_value = {"id": "PLUG-NFE-001", "status": "CANCELAMENTO_SOLICITADO"} + + with patch("NFe.registroNFe.Api", return_value=mock_api): + resultado = json.loads(main_instance.nfe("cancelar", { + "id": "PLUG-NFE-001", + "justificativa": "Erro de emissão", + "id_usuario": 1 + })) + + assert resultado["status"] == "ok" + + def test_status_cancelamento(self, main_instance, mock_api): + mock_api.get.return_value = {"id": "PLUG-NFE-001", "status": "CANCELADO"} + + with patch("NFe.registroNFe.Api", return_value=mock_api): + resultado = json.loads(main_instance.nfe("status_cancelamento", {"id": "PLUG-NFE-001", "id_usuario": 1})) + + assert resultado["status"] == "ok" + +class TestNFeCCe: + + def test_enviar_cce(self, main_instance, mock_api): + mock_api.post.return_value = {"id": "PLUG-NFE-001", "status": "CCE_ENVIADA"} + + with patch("NFe.registroNFe.Api", return_value=mock_api): + resultado = json.loads(main_instance.nfe("cce", { + "id": "PLUG-NFE-001", + "correcao": "Corrigir endereço do destinatário", + "id_usuario": 1 + })) + + assert resultado["status"] == "ok" + + def test_status_cce(self, main_instance, mock_api): + mock_api.get.return_value = {"id": "PLUG-NFE-001", "status": "CCE_AUTORIZADA"} + + with patch("NFe.registroNFe.Api", return_value=mock_api): + resultado = json.loads(main_instance.nfe("status_cce", {"id": "PLUG-NFE-001", "id_usuario": 1})) + + assert resultado["status"] == "ok" + +class TestNFeResumo: + + def test_resumo_por_cnpj(self, main_instance, mock_api): + mock_api.get.return_value = {"id": "PLUG-NFE-001", "chave": "3524..."} + + with patch("NFe.registroNFe.Api", return_value=mock_api): + resultado = json.loads(main_instance.nfe("resumo", { + "cnpj": "08187168000160", + "id_integracao": "INT-001", + "id_usuario": 1 + })) + + assert resultado["status"] == "ok" + +class TestNFeInvalido: + + def test_metodo_invalido(self, main_instance): + resultado = json.loads(main_instance.nfe("inexistente", {"id_usuario": 1})) + assert resultado["status"] == "error" + assert "não encontrado" in resultado["message"].lower() diff --git a/tecnospeed_queue/src/tests/test_nfse.py b/tecnospeed_queue/src/tests/test_nfse.py new file mode 100644 index 0000000..be1045a --- /dev/null +++ b/tecnospeed_queue/src/tests/test_nfse.py @@ -0,0 +1,146 @@ +""" +Testes E2E para o serviço NFSe. +Testa: POST, GET, DOWNLOAD, DOWNLOAD_XML, CANCELAR, +STATUS_CANCELAMENTO, RESUMO, HISTORICO. +""" +import json +import pytest +from unittest.mock import patch, MagicMock + + +@pytest.fixture +def dados_nfse(): + return { + "id_usuario": 1, + "id_empresa": 10, + "idIntegracao": "INT-NFSE-001", + "prestador": {"cpfCnpj": "08187168000160"}, + "tomador": { + "cpfCnpj": "08114280956", + "razaoSocial": "Tomador Teste", + "inscricaoMunicipal": "isento", + "email": "tomador@teste.com", + "endereco": { + "descricaoCidade": "São Paulo", + "cep": "01000000", + "tipoLogradouro": "Rua", + "logradouro": "Teste", + "tipoBairro": "Bairro", + "codigoCidade": "3550308", + "complemento": "", + "estado": "SP", + "numero": "100", + "bairro": "Centro" + } + }, + "servico": { + "codigo": "1.01", + "codigoTributacao": "1.01", + "discriminacao": "Serviços de tecnologia", + "cnae": "6204000", + "iss": {"tipoTributacao": 6, "exigibilidade": 1, "aliquota": 2.5}, + "valor": {"servico": 1500.0, "descontoCondicionado": 0, "descontoIncondicionado": 0} + } + } + + +class TestNFSePost: + def test_post_nfse_sucesso(self, main_instance, mock_api, dados_nfse): + mock_api.post.return_value = {"documents": [{"id": "PLUG-NFSE-001"}]} + + with patch("NFSe.nfse.Api", return_value=mock_api): + resultado = json.loads(main_instance.nfse("post", dados_nfse)) + + assert resultado["status"] == "ok" + assert resultado["data"]["documents"][0]["id"] == "PLUG-NFSE-001" + + def test_post_nfse_erro_api(self, main_instance, mock_api, dados_nfse): + mock_api.post.return_value = {"error": "CNPJ do prestador não cadastrado"} + + with patch("NFSe.nfse.Api", return_value=mock_api): + with pytest.raises(Exception): + main_instance.nfse("post", dados_nfse) + + +class TestNFSeGet: + def test_get_nfse_por_id(self, main_instance, mock_api): + mock_api.get.return_value = {"id": "PLUG-NFSE-001", "status": "CONCLUIDO"} + + with patch("NFSe.nfse.Api", return_value=mock_api): + resultado = json.loads(main_instance.nfse("get", {"id": "PLUG-NFSE-001", "id_usuario": 1})) + + assert resultado["status"] == "ok" + + def test_listar_nfse(self, main_instance, banco_teste): + """Listar NFSe sem ID deve buscar do banco.""" + resultado = json.loads(main_instance.nfse("get", {"id_usuario": 1})) + assert resultado["status"] == "ok" + + +class TestNFSeDownload: + def test_download_pdf(self, main_instance, mock_api): + mock_api.get.return_value = {"pdf_url": "https://..."} + + with patch("NFSe.nfse.Api", return_value=mock_api): + resultado = json.loads(main_instance.nfse("download", {"id": "PLUG-NFSE-001", "id_usuario": 1})) + + assert resultado["status"] == "ok" + + def test_download_xml(self, main_instance, mock_api): + mock_api.get.return_value = {"xml_url": "https://..."} + + with patch("NFSe.nfse.Api", return_value=mock_api): + resultado = json.loads(main_instance.nfse("download_xml", {"id": "PLUG-NFSE-001", "id_usuario": 1})) + + assert resultado["status"] == "ok" + + +class TestNFSeCancelar: + def test_cancelar_nfse(self, main_instance, mock_api): + mock_api.post.return_value = {"status": "CANCELAMENTO_SOLICITADO"} + + with patch("NFSe.nfse.Api", return_value=mock_api): + resultado = json.loads(main_instance.nfse("cancelar", {"id": "PLUG-NFSE-001", "id_usuario": 1})) + + assert resultado["status"] == "ok" + + def test_status_cancelamento(self, main_instance, mock_api): + mock_api.get.return_value = {"status": "CANCELADO"} + + with patch("NFSe.nfse.Api", return_value=mock_api): + resultado = json.loads(main_instance.nfse("status_cancelamento", {"id": "PLUG-NFSE-001", "id_usuario": 1})) + + assert resultado["status"] == "ok" + + +class TestNFSeResumo: + def test_consultar_resumo(self, main_instance, mock_api): + mock_api.get.return_value = {"id": "PLUG-NFSE-001", "numero": "12345"} + + with patch("NFSe.nfse.Api", return_value=mock_api): + resultado = json.loads(main_instance.nfse("resumo", { + "id": "PLUG-NFSE-001", + "cnpj": "08187168000160", + "id_usuario": 1 + })) + + assert resultado["status"] == "ok" + + +class TestNFSeHistorico: + def test_historico_nfse(self, main_instance, mock_api): + mock_api.get.return_value = [{"data": "2026-01-01", "status": "CONCLUIDO"}] + + with patch("NFSe.nfse.Api", return_value=mock_api): + resultado = json.loads(main_instance.nfse("historico", { + "id": "PLUG-NFSE-001", + "id_usuario": 1 + })) + + assert resultado["status"] == "ok" + + +class TestNFSeInvalido: + def test_metodo_invalido(self, main_instance): + resultado = json.loads(main_instance.nfse("inexistente", {"id_usuario": 1})) + assert resultado["status"] == "error" diff --git a/tecnospeed_queue/src/tests/test_queue.py b/tecnospeed_queue/src/tests/test_queue.py new file mode 100644 index 0000000..9bc6adb --- /dev/null +++ b/tecnospeed_queue/src/tests/test_queue.py @@ -0,0 +1,164 @@ +import sys +import os +import json +import time + +sys.path.append(os.path.dirname(os.path.abspath(__file__))) + +from Models.config import SystemConfig +from utils.redis import RedisClient +from utils.Exchange2 import QueueExchange2 + +def main(): + print("Iniciando script de testes da FILA (Queue Exchange)...") + + # Carregando configuração do Redis + redis_host = os.getenv('REDIS_HOST', 'localhost') + redis_client = RedisClient(host=redis_host, port=6379) + try: + system_config_data = redis_client.get("system_config") + if not system_config_data: + print("Erro: system_config não encontrado no Redis.") + return + + system_config = SystemConfig(**json.loads(system_config_data)) + print(f"[{system_config.origin}] Configuração carregada.") + except Exception as e: + print(f"Erro ao ler configs: {e}") + return + + # Inicializando mensageria de FILA + try: + # Nota: O QueueExchange2 envia para 'system_service_client' + exchange = QueueExchange2( + amqps=system_config.amqps, + origin=system_config.origin, + system="integracao", + service="tecnospeed", + version="2.1.0", + client="#" # '#' para bater com a fila padrão do worker (evita erro de validação length) + ) + except Exception as e: + print(f"Falha ao iniciar mensageria: {e}") + return + + # Configurando criptografia + servicekey = system_config.servicekey.ljust(32, '0')[:32] + exchange.setCrypto(serviceid=system_config.serviceid, servicekey=servicekey) + + # Payload (mesmo do teste RPC) + payload_interno = { + "fila": "empresa", + "cliente": "TestQueue", + "metodo": "POST", + "dados": { + "endereco" : { + "codigoPais": "1058", + "descricaoPais": "Brasil", + "tipoLogradouro": "Rua", + "logradouro": "T55", + "numero": "882", + "complemento": "Quadra99 Lote 11/14 Sala 1507", + "tipoBairro": "Zona", + "bairro": "Setor Bueno", + "codigoCidade": "5208707", + "descricaoCidade": "Goiania", + "estado": "GO", + "cep": "74215170" + }, + "nfse" : { + "config" : { + "rps" : { + "lote" : 12, + "numeracao" : [ + { + "numero" : 2, + "serie" : "CTBZ", + "numeracaoAtual" : 2 + } + ] + }, + "email" : { + "envio" : False + }, + "producao" : True, + "envioLimitado" : False, + "integracoes" : [] + }, + "ativo" : True, + "tipoContrato" : 0 + }, + "nfe" : { + "config" : { + "dfe" : { + "primeiroNsu" : True + }, + "email" : { + "envio" : False + }, + "integracoes" : { + "plugStorage" : { + "ativo" : False + } + }, + "producao" : False, + "impressaoFcp" : False, + "impressaoPartilha" : True, + "versaoManual" : "6.0", + "versaoEsquema" : "pl_009", + "numeracao" : [] + }, + "ativo" : False + }, + "nfce" : { + "config" : { + "email" : { + "envio" : False + }, + "integracoes" : { + "plugStorage" : { + "ativo" : False + } + }, + "producao" : False, + "versaoManual" : "6.0", + "versaoEsquema" : "pl_009", + "numeracao" : [] + }, + "ativo" : False + }, + "incentivoFiscal" : False, + "incentivadorCultural" : False, + "cpfCnpj" : "14358405000100", + "inscricaoMunicipal" : "1085328365", + "certificado" : "668698592698d7831c758d52", + "razaoSocial" : "Teste da SIlva do teste", + "simplesNacional" : True, + "regimeTributario" : 1, + "regimeTributarioEspecial" : 6 + }, + "config": { + "x_api_key": "2da392a6-79d2-4304-a8b7-959572c7e44d", + "user_postgres": "tecnospeed", + "pass_postgres": "u7QimG7E!SF//JC2c8", + "host_postgres": "postgres", + "port_postgres": "5432", + "database_postgres": "tecnospeed" + }, + "url_base": "https://api.sandbox.plugnotas.com.br" + } + + # Criptografando e enviando + exchange.setPayload(json.dumps(payload_interno)) + + print("\n--- DEBUG ENVIO ---") + print(f"Alvo (targetQueue): integracao_tecnospeed") + print(f"Config Enviada: {payload_interno['config']['host_postgres']}:{payload_interno['config']['port_postgres']}") + print("-------------------\n") + + print("Disparando mensagem para a FILA...") + exchange.sendMsg(json.dumps(payload_interno), service="tecnospeed") + print("Mensagem enviada com sucesso. Verifique os logs do worker mainQUEUE.py") + +if __name__ == "__main__": + main() diff --git a/tecnospeed_queue/src/tests/test_servicos.py b/tecnospeed_queue/src/tests/test_servicos.py new file mode 100644 index 0000000..1228a2a --- /dev/null +++ b/tecnospeed_queue/src/tests/test_servicos.py @@ -0,0 +1,328 @@ +import json +import pytest +from unittest.mock import patch, MagicMock +from utils.banco import EmpresaTable, ServicoTable, TomadorTable + +class TestEmpresaPost: + @pytest.fixture + def dados_empresa(self): + return { + "id_usuario": 1, + "endereco": { + "codigoPais": "1058", "descricaoPais": "BRASIL", + "tipoLogradouro": "Rua", "logradouro": "Teste", "numero": "123", + "complemento": "", "tipoBairro": "Bairro", "bairro": "Centro", + "codigoCidade": "5201405", "descricaoCidade": "GOIANIA", + "estado": "GO", "cep": "74000000" + }, + "nfse": { + "config": {"rps": {"lote": 1, "numeracao": [1]}, "email": {"envio": True}, + "producao": False, "envioLimitado": False, "integracoes": []}, + "ativo": True, "tipoContrato": 0 + }, + "nfe": { + "config": {"dfe": {"primeiroNsu": False}, "email": {"envio": False}, + "integracoes": {}, "producao": False, "impressaoFcp": False, + "impressaoPartilha": False, "versaoManual": "6.0", + "versaoEsquema": "PL_009_V4", "numeracao": [1]}, + "ativo": False + }, + "nfce": { + "config": {"email": {"envio": False}, "integracoes": {}, "producao": False, + "versaoManual": "6.0", "versaoEsquema": "PL_009_V4", "numeracao": [1]}, + "ativo": False + }, + "incentivoFiscal": False, "incentivadorCultural": False, + "cpfCnpj": "17956143000174", "inscricaoMunicipal": "12345", + "certificado": "CERT-UUID", "razaoSocial": "Empresa Teste SA", + "simplesNacional": True, "regimeTributario": 1, "regimeTributarioEspecial": 0 + } + + def test_post_empresa_sucesso(self, main_instance, mock_api, dados_empresa): + mock_api.post.return_value = {"_id": "PLUG-EMP-001", "cpfCnpj": "17956143000174"} + + with patch("empresas.empresa.Api", return_value=mock_api): + resultado = json.loads(main_instance.empresa("post", dados_empresa)) + + assert resultado["status"] == "ok" + + def test_post_empresa_erro(self, main_instance, mock_api, dados_empresa): + mock_api.post.return_value = { + "error": "Erro", + "response": {"error": {"message": "CNPJ já cadastrado", "data": []}} + } + + with patch("empresas.empresa.Api", return_value=mock_api): + resultado = json.loads(main_instance.empresa("post", dados_empresa)) + + assert resultado["status"] == "error" + + +class TestEmpresaGet: + def test_get_empresa_por_cnpj(self, main_instance, mock_api): + mock_api.get.return_value = {"_id": "PLUG-EMP-001", "cpfCnpj": "17956143000174", "razaoSocial": "Teste SA"} + + with patch("empresas.empresa.Api", return_value=mock_api): + resultado = json.loads(main_instance.empresa("get", {"cnpj": "17956143000174", "id_usuario": 1})) + + assert resultado["status"] == "ok" + + def test_listar_empresas(self, main_instance, mock_api): + mock_api.get_paginated.return_value = {"data": [{"cpfCnpj": "111", "razaoSocial": "Emp A"}], "total": 1} + + with patch("empresas.empresa.Api", return_value=mock_api): + resultado = json.loads(main_instance.empresa("get", {"id_usuario": 1})) + + assert resultado["status"] == "ok" + + +class TestEmpresaInvalido: + def test_metodo_invalido(self, main_instance): + resultado = json.loads(main_instance.empresa("inexistente", {"id_usuario": 1})) + assert resultado["status"] == "error" + + +class TestCertificadoPost: + @pytest.fixture + def dados_certificado(self): + return { + "id_usuario": 1, + "id_empresa": 10, + "arquivo": "CONTEUDO_PFX_BASE64", + "senha": "12345678", + "expirationAt": "2027-12-31" + } + + def test_post_certificado_sucesso(self, main_instance, mock_api, dados_certificado): + mock_api.post_form_data.return_value = {"data": {"id": "CERT-001"}} + + with patch("certificado_digital.certificado_digital.Api", return_value=mock_api): + resultado = json.loads(main_instance.certificado_digital("post", dados_certificado)) + + assert resultado["status"] == "ok" + assert resultado["data"] == "CERT-001" + + +class TestCertificadoGet: + def test_get_certificado_por_id(self, main_instance, mock_api): + mock_api.get.return_value = {"id": "CERT-001", "status": "VALIDO"} + + with patch("certificado_digital.certificado_digital.Api", return_value=mock_api): + resultado = json.loads(main_instance.certificado_digital("get", {"id": "CERT-001", "id_usuario": 1})) + + assert resultado["status"] == "ok" + + def test_listar_certificados(self, main_instance, mock_api): + mock_api.get.return_value = [{"id": "CERT-001"}, {"id": "CERT-002"}] + + with patch("certificado_digital.certificado_digital.Api", return_value=mock_api): + resultado = json.loads(main_instance.certificado_digital("get", {"id_usuario": 1})) + + assert resultado["status"] == "ok" + + +class TestCertificadoInvalido: + def test_metodo_invalido(self, main_instance): + resultado = json.loads(main_instance.certificado_digital("inexistente", {"id_usuario": 1})) + assert resultado["status"] == "error" + +class TestServicoPost: + """Testa criação de Serviço na API + banco.""" + + @pytest.fixture + def dados_servico_completo(self): + """Payload completo do Model Servico com todos campos obrigatórios.""" + return { + "id_usuario": 1, + "codigo": "1.01", + "idIntegracao": "SERV-INT-001", + "discriminacao": "Consultoria em TI", + "codigoTributacao": "1.01", + "cnae": "6204000", + "codigoCidadeIncidencia": "3550308", + "descricaoCidadeIncidencia": "São Paulo", + "unidade": "UN", + "quantidade": 1, + "iss": { + "tipoTributacao": 6, "exigibilidade": 1, + "retido": False, "aliquota": 2.5, "valor": 37.5, + "valorRetido": 0, "processoSuspensao": "" + }, + "obra": {"art": "", "codigo": ""}, + "valor": { + "servico": 1500.0, "baseCalculo": 1500.0, "deducoes": 0, + "descontoCondicionado": 0, "descontoIncondicionado": 0, + "liquido": 1500.0, "unitario": 1500.0, "valorAproximadoTributos": 0 + }, + "deducao": {"tipo": 0, "descricao": ""}, + "retencao": { + "pis": {"aliquota": 0, "valor": 0}, + "cofins": {"aliquota": 0, "valor": 0}, + "csll": {"aliquota": 0, "valor": 0}, + "inss": {"aliquota": 0, "valor": 0}, + "irrf": {"aliquota": 0, "valor": 0}, + "outrasRetencoes": 0, + "cpp": {"aliquota": 0, "valor": 0} + }, + "tributavel": True, + "ibpt": { + "simplificado": {"aliquota": 8.65}, + "detalhado": {"aliquota": {"municipal": 2.5, "estadual": 0, "federal": 6.15}} + }, + "responsavelRetencao": "" + } + + def test_post_servico_sucesso(self, main_instance, mock_api, dados_servico_completo): + mock_api.post.return_value = {"data": {"id": "SERV-001"}} + + with patch("NFSe.Servico.servico.Api", return_value=mock_api): + resultado = json.loads(main_instance.servico("post", dados_servico_completo)) + + assert resultado["status"] == "ok" + + +class TestServicoGet: + def test_listar_servicos(self, main_instance, banco_teste): + """Deve retornar lista vazia quando não há serviços.""" + resultado = json.loads(main_instance.servico("get", {"id_usuario": 1})) + assert resultado["status"] == "ok" + + def test_buscar_servico_por_id(self, main_instance, mock_api): + mock_api.get.return_value = {"id": "SERV-001", "discriminacao": "Consultoria"} + + with patch("NFSe.Servico.servico.Api", return_value=mock_api): + resultado = json.loads(main_instance.servico("get", {"id": "SERV-001", "id_usuario": 1})) + + assert resultado["status"] == "ok" + + +class TestServicoInvalido: + def test_metodo_invalido(self, main_instance): + resultado = json.loads(main_instance.servico("inexistente", {"id_usuario": 1})) + assert resultado["status"] == "error" + +class TestTomadorPost: + @pytest.fixture + def dados_tomador(self): + return { + "id_usuario": 1, + "cpfCnpj": "08114280956", + "inscricaoMunicipal": "isento", + "inscricaoEstadual": "", + "inscricaoSuframa": "", + "indicadorInscricaoEstadual": 9, + "razaoSocial": "Tomador Teste LTDA", + "nomeFantasia": "Tomador Teste", + "endereco": { + "tipoLogradouro": "Rua", "logradouro": "Teste", "numero": "100", + "complemento": "", "tipoBairro": "Bairro", "bairro": "Centro", + "codigoPais": "1058", "descricaoPais": "BRASIL", + "codigoCidade": "3550308", "descricaoCidade": "São Paulo", + "estado": "SP", "cep": "01000000" + }, + "telefone": {"ddd": "11", "numero": "999999999"}, + "email": "tomador@teste.com", + "orgaoPublico": False + } + + def test_post_tomador_sucesso(self, main_instance, mock_api, dados_tomador): + mock_api.post.return_value = {"id": "TOM-001", "cpfCnpj": "08114280956"} + + with patch("NFSe.Tomador.tomador.Api", return_value=mock_api): + resultado = json.loads(main_instance.tomador("post", dados_tomador)) + + assert resultado["status"] == "ok" + + +class TestTomadorGet: + def test_buscar_tomador(self, main_instance, mock_api): + mock_api.get.return_value = {"cpfCnpj": "08114280956", "razaoSocial": "Tomador Teste"} + + with patch("NFSe.Tomador.tomador.Api", return_value=mock_api): + resultado = json.loads(main_instance.tomador("get", {"cpf_cnpj": "08114280956", "id_usuario": 1})) + + assert resultado["status"] == "ok" + + +class TestTomadorInvalido: + def test_metodo_invalido(self, main_instance): + resultado = json.loads(main_instance.tomador("inexistente", {"id_usuario": 1})) + assert resultado["status"] == "error" + +class TestRelatorios: + def test_nfe_emitidas(self, main_instance, mock_api): + mock_api.get_paginated.return_value = {"data": [{"id": "NFE-1"}], "total": 1} + + with patch("Relatorios.relatorios.Api", return_value=mock_api): + resultado = json.loads(main_instance.relatorios("nfe_emitidas", {"id_usuario": 1})) + + assert resultado["status"] == "ok" + + def test_nfe_por_cnpj(self, main_instance, mock_api): + mock_api.get_paginated.return_value = {"data": [], "total": 0} + + with patch("Relatorios.relatorios.Api", return_value=mock_api): + resultado = json.loads(main_instance.relatorios("nfe_por_cnpj", {"cnpj": "08187168000160", "id_usuario": 1})) + + assert resultado["status"] == "ok" + + def test_nfse_emitidas(self, main_instance, mock_api): + mock_api.get_paginated.return_value = {"data": [], "total": 0} + + with patch("Relatorios.relatorios.Api", return_value=mock_api): + resultado = json.loads(main_instance.relatorios("nfse_emitidas", { + "data_inicial": "2026-01-01", "data_final": "2026-12-31", "id_usuario": 1 + })) + + assert resultado["status"] == "ok" + + def test_nfse_por_cnpj(self, main_instance, mock_api): + mock_api.get_paginated.return_value = {"data": [], "total": 0} + + with patch("Relatorios.relatorios.Api", return_value=mock_api): + resultado = json.loads(main_instance.relatorios("nfse_por_cnpj", { + "cnpj": "08187168000160", + "data_inicial": "2026-01-01", "data_final": "2026-12-31", + "id_usuario": 1 + })) + + assert resultado["status"] == "ok" + + def test_metodo_invalido(self, main_instance): + resultado = json.loads(main_instance.relatorios("inexistente", {"id_usuario": 1})) + assert resultado["status"] == "error" + +class TestWebhooks: + def test_consultar_webhooks(self, main_instance, mock_api): + mock_api.get.return_value = {"url": " ", "method": "POST"} + + with patch("webhooks.webhooks.Api", return_value=mock_api): + main_instance.webhooks("get", {"cnpj": "08187168000160", "id_usuario": 1}) + + def test_criar_webhook(self, main_instance, mock_api): + mock_api.post.return_value = {"status": "ok"} + + with patch("webhooks.webhooks.Api", return_value=mock_api): + main_instance.webhooks("post", {"cnpj": "08187168000160", "url": " ", "id_usuario": 1}) + + def test_deletar_webhook(self, main_instance, mock_api): + mock_api.delete.return_value = {"status": "ok"} + + with patch("webhooks.webhooks.Api", return_value=mock_api): + main_instance.webhooks("delete", {"cnpj": "08187168000160", "id_usuario": 1}) + + def test_atualizar_webhook(self, main_instance, mock_api): + mock_api.put.return_value = {"status": "ok"} + + with patch("webhooks.webhooks.Api", return_value=mock_api): + main_instance.webhooks("put", {"cnpj": "08187168000160", "url": " ", "id_usuario": 1}) + + def test_testar_webhook(self, main_instance, mock_api): + mock_api.post.return_value = {"status": "ok", "message": "Webhook testado"} + + with patch("webhooks.webhooks.Api", return_value=mock_api): + main_instance.webhooks("test", {"cnpj": "08187168000160", "id_usuario": 1}) + + def test_metodo_invalido(self, main_instance): + resultado = json.loads(main_instance.webhooks("inexistente", {"id_usuario": 1})) + assert resultado["status"] == "error" diff --git a/tecnospeed_queue/src/tests/test_simplified_payload.py b/tecnospeed_queue/src/tests/test_simplified_payload.py new file mode 100644 index 0000000..d40c494 --- /dev/null +++ b/tecnospeed_queue/src/tests/test_simplified_payload.py @@ -0,0 +1,80 @@ +import os +import sys +import json +import time + +sys.path.append(os.path.dirname(os.path.dirname(os.path.abspath(__file__)))) + +from Models.config import SystemConfig +from utils.redis import RedisClient +from utils.Exchange2 import RPCExchange2 + +def run_rpc_test(): + redis_host = os.getenv('REDIS_HOST', 'localhost') + redis_client = RedisClient(host=redis_host, port=6379) + try: + config_data = redis_client.get("system_config") + sc = SystemConfig(**json.loads(config_data)) + except Exception as e: + print(f"Erro ao carregar configs: {e}") + return + + config_rpc = { + "x_api_key": "2da392a6-79d2-4304-a8b7-959572c7e44d", + "user_postgres": "tecnospeed", + "pass_postgres": "u7QimG7E%21SF%2F%2FJC2c8", + "host_postgres": "banco", + "port_postgres": "5432", + "database_postgres": "tecnospeed" + } + + exchange = RPCExchange2(sc.amqps, sc.origin, "integracao", "tecnospeed", "2.1.0", client="#") + servicekey = sc.servicekey.ljust(32, '0')[:32] + exchange.setCrypto(sc.serviceid, servicekey) + + # Payload "simplificado" do front-end + full_payload = { + "fila": "empresa", + "metodo": "POST", + "cliente": "tray", + "url_base": "https://api.sandbox.plugnotas.com.br", + "config": config_rpc, + "dados": { + "cpfCnpj": "94074986000198", + "razaoSocial": "Tech nux", + "nomeFantasia": "Tech nux ltda", + "inscricaoMunicipal": "0001", + "simplesNacional": True, + "regimeTributario": 1, + "regimeTributarioEspecial": 1, + "incentivoCultural": False, + "incentivoFiscal": True, + "certificado": "cert-mock-01", + "endereco": { + "codigoPais": "1058", + "descricaoPais": "BRASIL", + "tipoLogradouro": "Avenida", + "logradouro": "Avenida", + "numero": "123", + "complemento": "Casa", + "tipoBairro": "Bairro", + "bairro": "Centro", + "codigoCidade": "5200175", + "descricaoCidade": "ÁGUA FRIA DE GOIÁS", + "estado": "GO", + "cep": "68907-460" + }, + "nfe": { "ativo": True }, + "nfse": { "ativo": False } + } + } + + print("Enviando payload via RPC...") + exchange.setPayload(json.dumps(full_payload)) + resposta = exchange.callRPC(json.dumps(full_payload), timeout=30) + + print("\nRESPOSTA DO SERVIDOR (RPC):") + print(resposta) + +if __name__ == "__main__": + run_rpc_test() diff --git a/tecnospeed_queue/src/tests/test_upload_certificado.py b/tecnospeed_queue/src/tests/test_upload_certificado.py new file mode 100644 index 0000000..c3c7b9d --- /dev/null +++ b/tecnospeed_queue/src/tests/test_upload_certificado.py @@ -0,0 +1,88 @@ +import sys +import os +import json +import time +from dotenv import load_dotenv + +# Carrega variáveis de ambiente +load_dotenv() + +# Adiciona o diretório atual ao path para importar as classes locais +sys.path.append(os.path.dirname(os.path.abspath(__file__))) + +from Models.config import SystemConfig +from utils.redis import RedisClient +from utils.Exchange2 import QueueExchange2 + +# Dados do Certificado (Preenchidos dinamicamente) +PFX_BASE64 = " " +PFX_PASSWORD = str(sys.argv[1]) if len(sys.argv) > 1 else " " + +def main(): + print(f"Iniciando teste de upload de certificado para senha: {PFX_PASSWORD}") + + # 1. Carregando configuração do Redis + redis_host = os.getenv('REDIS_HOST', 'localhost') + redis_client = RedisClient(host=redis_host, port=6379) + try: + system_config_data = redis_client.get("system_config") + if not system_config_data: + print("Erro: system_config não encontrado no Redis.") + return + + system_config = SystemConfig(**json.loads(system_config_data)) + except Exception as e: + print(f"Erro ao ler configs do Redis: {e}") + return + + # 2. Inicializando QueueExchange2 + try: + exchange = QueueExchange2( + amqps=system_config.amqps, + origin=system_config.origin, + system="integracao", + service="tecnospeed", + version="1.0.0", + client="#" + ) + + servicekey = system_config.servicekey.ljust(32, '0')[:32] + exchange.setCrypto(serviceid=system_config.serviceid, servicekey=servicekey) + except Exception as e: + print(f"Falha ao iniciar mensageria: {e}") + return + + # 3. Montando o payload interno + internal_payload = { + "fila": "certificado_digital", + "cliente": "UploadTest", + "metodo": "POST", + "dados": { + "arquivo": PFX_BASE64, + "senha": PFX_PASSWORD, + "id_empresa": "c19d874447b1kxlapg1xv783", + "id_usuario": 1, + "expirationAt": "2027-04-13" + }, + "config": { + "x_api_key": os.getenv("PLUGNOTAS_API_KEY"), + "user_postgres": os.getenv("POSTGRES_USER", "tecnospeed"), + "pass_postgres": os.getenv("POSTGRES_PASSWORD", "u7QimG7E!SF//JC2c8"), + "host_postgres": "postgres", + "port_postgres": "5432", + "database_postgres": os.getenv("POSTGRES_DB", "tecnospeed") + }, + "url_base": "https://api.sandbox.plugnotas.com.br" + } + + # 4. Enviando com criptografia (hmac/payload) + try: + msg_json = json.dumps(internal_payload) + exchange.setPayload(msg_json) + exchange.sendMsg(msg_json, service="tecnospeed") + print("Mensagem enviada com sucesso (criptografada)!") + except Exception as e: + print(f"Erro ao enviar mensagem: {e}") + +if __name__ == "__main__": + main() diff --git a/tecnospeed_queue/src/utils/Exchange2.py b/tecnospeed_queue/src/utils/Exchange2.py new file mode 100644 index 0000000..c31a6d0 --- /dev/null +++ b/tecnospeed_queue/src/utils/Exchange2.py @@ -0,0 +1,1047 @@ +import json, time, sys, inspect, os +import hmac, hashlib, uuid, re + +import pika, pprint +from datetime import datetime +from pytz import timezone +from cryptography.hazmat.primitives.ciphers import Cipher, algorithms, modes +from cryptography.hazmat.primitives import padding +from cryptography.hazmat.backends import default_backend + +from queue import Queue, Full, Empty +from threading import Thread, Event +from abc import ABC, abstractmethod +from typing import Sequence, Any +import logging + +from utils.exchangeModels import * + +versao = "v1.0.0 14/12/2023 - Baltazar - Prefetch count" +versao = "v1.1.0 06/02/2024 - MJ. - Detecta Serviço rabbitmq ausente no inicio" +versao = "v1.2.0 20/02/2024 - MJ. - Possibilita uma taksqueue por serviço" +versao = "v1.3.0 06/03/2024 - MJ. - Verboso apenas nos erros" +versao = "v1.5.0 07/03/2024 - MJ. - External Rabbitmq Connection" +versao = "v1.6.0 26/06/2024 - MJ. - Conversao dos parametros host, service e client para lowercase" +versao = "v1.7.0 26/07/2024 - MJ. - Prepacao para utilizar Cloudamqp ou Rabbitmq local amqps/system" +versao = "v2.0.0 16/08/2024 - MJ. - Nova classe RPC e restruturação de Exchange2 e Tasks" +versao = "v2.1.0 21/11/2024 - MJ. - Clareza no log e tratamento de erros" +versao = "v2.2.0 31/01/2025 - MJ. - Remoção de alguns logs e melhorias de performance" +versao = "v2.3.0 12/02/2025 - MJ. - Melhorias na conexão com o Rabbitmq" +versao = "v2.4.0 14/02/2025 - MJ. - Check amqp url parameters" +versao = "v2.4.1 19/02/2025 - MJ. - Correcao prefetch_count=1" +versao = "v2.5.1 13/05/2025 - MJ. - Payload até 128kb - ajuste documentação e visual" +versao = "v2.5.2 01/10/2025 - MJ. - Evita especificar um client vazio" +versao = "v3" +versaostr = f"{versao}0.0 16/01/2026 S.D.G. - SG_IC2 LoggerExchange" + +def is_amqp_url(url): + pattern = r"^amqps?:\/\/([a-zA-Z0-9_\-\.]+):([^@]+)@([a-zA-Z0-9_\-\.]+)(:[0-9]+)?(\/[a-zA-Z0-9_\-\.]*)?$" + + return bool(re.match(pattern, url)) + +# QUEUE EXCHANGE - Apoia no comunicação inter-serviços de um sistema +# EXCHANGE TIPO DIRECT +# NAO DURAVEL - EXCLUSIVO - AUTODELETE +# SE NAO HOUVER QUEM RECEBA A MSG A MSG SERÁ PERDIDA -> O ENVIO FUNCIONA MESMO QUE NINGUEM EXISTA PARA RECEBER +class QueueExchange2: + def __init__( self, + amqps:str, + origin:str, + system:str, + service:str, + version:str, + client:str = '#', + callback: object = None, + delivery_mode = 1): + + paramOK = len(service) and len(system) and len(amqps) and len(origin) and len(version) and len(client) + if not (paramOK) or not is_amqp_url(amqps): + if not is_amqp_url(amqps): + msg = f"NOT A VALID AMQP URL: {amqps}" + else: + msg = f"VERIFY PARAMETERS: is_amqp_url: {is_amqp_url(amqps)}\n amqps[{amqps}]\n origin[{origin}]\n system[{system}]\n service[{service}]\n version[{version}\n client[{client}]" + raise Exception(msg) + + self.delivery = delivery_mode + self.payload = "" + self.hmac = "" + self.version = version + self.serviceid = "" + self.type = "msg" + self.origin = origin.lower() + self.client = client.lower() + self.service = service.lower() + self.system = system.lower() + self.client = client + self.params = pika.URLParameters(amqps) + self.callback = callback + self.queue = f"{self.system}_{service}_{self.client}" if client != '#' else f"{self.system}_{service}" + if callback is not None: + try: + self.connection = pika.BlockingConnection(self.params) + self.channelIn = self.connection.channel() # start main channel + self.channelIn.exchange_declare (exchange = f"{self.system}.direct", exchange_type = 'direct') + self.channelIn.queue_declare (queue = self.queue, durable = True) + self.channelIn.queue_bind (queue = self.queue, exchange = f"{self.system}.direct", routing_key= self.queue) + self.channelIn.basic_qos (prefetch_count=1) + self.consumerTag = self.channelIn.basic_consume (queue = self.queue, + auto_ack = False, + on_message_callback = self.callback) + except Exception as error: + raise(error) + else: + conexao = pika.BlockingConnection(self.params) #testa conexao + conexao.close() + + def cancel(self): + self.channelIn.basic_cancel(self.consumerTag) + + # METODOS INTERNOS _ + # MODELO MSG QUE TRAFEGA ENTRE SERVIÇOS E CONTROLLER + """ + MSG JSON CONTROLADOR + data: + { + "salesContractId": , + "client": "bambui", + "service": "SALES_CONTRACT_CREATED" + } + + message: + { ..., + "serviceid": , + "payload": aes-256 , + "hmac": hmac , + "type": "work", + ... + } + """ + def _setMsg(self, msg:str, caller:str): + now = datetime.now(timezone('America/Sao_Paulo')) + message =msg.strip('"') + + self.Msg = { + "system": self.system, + "target": self.target, + "service": self.service, + "serviceid": self.serviceid, + "payload": self.payload, + "hmac": self.hmac, + "type": self.type, + "version": self.version, + "origin": self.origin, + "client": self.client, + "caller": caller, + "logDate": f"{now}", + "msg": f"{message}" + } + + # Envia uma mensagem para um serviço associado ao sistema + # controller é o nome padrão do serviço, caso nao seja informado + def sendMsg(self, message: str, service:str = 'controller'): + trials = 5 + waitTime = 12 + messageSent = False + caller = inspect.stack()[1].function + if caller == '': caller = 'main' + + self.target = service + self._setMsg(message, caller) + targetQueue = f"{self.system}_{self.target}_{self.client}" if self.client != '#' else f"{self.system}_{self.target}" + while not messageSent and trials: + try: + connection = pika.BlockingConnection(self.params) + channelOut = connection.channel() # start a channel + #channelOut.exchange_declare(exchange = f"{self.system}.direct", exchange_type = 'direct') + #channelOut.queue_declare (queue= targetQueue, durable= True) + #channelOut.queue_bind (queue= targetQueue, exchange= f"{self.system}.direct", routing_key= targetQueue) + channelOut.basic_publish( + exchange = f"{self.system}.direct", + routing_key = targetQueue, + body = json.dumps(self.Msg), + properties = pika.BasicProperties(content_type = 'text/plain', delivery_mode= self.delivery)) + messageSent = True + connection.close() + except Exception as ex: + print(f"EXCH2: TROUBLE CNX RABBITMQ REMAIN ATTEMPT: {trials}") + print(ex, flush=True) + sys.stdout.flush() + time.sleep(waitTime) + trials -= 1 + if messageSent: + self.payload = "" + self.hmac = "" + else: + print(f"EXCH2: ERROR MESSAGE NOT SENT TO [{targetQueue}]", flush=True) + + def start_consuming(self): + if self.callback is not None: + self.consumerTag = self.channelIn.start_consuming() + + def setCrypto(self, serviceid:str, servicekey:str): + self.serviceid = serviceid + self.servicekey = servicekey + self.Crypto = Payload(serviceid, servicekey) + + def setPayload(self, data:str): + try: + self.hmac = "" + self.payload = "" + self.hmac, self.payload = self.Crypto.setPayload(data) + except Exception as ex: + print(f"EXCH2: CRYPTO ERROR: {ex}", flush=True) + + def getPayload(self, hmac:str, safeData:str): + try: + self.hmac = "" + self.payload = "" + self.hmac, self.payload = self.Crypto.getPayload(safeData) + except Exception as ex: + print(ex) + return False + + return hmac == self.hmac + +# CRIPTOGRAFIA DO PAYLOAD +class Payload: + def __init__(self, serviceid:str, servicekey:str): + self.serviceid = serviceid + self.servicekey = servicekey + + + def setPayload(self, data:str): + data = data.encode('utf-8') + tamanho = len(data) + if tamanho > 1024 * 1024 * 10 or tamanho <= 0: + raise Exception("EXCHANGE2 Payload setPayload: Invalid Payload size") + + hmacData= hmac.new(self.serviceid.encode('utf-8'), data, 'sha256').hexdigest() + iv = os.urandom(16) + padder = padding.PKCS7(128).padder() + datarw = padder.update(data) + padder.finalize() + cipher = Cipher(algorithms.AES(self.servicekey.encode('utf-8')), modes.CBC(iv)) + encrypt = cipher.encryptor() + safeData = encrypt.update(datarw) + encrypt.finalize() + payload = (iv + safeData) + + return (hmacData, payload.hex()) + + def getPayload(self, safeData:str): + safeData = bytes.fromhex(safeData) + iv = safeData[:16] + cipher = Cipher(algorithms.AES(self.servicekey.encode('utf-8')), modes.CBC(iv)) + decrypt = cipher.decryptor() + rawdata = decrypt.update(safeData[16:]) + decrypt.finalize() + if len(rawdata)%16 != 0: + raise Exception("EXCHANGE2 Payload getPayload: Invalid Payload size block {0}".format(len(rawdata))) + + unpadder = padding.PKCS7(128).unpadder() + data = unpadder.update(rawdata) + unpadder.finalize() + hmacData = hmac.new(self.serviceid.encode('utf-8'), data, 'sha256').hexdigest() + + return (hmacData, data.decode('utf-8')) + +# TASK EXCHANGE +# PERMITE CRIAR UM DISTRIBUIDOR DE TAREFAS +# O DISTRIBUIDOR DEVE RECEBER UMA FUNCAO DE CALLBACK +class TaskExchange2: + def __init__(self, + amqps:str, + origin:str, + system:str, + service:str, + version:str, + client:str = '#', callback: object = None, delivery_mode:int = 2): + paramOK = len(service) and len(system) and len(amqps) and len(origin) and len(version) + if not (paramOK) or not is_amqp_url(amqps): + if not is_amqp_url(amqps): + msg = f"NOT A VALID AMQP URL {amqps}" + else: + msg = f"VERIFY PARAMETERS: is_amqp_url: {is_amqp_url(amqps)}\n amqps[{amqps}]\n origin[{origin}]\n system[{system}]\n service[{service}]\n version[{version}]" + raise Exception(msg) + + self.delivery = delivery_mode + self.payload = "" + self.hmac = "" + self.version = version + self.serviceid = "" + self.type = "task" + self.origin = origin.lower() + self.service = service.lower() + self.system = system.lower() + self.client = client.lower() + self.params = pika.URLParameters(amqps) + self.callback = callback + self.queue = f'{self.system}_{self.service}_tasks_{client}' if client != '#' else f'{self.system}_{self.service}_tasks' + print(f"EXCH2: TASK EXCHANGE: [{self.queue}] [{amqps}]", flush=True) + if callback is not None: + self.connection = pika.BlockingConnection(self.params) + self.channelIn = self.connection.channel() # start main channel + self.channelIn.exchange_declare (exchange = f"{self.system}.direct", exchange_type = 'direct') + self.channelIn.queue_declare (queue = self.queue, durable = True) + self.channelIn.queue_bind (queue = self.queue, exchange = f"{self.system}.direct", routing_key= self.queue) + self.channelIn.basic_qos (prefetch_count=1) + self.consumerTag = self.channelIn.basic_consume (queue = self.queue, + auto_ack = False, + on_message_callback = self.callback) + else: + conexao = pika.BlockingConnection(self.params) #testa conexao + conexao.close() + def _setTask(self, msg:str, caller:str): + now = datetime.now(timezone('America/Sao_Paulo')) + message =msg.strip('"') + + self.Msg = { + "system": self.system, + "target": self.target, + "service": self.service, + "serviceid": self.serviceid, + "payload": self.payload, + "hmac": self.hmac, + "type": self.type, + "version": self.version, + "origin": self.origin, + "client": self.client, + "caller": caller, + "logDate": f"{now}", + "msg": f"{message}" + } + + def cancel(self): + self.channelIn.basic_cancel(self.consumerTag) + + def sendTask(self, message:str): + trials = 5 + waitTime = 12 + messageSent = False + caller = 'TaskExchange2' + self.target = 'workers' + self._setTask(message, caller) + + while not messageSent and trials: + try: + connection = pika.BlockingConnection(self.params) + channelOut = connection.channel() # start a channel + channelOut.exchange_declare(exchange = f"{self.system}.direct", exchange_type = 'direct') + channelOut.queue_declare (queue = self.queue, durable = True) + channelOut.queue_bind (queue = self.queue, exchange = f"{self.system}.direct", routing_key= self.queue) + + channelOut.basic_publish (exchange = f"{self.system}.direct", + routing_key = self.queue, + body = json.dumps(self.Msg), + properties = pika.BasicProperties(content_type = 'text/plain', + delivery_mode= self.delivery)) + messageSent = True + connection.close() + except Exception as ex: + print(f"EXCH2: TROUBLE CNX RABBITMQ REMAIN ATTEMPT: {trials}") + print(ex, flush=True) + time.sleep(waitTime) + trials -= 1 + if not messageSent: + print(f"EXCH2: ERROR TASK NOT SENT TO: {self.queue}", flush=True) + + def start_consuming(self): + if self.callback is not None: + print(f"EXCH2: SEND TASKS TO: [{self.queue}]", flush=True) + self.consumerTag = self.channelIn.start_consuming() + + def setCrypto(self, serviceid:str, servicekey:str): + self.serviceid = serviceid + self.servicekey = servicekey + self.Crypto = Payload(serviceid, servicekey) + + def setPayload(self, data:str): + try: + self.hmac = "" + self.payload = "" + self.hmac, self.payload = self.Crypto.setPayload(data) + except Exception as ex: + print(f"EXCH2: CRYPTO ERROR: {ex}", flush=True) + + def getPayload(self, hmac:str, safeData:str): + try: + self.hmac = "" + self.payload = "" + self.hmac, self.payload = self.Crypto.getPayload(safeData) + except Exception as ex: + print(f"EXCH2: CRYPTO ERROR: {ex}", flush=True) + return False + + return hmac == self.hmac + +# RPC - QUANDO FOR UTILIZAR COMO ENDPOINT +# FORNECER FUNCAO DE TRABALHO NA INICIALIZACAO +# COMO RPC Server: FILA ONDE RECEBERA AS TAREFAS => SISTEM_SERVICE_RPC ou SISTEM_SERVICE_RPC_CLIENT +# COMO RPC Client: UTILIZA callRPC +class RPCExchange2: + def __init__(self, + amqps:str, # ENDERECO URL RABBITMQ - CLOUDAMQP + origin:str, # NOME DO HOST ONDE O SCRIPT ESTÁ RODANDO + system:str, # NOME DO SISTEMA QUE O SERVIÇO FAZ PARTE + service:str, # NOME DO SERVIÇO DENTRO DO SISTEMA + version:str, # VERSÃO DO SERVIÇO + client:str = '#', # CASO ESPECIFICO A UM CLIENTE, COMO NO CASO DO SG_ETL + workfunction=None, # FUNCAO DE TRABALHO PARA O USO COMO RCP Server, i.e. para substituir um endpoint fastapi + delivery_mode=2 # VAI PERSISTIR AS MENSAGENS NO MODO 2 + ): + paramOK = len(service) and len(system) and len(amqps) and len(origin) and len(version) + + if not (paramOK) or not is_amqp_url(amqps): + if not is_amqp_url(amqps): + msg = f"RPCExchange2: NOT A VALID AMQP URL: {amqps}" + else: + msg = f"RPCExchange2: VERIFY PARAMETERS: is_amqp_url: {is_amqp_url(amqps)}\n amqps[{amqps}]\n origin[{origin}]\n system[{system}]\n service[{service}]\n version[{version}]" + raise Exception(msg) + + self.callback = workfunction + self.delivery = delivery_mode + self.payload = "" + self.hmac = "" + self.version = version + self.serviceid = "" + self.servicekey = "" + self.correlation_id = "" + self.consumerTag = None + self.type = "rpc" + self.origin = origin.lower() + self.service = service.lower() + self.system = system.lower() + self.client = client.lower() + self.params = pika.URLParameters(amqps) + # Server ouve nessa fila - Cliente publica nesta fila + self.queue = f'{self.system}_{self.service}_rpc_{client}' if client != '#' else f'{self.system}_{self.service}_rpc' + self.connection = pika.BlockingConnection(self.params) + self.channel = self.connection.channel() # start main channel + self.channel.exchange_declare (exchange = f"{self.system}.direct", exchange_type = 'direct') + + if (workfunction is not None): # FUNCIONAMENTO COMO RPC SERVER + self.target = 'rpcclt' # SINALIZA O DESTINO DA MSG SER UM RPC CLIENT + self.channel.queue_declare (queue = self.queue, exclusive = False, durable=True, auto_delete = True) + self.channel.queue_bind (exchange = f"{self.system}.direct", queue = self.queue, routing_key = self.queue) + self.channel.basic_qos (prefetch_count = 1) + self.consumerTag = self.channel.basic_consume(queue = self.queue, + auto_ack = True, + on_message_callback = self._RPCResp) # de lá chamamos self.callback + else: # FUNCIONAMENTO COMO RPC CLIENT + self.target = 'rpcsrv' # SINALIZA O DESTINO DA MSG SER UM RPC SERVER + + # PREPARA QUEUE DE RETORNO + result = self.channel.queue_declare(queue='', exclusive=True, auto_delete=True) + self.callback_queue = result.method.queue + self.channel.queue_bind(exchange = f"{self.system}.direct", + queue = self.callback_queue, + routing_key = self.callback_queue) + + self.channel.basic_consume( + queue = self.callback_queue, + on_message_callback = self._on_response, + auto_ack = True + ) + #print("EXCH2: RPC CLT - QUEUE:", self.callback_queue, flush=True) + + # RPC - QUANDO FOR UTILIZAR COMO ENDPOINT + def _RPCResp(self, ch, method, props, body): + #ch.basic_ack(delivery_tag = method.delivery_tag) + message = body.decode("utf-8") + reply_to = getattr(props, 'reply_to', None) + correlation_id = getattr(props, 'correlation_id', None) + resposta = "{}" # SEM WORKFUNCTION + # ENVIA A MENSAGEM AO CALLBACK + self.payload = "" + self.hmac = "" + try: + if self.callback is not None: + resposta = self.callback(message) + rspObj = json.loads(resposta) + # para enviar um payload criptografado na resposta ao caller + # retorne um objeto contendo serviceid, servicekey e payload + # essas chaves serao retiradas do objeto e o restante da informacao + # irá no chave msg + if 'payload' in rspObj and 'serviceid' in rspObj and 'servicekey' in rspObj: + self.setCrypto(rspObj['serviceid'], rspObj['servicekey']) + self.setPayload(json.dumps(rspObj['payload'])) + rspObj.pop('payload', None) + rspObj.pop('serviceid', None) + rspObj.pop('servicekey', None) + resposta = json.dumps(rspObj) + + + except Exception as ex: + erro = f"ERROR CALLBACK: {str(ex)}" + print(erro, flush=True) + resposta = json.dumps({"status": "error", "message": erro}) + + # ENVIA RESPOSTA AO CALLER + msg = json.loads(message) + msg = msg['msg'] + #print(f"RPC <== {msg}", flush=True) + #print(f"RPC ==> {resposta}", flush=True) + try: + if reply_to: + self.target = "rpcclt" + self._setJSON(resposta, 'RPCResp') + + ch.basic_publish(exchange = f"{self.system}.direct", + routing_key = reply_to, + body = json.dumps(self.Msg), + properties = pika.BasicProperties(content_type = 'application/json', + delivery_mode = self.delivery, + correlation_id = correlation_id)) + else: + print(f"RPCExchange2: NO TARGET FOUND: {resposta}", props, flush=True) + + except Exception as ex: + print(ex) + resposta = f"RPCExchange2: ERRO MSG CALLER: {str(ex)}" + raise Exception(resposta) + + + def _setJSON(self, msg:str, caller:str): + now = datetime.now(timezone('America/Sao_Paulo')) + message =msg.strip('"') + + self.Msg = { + "system": self.system, + "target": self.target, + "service": self.service, + "serviceid": self.serviceid, + "payload": self.payload, + "hmac": self.hmac, + "type": self.type, + "correlation_id": self.correlation_id, + "version": self.version, + "origin": self.origin, + "client": self.client, + "caller": caller, + "logDate": f"{now}", + "msg": f"{message}" + } + self.hmac = "" + self.payload = "" + + + def _on_response(self, ch, method, properties, body): + try: + if self.correlation_id == properties.correlation_id: + self.response = body.decode("utf-8") + except Exception as ex: + print(f"RPCExchange2: RPC ERROR: {ex}", flush=True) + sys.exit(0) + + return self.response + + # ENVIA A MENSAGEM AO CALLBACK + # RPC CLIENT CHAMA O RPC SERVER + def callRPC(self, message:str, timeout:int=10): + # ENVIA A MENSAGEM + self.response = None + self.correlation_id = str(uuid.uuid4()) + self._setJSON(message, 'callRPC') # COLOCA A message DENTRO DA CHAVE msg em self.Msg + + self.channel.exchange_declare(exchange = f"{self.system}.direct", exchange_type = 'direct') + self.channel.queue_declare (queue= self.queue, durable=True, exclusive=False, auto_delete=True) + self.channel.queue_bind (queue= self.queue, exchange= f"{self.system}.direct", routing_key= self.queue) + + self.channel.basic_publish (exchange = f"{self.system}.direct", + routing_key = self.queue, + body = json.dumps(self.Msg), + properties = pika.BasicProperties(content_type = 'application/json', + delivery_mode = self.delivery, + reply_to = self.callback_queue, + correlation_id= self.correlation_id)) + # AGUARDA O RETORNO + while self.response is None: + self.connection.process_data_events(time_limit=timeout) + return self.response + + def cancel(self): + print(f"RPCExchange2: CONSUMER TAG CANCELED: {self.consumerTag}", flush=True) + self.channel.basic_cancel(self.consumerTag) + + def start_consuming(self): + if self.callback is not None: + print(f"RPCExchange2: {self.service.upper()} RPC SRV QUEUE: [{self.queue}]", flush=True) + self.channel.start_consuming() + + def setCrypto(self, serviceid:str, servicekey:str): + self.serviceid = serviceid + self.servicekey = servicekey + self.Crypto = Payload(serviceid, servicekey) + + def setPayload(self, data:str): + print(f"RPCExchange2: SET PAYLOAD", flush=True) + try: + self.hmac = "" + self.payload = "" + self.hmac, self.payload = self.Crypto.setPayload(data) + except Exception as ex: + print(ex) + + def getPayload(self, hmac:str, safeData:str): + print(f"RPCExchange2: GET PAYLOAD", flush=True) + try: + self.hmac = "" + self.payload = "" + self.hmac, self.payload = self.Crypto.getPayload(safeData) + except Exception as ex: + print(ex) + return False + + return hmac == self.hmac + +# CADA APLICAÇÃO CRIA UMA CLASSE QUE ESTENDE IC2Builder IMPLEMENTANDO +# OS METODOS ABSTRATOS DE CADA REALMTYPE +class IC2Builder(ABC): + @abstractmethod + def _setLogs(self, message: tuple) -> InfoConnector: ... + @abstractmethod + def _setErrors(self, message: tuple) -> InfoConnector: ... + @abstractmethod + def _setOperational(self, message: tuple) -> InfoConnector: ... + @abstractmethod + def _setStatus(self, message: tuple) -> InfoConnector: ... + @abstractmethod + def _setMetrics(self, message: tuple) -> InfoConnector: ... + + def build(self, realm: RealmType, message: Sequence[Any]) -> InfoConnector: + handlers = { + RealmType.LOGS: self._setLogs, + RealmType.ERRORS: self._setErrors, + RealmType.OPERATIONAL: self._setOperational, + RealmType.STATUS: self._setStatus, + RealmType.METRICS: self._setMetrics, + } + + try: + return handlers[realm](message) + except KeyError: + raise ValueError(f"RealmType não suportado: {realm}") + +class LoggerExchange2: + def __enter__(self): return self + def __exit__(self, exc_type, exc, tb): self.close() + + def __init__(self, + ic2_builder: IC2Builder, + amqps : str, + system : str, + service : str, + version : str, + origin : str = "", + client : str = "#", + logType : str = 'logs', + logLocal : bool = False, + callback : object = None): + self.callback = callback + self.ic2_builder = ic2_builder + self.system = system.lower() + self.target = "" + self.service = service.lower() + self.serviceid = "AF1699F719F1B6C81DE2BD38AB811994E3089B35C8E9314813A770AC7AE25D8D" + self.servicekey = "KoG704lAafBA7Yafj8jyg5lAHfarrQ12" + self.payload = "" + self.hmac = "" + self.type = "logger" + self.version = version + self.origin = origin + self.client = client.lower() + self.params = pika.URLParameters(amqps) + self.params.socket_timeout = 15 + self.params.heartbeat = 60 + self.logType = logType.lower() + self.logTypeSvd = logType.lower() + self.logLocal = logLocal + self.pp = pprint.PrettyPrinter() + self.diaMes = datetime.now().strftime("%d") # PARA SABER SE ATUALIZA OU NÃO O ARQUIVO DE LOG LOCAL + self.flush = True + self.base_routeKey = f"{self.service}.{self.system}" + self.connection = None + self.channel = None + self.logging = logging.getLogger(self.base_routeKey) + self.Crypto = Payload(self.serviceid, self.servicekey) + self.exchange_name = 'info_connector.topic' + + try: + self._connect() + except Exception as err: + self.logging.error(f"ERROR CONNECTION {err}") + raise + + if self.logLocal: self.setLogLocalName() + + # --- async logging --- + self.queue_size = 1000 + self.queue = Queue(maxsize=self.queue_size) + self.stop_event = Event() + self.worker_thread = Thread( + target = self._worker, + name = "logger-worker", + daemon = True + ) + self.worker_thread.start() + + self.logging.info(f"params: {self.params} amqps: {amqps}") + if self.callback is not None: + try: + self.queueIC2 = "info_connector_collector" + self.channelIn = self.connection.channel() + self.channelIn.queue_declare (queue = self.queueIC2, exclusive=True, durable=True) + self.channelIn.queue_bind (queue = self.queueIC2, exchange = self.exchange_name, routing_key = "#") + self.channelIn.basic_qos (prefetch_count = 1) + self.consumerTag = self.channelIn.basic_consume (queue = self.queueIC2, + auto_ack = False, + on_message_callback = self.callback) + self.logging.info("COLLECTING TOPIC MESSAGES VHOST info_connector_host EXCHANGE info_connector.topic KEY #") + except Exception as error: + raise(error) + else: + conexao = pika.BlockingConnection(self.params) #testa conexao + conexao.close() + self.logging.info(f"Logger Messages topics: {self.logType}.{self.base_routeKey}.{self.client}") + + + # PERSISTENCIA LOGS MESSGERIA + def _worker(self): + while not self.stop_event.is_set() or not self.queue.empty(): + try: + payload, client = self.queue.get(timeout=0.5) + except Empty: + continue + + try: + self.sendMsgWork(payload, client) + except Exception as e: + # última linha de defesa + self.logging.error(f"ASYNC LOGGER FAILURE: {e}") + if self.logLocal: + self.logging.info(payload) + finally: + self.queue.task_done() + + # PERSISTENCIA CONEXAO AMQPS + def _connect(self): + """Conecta ou reconecta ao RabbitMQ.""" + if self.connection and self.connection.is_open: + return + try: + if self.connection: + self.connection.close() + except Exception: + pass + try: + self.connection = pika.BlockingConnection(self.params) + self.channel = self.connection.channel() + self.channel.exchange_declare(exchange = self.exchange_name, + exchange_type = 'topic', + auto_delete = False) + + self.logging.info("Conexão ao RabbitMQ estabelecida.") + except pika.exceptions.AMQPConnectionError as e: + self.logging.error(f"Falha na conexão: {e}") + raise + + def _setMsg(self, msg:str, caller:str): + now = datetime.now(timezone('America/Sao_Paulo')) # melhoria: acrescentar timezone info no system_config + return { + "system" : self.system, + "target" : self.target, + "service" : self.service, + "serviceid": self.serviceid, + "payload" : self.payload, + "hmac" : self.hmac, + "type" : self.type, + "version" : self.version, + "origin" : self.origin, + "client" : self.client, + "caller" : caller, + "logDate" : f"{now}", + "msg" : msg + } + + def _sendMsg(self, msg: str, caller: str, *args): + # Avalia arquivo de logs + if self.logLocal: + current_dia = datetime.now().strftime("%d") + if current_dia != self.diaMes: + self.diaMes = current_dia + self.setLogLocalName() + + message = msg.strip() + + try: + if args and isinstance(args[0], (list, tuple)): + for item in args[0]: + msgArg = pprint.pformat(item).replace("'", "").strip() + message = f"{message}, {msgArg}" + + Msg = self._setMsg(message, caller) + payload = json.dumps(Msg) + + # console / docker logs continuam síncronos + self.pp.pprint(f"{Msg['logDate'][0:21]} {Msg['msg']}") + + # log local imediato (garantia mínima) + if self.logLocal: + Msg['payload'] = Msg['payload'][:20] + self.logging.info(self.pp.pformat(Msg)) + + # envio assíncrono + try: + self.queue.put_nowait((payload, self.client)) + except Full: + # fila cheia → degrada com dignidade + self.logging.warning("LOGGER QUEUE FULL — DROPPING MESSAGE") + + if self.flush: + sys.stdout.flush() + + except Exception as e: + erro = f"LOGGER ERROR: {e}" + self.logging.error(erro) + print(erro, flush=True) + + + def _sendMsgLocal(self, msg: str, caller: str, *args): + # Avalia arquivo de logs + if self.logLocal: + current_dia = datetime.now().strftime("%d") + if current_dia != self.diaMes: + self.diaMes = current_dia + self.setLogLocalName() + + message = msg.strip() + + try: + if args and isinstance(args[0], (list, tuple)): + for item in args[0]: + msgArg = pprint.pformat(item).replace("'", "").strip() + message = f"{message}, {msgArg}" + + Msg = self._setMsg(message, caller) + payload = json.dumps(Msg) + + # console / docker logs continuam síncronos + self.pp.pprint(f"{Msg['logDate'][0:21]} {Msg['msg']}") + + # log local imediato (garantia mínima) + if self.logLocal: + Msg['payload'] = Msg['payload'][:20] + self.logging.info(self.pp.pformat(Msg)) + + except Exception as e: + erro = f"LOGGER ERROR: {e}" + self.logging.error(erro) + print(erro, flush=True) + + def start_consuming(self): + if self.callback is not None: + self.consumerTag = self.channelIn.start_consuming() + + def close(self): + # sinaliza parada + self.stop_event.set() + + # aguarda flush da fila (timeout defensivo) + try: + self.queue.join() + except Exception: + pass + + if self.worker_thread.is_alive(): + self.worker_thread.join(timeout=2) + + if self.channel: + self.channel.close() + if self.connection: + self.connection.close() + + def setLogLocalName(self): + self.logLocalName = 'logs/'+ self.system.split()[0] + '_' + self.diaMes + formatter = logging.Formatter("%(asctime)s - %(name)s - %(levelname)s - %(message)s") + fhandler = logging.FileHandler(self.logLocalName) + fhandler.setLevel(logging.INFO) + fhandler.setFormatter(formatter) + self.logging.propagate = False + self.logging.setLevel(logging.INFO) + if not any(isinstance(h, logging.FileHandler) for h in self.logging.handlers): + self.logging.addHandler(fhandler) + + # Funcionamento: caso nao seja possivel enviar a msg, tentar nova conexao + # e envio da msg original mais uma vez + def sendMsgWork(self, message:str, client:str = "#"): + messageSent = False + trials = 5 + waitTime = 5 + while not messageSent and trials > 0: + self._connect() + # Cliente informado - MAIOR PRECEDENCIA + if client != "#": + routeKey = self.base_routeKey + f".{client.lower()}" + elif self.client != "#": + routeKey = self.base_routeKey + f".{self.client.lower()}" + else: + routeKey = self.base_routeKey + + try: + routing_key = f"{self.logType}.{routeKey}" + self.channel.basic_publish( + exchange = "info_connector.topic", + routing_key = routing_key, + body = message, + properties = pika.BasicProperties( + content_type = 'text/plain', + delivery_mode= pika.DeliveryMode.Transient)) + messageSent = True + except (pika.exceptions.ConnectionClosed, pika.exceptions.ChannelClosed, pika.exceptions.AMQPConnectionError) as ex: + self.logging.warning(f"TROUBLE CNX LOGGER REMAIN ATTEMPT: {trials} {ex}") + self._connect() + except Exception as ex: + self.logging.error(f"LOGGER ERROR: {ex}") + self.logging.warning(str(self.params)) + finally: + if not messageSent: + trials -= 1 + time.sleep(waitTime) + waitTime *= 2 + + if not messageSent: + self.logging.error(f"LOGGER ERROR: {message}") + + # Para quando desejar trocar o header do topico + # Salva o tópico padrão especificado no momento da inicialização + def setLogType(self, logType:str): + if not logType or len(logType) > 25: + return + self.logTypeSvd = self.logType + self.logType = logType.lower() + + # Volta ao header topico padrao + def clrLogType(self): + self.logType = self.logTypeSvd + + def log_logs(self, msg: str): + self.logIC2(RealmType.LOGS, (msg,)) + + def log_errors(self, msg: str): + self.logIC2(RealmType.ERRORS, (msg,)) + + def logIC2(self, realm: RealmType, msg: tuple = ()): + ic_obj = self.ic2_builder.build(realm, msg) + payload = ic_obj.model_dump_json() + self.setPayload(data=payload) + self.setLogType(logType=realm.value) + self._sendMsg(f"LOGIC2 {realm.value.upper()} [{str(msg)[:20]} ...]", caller="IC2") + self.clrLogType() + self.clrPayload() + + def logMsgError(self, msg:str, *args): + self.setLogType('error.logs') + caller = inspect.stack()[1].function + if caller == '': caller = 'main' + + self._sendMsgLocal(msg, caller, args) + self.clrLogType() + + # LOGS GERAIS + # INFORM logs...# + # Log geral de acordo com o valor logType + def logMsg (self, msg: str, *args): + caller = inspect.stack()[1].function + if caller == '': caller = 'main' + Msg = f"{msg}" + self._sendMsgLocal(Msg, caller, args) + + # FUNÇÕES PARA CRYPTOGRAFIA + def clrPayload(self): + self.hmac = "" + self.payload = "" + + def setPayload(self, data:str): + try: + self.hmac = "" + self.payload = "" + self.hmac, self.payload = self.Crypto.setPayload(data) + except Exception as ex: + print(f"EXCH2: CRYPTO ERROR: {ex}", flush=True) + + def getPayload(self, hmac:str, safeData:str): + try: + self.hmac = "" + self.payload = "" + self.hmac, self.payload = self.Crypto.getPayload(safeData) + except Exception as ex: + print(f"EXCH2: CRYPTO ERROR: {ex}", flush=True) + return False + + return hmac == self.hmac + +class AppIC2Builder(IC2Builder): + def __init__(self, service_name: str, versionstr: str, origin: str): + self.service_name = service_name + self.versionstr = versionstr + self.origin = origin + + def _get_common_fields(self): + now = datetime.now().strftime("%Y-%m-%d %H:%M:%S") + return { + "log_date": now, + "service_date": now, + "service": self.service_name, + "versionstr": self.versionstr, + "origin": self.origin + } + + def _setLogs(self, message: tuple) -> InfoConnector: + msg_str = str(message[0]) if len(message) > 0 else "" + ent_id = str(message[1]) if len(message) > 1 else "0" + ent_name = str(message[2]) if len(message) > 2 else "Unknown" + tenant = str(message[3]) if len(message) > 3 else "#" + + ic_msg = ICLogs(**self._get_common_fields(), enterprise_id=ent_id, enterprise_name=ent_name, message=msg_str, tenant=tenant) + return InfoConnector(realm=RealmType.LOGS, messageIC=ic_msg) + + def _setErrors(self, message: tuple) -> InfoConnector: + msg_str = str(message[0]) if len(message) > 0 else "" + ent_id = str(message[1]) if len(message) > 1 else "0" + ent_name = str(message[2]) if len(message) > 2 else "Unknown" + tenant = str(message[3]) if len(message) > 3 else "#" + + ic_msg = ICErrors(**self._get_common_fields(), enterprise_id=ent_id, enterprise_name=ent_name, message=msg_str, tenant=tenant) + return InfoConnector(realm=RealmType.ERRORS, messageIC=ic_msg) + + def _setOperational(self, message: tuple) -> InfoConnector: + acao = str(message[0]) if len(message) > 0 else "" + tabela = str(message[1]) if len(message) > 1 else "" + id_usuario = str(message[2]) if len(message) > 2 else "0" + id_registro = str(message[3]) if len(message) > 3 else "0" + status_str = str(message[4]).upper() if len(message) > 4 else "INICIO" + mensagem = str(message[5]) if len(message) > 5 else "" + json_data = str(message[6]) if len(message) > 6 else "{}" + + if status_str == 'SUCESSO': op_status = OpStatus.OK + elif status_str == 'ERRO': op_status = OpStatus.ERROR + else: op_status = OpStatus.ACTIVE + + common = self._get_common_fields() + ic_msg = ICOperational( + log_date=common["log_date"], service_date=common["service_date"], service_name=self.service_name, + versionstr=self.versionstr, origin=self.origin, enterprise_id=id_usuario, enterprise_name=f"User {id_usuario}", + status=op_status, step_status=acao, message=f"{mensagem} | Tabela: {tabela} | ID_Reg: {id_registro} | Data: {json_data}" + ) + return InfoConnector(realm=RealmType.OPERATIONAL, messageIC=ic_msg) + + def _setStatus(self, message: tuple) -> InfoConnector: + common = self._get_common_fields() + ic_msg = ICStatus( service=self.service_name, versionstr=self.versionstr, system_name="integracao", + startup_date=common["log_date"], access_date=common["log_date"], origin=self.origin ) + return InfoConnector(realm=RealmType.STATUS, messageIC=ic_msg) + + def _setMetrics(self, message: tuple) -> InfoConnector: + metric_name = str(message[0]) if len(message) > 0 else "" + value = str(message[1]) if len(message) > 1 else "0" + unit = str(message[2]) if len(message) > 2 else "" + + now = datetime.now().strftime("%Y-%m-%d %H:%M:%S") + ic_msg = ICMetrics( metric_date=now, metric=metric_name, metric_name=metric_name, value=value, unit=unit, tenant="#" ) + return InfoConnector(realm=RealmType.METRICS, messageIC=ic_msg) diff --git a/tecnospeed_queue/src/utils/api.py b/tecnospeed_queue/src/utils/api.py new file mode 100644 index 0000000..e7c48ce --- /dev/null +++ b/tecnospeed_queue/src/utils/api.py @@ -0,0 +1,158 @@ +import requests + +# Wrapper base para realizar requests (GET/POST/PUT/DELETE) à API da PlugNotas de forma padronizada +class Api: + def __init__(self, api_key): + self.api_key = api_key + + def get(self, url): + try: + headers = { + "x-api-key": self.api_key + } + response = requests.get(url, headers=headers, timeout=30) + response.raise_for_status() + return response.json() + except requests.exceptions.RequestException as e: + resp_data = {} + if 'response' in locals() and response is not None: + try: + resp_data = response.json() + except ValueError: + resp_data = response.text + return {"error": str(e), "response": resp_data} + + def get_paginated(self, url, max_pages=100): + try: + headers = { + "x-api-key": self.api_key + } + all_data = [] + current_url = url + page = 0 + + while current_url and page < max_pages: + separator = "&" if "?" in current_url else "?" + request_url = current_url if page == 0 else current_url + + response = requests.get(request_url, headers=headers, timeout=30) + response.raise_for_status() + result = response.json() + + if isinstance(result, list): + all_data.extend(result) + break + elif isinstance(result, dict): + page_data = result.get("data", []) + if isinstance(page_data, list): + all_data.extend(page_data) + elif page_data: + all_data.append(page_data) + + hash_proxima = result.get("hashProximaPagina") + if hash_proxima: + separator = "&" if "?" in url else "?" + current_url = f"{url}{separator}hashProximaPagina={hash_proxima}" + page += 1 + else: + break + else: + break + + return {"data": all_data, "total_paginas": page + 1, "total_registros": len(all_data)} + except requests.exceptions.RequestException as e: + resp_data = {} + if 'response' in locals() and response is not None: + try: + resp_data = response.json() + except ValueError: + resp_data = response.text + return {"error": str(e), "response": resp_data} + + def post(self, url, data): + try: + headers = { + "Content-Type": "application/json", + "x-api-key": self.api_key + } + response = requests.post(url, headers=headers, json=data, timeout=30) + response.raise_for_status() + return response.json() + except requests.exceptions.RequestException as e: + resp_data = {} + if 'response' in locals() and response is not None: + try: + resp_data = response.json() + except ValueError: + resp_data = response.text + return {"error": str(e), "response": resp_data} + + def put(self, url, data): + try: + headers = { + "x-api-key": self.api_key + } + response = requests.put(url, headers=headers, json=data, timeout=30) + response.raise_for_status() + return response.json() + except requests.exceptions.RequestException as e: + return {"error": str(e)} + + def patch(self, url, data): + try: + headers = { + "x-api-key": self.api_key + } + response = requests.patch(url, headers=headers, json=data, timeout=30) + response.raise_for_status() + return response.json() + except requests.exceptions.RequestException as e: + return {"error": str(e)} + + def delete(self, url): + try: + headers = { + "x-api-key": self.api_key + } + response = requests.delete(url, headers=headers, timeout=30) + response.raise_for_status() + return response.json() + except requests.exceptions.RequestException as e: + return {"error": str(e)} + + def post_file(self, url, file): + try: + headers = { + "x-api-key": self.api_key + } + response = requests.post(url, headers=headers, files=file, timeout=60) + response.raise_for_status() + return response.json() + except requests.exceptions.RequestException as e: + return {"error": str(e)} + + def post_form_data(self, url, data): + try: + headers = { + "x-api-key": self.api_key + } + response = requests.post(url, headers=headers, files=data, timeout=60) + response.raise_for_status() + return response.json() + except requests.exceptions.RequestException as e: + return {"error": str(e)} + + def put_form_data(self, url, data): + try: + headers = { + "x-api-key": self.api_key + } + response = requests.put(url, headers=headers, files=data, timeout=60) + response.raise_for_status() + return response.json() + except requests.exceptions.RequestException as e: + return {"error": str(e)} + +# api = Api("2da392a6-79d2-4304-a8b7-959572c7e44d") +# response = api.get("https://api.sandbox.plugnotas.com.br/certificado") +# print(response) diff --git a/tecnospeed_queue/src/utils/banco.py b/tecnospeed_queue/src/utils/banco.py new file mode 100644 index 0000000..3a01938 --- /dev/null +++ b/tecnospeed_queue/src/utils/banco.py @@ -0,0 +1,243 @@ +from datetime import datetime +import uuid +from sqlalchemy import Column, Integer, String, DateTime, Text, create_engine, Boolean +from sqlalchemy.orm import declarative_base, sessionmaker +from sqlalchemy.exc import SQLAlchemyError + +Base = declarative_base() + +class CertificadoDigitalTable(Base): + __tablename__ = 'certificado_digital' + id = Column(Integer, primary_key=True, autoincrement=True) + id_tecnospeed = Column(String(50), nullable=False) + certificado = Column(Text, nullable=False) + createdAt = Column(DateTime, default=datetime.now) + updatedAt = Column(DateTime, default=datetime.now, onupdate=datetime.now) + deletedAt = Column(DateTime, nullable=True) + expirationAt = Column(DateTime, nullable=False) + senha = Column(String(50), nullable=False) + status = Column(Boolean, nullable=False) + id_empresa = Column(String(50), nullable=True) + id_usuario = Column(Integer, nullable=True) + cnpj = Column(String(50), nullable=True) + email = Column(String(255), nullable=True) + +class EnterpriseTable(Base): + __tablename__ = 'Enterprise' + id = Column(String(50), primary_key=True) + nome_fantasia = Column(String(255), nullable=True) # Antigo 'name' + razao_social = Column(String(255), nullable=True) + cnpj = Column(String(50), nullable=False) + banco = Column(Text, nullable=True) + email = Column(String(255), nullable=True) + createdAt = Column(DateTime, default=datetime.now) + updatedAt = Column(DateTime, default=datetime.now, onupdate=datetime.now) + deletedAt = Column(DateTime, nullable=True) + status = Column(Boolean, nullable=False, default=True) + +class LogoTable(Base): + __tablename__ = 'logo' + id = Column(Integer, primary_key=True, autoincrement=True) + logo = Column(Text, nullable=False) + id_empresa = Column(String(50), nullable=False) + createdAt = Column(DateTime, default=datetime.now) + updatedAt = Column(DateTime, default=datetime.now, onupdate=datetime.now) + deletedAt = Column(DateTime, nullable=True) + status = Column(Boolean, nullable=False) + +class LogTable(Base): + __tablename__ = 'log' + id = Column(Integer, primary_key=True, autoincrement=True) + id_usuario = Column(Integer, nullable=False) + createdAt = Column(DateTime, default=datetime.now) + updatedAt = Column(DateTime, default=datetime.now, onupdate=datetime.now) + deletedAt = Column(DateTime, nullable=True) + acao = Column(String(50), nullable=False) + tabela = Column(String(50), nullable=False) + id_registro = Column(String(50), nullable=False) + json = Column(Text, nullable=False) + status = Column(String(20), nullable=False, default='INICIO') + mensagem = Column(Text, nullable=False, default='') + cliente = Column(String(100), nullable=True) + correlation_id = Column(String(50), nullable=True) + +class ServicoTable(Base): + __tablename__ = 'servico' + id = Column(Integer, primary_key=True, autoincrement=True) + id_tecnospeed = Column(String(50), nullable=False) + descricao = Column(Text, nullable=False) + id_usuario = Column(Integer, nullable=False) + createdAt = Column(DateTime, default=datetime.now) + updatedAt = Column(DateTime, default=datetime.now, onupdate=datetime.now) + deletedAt = Column(DateTime, nullable=True) + status = Column(Boolean, nullable=False) + +class TomadorTable(Base): + __tablename__ = 'tomador' + id = Column(Integer, primary_key=True, autoincrement=True) + id_tecnospeed = Column(String(50), nullable=False) + nome = Column(String(50), nullable=False) + cpf_cnpj = Column(String(50), nullable=False) + email = Column(String(50), nullable=False) + telefone = Column(String(50), nullable=False) + id_usuario = Column(Integer, nullable=False) + createdAt = Column(DateTime, default=datetime.now) + updatedAt = Column(DateTime, default=datetime.now, onupdate=datetime.now) + deletedAt = Column(DateTime, nullable=True) + status = Column(Boolean, nullable=False) + +class NFSeTable(Base): + __tablename__ = 'nfse' + id = Column(Integer, primary_key=True, autoincrement=True) + id_tecnospeed = Column(String(50), nullable=False) + id_empresa = Column(String(50), nullable=False) + id_usuario = Column(Integer, nullable=False) + createdAt = Column(DateTime, default=datetime.now) + updatedAt = Column(DateTime, default=datetime.now, onupdate=datetime.now) + deletedAt = Column(DateTime, nullable=True) + status = Column(Boolean, nullable=False) + +class NFeTable(Base): + __tablename__ = 'nfe' + id = Column(Integer, primary_key=True, autoincrement=True) + id_tecnospeed = Column(String(50), nullable=False) + id_empresa = Column(String(50), nullable=False) + id_usuario = Column(Integer, nullable=False) + createdAt = Column(DateTime, default=datetime.now) + updatedAt = Column(DateTime, default=datetime.now, onupdate=datetime.now) + deletedAt = Column(DateTime, nullable=True) + status = Column(Boolean, nullable=False) + +# Cache de motores criados pelo SQLAlchemy por URL de conexao +_engines = {} + +# Abstração de conexão base e de persistência de instâncias de metadados relacionais usando PostgreSQL +class Banco: + def __init__(self, host, port, user, password, database): + conn_str = f'postgresql://{user}:{password}@{host}:{port}/{database}' + if conn_str not in _engines: + _engines[conn_str] = create_engine( + conn_str, + pool_pre_ping=True, + pool_recycle=3600 + ) + self.engine = _engines[conn_str] + self.Session = sessionmaker(bind=self.engine) + self.session = self.Session() + + def close(self): + """Encerra a sessão SQLAlchemy atual liberando do pooler.""" + if hasattr(self, 'session') and self.session is not None: + self.session.close() + + def create_tables(self): + """Cria as tabelas no banco de dados.""" + try: + Base.metadata.create_all(self.engine) + except SQLAlchemyError as e: + print(f"Erro ao criar tabelas (SQLAlchemy): {e}") + except Exception as e: + print(f"Erro ao criar tabelas: {e}") + + def add(self, obj): + """Adiciona um novo registro no banco.""" + try: + self.session.add(obj) + self.session.commit() + return obj + except SQLAlchemyError as e: + self.session.rollback() + print(f"Erro ao adicionar registro (SQLAlchemy): {e}") + raise e + except Exception as e: + self.session.rollback() + print(f"Erro ao adicionar registro: {e}") + raise e + + def get(self, model, obj_id): + """Busca um registro pelo ID.""" + try: + return self.session.query(model).get(obj_id) + except SQLAlchemyError as e: + self.session.rollback() + print(f"Erro ao buscar registro (SQLAlchemy): {e}") + raise e + except Exception as e: + self.session.rollback() + print(f"Erro ao buscar registro: {e}") + raise e + + def get_all(self, model): + """Retorna todos os registros de um modelo.""" + try: + return self.session.query(model).all() + except SQLAlchemyError as e: + print(f"Erro ao buscar registros (SQLAlchemy): {e}") + raise e + except Exception as e: + print(f"Erro ao buscar registros: {e}") + raise e + + def update(self, obj): + """Atualiza um registro no banco.""" + try: + self.session.commit() + return obj + except SQLAlchemyError as e: + self.session.rollback() + print(f"Erro ao atualizar registro (SQLAlchemy): {e}") + raise e + except Exception as e: + self.session.rollback() + print(f"Erro ao atualizar registro: {e}") + raise e + + def delete(self, obj): + """Deleta um registro do banco.""" + try: + self.session.delete(obj) + self.session.commit() + return True + except SQLAlchemyError as e: + self.session.rollback() + print(f"Erro ao deletar registro (SQLAlchemy): {e}") + raise e + except Exception as e: + self.session.rollback() + print(f"Erro ao deletar registro: {e}") + raise e + + def rollback(self): + """Desfaz as alterações pendentes.""" + try: + self.session.rollback() + except SQLAlchemyError as e: + print(f"Erro ao desfazer alterações (SQLAlchemy): {e}") + except Exception as e: + print(f"Erro ao desfazer alterações: {e}") + + def logar_sistema(self, id_usuario, acao, tabela, id_registro=0, json="{}", status="INICIO", mensagem="", cliente=None, correlation_id=None): + """Registra uma ação no log do sistema.""" + try: + log = LogTable( + id_usuario=id_usuario, + acao=acao, + tabela=tabela, + id_registro=id_registro, + json=json, + status=status, + mensagem=mensagem, + cliente=cliente, + correlation_id=correlation_id + ) + self.add(log) + except SQLAlchemyError as e: + print(f"Erro ao logar ação (SQLAlchemy): {e}") + except Exception as e: + print(f"Erro ao logar ação: {e}") + + @staticmethod + def gerar_correlation_id(): + """Gera um ID único para rastrear toda a cadeia de uma operação.""" + return str(uuid.uuid4())[:8] + diff --git a/tecnospeed_queue/src/utils/enviaListerner.py b/tecnospeed_queue/src/utils/enviaListerner.py new file mode 100644 index 0000000..6db4635 --- /dev/null +++ b/tecnospeed_queue/src/utils/enviaListerner.py @@ -0,0 +1,35 @@ +import requests +import json +import os + +class enviaListerner: + + def __init__(self, logger=None): + self.url = os.getenv("LISTENER_INTERNAL_URL", "http://endpoint_listener:8000/listenerPlugnotas") + self.logger = logger + + def notificar_emissao(self, id_tecnospeed: str, documento: str, cnpj: str, status: str = "PROCESSANDO"): + """ + Envia um POST para o listener local para sincronização imediata. + """ + payload = { + "id": id_tecnospeed, + "status": status, + "documento": documento, + "emitente": cnpj + } + + try: + response = requests.post(self.url, json=payload, timeout=5) + + if self.logger: + if response.status_code in [200, 201]: + self.logger.log_logs(f"Notificação interna enviada com sucesso - ID: {id_tecnospeed} | Status: {status}") + else: + self.logger.log_errors(f"Falha na notificação interna - Status: {response.status_code} | Resposta: {response.text}") + + return response.status_code in [200, 201] + except Exception as e: + if self.logger: + self.logger.log_errors(f"Erro ao tentar notificar listener interno: {e}") + return False diff --git a/tecnospeed_queue/src/utils/exchangeModels.py b/tecnospeed_queue/src/utils/exchangeModels.py new file mode 100644 index 0000000..1e19116 --- /dev/null +++ b/tecnospeed_queue/src/utils/exchangeModels.py @@ -0,0 +1,127 @@ + +__version__ = "1.0.0 - 18/02/2025" # VERSAO PARA TODOS OS SISTEMAS TRANSMITINDO PARA O SERVIÇO LOGGER_CONTROLLER DO SG_IC2 +__version__ = "1.0.1 - 15/01/2026 - S.D.G. - Ajustes" +__version__ = "1.0.2 - 23/01/2026 - S.D.G. - ForwardCompatibleModel & SerializeAsAny" +from pydantic import BaseModel, field_validator, ValidationInfo, Field, ConfigDict, SerializeAsAny +from enum import StrEnum +from typing import Union, Any, Optional + +class ForwardCompatibleModel(BaseModel): + """ + Base para todos os modelos que permite campos extras. + O Pydantic V2 gerencia automaticamente o dicionário 'model_extra'. + """ + model_config = ConfigDict(extra='allow') + +class RealmType(StrEnum): + LOGS = "LOGS" + METRICS = "METRICS" + STATUS = "STATUS" + ERRORS = "ERRORS" + OPERATIONAL = "OPERATIONAL" + +class OpStatus(StrEnum): + ACTIVE = "ACTIVE" + OK = "OK" + ERROR = "ERROR" + +class ICLogs(ForwardCompatibleModel): + log_date : str + service_date : str + service : str + versionstr : str + origin : str + enterprise_id : str + enterprise_name : str + message : str + tenant : Optional[str] = Field(default='#', description="Tenant ou '#' para global") + +class ICErrors(ForwardCompatibleModel): + log_date : str + service_date : str + service : str + versionstr : str + origin : str + enterprise_id : str + enterprise_name : str + message : str + tenant : Optional[str] = Field(default='#', description="Tenant ou '#' para global") + +class ICMetrics(ForwardCompatibleModel): + metric_date : str + metric : str + metric_name : str + value : str + unit : str + tenant : Optional[str] = Field(default='#', description="Tenant ou '#' para global") + +class ICOperational(ForwardCompatibleModel): + log_date : str # YYYY-MM-DD HH:MM:S + service_date : str # YYYY-MM-DD HH:MM:S + service_name : str + versionstr : str + origin : str + enterprise_id : str + enterprise_name : str + status : OpStatus + step_status : str + message : str + +class ICStatus(ForwardCompatibleModel): + service : str + versionstr : str + system_name : str + startup_date : str + access_date : str + origin : str + sid : Optional[str] = "" + task_count : Optional[int] = 0 + cpu : Optional[float] = 0.0 + memory : Optional[float] = 0.0 + disk : Optional[float] = 0.0 + network : Optional[float] = 0.0 + +# Modelo polimórfico gerador de metadados JSON para o Exchange de Logs do Sistema Inteirador +class InfoConnector(ForwardCompatibleModel): + realm: RealmType + + # SerializeAsAny garante que se 'messageIC' for uma subclasse (como 'Operation'), + # os campos extras da subclasse (como 'reserva_id') aparecerão no JSON final. + messageIC: SerializeAsAny[Union[ICLogs, ICErrors, ICMetrics, ICStatus, ICOperational]] + + @field_validator('messageIC', mode='before') + @classmethod + def validate_schema(cls, v: Any, info: ValidationInfo) -> Any: + realm = info.data.get("realm") + if realm is None: + # Se realm não existe, o próprio Pydantic lançará erro de validação do campo 'realm' + return v + + # Mapeamento de Realm para Classe Esperada + target_map = { + RealmType.LOGS : ICLogs, + RealmType.ERRORS : ICErrors, + RealmType.METRICS : ICMetrics, + RealmType.STATUS : ICStatus, + RealmType.OPERATIONAL: ICOperational, + } + + expected_class = target_map.get(realm) + if not expected_class: + raise ValueError(f"Invalid realm: {realm}") + + # Se for um dicionário (vindo de um JSON), validamos contra a classe mapeada + if isinstance(v, dict): + return expected_class.model_validate(v) + + # Se já for um objeto instanciado (como no seu Builder), verificamos a herança + if not isinstance(v, expected_class): + raise ValueError(f"Expected {expected_class.__name__} for realm {realm}, got {type(v).__name__}") + + return v + + model_config = ConfigDict( + extra="allow", + frozen=False, + str_strip_whitespace=True + ) \ No newline at end of file diff --git a/tecnospeed_queue/src/utils/redis.py b/tecnospeed_queue/src/utils/redis.py new file mode 100644 index 0000000..a712eb1 --- /dev/null +++ b/tecnospeed_queue/src/utils/redis.py @@ -0,0 +1,107 @@ +import redis + +# Fornece mecanismos de Caching e Persistência Volátil/PubSub na aplicação usando Redis +class RedisClient: + def __init__(self, host='localhost', port=6379, db=0): + """ + Inicializa o cliente Redis com as configurações fornecidas. + """ + self.client = redis.Redis(host=host, port=port, db=db, decode_responses=True) + + def set(self, key, value): + """ + Define um valor para uma chave no Redis. + """ + self.client.set(key, value) + + def setex(self, key, value, ex): + """ + Define um valor para uma chave no Redis com um tempo de expiração em segundos. + """ + self.client.set(key, value, ex) + + def get(self, key): + """ + Obtém o valor de uma chave no Redis. + """ + return self.client.get(key) + + def get_keys(self, pattern="*"): + """ + Obtém todas as chaves que correspondem a um padrão. + """ + return self.client.keys(pattern) + + def get_all(self): + """ + Obtém todas as chaves e seus valores no Redis. + """ + keys = self.client.keys('*') + return {key: self.client.get(key) for key in keys} + + def delete(self, key): + """ + Remove uma chave do Redis. + """ + self.client.delete(key) + + def exists(self, key): + """ + Verifica se uma chave existe no Redis. + Retorna True se a chave existir, caso contrário, False. + """ + return self.client.exists(key) > 0 + + def close(self): + """ + Fecha a conexão com o Redis. + """ + self.client.close() + + def set_expiration(self, key, value, seconds): + """ + Define um valor para uma chave e configura um tempo de expiração em segundos. + """ + self.client.set(key, value, ex=seconds) + + def increment(self, key, amount=1): + """ + Incrementa o valor de uma chave numérica no Redis. + """ + return self.client.incr(key, amount) + + def decrement(self, key, amount=1): + """ + Decrementa o valor de uma chave numérica no Redis. + """ + return self.client.decr(key, amount) + + def hset(self, name, key, value): + """ + Define um campo em um hash no Redis. + """ + self.client.hset(name, key, value) + + def hget(self, name, key): + """ + Obtém o valor de um campo em um hash no Redis. + """ + return self.client.hget(name, key) + + def hgetall(self, name): + """ + Obtém todos os campos e valores de um hash no Redis. + """ + return self.client.hgetall(name) + + def expire(self, key, seconds): + """ + Define um tempo de expiração para uma chave existente. + """ + self.client.expire(key, seconds) + + def ttl(self, key): + """ + Obtém o tempo restante de vida (TTL) de uma chave. + """ + return self.client.ttl(key) \ No newline at end of file diff --git a/tecnospeed_queue/src/webhooks/webhooks.py b/tecnospeed_queue/src/webhooks/webhooks.py new file mode 100644 index 0000000..4106a1c --- /dev/null +++ b/tecnospeed_queue/src/webhooks/webhooks.py @@ -0,0 +1,64 @@ +from utils.api import Api +from utils.banco import Banco +from utils.Exchange2 import LoggerExchange2 +from utils.exchangeModels import RealmType +import json + +# Serviço responsável por vincular rotas (webhooks) por CNPJ onde a PlugNotas enviará notificações +class Webhooks: + def __init__(self, url_base, api_key, banco:Banco, logger: LoggerExchange2 = None): + self.api = Api(api_key) + self.url = f"{url_base}/empresa/" + self.banco = banco + self.logger = logger + + def _log(self, realm: RealmType, msg: str): + if self.logger: + self.logger.logIC2(realm, (msg,)) + + def consultar_webhooks(self, cnpj): + try: + resposta = self.api.get(f"{self.url}{cnpj}/webhook") + self._log(RealmType.OPERATIONAL, f"Webhook consultado - CNPJ: {cnpj}") + return resposta + except Exception as e: + self._log(RealmType.ERRORS, f"Erro ao consultar webhook: {e}") + return {"error": str(e)} + + def criar_webhook(self, cnpj, url): + try: + resposta = self.api.post(f"{self.url}{cnpj}/webhook", {"url": url}) + self._log(RealmType.OPERATIONAL, f"Webhook criado - CNPJ: {cnpj}") + return resposta + except Exception as e: + self._log(RealmType.ERRORS, f"Erro ao criar webhook: {e}") + return {"error": str(e)} + + def deletar_webhook(self, cnpj): + try: + resposta = self.api.delete(f"{self.url}{cnpj}/webhook") + self._log(RealmType.OPERATIONAL, f"Webhook deletado - CNPJ: {cnpj}") + return resposta + except Exception as e: + self._log(RealmType.ERRORS, f"Erro ao deletar webhook: {e}") + return {"error": str(e)} + + def atualizar_webhook(self, cnpj, url): + try: + resposta = self.api.put(f"{self.url}{cnpj}/webhook", {"url": url,"method": "POST"}) + self._log(RealmType.OPERATIONAL, f"Webhook atualizado - CNPJ: {cnpj}") + return resposta + except Exception as e: + self._log(RealmType.ERRORS, f"Erro ao atualizar webhook: {e}") + return {"error": str(e)} + + def testar_webhook(self, cnpj): + try: + resposta = self.api.post(f"{self.url}{cnpj}/webhook/verify", {}) + self._log(RealmType.OPERATIONAL, f"Webhook testado - CNPJ: {cnpj}") + return resposta + except Exception as e: + self._log(RealmType.ERRORS, f"Erro ao testar webhook: {e}") + return {"error": str(e)} + + \ No newline at end of file diff --git a/tecnospeed_rpc/Dockerfile b/tecnospeed_rpc/Dockerfile new file mode 100644 index 0000000..2b08d1d --- /dev/null +++ b/tecnospeed_rpc/Dockerfile @@ -0,0 +1,28 @@ +FROM python:3.12-slim +ARG INSTALL_GCC + +ENV LANG=C.UTF-8 \ + LC_ALL=C.UTF-8 + +RUN apt-get update && apt-get install -y --no-install-recommends \ + locales \ + vim \ + iputils-ping \ + && locale-gen en_US.UTF-8 \ + && if [ "$INSTALL_GCC" = "true" ]; then \ + apt-get install -y --no-install-recommends build-essential curl; \ + fi \ + && apt-get clean && apt-get autoremove -y \ + && rm -rf /var/lib/apt/lists/* + +RUN pip install --no-cache-dir unidecode + +# PARTE ESPECIFICA DA IMAGEM +WORKDIR /app +RUN mkdir /app/logs +COPY requirements.txt ./ +RUN pip install --no-cache-dir -r requirements.txt + +COPY src . + +CMD [ "python", "tecnospeed_rpc.py"] diff --git a/tecnospeed_rpc/requirements.txt b/tecnospeed_rpc/requirements.txt new file mode 100644 index 0000000..cd6db41 --- /dev/null +++ b/tecnospeed_rpc/requirements.txt @@ -0,0 +1,13 @@ +sqlalchemy +pydantic +requests +datetime +redis +pytz +pika +cryptography +psycopg2-binary +fastapi +uvicorn[standard] +pytest +pytest-cov \ No newline at end of file diff --git a/tecnospeed_rpc/src/Models/config.py b/tecnospeed_rpc/src/Models/config.py new file mode 100644 index 0000000..76500ee --- /dev/null +++ b/tecnospeed_rpc/src/Models/config.py @@ -0,0 +1,18 @@ +from pydantic import BaseModel + +# Dados de conexão ao banco de dados isolado do cliente e da API PlugNotas +class Config(BaseModel): + api_key: str | None = None + x_api_key: str | None = None + user_postgres: str + pass_postgres: str + host_postgres: str + port_postgres: str + database_postgres: str + +# Informações de mensageria para a troca de arquivos (AMQP) via subagent +class SystemConfig(BaseModel): + serviceid: str + servicekey: str + origin: str + amqps: str \ No newline at end of file diff --git a/tecnospeed_rpc/src/Models/empresa.py b/tecnospeed_rpc/src/Models/empresa.py new file mode 100644 index 0000000..0aad758 --- /dev/null +++ b/tecnospeed_rpc/src/Models/empresa.py @@ -0,0 +1,97 @@ +from pydantic import BaseModel + + +################### +## POST /empresas # +################### +class Endereco(BaseModel): + codigoPais: str + descricaoPais: str + tipoLogradouro: str | None = None + logradouro: str + numero: str + complemento: str | None = None + tipoBairro: str | None = None + bairro: str + codigoCidade: str + descricaoCidade: str + estado: str + cep: str + +class Rps(BaseModel): + lote: int + numeracao: list + +class Email(BaseModel): + envio: bool + +class Config(BaseModel): + rps: Rps | None = None + email: Email | None = None + producao: bool | None = None + envioLimitado: bool | None = None + integracoes: list | None = None + prefeitura: dict | str | int | None = None + +class Nfse(BaseModel): + config: Config | None = None + ativo: bool = False + tipoContrato: int | None = 0 + +class Dfe(BaseModel): + primeiroNsu: bool + +class ConfigNfe(BaseModel): + dfe: Dfe | None = None + email: Email | None = None + integracoes: dict | None = None + producao: bool | None = None + impressaoFcp: bool | None = None + impressaoPartilha: bool | None = None + versaoManual: str | None = None + versaoEsquema: str | None = None + numeracao: list | None = None + calculoAutomaticoIbpt: bool | None = True + +class Nfe(BaseModel): + config: ConfigNfe | None = None + ativo: bool = False + tipoContrato: int | None = 0 + +class ConfigNfce(BaseModel): + email: Email | None = None + integracoes: dict | None = None + producao: bool | None = None + versaoManual: str | None = None + versaoEsquema: str | None = None + numeracao: list | None = None + +class Nfce(BaseModel): + config: ConfigNfce | None = None + ativo: bool = False + tipoContrato: int | None = 0 + + +# Entidade Empresa que cadastra a credencial emissora no sistema da PlugNotas +class Empresa(BaseModel): + endereco: Endereco + nfse: Nfse | None = None + nfe: Nfe | None = None + nfce: Nfce | None = None + incentivoFiscal: bool + incentivadorCultural: bool + cpfCnpj: str + inscricaoMunicipal: str + inscricaoEstadual: str | None = None + certificado: str + razaoSocial: str + nomeFantasia: str | None = None + simplesNacional: bool + regimeTributario: int + regimeTributarioEspecial: int + id: str | None = None + banco: str | None = None + email: str | None = None + id_empresa: int | None = None + + diff --git a/tecnospeed_rpc/src/Models/nfe.py b/tecnospeed_rpc/src/Models/nfe.py new file mode 100644 index 0000000..84be012 --- /dev/null +++ b/tecnospeed_rpc/src/Models/nfe.py @@ -0,0 +1,115 @@ +from pydantic import BaseModel +from typing import List, Optional, Literal + +EstadoEnum = Literal[ + "AC", "AL", "AM", "AP", "BA", "CE", "DF", "ES", "GO", "MA", "MG", "MS", "MT", + "PA", "PB", "PE", "PI", "PR", "RJ", "RN", "RO", "RR", "RS", "SC", "SE", "SP", "TO" +] + +TipoLogradouroEnum = Literal[ + "Alameda", "Avenida", "Chácara", "Colônia", "Condomínio", "Estância", "Estrada", + "Fazenda", "Praça", "Prolongamento", "Rodovia", "Rua", "Sítio", "Travessa", "Vicinal", "Eqnp" +] + +class Emitente(BaseModel): + cpfCnpj: str + inscricaoEstadual: Optional[str] = None + +class Endereco(BaseModel): + logradouro: str + numero: str + bairro: str + codigoCidade: str + estado: EstadoEnum + cep: str + tipoLogradouro: TipoLogradouroEnum + codigoPais: Optional[str] = "1058" + complemento: Optional[str] = None + descricaoCidade: Optional[str] = None + descricaoPais: Optional[str] = "Brasil" + +class Destinatario(BaseModel): + cpfCnpj: str + razaoSocial: Optional[str] = None + nomeFantasia: Optional[str] = None + email: Optional[str] = None + inscricaoEstadual: Optional[str] = None + inscricaoMunicipal: Optional[str] = None + inscricaoSuframa: Optional[str] = None + orgaoPublico: Optional[bool] = False + endereco: Endereco + +class ValorTributavel(BaseModel): + comercial: float + tributavel: float + +class BaseCalculoIcms(BaseModel): + modalidadeDeterminacao: int + valor: float + +class Icms(BaseModel): + origem: str + cst: str + baseCalculo: BaseCalculoIcms + aliquota: float + valor: float + +class BaseCalculoPis(BaseModel): + valor: float + quantidade: int + +class Pis(BaseModel): + cst: str + baseCalculo: BaseCalculoPis + aliquota: float + valor: float + +class BaseCalculoCofins(BaseModel): + valor: float + +class Cofins(BaseModel): + cst: str + baseCalculo: BaseCalculoCofins + aliquota: float + valor: float + +class Tributos(BaseModel): + icms: Optional[Icms] = None + pis: Optional[Pis] = None + cofins: Optional[Cofins] = None + +class ItemNFe(BaseModel): + codigo: Optional[str] = None + descricao: str + ncm: str + cest: Optional[str] = None + cfop: str + valorUnitario: ValorTributavel + valor: Optional[float] = None + tributos: Tributos + +class Pagamento(BaseModel): + aVista: Optional[bool] = True + meio: str + valor: float + +class Telefone(BaseModel): + ddd: str + numero: str + +class ResponsavelTecnico(BaseModel): + cpfCnpj: str + nome: str + email: str + telefone: Telefone + +class NFe(BaseModel): + idIntegracao: str + presencial: Optional[int] = None + consumidorFinal: Optional[bool] = None + natureza: Optional[str] = None + emitente: Emitente + destinatario: Destinatario + itens: List[ItemNFe] + pagamentos: List[Pagamento] + responsavelTecnico: Optional[ResponsavelTecnico] = None diff --git a/tecnospeed_rpc/src/Models/nfse.py b/tecnospeed_rpc/src/Models/nfse.py new file mode 100644 index 0000000..857ad97 --- /dev/null +++ b/tecnospeed_rpc/src/Models/nfse.py @@ -0,0 +1,49 @@ +from pydantic import BaseModel +from typing import Optional + +class Prestador(BaseModel): + cpfCnpj: str + +class Endereco(BaseModel): + descricaoCidade: str + cep: str + tipoLogradouro: str + logradouro: str + tipoBairro: Optional[str] = None + codigoCidade: str + complemento: Optional[str] = None + estado: str + numero: str + bairro: str + +class Tomador(BaseModel): + cpfCnpj: str + razaoSocial: str + inscricaoMunicipal: Optional[str] = None + email: str + endereco: dict + +class ISS(BaseModel): + tipoTributacao: int + exigibilidade: int + aliquota: float + +class Valor(BaseModel): + servico: float + descontoCondicionado: Optional[float] = 0 + descontoIncondicionado: Optional[float] = 0 + +class Servico(BaseModel): + codigo: str + codigoTributacao: str + discriminacao: str + cnae: str + iss: ISS + valor: Valor + +# Estrutura padrão de envio de uma NFSe contendo Prestador, Tomador e Serviço prestado +class NFSe(BaseModel): + idIntegracao: str + prestador: Prestador + tomador: Tomador + servico: Servico diff --git a/tecnospeed_rpc/src/Models/payload.py b/tecnospeed_rpc/src/Models/payload.py new file mode 100644 index 0000000..841551b --- /dev/null +++ b/tecnospeed_rpc/src/Models/payload.py @@ -0,0 +1,518 @@ +from enum import Enum +from pydantic import BaseModel, ConfigDict, Field + +from Models.config import Config +from Models.empresa import Empresa +from Models.nfe import NFe +from Models.nfse import NFSe +from Models.servico import Servico +from Models.tomador import Tomador + + +class Fila(str, Enum): + NFE = "nfe" + NFSE = "nfse" + CERTIFICADO_DIGITAL = "certificado_digital" + EMPRESA = "empresa" + SERVICO = "servico" + TOMADOR = "tomador" + RELATORIOS = "relatorios" + WEBHOOKS = "webhooks" + + +class NFeMetodo(str, Enum): + POST = "post" + GET = "get" + DOWNLOAD = "download" + DOWNLOAD_XML = "download_xml" + CANCELAR = "cancelar" + STATUS_CANCELAMENTO = "status_cancelamento" + CCE = "cce" + STATUS_CCE = "status_cce" + RESUMO = "resumo" + + +class NFSeMetodo(str, Enum): + POST = "post" + GET = "get" + DOWNLOAD = "download" + DOWNLOAD_XML = "download_xml" + CANCELAR = "cancelar" + STATUS_CANCELAMENTO = "status_cancelamento" + RESUMO = "resumo" + HISTORICO = "historico" + + +class CertificadoMetodo(str, Enum): + POST = "post" + GET = "get" + PUT = "put" + + +class EmpresaMetodo(str, Enum): + POST = "post" + GET = "get" + POST_FILE = "post_file" + PATCH = "patch" + + +class ServicoMetodo(str, Enum): + POST = "post" + GET = "get" + PUT = "put" + DELETE = "delete" + + +class TomadorMetodo(str, Enum): + POST = "post" + GET = "get" + PUT = "put" + DELETE = "delete" + + +class RelatorioMetodo(str, Enum): + NFE_EMITIDAS = "nfe_emitidas" + NFE_POR_CNPJ = "nfe_por_cnpj" + NFSE_EMITIDAS = "nfse_emitidas" + NFSE_POR_CNPJ = "nfse_por_cnpj" + + +class WebhookMetodo(str, Enum): + GET = "get" + POST = "post" + DELETE = "delete" + PUT = "put" + TEST = "test" + + +class BaseEnvelope(BaseModel): + cliente: str | None = None + config: Config + url_base: str | None = None + + +# --- Base Data Models --- + +class BaseDados(BaseModel): + model_config = ConfigDict(populate_by_name=True) + id_usuario: int | None = 1 + + +class NFePostDadosWrapped(BaseDados): + nfe: NFe + id_empresa: str | None = None + + +class NFePostDadosDirect(NFe): + id_usuario: int | None = 1 + id_empresa: str | None = None + + +class NFeIdDados(BaseDados): + id: str + + +class NFeCancelarDados(NFeIdDados): + justificativa: str | None = None + + +class NFeCCEDados(NFeIdDados): + correcao: str + + +class NFeResumoDados(BaseDados): + cnpj: str + id_integracao: str | None = None + id: str | None = None + + +class NFSePostDados(NFSe): + id_usuario: int | None = 1 + id_empresa: str | None = None + + +class NFSeIdDados(BaseDados): + id: str + + +class NFSeGetDados(BaseDados): + id: str | None = None + + +class NFSeResumoDados(NFSeIdDados): + cnpj: str + + +class NFSeHistoricoDados(NFSeIdDados): + cnpj: str | None = None + + +class CertificadoPostDados(BaseDados): + arquivo: str + senha: str + id_empresa: str | None = None + email: str | None = None + expiration_at: str | None = Field(None, alias="expirationAt") + data_expiracao: str | None = None + + +class CertificadoGetDados(BaseDados): + id: str | None = None + + +class CertificadoPutDados(CertificadoPostDados): + id: str + + +class EmpresaPostDados(Empresa): + id_usuario: int | None = 1 + + +class EmpresaGetDados(BaseDados): + cnpj: str | None = None + + +class EmpresaPostFileDados(BaseDados): + file: str + cnpj: str + + +class EmpresaPatchDados(BaseDados): + cnpj: str + certificado: str + + +class ServicoPostDados(Servico): + id_usuario: int | None = 1 + + +class ServicoGetDados(BaseDados): + id: str | None = None + + +class ServicoPutDados(Servico): + id: str + id_usuario: int | None = 1 + + +class ServicoDeleteDados(BaseDados): + id: str + + +class TomadorPostDados(Tomador): + id_usuario: int | None = 1 + + +class TomadorGetDados(BaseDados): + cpf_cnpj: str | None = None + + +class TomadorPutDados(Tomador): + cpf_cnpj: str + id_usuario: int | None = 1 + + +class TomadorDeleteDados(BaseDados): + cpf_cnpj: str + + +class RelatorioVazioDados(BaseDados): + pass + + +class RelatorioCnpjDados(BaseDados): + cnpj: str + + +class RelatorioPeriodoDados(BaseDados): + data_inicial: str | None = None + data_final: str | None = None + + +class RelatorioCnpjPeriodoDados(RelatorioPeriodoDados): + cnpj: str + + +class WebhookGetDeleteTestDados(BaseDados): + cnpj: str + + +class WebhookPostPutDados(WebhookGetDeleteTestDados): + url: str + + +# --- Envelopes --- + +class NFePostEnvelope(BaseEnvelope): + fila: Fila = Fila.NFE + metodo: NFeMetodo = NFeMetodo.POST + dados: NFePostDadosDirect | NFePostDadosWrapped + + +class NFeIdEnvelope(BaseEnvelope): + fila: Fila = Fila.NFE + metodo: NFeMetodo + dados: NFeIdDados + + +class NFeCancelarEnvelope(BaseEnvelope): + fila: Fila = Fila.NFE + metodo: NFeMetodo = NFeMetodo.CANCELAR + dados: NFeCancelarDados + + +class NFeCCEEnvelope(BaseEnvelope): + fila: Fila = Fila.NFE + metodo: NFeMetodo = NFeMetodo.CCE + dados: NFeCCEDados + + +class NFeResumoEnvelope(BaseEnvelope): + fila: Fila = Fila.NFE + metodo: NFeMetodo = NFeMetodo.RESUMO + dados: NFeResumoDados + + +class NFSePostEnvelope(BaseEnvelope): + fila: Fila = Fila.NFSE + metodo: NFSeMetodo = NFSeMetodo.POST + dados: NFSePostDados + + +class NFSeGetEnvelope(BaseEnvelope): + fila: Fila = Fila.NFSE + metodo: NFSeMetodo = NFSeMetodo.GET + dados: NFSeGetDados + + +class NFSeIdEnvelope(BaseEnvelope): + fila: Fila = Fila.NFSE + metodo: NFSeMetodo + dados: NFSeIdDados + + +class NFSeResumoEnvelope(BaseEnvelope): + fila: Fila = Fila.NFSE + metodo: NFSeMetodo = NFSeMetodo.RESUMO + dados: NFSeResumoDados + + +class NFSeHistoricoEnvelope(BaseEnvelope): + fila: Fila = Fila.NFSE + metodo: NFSeMetodo = NFSeMetodo.HISTORICO + dados: NFSeHistoricoDados + + +class CertificadoPostEnvelope(BaseEnvelope): + fila: Fila = Fila.CERTIFICADO_DIGITAL + metodo: CertificadoMetodo = CertificadoMetodo.POST + dados: CertificadoPostDados + + +class CertificadoGetEnvelope(BaseEnvelope): + fila: Fila = Fila.CERTIFICADO_DIGITAL + metodo: CertificadoMetodo = CertificadoMetodo.GET + dados: CertificadoGetDados + + +class CertificadoPutEnvelope(BaseEnvelope): + fila: Fila = Fila.CERTIFICADO_DIGITAL + metodo: CertificadoMetodo = CertificadoMetodo.PUT + dados: CertificadoPutDados + + +class EmpresaPostEnvelope(BaseEnvelope): + fila: Fila = Fila.EMPRESA + metodo: EmpresaMetodo = EmpresaMetodo.POST + dados: EmpresaPostDados + + +class EmpresaGetEnvelope(BaseEnvelope): + fila: Fila = Fila.EMPRESA + metodo: EmpresaMetodo = EmpresaMetodo.GET + dados: EmpresaGetDados + + +class EmpresaPostFileEnvelope(BaseEnvelope): + fila: Fila = Fila.EMPRESA + metodo: EmpresaMetodo = EmpresaMetodo.POST_FILE + dados: EmpresaPostFileDados + + +class EmpresaPatchEnvelope(BaseEnvelope): + fila: Fila = Fila.EMPRESA + metodo: EmpresaMetodo = EmpresaMetodo.PATCH + dados: EmpresaPatchDados + + +class ServicoPostEnvelope(BaseEnvelope): + fila: Fila = Fila.SERVICO + metodo: ServicoMetodo = ServicoMetodo.POST + dados: ServicoPostDados + + +class ServicoGetEnvelope(BaseEnvelope): + fila: Fila = Fila.SERVICO + metodo: ServicoMetodo = ServicoMetodo.GET + dados: ServicoGetDados + + +class ServicoPutEnvelope(BaseEnvelope): + fila: Fila = Fila.SERVICO + metodo: ServicoMetodo = ServicoMetodo.PUT + dados: ServicoPutDados + + +class ServicoDeleteEnvelope(BaseEnvelope): + fila: Fila = Fila.SERVICO + metodo: ServicoMetodo = ServicoMetodo.DELETE + dados: ServicoDeleteDados + + +class TomadorPostEnvelope(BaseEnvelope): + fila: Fila = Fila.TOMADOR + metodo: TomadorMetodo = TomadorMetodo.POST + dados: TomadorPostDados + + +class TomadorGetEnvelope(BaseEnvelope): + fila: Fila = Fila.TOMADOR + metodo: TomadorMetodo = TomadorMetodo.GET + dados: TomadorGetDados + + +class TomadorPutEnvelope(BaseEnvelope): + fila: Fila = Fila.TOMADOR + metodo: TomadorMetodo = TomadorMetodo.PUT + dados: TomadorPutDados + + +class TomadorDeleteEnvelope(BaseEnvelope): + fila: Fila = Fila.TOMADOR + metodo: TomadorMetodo = TomadorMetodo.DELETE + dados: TomadorDeleteDados + + +class RelatorioNFeEmitidasEnvelope(BaseEnvelope): + fila: Fila = Fila.RELATORIOS + metodo: RelatorioMetodo = RelatorioMetodo.NFE_EMITIDAS + dados: RelatorioVazioDados + + +class RelatorioNFeCnpjEnvelope(BaseEnvelope): + fila: Fila = Fila.RELATORIOS + metodo: RelatorioMetodo = RelatorioMetodo.NFE_POR_CNPJ + dados: RelatorioCnpjDados + + +class RelatorioNFSeEmitidasEnvelope(BaseEnvelope): + fila: Fila = Fila.RELATORIOS + metodo: RelatorioMetodo = RelatorioMetodo.NFSE_EMITIDAS + dados: RelatorioPeriodoDados + + +class RelatorioNFSeCnpjEnvelope(BaseEnvelope): + fila: Fila = Fila.RELATORIOS + metodo: RelatorioMetodo = RelatorioMetodo.NFSE_POR_CNPJ + dados: RelatorioCnpjPeriodoDados + + +class WebhookGetDeleteTestEnvelope(BaseEnvelope): + fila: Fila = Fila.WEBHOOKS + metodo: WebhookMetodo + dados: WebhookGetDeleteTestDados + + +class WebhookPostPutEnvelope(BaseEnvelope): + fila: Fila = Fila.WEBHOOKS + metodo: WebhookMetodo + dados: WebhookPostPutDados + + +# --- Generic Envelope for basic structural validation --- + +class GenericEnvelope(BaseEnvelope): + """Validates that minimum required fields exist before routing.""" + fila: Fila + metodo: str + dados: dict | None = None + + +# --- Payload Validation Router --- + +# Maps (fila, metodo) -> Envelope class +_ENVELOPE_MAP: dict[tuple[str, str], type[BaseEnvelope]] = { + # NFe + ("nfe", "post"): NFePostEnvelope, + ("nfe", "get"): NFeIdEnvelope, + ("nfe", "download"): NFeIdEnvelope, + ("nfe", "download_xml"): NFeIdEnvelope, + ("nfe", "cancelar"): NFeCancelarEnvelope, + ("nfe", "status_cancelamento"): NFeIdEnvelope, + ("nfe", "cce"): NFeCCEEnvelope, + ("nfe", "status_cce"): NFeIdEnvelope, + ("nfe", "resumo"): NFeResumoEnvelope, + # NFSe + ("nfse", "post"): NFSePostEnvelope, + ("nfse", "get"): NFSeGetEnvelope, + ("nfse", "download"): NFSeIdEnvelope, + ("nfse", "download_xml"): NFSeIdEnvelope, + ("nfse", "cancelar"): NFSeIdEnvelope, + ("nfse", "status_cancelamento"): NFSeIdEnvelope, + ("nfse", "resumo"): NFSeResumoEnvelope, + ("nfse", "historico"): NFSeHistoricoEnvelope, + # Certificado Digital + ("certificado_digital", "post"): CertificadoPostEnvelope, + ("certificado_digital", "get"): CertificadoGetEnvelope, + ("certificado_digital", "put"): CertificadoPutEnvelope, + # Empresa + ("empresa", "post"): EmpresaPostEnvelope, + ("empresa", "get"): EmpresaGetEnvelope, + ("empresa", "post_file"): EmpresaPostFileEnvelope, + ("empresa", "patch"): EmpresaPatchEnvelope, + # Serviço + ("servico", "post"): ServicoPostEnvelope, + ("servico", "get"): ServicoGetEnvelope, + ("servico", "put"): ServicoPutEnvelope, + ("servico", "delete"): ServicoDeleteEnvelope, + # Tomador + ("tomador", "post"): TomadorPostEnvelope, + ("tomador", "get"): TomadorGetEnvelope, + ("tomador", "put"): TomadorPutEnvelope, + ("tomador", "delete"): TomadorDeleteEnvelope, + # Relatórios + ("relatorios", "nfe_emitidas"): RelatorioNFeEmitidasEnvelope, + ("relatorios", "nfe_por_cnpj"): RelatorioNFeCnpjEnvelope, + ("relatorios", "nfse_emitidas"): RelatorioNFSeEmitidasEnvelope, + ("relatorios", "nfse_por_cnpj"): RelatorioNFSeCnpjEnvelope, + # Webhooks + ("webhooks", "get"): WebhookGetDeleteTestEnvelope, + ("webhooks", "post"): WebhookPostPutEnvelope, + ("webhooks", "delete"): WebhookGetDeleteTestEnvelope, + ("webhooks", "put"): WebhookPostPutEnvelope, + ("webhooks", "test"): WebhookGetDeleteTestEnvelope, +} + + +def validate_payload(payload_dict: dict) -> BaseEnvelope: + # Step 1: Basic structural validation + generic = GenericEnvelope(**payload_dict) + + fila = generic.fila.value + metodo = generic.metodo.lower() + + # Step 2: Find specific envelope + key = (fila, metodo) + envelope_cls = _ENVELOPE_MAP.get(key) + + if envelope_cls is None: + raise ValueError(f"Combinação fila/metodo desconhecida: {fila}/{metodo}") + + # Step 3: Normalize metodo in the dict to match enum lowercase values + payload_dict['metodo'] = metodo + + # Step 4: Full typed validation + return envelope_cls(**payload_dict) + + + diff --git a/tecnospeed_rpc/src/Models/servico.py b/tecnospeed_rpc/src/Models/servico.py new file mode 100644 index 0000000..afb717f --- /dev/null +++ b/tecnospeed_rpc/src/Models/servico.py @@ -0,0 +1,150 @@ +from pydantic import BaseModel +from enum import IntEnum + +class TipoTributacao(IntEnum): + NAO_DEFINIDO = 0 + ISENTO_ISS = 1 + IMUNE = 2 + NAO_INCIDENCIA = 3 + NAO_TRIBUTAVEL = 4 + RETIDO = 5 + TRIBUTAVEL_MUNICIPIO = 6 + TRIBUTAVEL_FORA_MUNICIPIO = 7 + TRIBUTAVEL_MUNICIPIO_TOMADOR = 8 + +class TipoExigibilidade(IntEnum): + EXIGIVEL = 1 + NAO_INCIDENCIA = 2 + ISENCAO = 3 + EXPORTACAO = 4 + IMUNIDADE = 5 + SUSPENSO_ACAO_JUDICIAL = 6 + SUSPENSO_ACAO_ADMINISTRATIVA = 7 + +class ISS(BaseModel): + tipoTributacao: int #TipoTributacao + exigibilidade: int #TipoExigibilidade + retido: bool | None = None # Se o ISS foi retido na fonte + aliquota: float # Aliquota do ISS + valor: float # Valor do ISS + valorRetido: float # Valor do ISS retido + processoSuspensao: str # Numero processo de suspensão do ISS +############################################################################## +# tipoTributacao integer +# Default: 6 +# Enum: 0 1 2 3 4 5 6 7 8 +# Tipo de Tributação do Serviço, valores: +# 0 - Não definido +# 1 - Isento de ISS +# 2 - Imune +# 3 - Não Incidência no Município +# 4 - Não Tributável +# 5 - Retido +# 6 - Tributável Dentro do Município +# 7 - Tributável Fora do Município +# 8 - Tributável Dentro do Município pelo tomador +############################################################################## +# exigibilidade integer +# Default: 1 +# Enum: 1 2 3 4 5 6 7 +# Exigibilidade do ISS, valores: +# 1 - Exigível +# 2 - Não Incidência +# 3 - Isenção +# 4 - Exportação +# 5 - Imunidade +# 6 - Supenso por Ação Judicial +# 7 - Suspenso por Ação Administrativa +############################################################################## +# responsavelRetencao string +# Valores aceitos: +# 1 - Tomador +# 2 - Intermediário +############################################################################## + +class Obra(BaseModel): + art: str # Anotação de Responsabilidade Técnica (Codigo ART) + codigo: str # Código da obra + +class Valor(BaseModel): + servico: float # Valor bruto do serviço prestado. + baseCalculo: float # Valor da base de cálculo dos impostos. + deducoes: float # Valor deduções. + descontoCondicionado: float # Valor do desconto condicionado. + descontoIncondicionado: float # Valor do desconto incondicionado. + liquido: float # Valor liquido do serviço, caso você não informe esse campo, vamos calcular automáticamente. + unitario: float # Valor unitário do serviço. + valorAproximadoTributos: float # Valor aproximado dos tributos. + +class Deducao(BaseModel): + tipo: int + descricao: str # Descrição da dedução +############################################################################## +# tipo integer +# Tipo de dedução: +# 0 - Sem Deduções +# 1 - Materiais +# 2 - Subempreitada de Mão de Obra +# 3 - Serviços +# 4 - Produção Externa +# 5 - Alimentação e Bebidas +# 6 - Reembolso de Despesas +# 7 - Repasse Consorciado +# 8 - Repasse Plano de Saúde +# 99 - Outras Deduções +############################################################################## + +class RetencaoInterno(BaseModel): + aliquota: float # Aliquota da retenção + valor: float # Valor da retenção + +class Retencao(BaseModel): + pis: RetencaoInterno + cofins: RetencaoInterno + csll: RetencaoInterno + inss: RetencaoInterno + irrf: RetencaoInterno + outrasRetencoes: float # Valor de outras retenções + cpp: RetencaoInterno + +class Aliquota(BaseModel): + municipal: float # Aliquota municipal + estadual: float # Aliquota estadual + federal: float # Aliquota federal + +class Detalhado(BaseModel): + aliquota: Aliquota + +class Simplificado(BaseModel): + aliquota: float # Aliquota geral de impostos + +class IBPT(BaseModel): + simplificado: Simplificado + detalhado: Detalhado + +# Representa o detalhamento do serviço prestado na NFSe, impostos incidentes e deduções +class Servico(BaseModel): + codigo: str # Código de serviço, também conhecido como Código Item Lista Serviço, geralmente segue a LC116, podendo váriar de acordo com a prefeitura.Você pode encontrar esse dado no portal da prefeitura, em uma nota emitida, ou junto ao contador. + idIntegracao: str # Código EXTERNO de integração do serviço. Utilizado SOMENTE quando se cadastra previamente um serviço. + discriminacao: str # Descrição do serviço prestado + codigoTributacao: str # Código tributação no Município + cnae: str # O código CNAE ou, por extenso, Classificação Nacional de Atividades Econômicas, é uma forma de padronizar, em todo o território nacional, os códigos de atividades econômicas e os critérios de enquadramento usados pelos mais diversos órgãos da administração tributária do Brasil. + codigoCidadeIncidencia: str # Código IBGE da cidade de incidência do ISS. + descricaoCidadeIncidencia: str # Descrição da cidade de incidência do ISS. + unidade: str # Unidade do serviço prestado. + quantidade: int # Quantidade dos serviços prestados. + iss: ISS + obra: Obra + valor: Valor + deducao: Deducao + retencao: Retencao + tributavel: bool # Indica se o serviço está sujeito a tributação. + ibpt: IBPT + responsavelRetencao: str + +############################################################################## +# responsavelRetencao Valores aceitos: +# 1 - Tomador +# 2 - Intermediário +# Caso o responsável pela retenção seja o próprio prestador, não gerar o campo. +############################################################################## diff --git a/tecnospeed_rpc/src/Models/tomador.py b/tecnospeed_rpc/src/Models/tomador.py new file mode 100644 index 0000000..4e3cf5a --- /dev/null +++ b/tecnospeed_rpc/src/Models/tomador.py @@ -0,0 +1,34 @@ +from pydantic import BaseModel + + +class Endereco(BaseModel): + tipoLogradouro: str + logradouro: str + numero: str + complemento: str + tipoBairro: str + bairro: str + codigoPais: str + descricaoPais: str + codigoCidade: str + descricaoCidade: str + estado: str + cep: str + +class Telefone(BaseModel): + ddd: str + numero: str + +# Representa o cliente (Tomador do Serviço) vinculado a uma nota fiscal de serviço +class Tomador(BaseModel): + cpfCnpj: str + inscricaoMunicipal: str + inscricaoEstadual: str + inscricaoSuframa: str + indicadorInscricaoEstadual: int + razaoSocial: str + nomeFantasia: str + endereco: Endereco + telefone: Telefone + email: str + orgaoPublico: bool diff --git a/tecnospeed_rpc/src/NFSe/Servico/servico.py b/tecnospeed_rpc/src/NFSe/Servico/servico.py new file mode 100644 index 0000000..4787089 --- /dev/null +++ b/tecnospeed_rpc/src/NFSe/Servico/servico.py @@ -0,0 +1,105 @@ +from Models.servico import Servico +from utils.api import Api +from utils.banco import Banco, ServicoTable +from utils.Exchange2 import LoggerExchange2 +from utils.exchangeModels import RealmType +from sqlalchemy.exc import SQLAlchemyError +import json + +# Controller responsável pelos cadastros e busca de Serviços a serem vinculados em NFSe +class ServicoController: + def __init__(self, url_base, api_key, banco: Banco, logger: LoggerExchange2 = None): + self.api = Api(api_key) + self.url = f"{url_base}/nfse/servico" + self.banco = banco + self.logger = logger + + def _log(self, realm: RealmType, msg: str): + if self.logger: + self.logger.logIC2(realm, (msg,)) + + def cadastrar_servico(self, servico: Servico, data: dict): + try: + resposta = self.api.post(self.url, servico.model_dump()) + if 'error' in resposta: + self._log(RealmType.ERRORS, f"Erro na API ao cadastrar serviço") + return resposta + + servico_novo = ServicoTable( + descricao=servico.discriminacao, + id_tecnospeed=resposta['data']['id'], + id_usuario=data.get('id_usuario', 1), + status=True + ) + self.banco.add(servico_novo) + self._log(RealmType.OPERATIONAL, f"Serviço cadastrado com sucesso - ID: {resposta['data']['id']}") + return resposta + except SQLAlchemyError as e: + self.banco.rollback() + self._log(RealmType.ERRORS, f"Erro no banco ao cadastrar serviço: {e}") + return {"error": f"Erro ao cadastrar serviço: {str(e)}"} + except Exception as e: + return {"error": f"Erro inesperado: {str(e)}"} + + def buscar_servico(self, id_servico, data: dict = None): + try: + resposta = self.api.get(f"{self.url}/{id_servico}") + self._log(RealmType.OPERATIONAL, f"Serviço consultado - ID: {id_servico}") + return resposta + except Exception as e: + self._log(RealmType.ERRORS, f"Erro ao buscar serviço: {e}") + return {"error": f"Erro ao buscar serviço: {str(e)}"} + + def listar_servicos(self, data: dict = None): + try: + servicos = self.banco.session.query(ServicoTable.id_tecnospeed, ServicoTable.descricao, ServicoTable.status).all() + resultado = [{"id_tecnospeed": servico.id_tecnospeed, "descricao": servico.descricao, "status": servico.status} for servico in servicos] + self._log(RealmType.OPERATIONAL, f"Listagem de serviços - {len(resultado)} registros") + return resultado + except SQLAlchemyError as e: + self._log(RealmType.ERRORS, f"Erro ao listar serviços: {e}") + return {"error": f"Erro ao listar serviços: {str(e)}"} + except Exception as e: + return {"error": f"Erro inesperado: {str(e)}"} + + def atualizar_servico(self, id_servico, servico: Servico): + try: + resposta = self.api.patch(f"{self.url}/{id_servico}", servico.model_dump()) + servico_existente = self.banco.session.query(ServicoTable).filter_by(id=id_servico).first() + + if not servico_existente: + self._log(RealmType.ERRORS, f"Serviço não encontrado - ID: {id_servico}") + return {"error": "Serviço não encontrado"} + + servico_existente.descricao = servico.discriminacao + self.banco.session.commit() + + self._log(RealmType.OPERATIONAL, f"Serviço atualizado com sucesso - ID: {id_servico}") + return resposta + except SQLAlchemyError as e: + self.banco.rollback() + self._log(RealmType.ERRORS, f"Erro ao atualizar serviço: {e}") + return {"error": f"Erro ao atualizar serviço: {str(e)}"} + except Exception as e: + return {"error": f"Erro inesperado: {str(e)}"} + + def deletar_servico(self, id_servico): + try: + resposta = self.api.delete(f"{self.url}/{id_servico}") + servico_existente = self.banco.session.query(ServicoTable).filter_by(id=id_servico).first() + + if not servico_existente: + self._log(RealmType.ERRORS, f"Serviço não encontrado para deletar - ID: {id_servico}") + return {"error": "Serviço não encontrado"} + + self.banco.session.delete(servico_existente) + self.banco.session.commit() + + self._log(RealmType.OPERATIONAL, f"Serviço deletado com sucesso - ID: {id_servico}") + return resposta + except SQLAlchemyError as e: + self.banco.rollback() + self._log(RealmType.ERRORS, f"Erro ao deletar serviço: {e}") + return {"error": f"Erro ao deletar serviço: {str(e)}"} + except Exception as e: + return {"error": f"Erro inesperado: {str(e)}"} \ No newline at end of file diff --git a/tecnospeed_rpc/src/NFSe/Tomador/tomador.py b/tecnospeed_rpc/src/NFSe/Tomador/tomador.py new file mode 100644 index 0000000..e10cc78 --- /dev/null +++ b/tecnospeed_rpc/src/NFSe/Tomador/tomador.py @@ -0,0 +1,91 @@ +from utils.api import Api +from utils.banco import Banco, TomadorTable +from utils.Exchange2 import LoggerExchange2 +from utils.exchangeModels import RealmType +from Models.tomador import Tomador + +import json + +# Controller para gerenciamento no banco e na API dos Tomadores (clientes finais) das notas +class TomadorController: + def __init__(self, url_base, api_key, banco: Banco, logger: LoggerExchange2 = None): + self.api = Api(api_key) + self.url = f"{url_base}/nfse/tomador" + self.banco = banco + self.logger = logger + + def _log(self, realm: RealmType, msg: str): + if self.logger: + self.logger.logIC2(realm, (msg,)) + + def cadastrar_tomador(self, tomador: Tomador, data: dict): + try: + retorno = self.api.post(self.url, tomador.model_dump()) + if 'error' in retorno: + self._log(RealmType.ERRORS, f"Erro na API ao cadastrar tomador") + return retorno + + tomador_novo = TomadorTable( + id_tecnospeed=retorno['id'], + nome=tomador.razaoSocial, + cpf_cnpj=tomador.cpfCnpj, + email=tomador.email, + telefone=f"({tomador.telefone.ddd}) {tomador.telefone.numero}", + id_usuario=data.get('id_usuario', 1), + status=True + ) + + self.banco.add(tomador_novo) + self._log(RealmType.OPERATIONAL, f"Tomador cadastrado com sucesso - CPF/CNPJ: {tomador.cpfCnpj}") + return retorno + except Exception as e: + self._log(RealmType.ERRORS, f"Erro ao cadastrar tomador: {e}") + return {"error": str(e)} + + def buscar_tomador(self, cpf_cnpj): + try: + tomador = self.api.get(f"{self.url}/{cpf_cnpj}") + self._log(RealmType.OPERATIONAL, f"Tomador consultado - CPF/CNPJ: {cpf_cnpj}") + return tomador + except Exception as e: + self._log(RealmType.ERRORS, f"Erro ao buscar tomador: {e}") + return {"error": str(e)} + + def atualizar_tomador(self, cpf_cnpj, tomador: Tomador): + try: + tomador_existente = self.banco.session.query(TomadorTable).filter_by(cpf_cnpj=cpf_cnpj).first() + if not tomador_existente: + self._log(RealmType.ERRORS, f"Tomador não encontrado - CPF/CNPJ: {cpf_cnpj}") + return {"error": "Tomador não encontrado"} + + resposta = self.api.patch(f"{self.url}/{cpf_cnpj}", tomador.model_dump()) + tomador_existente.nome = tomador.razaoSocial + tomador_existente.email = tomador.email + tomador_existente.telefone = f"({tomador.telefone.ddd}) {tomador.telefone.numero}" + + self.banco.session.commit() + self._log(RealmType.OPERATIONAL, f"Tomador atualizado com sucesso - CPF/CNPJ: {cpf_cnpj}") + return resposta + except Exception as e: + self.banco.session.rollback() + self._log(RealmType.ERRORS, f"Erro ao atualizar tomador: {e}") + return {"error": str(e)} + + def deletar_tomador(self, cpf_cnpj): + try: + tomador_existente = self.banco.session.query(TomadorTable).filter_by(cpf_cnpj=cpf_cnpj).first() + if not tomador_existente: + self._log(RealmType.ERRORS, f"Tomador não encontrado para deletar - CPF/CNPJ: {cpf_cnpj}") + return {"error": "Tomador não encontrado"} + + resposta = self.api.delete(f"{self.url}/{cpf_cnpj}") + self.banco.session.delete(tomador_existente) + self.banco.session.commit() + + self._log(RealmType.OPERATIONAL, f"Tomador deletado com sucesso - CPF/CNPJ: {cpf_cnpj}") + return resposta + except Exception as e: + self.banco.session.rollback() + self._log(RealmType.ERRORS, f"Erro ao deletar tomador: {e}") + return {"error": str(e)} + diff --git a/tecnospeed_rpc/src/NFSe/nfse.py b/tecnospeed_rpc/src/NFSe/nfse.py new file mode 100644 index 0000000..cd432e7 --- /dev/null +++ b/tecnospeed_rpc/src/NFSe/nfse.py @@ -0,0 +1,167 @@ +from Models.nfse import NFSe +from utils.api import Api +from utils.banco import Banco, NFSeTable +from utils.Exchange2 import LoggerExchange2 +from utils.exchangeModels import RealmType +from utils.enviaListerner import enviaListerner + +import json + +# Serviço responsável pelo controle, emissão e cancelamento das Notas Fiscais de Serviço Eletrônicas (NFSe) +class NFSeService: + def __init__(self, api_key, url_base, banco: Banco, logger: LoggerExchange2 = None): + self.api = Api(api_key) + self.url = f"{url_base}/nfse" + self.banco = banco + self.logger = logger + self.notificador = enviaListerner(logger=logger) if logger else None + + def _log(self, realm: RealmType, msg: str): + if self.logger: + self.logger.logIC2(realm, (msg,)) + + def cadastrar_nfse(self, nfse: NFSe, data: dict): + try: + id_empresa = data.get('id_empresa', "0") + resposta = self.api.post(self.url, [nfse.model_dump()]) + if 'error' in resposta: + self._log(RealmType.ERRORS, f"Erro na API ao cadastrar NFSe: {resposta.get('error')}") + return resposta + + # A API da Plugnotas retorna 'documents' contendo uma lista com os sucessos + documentos = resposta.get('documents', []) + if not documentos: + self._log(RealmType.ERRORS, f"Nenhum documento retornado pela API. Resposta: {resposta}") + return resposta + + id_recebido = documentos[0].get('id') + if not id_recebido: + raise Exception("ID do documento não encontrado na resposta.") + + nfse_nova = NFSeTable( + id_tecnospeed=id_recebido, + id_empresa=id_empresa, + id_usuario=data.get('id_usuario', 1), + status=True + ) + + self.banco.add(nfse_nova) + self._log(RealmType.OPERATIONAL, f"NFSe cadastrada com sucesso - ID: {id_recebido}") + + # Notifica o listener interno para sincronização imediata + if self.notificador: + self.notificador.notificar_emissao(id_tecnospeed=id_recebido, documento="nfse", cnpj=id_empresa) + + return resposta + except Exception as e: + self._log(RealmType.ERRORS, f"Erro ao cadastrar NFSe: {e}") + raise Exception(f"Erro ao cadastrar NFSe: {e}") + + def buscar_nfse(self, id_nfse): + try: + resposta = self.api.get(f"{self.url}/{id_nfse}") + self._log(RealmType.OPERATIONAL, f"NFSe consultada com sucesso - ID: {id_nfse}") + return resposta + except Exception as e: + self._log(RealmType.ERRORS, f"Erro ao buscar NFSe: {e}") + raise Exception(f"Erro ao buscar NFSe: {e}") + + def listar_nfse(self): + try: + nfse = self.banco.session.query(NFSeTable.id, NFSeTable.id_tecnospeed).all() + resultado = [{"id": n[0], "descricao": n[1]} for n in nfse] + self._log(RealmType.OPERATIONAL, f"Listagem de NFSe - {len(resultado)} registros") + return resultado + except Exception as e: + self._log(RealmType.ERRORS, f"Erro ao listar NFSe: {e}") + raise Exception(f"Erro ao listar NFSe: {e}") + + def baixar_nfse(self, id_nfse): + try: + resposta = self.api.get(f"{self.url}/pdf/{id_nfse}") + self._log(RealmType.OPERATIONAL, f"PDF NFSe baixado com sucesso - ID: {id_nfse}") + return resposta + except Exception as e: + self._log(RealmType.ERRORS, f"Erro ao baixar NFSe: {e}") + raise Exception(f"Erro ao baixar NFSe: {e}") + + def baixar_xml_nfse(self, id_nfse): + try: + resposta = self.api.get(f"{self.url}/xml/{id_nfse}") + self._log(RealmType.OPERATIONAL, f"XML NFSe baixado com sucesso - ID: {id_nfse}") + return resposta + except Exception as e: + self._log(RealmType.ERRORS, f"Erro ao baixar XML NFSe: {e}") + raise Exception(f"Erro ao baixar XML NFSe: {e}") + + def solicitar_cancelamento_nfse(self, id_nfse): + try: + resposta = self.api.post(f"{self.url}/cancelar/{id_nfse}", {}) + self._log(RealmType.OPERATIONAL, f"Cancelamento NFSe solicitado - ID: {id_nfse}") + return resposta + except Exception as e: + self._log(RealmType.ERRORS, f"Erro ao solicitar cancelamento NFSe: {e}") + raise Exception(f"Erro ao solicitar cancelamento NFSe: {e}") + + def consultar_status_cancelamento_nfse(self, id_nfse): + try: + resposta = self.api.get(f"{self.url}/cancelar/status/{id_nfse}") + self._log(RealmType.OPERATIONAL, f"Status cancelamento NFSe consultado - ID: {id_nfse}") + return resposta + except Exception as e: + self._log(RealmType.ERRORS, f"Erro ao consultar status cancelamento NFSe: {e}") + raise Exception(f"Erro ao consultar status cancelamento NFSe: {e}") + + def consultar_resumo_nfse(self, id_nfse, cnpj): + try: + resposta = self.api.get(f"{self.url}/consultar/{id_nfse}/{cnpj}") + self._log(RealmType.OPERATIONAL, f"Resumo NFSe consultado - ID: {id_nfse}") + return resposta + except Exception as e: + self._log(RealmType.ERRORS, f"Erro ao consultar resumo NFSe: {e}") + raise Exception(f"Erro ao consultar resumo NFSe: {e}") + + def baixar_histotico_nfse(self, id_nfse, cnpj = None): + try: + resposta = self.api.get(f"{self.url}/historico/{id_nfse}") if cnpj is None else self.api.get(f"{self.url}/historico/{id_nfse}/{cnpj}") + self._log(RealmType.OPERATIONAL, f"Histórico NFSe baixado - ID: {id_nfse}") + return resposta + except Exception as e: + self._log(RealmType.ERRORS, f"Erro ao baixar histórico NFSe: {e}") + raise Exception(f"Erro ao baixar histórico NFSe: {e}") + + def enviar_email_nfse(self, id_nfse, email:list[str], reenvio:bool = False): + try: + resposta = self.api.post(f"{self.url}/email/{id_nfse}", {"reenvio": reenvio,"destinatarios": email}) + self._log(RealmType.OPERATIONAL, f"Email NFSe enviado para {email} - ID: {id_nfse}") + return resposta + except Exception as e: + self._log(RealmType.ERRORS, f"Erro ao enviar email NFSe: {e}") + raise Exception(f"Erro ao enviar email NFSe: {e}") + + def resolver_nfse(self, id_nfse, identificacaoNota:str): + try: + resposta = self.api.post(f"{self.url}/resolve/{id_nfse}", {"identificacaoNota": identificacaoNota}) + self._log(RealmType.OPERATIONAL, f"NFSe resolvida com sucesso - ID: {id_nfse}") + return resposta + except Exception as e: + self._log(RealmType.ERRORS, f"Erro ao resolver NFSe: {e}") + raise Exception(f"Erro ao resolver NFSe: {e}") + + def consultar_nfse_periodo(self, data_inicio, data_fim): + try: + resposta = self.api.get_paginated(f"{self.url}/periodo/{data_inicio}/{data_fim}") + self._log(RealmType.OPERATIONAL, f"NFSe consultada por período: {data_inicio} a {data_fim}") + return resposta + except Exception as e: + self._log(RealmType.ERRORS, f"Erro ao consultar NFSe por período: {e}") + raise Exception(f"Erro ao consultar NFSe por período: {e}") + + def baixar_recibo_provisorio_servico(self, id_nfse, cnpj = None): + try: + resposta = self.api.get(f"{self.url}/rps/pdf/{id_nfse}") if cnpj is None else self.api.get(f"{self.url}/rps/pdf/{id_nfse}/{cnpj}") + self._log(RealmType.OPERATIONAL, f"RPS baixado com sucesso - ID: {id_nfse}") + return resposta + except Exception as e: + self._log(RealmType.ERRORS, f"Erro ao baixar recibo provisório de serviço: {e}") + raise Exception(f"Erro ao baixar recibo provisório de serviço: {e}") diff --git a/tecnospeed_rpc/src/NFe/registroNFe.py b/tecnospeed_rpc/src/NFe/registroNFe.py new file mode 100644 index 0000000..96c54ff --- /dev/null +++ b/tecnospeed_rpc/src/NFe/registroNFe.py @@ -0,0 +1,126 @@ +import json as json_lib +from utils.api import Api +from utils.banco import Banco, NFeTable +from Models.nfe import NFe +from typing import List +from utils.Exchange2 import LoggerExchange2 +from utils.exchangeModels import RealmType +from utils.enviaListerner import enviaListerner + +class RegistroNFeService: + # Classe responsável por fornecer as operações de comunicação com a PlugNotas para o documento NFe + def __init__(self, api_key: str, url_base: str, banco: Banco, logger: LoggerExchange2 = None): + self.api = Api(api_key) + self.url = f"{url_base}/nfe" + self.banco = banco + self.logger = logger + self.notificador = enviaListerner(logger=logger) if logger else None + + def cadastrar_nfe(self, nfe_list: List[NFe], data: dict): + # Envia uma lista de NFes em lote ou unitária para emissão e registra no banco de dados + try: + id_empresa = data.get('id_empresa', "0") + payload = [nfe.model_dump() for nfe in nfe_list] + resposta = self.api.post(self.url, payload) + if 'error' in resposta: + id_usr = data.get('id_usuario', 1) + if self.logger: self.logger.logIC2(RealmType.OPERATIONAL, ('INSERT', 'nfe', id_usr, '0', 'ERRO', f"Erro na API: {resposta.get('error')}", json_lib.dumps(resposta))) + return resposta + if 'documents' in resposta: + for doc in resposta['documents']: + id_tecnospeed = doc.get('id') + if id_tecnospeed: + nfe_nova = NFeTable( + id_tecnospeed=id_tecnospeed, + id_empresa=id_empresa, + id_usuario=data.get('id_usuario', 1), + status=True + ) + self.banco.add(nfe_nova) + id_usr = data.get('id_usuario', 1) + if self.logger: self.logger.logIC2(RealmType.OPERATIONAL, ('INSERT', 'nfe', id_usr, str(nfe_nova.id), 'SUCESSO', 'NFe cadastrada com sucesso', json_lib.dumps(doc))) + + # Notifica o listener interno para sincronização imediata + if self.notificador: + self.notificador.notificar_emissao(id_tecnospeed=id_tecnospeed, documento="nfe", cnpj=id_empresa) + + return resposta + except Exception as e: + if self.logger: self.logger.logIC2(RealmType.OPERATIONAL, ('INSERT', 'nfe', data.get('id_usuario', 1), '0', 'ERRO', f"Erro ao cadastrar NFe: {e}", "{}")) + raise Exception(f"Erro ao cadastrar NFe: {e}") + + def cancelar_nfe(self, id_nota: str, justificativa: str): + # Solicita o cancelamento de uma NFe existente mediante uma justificativa + try: + resposta = self.api.post(f"{self.url}/{id_nota}/cancelamento", {"justificativa": justificativa}) + if self.logger: self.logger.logIC2(RealmType.OPERATIONAL, ('DELETE', 'nfe', 1, id_nota, 'SUCESSO', 'NFe cancelada com sucesso', json_lib.dumps(resposta))) + return resposta + except Exception as e: + if self.logger: self.logger.logIC2(RealmType.OPERATIONAL, ('DELETE', 'nfe', 1, id_nota, 'ERRO', f"Erro ao cancelar NFe: {e}", "{}")) + raise Exception(f"Erro ao cancelar NFe: {e}") + + def enviar_cce(self, id_nota: str, correcao: str): + # Envia uma Carta de Correção Eletrônica (CCe) para retificar uma NFe já emitida + try: + resposta = self.api.post(f"{self.url}/{id_nota}/cce", {"correcao": correcao}) + if self.logger: self.logger.logIC2(RealmType.OPERATIONAL, ('UPDATE', 'nfe', 1, id_nota, 'SUCESSO', 'CCe enviada com sucesso', json_lib.dumps(resposta))) + return resposta + except Exception as e: + if self.logger: self.logger.logIC2(RealmType.OPERATIONAL, ('UPDATE', 'nfe', 1, id_nota, 'ERRO', f"Erro ao enviar CCe NFe: {e}", "{}")) + raise Exception(f"Erro ao enviar CCe NFe: {e}") + + def consultar_resumo(self, id_nota: str): + # Consulta o resumo e o status atual da NFe na PlugNotas pelo seu ID + try: + resposta = self.api.get(f"{self.url}/{id_nota}/resumo") + if self.logger: self.logger.logIC2(RealmType.OPERATIONAL, ('SELECT', 'nfe', 1, id_nota, 'SUCESSO', 'Resumo de NFe consultado', json_lib.dumps(resposta))) + return resposta + except Exception as e: + if self.logger: self.logger.logIC2(RealmType.OPERATIONAL, ('SELECT', 'nfe', 1, id_nota, 'ERRO', f"Erro ao consultar resumo NFe: {e}", "{}")) + raise Exception(f"Erro ao consultar resumo NFe: {e}") + + def consultar_resumo_por_cnpj_integracao(self, cnpj: str, id_integracao: str): + try: + resposta = self.api.get(f"{self.url}/{cnpj}/{id_integracao}/resumo") + if self.logger: self.logger.logIC2(RealmType.OPERATIONAL, ('SELECT', 'nfe', id_integracao, id_integracao, 'SUCESSO', 'Resumo por CNPJ/Integração consultado', json_lib.dumps(resposta))) + return resposta + except Exception as e: + if self.logger: self.logger.logIC2(RealmType.OPERATIONAL, ('SELECT', 'nfe', id_integracao, id_integracao, 'ERRO', f"Erro ao consultar resumo NFe por CNPJ/Integração: {e}", "{}")) + raise Exception(f"Erro ao consultar resumo NFe por CNPJ/Integração: {e}") + + def consultar_status_cancelamento(self, id_nota: str): + try: + resposta = self.api.get(f"{self.url}/{id_nota}/cancelamento/status") + if self.logger: self.logger.logIC2(RealmType.OPERATIONAL, ('SELECT', 'nfe', id_nota, id_nota, 'SUCESSO', 'Status de cancelamento consultado', json_lib.dumps(resposta))) + return resposta + except Exception as e: + if self.logger: self.logger.logIC2(RealmType.OPERATIONAL, ('SELECT', 'nfe', id_nota, id_nota, 'ERRO', f"Erro ao consultar status de cancelamento NFe: {e}", "{}")) + raise Exception(f"Erro ao consultar status de cancelamento NFe: {e}") + + def consultar_status_cce(self, id_nota: str): + try: + resposta = self.api.get(f"{self.url}/{id_nota}/cce/status") + if self.logger: self.logger.logIC2(RealmType.OPERATIONAL, ('SELECT', 'nfe', id_nota, id_nota, 'SUCESSO', 'Status CCe consultado', json_lib.dumps(resposta))) + return resposta + except Exception as e: + if self.logger: self.logger.logIC2(RealmType.OPERATIONAL, ('SELECT', 'nfe', id_nota, id_nota, 'ERRO', f"Erro ao consultar status CCe NFe: {e}", "{}")) + raise Exception(f"Erro ao consultar status CCe NFe: {e}") + + def baixar_pdf(self, id_nota: str): + # Obtém o link para download ou o stream do PDF da Nota Fiscal processada + try: + resposta = self.api.get(f"{self.url}/{id_nota}/pdf") + if self.logger: self.logger.logIC2(RealmType.OPERATIONAL, ('DOWNLOAD (PDF)', 'nfe', id_nota, id_nota, 'SUCESSO', 'PDF baixado com sucesso', json_lib.dumps(resposta) if isinstance(resposta, dict) else '{}')) + return resposta + except Exception as e: + if self.logger: self.logger.logIC2(RealmType.OPERATIONAL, ('DOWNLOAD (PDF)', 'nfe', id_nota, id_nota, 'ERRO', f"Erro ao baixar PDF NFe: {e}", "{}")) + raise Exception(f"Erro ao baixar PDF NFe: {e}") + + def baixar_xml(self, id_nota: str): + try: + resposta = self.api.get(f"{self.url}/{id_nota}/xml") + if self.logger: self.logger.logIC2(RealmType.OPERATIONAL, ('DOWNLOAD (XML)', 'nfe', id_nota, id_nota, 'SUCESSO', 'XML baixado com sucesso', json_lib.dumps(resposta) if isinstance(resposta, dict) else '{}')) + return resposta + except Exception as e: + if self.logger: self.logger.logIC2(RealmType.OPERATIONAL, ('DOWNLOAD (XML)', 'nfe', id_nota, id_nota, 'ERRO', f"Erro ao baixar XML NFe: {e}", "{}")) + raise Exception(f"Erro ao baixar XML NFe: {e}") diff --git a/tecnospeed_rpc/src/Relatorios/relatorios.py b/tecnospeed_rpc/src/Relatorios/relatorios.py new file mode 100644 index 0000000..1155445 --- /dev/null +++ b/tecnospeed_rpc/src/Relatorios/relatorios.py @@ -0,0 +1,70 @@ +from utils.api import Api +from utils.banco import Banco +from utils.Exchange2 import LoggerExchange2 +from utils.exchangeModels import RealmType + +from datetime import datetime, timedelta +import json + +# Extração e consumo de relatórios massivos de emissões diretamente da API e consolidações em lotes +class Relatorios: + def __init__(self, url_base, api_key, banco: Banco, logger: LoggerExchange2 = None): + self.api = Api(api_key) + self.url = f"{url_base}/" + self.banco = banco + self.logger = logger + + def _log(self, realm: RealmType, msg: str): + if self.logger: + self.logger.logIC2(realm, (msg,)) + + def nfe_emitidas(self, data: dict = None): + try: + resposta = self.api.get_paginated(f"{self.url}nfe/relatorio") + self._log(RealmType.OPERATIONAL, "Relatório de NFe emitidas consultado") + return resposta + except Exception as e: + msg_erro = f"Erro ao consultar relatório NFe emitidas: {e}" + self._log(RealmType.ERRORS, msg_erro) + return {"error": msg_erro} + + def nfe_por_cnpj(self, cnpj, data: dict = None): + try: + resposta = self.api.get_paginated(f"{self.url}nfe/{cnpj}/relatorio") + self._log(RealmType.OPERATIONAL, f"Relatório de NFe por CNPJ: {cnpj}") + return resposta + except Exception as e: + msg_erro = f"Erro ao consultar relatório NFe por CNPJ: {e}" + self._log(RealmType.ERRORS, msg_erro) + return {"error": msg_erro} + + def nfse_emitidas(self, data_inicial=None, data_final=None, data: dict = None): + try: + if not data_inicial: + data_inicial = (datetime.now() - timedelta(days=30)).strftime('%Y-%m-%d') + if not data_final: + data_final = datetime.now().strftime('%Y-%m-%d') + + resposta = self.api.get_paginated(f"{self.url}nfse/relatorio?from={data_inicial}&to={data_final}") + self._log(RealmType.OPERATIONAL, f"Relatório NFSe emitidas: {data_inicial} a {data_final}") + return resposta + except Exception as e: + msg_erro = f"Erro ao consultar relatório NFSe emitidas: {e}" + self._log(RealmType.ERRORS, msg_erro) + return {"error": msg_erro} + + def nfse_por_cnpj(self, cnpj, data_inicial=None, data_final=None, data: dict = None): + try: + if not data_inicial: + data_inicial = (datetime.now() - timedelta(days=30)).strftime('%Y-%m-%d') + if not data_final: + data_final = datetime.now().strftime('%Y-%m-%d') + + resposta = self.api.get_paginated(f"{self.url}nfse/{cnpj}/relatorio?from={data_inicial}&to={data_final}") + self._log(RealmType.OPERATIONAL, f"Relatório NFSe por CNPJ: {cnpj}") + return resposta + except Exception as e: + msg_erro = f"Erro ao consultar relatório NFSe por CNPJ: {e}" + self._log(RealmType.ERRORS, msg_erro) + return {"error": msg_erro} + diff --git a/tecnospeed_rpc/src/certificado_digital/certificado_digital.py b/tecnospeed_rpc/src/certificado_digital/certificado_digital.py new file mode 100644 index 0000000..f1ddf19 --- /dev/null +++ b/tecnospeed_rpc/src/certificado_digital/certificado_digital.py @@ -0,0 +1,205 @@ +from utils.api import Api +from utils.banco import Banco, CertificadoDigitalTable, EnterpriseTable +from datetime import date +from utils.Exchange2 import LoggerExchange2 +from utils.exchangeModels import RealmType +import json +import base64 + +class CertificadoDigital: + # Serviço que gerencia o upload e atualização de certificados digitais A1/A3 na plataforma da PlugNotas + def __init__(self, url_base, banco: Banco, api_key: str, logger: LoggerExchange2 = None): + self.api = Api(api_key=api_key) + self.banco = banco + self.url = f"{url_base}/certificado" + self.logger = logger + + def post_form_data(self, data): + # Realiza o envio multipart/form-data do arquivo físico do certificado .pfx para a API e o salva no banco + try: + files = { + 'arquivo': ( + 'certificado.pfx', + base64.b64decode(data['arquivo']) if isinstance(data['arquivo'], str) else data['arquivo'], + 'application/x-pkcs12' + ), + 'senha': (None, data['senha']) + } + if 'email' in data: + files['email'] = (None, data['email']) + + cnpj = data.get('cnpj') + if not cnpj: + raise ValueError("CNPJ é obrigatório para cadastrar o certificado.") + + # 1. Validar existência local (Banco de Dados) + empresa_local = self.banco.session.query(EnterpriseTable).filter_by(cnpj=cnpj).first() + if not empresa_local: + msg = f"Empresa com CNPJ {cnpj} não encontrada na base de dados local. Cadastre a empresa primeiro." + if self.logger: self.logger.logIC2(RealmType.OPERATIONAL, ('INSERT', 'certificado_digital', cnpj, '0', 'ERRO', msg, "{}")) + raise ValueError(msg) + + # 2. Validar existência na API (PlugNotas) - Resiliente à quedas + try: + url_empresa = f"{self.url.replace('/certificado', '/empresa')}/{cnpj}" + check_api = self.api.get(url_empresa) + + if isinstance(check_api, dict) and "error" in check_api: + err_msg = str(check_api.get("error", "")).lower() + # Se rertornar 404 explicitamente, barramos. + if "404" in err_msg or "not found" in err_msg: + msg = f"Empresa com CNPJ {cnpj} não encontrada na API da PlugNotas. Sincronize o cadastro." + if self.logger: self.logger.logIC2(RealmType.OPERATIONAL, ('INSERT', 'certificado_digital', cnpj, '0', 'ERRO', msg, "{}")) + raise ValueError(msg) + + # Para outros erros (500, timeout), logamos aviso e confiamos no banco local + if self.logger: self.logger.logIC2(RealmType.LOGS, (f"API PlugNotas instável para check de empresa {cnpj}. Prosseguindo via banco local.",)) + + except Exception as e: + # Caso o próprio Api.get levante exceção não tratada + if self.logger: self.logger.logIC2(RealmType.LOGS, (f"Erro inesperado ao consultar empresa na API. Prosseguindo via banco: {e}",)) + + # 3. Enviar o certificado para a PlugNotas + response = self.api.post_form_data(self.url, files) + id_tecnospeed = response.get('data', {}).get('id') + + if not id_tecnospeed: + if self.logger: self.logger.logIC2(RealmType.OPERATIONAL, ('INSERT', 'certificado_digital', cnpj, '0', 'ERRO', 'ID do certificado não encontrado na resposta da API', json.dumps(response))) + raise ValueError("Erro ao obter ID do certificado na resposta da API.") + + # 4. Vincular o certificado à empresa (PATCH) + try: + # Endpoint: PATCH /empresa/{cnpj}/certificado + url_patch = f"{self.url.replace('/certificado', '/empresa')}/{cnpj}/certificado" + self.api.patch(url_patch, data={"certificado": id_tecnospeed}) + except Exception as e: + msg = f"Erro ao vincular certificado {id_tecnospeed} à empresa {cnpj}: {e}" + if self.logger: self.logger.logIC2(RealmType.OPERATIONAL, ('UPDATE', 'empresa', cnpj, id_tecnospeed, 'ERRO', msg, "{}")) + # Opcional: decidir se deve falhar aqui ou apenas logar. Como é vínculo obrigatório, vamos subir o erro. + raise Exception(msg) + + expiration_value = data.get('expirationAt') or data.get('data_expiracao') + certificado = CertificadoDigitalTable( + id_tecnospeed=id_tecnospeed, + certificado=data['arquivo'], + expirationAt=date.fromisoformat(expiration_value) if expiration_value else date.today(), + senha=data['senha'], + status=True, + cnpj=cnpj, + email=data.get('email') + ) + + self.banco.add(certificado) + if self.logger: self.logger.logIC2(RealmType.OPERATIONAL, ('INSERT', 'certificado_digital', cnpj, id_tecnospeed, 'SUCESSO', 'Certificado digital cadastrado e vinculado com sucesso', json.dumps(response))) + + return response + except Exception as e: + if self.logger: self.logger.logIC2(RealmType.OPERATIONAL, ('INSERT', 'certificado_digital', data.get('id_usuario', 0), '0', 'ERRO', f"Erro ao adicionar certificado: {e}", "{}")) + raise + + def contar_vencidos(self, lista_certificados): + # Auxiliar para contar certificados ativos e vencidos na listagem + if isinstance(lista_certificados, dict) and "data" in lista_certificados: + lista = lista_certificados["data"] + elif isinstance(lista_certificados, list): + lista = lista_certificados + else: + return {"certificados_ativos": 0, "certificados_vencidos": 0} + + ativos = 0 + vencidos = 0 + hoje = date.today() + + for cert in lista: + if not isinstance(cert, dict): continue + + # Tenta encontrar o campo de expiração (vencimento ou outros nomes conhecidos) + exp_str = cert.get("vencimento") or cert.get("expirationAt") or cert.get("dataVencimento") or cert.get("data_expiracao") + + if exp_str: + try: + # Tenta converter a string de data: + # 1. Formato DD/MM/YYYY (do snipped do usuário) + # 2. Formato ISO YYYY-MM-DD + if "/" in exp_str: + partes = exp_str.split(" ")[0].split("/") + if len(partes) == 3: + data_exp = date(int(partes[2]), int(partes[1]), int(partes[0])) + else: + data_exp = date.fromisoformat(exp_str[:10]) + + if data_exp >= hoje: + ativos += 1 + else: + vencidos += 1 + except: + continue + + return { + "certificados_ativos": ativos, + "certificados_vencidos": vencidos + } + + def get(self, id=None): + # Consulta a situação de um certificado digital específico pelo ID ou lista todos vinculados + try: + if id: + resultado = self.api.get(f"{self.url}/{id}") + if self.logger: self.logger.logIC2(RealmType.OPERATIONAL, ('SELECT', 'certificado_digital', id, id, 'SUCESSO', f"Certificado consultado - ID: {id}", json.dumps(resultado))) + return resultado + else: + resposta = self.api.get(self.url) + + # Calcula totais + totais = self.contar_vencidos(resposta) + + # Organiza a resposta unificada + resultado = { + "certificados": resposta if isinstance(resposta, list) else resposta.get("data", []), + "totais": totais + } + + if self.logger: self.logger.logIC2(RealmType.OPERATIONAL, ('SELECT', 'certificado_digital', 'GERAL', '0', 'SUCESSO', 'Listagem de certificados digitais', json.dumps(resultado))) + return resultado + except Exception as e: + if self.logger: self.logger.logIC2(RealmType.OPERATIONAL, ('SELECT', 'certificado_digital', id or 'GERAL', '0', 'ERRO', f"Erro ao buscar certificado: {e}", "{}")) + raise + + def put_certificado(self, id, data): + # Atualiza o arquivo físico ou a senha de um certificado já existente + try: + certificado = self.banco.session.query(CertificadoDigitalTable).filter_by(id_tecnospeed=id).first() + id_usr = data.get('id_usuario', 0) + + if not certificado: + if self.logger: self.logger.logIC2(RealmType.OPERATIONAL, ('UPDATE', 'certificado_digital', id_usr, id, 'ERRO', f"Certificado com ID {id} não encontrado", "{}")) + raise ValueError(f"Certificado com ID {id} não encontrado no banco.") + + files = { + 'arquivo': ( + 'certificado.pfx', + base64.b64decode(data['arquivo']) if isinstance(data['arquivo'], str) else data['arquivo'], + 'application/x-pkcs12' + ), + 'senha': (None, data['senha']) + } + if 'email' in data: + files['email'] = (None, data['email']) + + self.api.put_form_data(f"{self.url}/{id}", files) + response = {"id": id, "status": "atualizado", "message": "Certificado atualizado localmente e na API"} + + certificado.certificado = data['arquivo'] + certificado.senha = data['senha'] + expiration_value = data.get('expirationAt') or data.get('data_expiracao') + if expiration_value: + certificado.expirationAt = date.fromisoformat(expiration_value) + certificado.updatedAt = date.today() + + self.banco.update(certificado) + if self.logger: self.logger.logIC2(RealmType.OPERATIONAL, ('UPDATE', 'certificado_digital', id_usr, id, 'SUCESSO', 'Certificado digital atualizado com sucesso', json.dumps({"id": id}))) + + return response + except Exception as e: + if self.logger: self.logger.logIC2(RealmType.OPERATIONAL, ('UPDATE', 'certificado_digital', data.get('id_usuario', 0), id, 'ERRO', f"Erro ao atualizar certificado: {e}", "{}")) + raise diff --git a/tecnospeed_rpc/src/empresas/empresa.py b/tecnospeed_rpc/src/empresas/empresa.py new file mode 100644 index 0000000..1022739 --- /dev/null +++ b/tecnospeed_rpc/src/empresas/empresa.py @@ -0,0 +1,278 @@ +from Models.empresa import Empresa +from utils.api import Api +from utils.banco import Banco, EnterpriseTable, LogoTable +from utils.Exchange2 import LoggerExchange2 +from utils.exchangeModels import RealmType +from sqlalchemy.exc import SQLAlchemyError +import json +import string +import random +import time +import base64 +import binascii + +def generate_cuid(): + ts = hex(int(time.time() * 1000))[2:] + rand = ''.join(random.choices(string.ascii_lowercase + string.digits, k=12)) + return f"c{ts}{rand}"[:25] + +# Gerenciador de contas de Empresas emissoras de notas e seus respectivos certificados e logos +class EmpresaController: + def __init__(self, url_base, api_key: str, banco: Banco, logger: LoggerExchange2 = None, client: str = 'desconhecido'): + self.api = Api(api_key=api_key) + self.banco = banco + self.url = f"{url_base}/empresa" + self.logger = logger + self.client = client + + def _log(self, realm: RealmType, msg: str): + if self.logger: + self.logger.logIC2(realm, (msg,)) + + def post(self, empresa: Empresa, data: dict, correlation_id: str = None): + try: + dict_api = empresa.model_dump(exclude={'id', 'id_empresa', 'banco', 'email'}, exclude_none=True) + + # Injeção das configurações fixas e obrigatórias para a PlugNotas + if dict_api.get("nfe") and dict_api["nfe"].get("ativo"): + nfe_config_front = dict_api["nfe"].get("config") or {} + dict_api["nfe"]["config"] = { + "producao": False, + "email": {"envio": False}, + "dfe": {"primeiroNsu": False}, + "impressaoFcp": False, + "impressaoPartilha": False, + **nfe_config_front + } + dict_api["nfe"]["tipoContrato"] = 0 + + if dict_api.get("nfse") and dict_api["nfse"].get("ativo"): + nfse_config_front = dict_api["nfse"].get("config") or {} + dict_api["nfse"]["config"] = { + "producao": False, + "email": {"envio": False}, + "rps": {"lote": 1, "numeracao": [{"numero": 1, "serie": "1"}]}, + "prefeitura": {"login": "12345", "senha": "123"}, + **nfse_config_front + } + dict_api["nfse"]["tipoContrato"] = 0 + print("===========================") + print("ENVIANDO PARA A API:", json.dumps(dict_api, indent=2)) + print("===========================") + retorno = self.api.post(data=dict_api, url=self.url) + if 'error' in retorno: + err_data = retorno.get('response', {}).get('error', {}) + msg = err_data.get('message', 'Erro desconhecido') + detalhes = err_data.get('data', []) + if detalhes: + msg += f" Detalhes: {detalhes}" + + print(f"\n[ERRO API PLUGNOTAS] {msg}\n", flush=True) + self._log(RealmType.ERRORS, f"Erro na API ao cadastrar empresa: {msg}") + raise Exception(msg) + + # Gerar ID CUID se não vier no objeto ou se for nulo + empresa_id = empresa.id if empresa.id else generate_cuid() + + empresa_nova = EnterpriseTable( + id=empresa_id, + cnpj=empresa.cpfCnpj, + razao_social=empresa.razaoSocial, + nome_fantasia=empresa.nomeFantasia if empresa.nomeFantasia else empresa.razaoSocial, + banco=empresa.banco, + email=empresa.email, + status=True + ) + self.banco.add(empresa_nova) + self._log(RealmType.OPERATIONAL, f"Empresa cadastrada com sucesso - CNPJ: {empresa.cpfCnpj} (ID: {empresa_id})") + + if isinstance(retorno, dict) and 'data' in retorno: + retorno['data']['id'] = empresa_nova.id + retorno['data']['id_plugnotas'] = retorno['data'].get('_id') + retorno['data']['nome_fantasia'] = empresa_nova.nome_fantasia + retorno['data']['razao_social'] = empresa_nova.razao_social + retorno['data']['status'] = empresa_nova.status + retorno['data']['nfe'] = {'ativo': dict_api.get('nfe', {}).get('ativo', False) if dict_api.get('nfe') else False} + retorno['data']['nfse'] = {'ativo': dict_api.get('nfse', {}).get('ativo', False) if dict_api.get('nfse') else False} + retorno['data'].pop('razaoSocial', None) + retorno['data'].pop('nomeFantasia', None) + retorno['data'].pop('id_empresa', None) + + # Persistência de Auditoria: Salvando o retorno completo da PlugNotas no banco local + try: + self.banco.logar_sistema( + id_usuario=data.get("id_usuario", 1), + acao="CADASTRO_EMPRESA_PLUGNOTAS", + tabela="Enterprise", + id_registro=str(empresa_nova.id), + json=json.dumps(retorno), + status="SUCESSO", + mensagem=f"Empresa {empresa.cpfCnpj} cadastrada na PlugNotas com sucesso", + cliente=self.client, + correlation_id=correlation_id + ) + except Exception as e_log: + self._log(RealmType.ERRORS, f"Erro ao salvar log de auditoria: {e_log}") + + return retorno + except SQLAlchemyError as e: + self.banco.rollback() + self._log(RealmType.ERRORS, f"Erro no banco ao cadastrar empresa: {e}") + return {"error": f"Erro ao cadastrar empresa: {str(e)}"} + except Exception as e: + return {"error": f"Erro inesperado: {str(e)}"} + + def _map_empresa_data(self, emp_plug: dict) -> dict: + """Mapeia os dados da PlugNotas para o formato simplificado do front-end.""" + cnpj = emp_plug.get("cpfCnpj") + empresa_local = self.banco.session.query(EnterpriseTable).filter_by(cnpj=cnpj).first() + + return { + "_id": emp_plug.get("_id"), + "cpfCnpj": cnpj, + "nomeFantasia": empresa_local.nome_fantasia if empresa_local else emp_plug.get("nomeFantasia", ""), + "razaoSocial": empresa_local.razao_social if empresa_local else emp_plug.get("razaoSocial", ""), + "email": empresa_local.email if empresa_local else "", + "certificado": emp_plug.get("certificado", ""), + "createdAt": emp_plug.get("createdAt", ""), + "updatedAt": emp_plug.get("updatedAt", ""), + "nfse": { + "ativo": emp_plug.get("nfse", {}).get("ativo", False) + }, + "nfe": { + "ativo": emp_plug.get("nfe", {}).get("ativo", False) + } + } + + def get(self, cnpj: str = None, data: dict = None, correlation_id: str = None): + try: + if cnpj: + # Consulta de uma única empresa + resposta_plug = self.api.get(f"{self.url}/{cnpj}") + + if isinstance(resposta_plug, dict) and "error" not in resposta_plug: + mapped_data = self._map_empresa_data(resposta_plug) + # Envelope para consulta única (padrão solicitado) + resposta = { + "data": mapped_data, + "page": 1, + "size": 1, + "total": 1, + "totalPages": 1 + } + else: + resposta = resposta_plug + + else: + # Consulta paginada (Lista) + resposta_plug = self.api.get_paginated(self.url) + + if isinstance(resposta_plug, dict) and "data" in resposta_plug: + mapped_list = [self._map_empresa_data(emp) for emp in resposta_plug["data"]] + resposta = { + "data": mapped_list, + "page": resposta_plug.get("page", 1), + "size": resposta_plug.get("size", len(mapped_list)), + "total": resposta_plug.get("total", len(mapped_list)), + "totalPages": resposta_plug.get("totalPages", 1) + } + else: + resposta = resposta_plug + + self._log(RealmType.OPERATIONAL, f"Empresa consultada - CNPJ: {cnpj or 'TODAS'}") + return resposta + except Exception as e: + self._log(RealmType.ERRORS, f"Erro ao buscar empresa: {e}") + return {"error": f"Erro ao buscar empresa: {str(e)}"} + + def post_file(self, file, cnpj, data=None, correlation_id: str = None): + try: + empresa = self.banco.session.query(EnterpriseTable).filter_by(cnpj=cnpj).first() + if not empresa: + self._log(RealmType.ERRORS, f"Empresa não encontrada para upload de logo - CNPJ: {cnpj}") + return {"error": "Empresa não encontrada"} + + # Decodificação de Base64 para Binário (PlugNotas exige multipart/form-data com binário) + try: + # Remove cabeçalhos data:image/... se estiverem presentes + if isinstance(file, str) and "," in file: + file = file.split(",")[1] + + binary_data = base64.b64decode(file) + except (binascii.Error, ValueError) as e_b64: + self._log(RealmType.ERRORS, f"Erro ao decodificar base64 do logotipo: {e_b64}") + return {"error": "Arquivo inválido - Base64 malformado"} + + # Verificação de tamanho (PlugNotas limite 200kb) + if len(binary_data) > 200 * 1024: + self._log(RealmType.ERRORS, f"Logotipo excede o tamanho máximo de 200kb - Tamanho: {len(binary_data)/1024:.2f}kb") + return {"error": "O logotipo excede o limite de 200kb"} + + logo = LogoTable( + id_empresa=empresa.id, + logo=file, # Mantemos o base64 no banco caso queiramos exibir no front local + status=True + ) + self.banco.add(logo) + + files = {'arquivo': ('logo.jpg', binary_data, 'image/jpeg')} + resposta = self.api.post_file(file=files, url=f"{self.url}/{cnpj}/logotipo") + + # Persistência de Auditoria + try: + self.banco.logar_sistema( + id_usuario=data.get("id_usuario", 1) if data else 1, + acao="UPLOAD_LOGOTIPO", + tabela="logo", + id_registro=str(logo.id), + json=json.dumps(resposta), + status="SUCESSO" if "error" not in resposta else "ERRO", + mensagem=f"Upload de logo para CNPJ {cnpj}", + cliente=self.client, + correlation_id=correlation_id + ) + except Exception as e_log: + self._log(RealmType.ERRORS, f"Erro ao salvar log de auditoria de logo: {e_log}") + + self._log(RealmType.OPERATIONAL, f"Logo enviada com sucesso - CNPJ: {cnpj}") + return resposta + + except SQLAlchemyError as e: + self.banco.rollback() + self._log(RealmType.ERRORS, f"Erro no banco ao salvar logo: {e}") + return {"error": f"Erro ao salvar logo: {str(e)}"} + except Exception as e: + self._log(RealmType.ERRORS, f"Erro inesperado ao enviar logo: {e}") + return {"error": f"Erro inesperado: {str(e)}"} + + def patch_certificado(self, cnpj: str, certificado: str, data: dict, correlation_id: str = None): + try: + # 1. Validar existência local (Banco de Dados) + empresa = self.banco.session.query(EnterpriseTable).filter_by(cnpj=cnpj).first() + if not empresa: + self._log(RealmType.ERRORS, f"Empresa não encontrada no banco local - CNPJ: {cnpj}") + return {"error": "Empresa não encontrada no banco local. Cadastre a empresa primeiro."} + + # 2. Validar existência na API (PlugNotas) - Resiliente à quedas + try: + check_api = self.api.get(f"{self.url}/{cnpj}") + if isinstance(check_api, dict) and "error" in check_api: + err_msg = str(check_api.get("error", "")).lower() + # Se rertornar 404 explicitamente, barramos. + if "404" in err_msg or "not found" in err_msg: + self._log(RealmType.ERRORS, f"Empresa não encontrada na API PlugNotas - CNPJ: {cnpj}") + return {"error": "Empresa não encontrada na API PlugNotas. Sincronize o cadastro."} + + # Para outros erros (500, timeout), logamos aviso e confiamos no banco local + self._log(RealmType.LOGS, f"API PlugNotas instável para check de empresa {cnpj}. Prosseguindo via banco local.") + except Exception as e: + self._log(RealmType.LOGS, f"Erro ao consultar empresa na API (check resiliente): {e}") + + # 3. Vincular o certificado + resposta = self.api.patch(url=f"{self.url}/{cnpj}/certificado", data={"certificado": certificado}) + self._log(RealmType.OPERATIONAL, f"Certificado da empresa atualizado - CNPJ: {cnpj}") + return resposta + except Exception as e: + self._log(RealmType.ERRORS, f"Erro ao atualizar certificado: {e}") + return {"error": f"Erro ao atualizar certificado: {str(e)}"} + diff --git a/tecnospeed_rpc/src/logs/integracao_01 b/tecnospeed_rpc/src/logs/integracao_01 new file mode 100644 index 0000000..ee7eea0 --- /dev/null +++ b/tecnospeed_rpc/src/logs/integracao_01 @@ -0,0 +1,24 @@ +2026-04-01 09:09:33,082 - integracao - INFO - { 'caller': 'ciclo_app', + 'client': '', + 'logDate': '2026-04-01 09:09:33.082579-03:00', + 'msg': '[local] Configuração lida com sucesso pelo Listener Webhook.', + 'origin': '', + 'service': 'tecnospeed', + 'system': 'integracao', + 'version': '2.0.1'} +2026-04-01 09:09:33,700 - integracao - INFO - { 'caller': 'ciclo_app', + 'client': '', + 'logDate': '2026-04-01 09:09:33.700242-03:00', + 'msg': 'Exchange inicializado com sucesso para o Listener Webhook.', + 'origin': '', + 'service': 'tecnospeed', + 'system': 'integracao', + 'version': '2.0.1'} +2026-04-01 09:14:24,402 - integracao - INFO - { 'caller': 'ciclo_app', + 'client': '', + 'logDate': '2026-04-01 09:14:24.402193-03:00', + 'msg': 'Listener Webhook encerrando...', + 'origin': '', + 'service': 'tecnospeed', + 'system': 'integracao', + 'version': '2.0.1'} diff --git a/tecnospeed_rpc/src/logs/integracao_02 b/tecnospeed_rpc/src/logs/integracao_02 new file mode 100644 index 0000000..3fc1caa --- /dev/null +++ b/tecnospeed_rpc/src/logs/integracao_02 @@ -0,0 +1,194 @@ +2026-04-02 09:14:16,362 - integracao - INFO - { 'caller': 'ciclo_app', + 'client': '', + 'logDate': '2026-04-02 09:14:16.362304-03:00', + 'msg': '[local] Configuração lida com sucesso pelo Listener Webhook.', + 'origin': '', + 'service': 'tecnospeed', + 'system': 'integracao', + 'version': '2.0.1'} +2026-04-02 09:14:16,932 - integracao - INFO - { 'caller': 'ciclo_app', + 'client': '', + 'logDate': '2026-04-02 09:14:16.932459-03:00', + 'msg': 'Exchange inicializado com sucesso para o Listener Webhook.', + 'origin': '', + 'service': 'tecnospeed', + 'system': 'integracao', + 'version': '2.0.1'} +2026-04-02 14:14:30,014 - integracao - INFO - { 'caller': 'ciclo_app', + 'client': '', + 'logDate': '2026-04-02 14:14:30.013914-03:00', + 'msg': '[local] Configuração lida com sucesso pelo Listener Webhook.', + 'origin': '', + 'service': 'tecnospeed', + 'system': 'integracao', + 'version': '2.0.1'} +2026-04-02 14:14:31,040 - integracao - INFO - { 'caller': 'ciclo_app', + 'client': '', + 'logDate': '2026-04-02 14:14:31.039753-03:00', + 'msg': 'Exchange inicializado com sucesso para o Listener Webhook.', + 'origin': '', + 'service': 'tecnospeed', + 'system': 'integracao', + 'version': '2.0.1'} +2026-04-02 14:22:20,024 - integracao - INFO - { 'caller': 'receber_webhook_plugnotas', + 'client': '', + 'logDate': '2026-04-02 14:22:20.024167-03:00', + 'msg': 'Webhook recebido - Documento: nfe | Status: CONCLUIDO | ID: ' + '69cab453... | IDIntegracao: | Emitente:', + 'origin': '', + 'service': 'tecnospeed', + 'system': 'integracao', + 'version': '2.0.1'} +2026-04-02 14:22:20,263 - integracao - INFO - { 'caller': 'receber_webhook_plugnotas', + 'client': '', + 'logDate': '2026-04-02 14:22:20.263204-03:00', + 'msg': 'Webhook repassado ao worker com sucesso - Documento: nfe | Status: ' + 'CONCLUIDO | ID: 69cab453...', + 'origin': '', + 'service': 'tecnospeed', + 'system': 'integracao', + 'version': '2.0.1'} +2026-04-02 14:23:45,608 - integracao - INFO - { 'caller': 'receber_webhook_plugnotas', + 'client': '', + 'logDate': '2026-04-02 14:23:45.608300-03:00', + 'msg': 'Webhook recebido - Documento: nfe | Status: CONCLUIDO | ID: ' + '69cab45329f21a7de7691961 | IDIntegracao: XXXYY99999_TESTE445 | ' + 'Emitente: 08187168000160', + 'origin': '', + 'service': 'tecnospeed', + 'system': 'integracao', + 'version': '2.0.1'} +2026-04-02 14:23:46,107 - integracao - INFO - { 'caller': 'receber_webhook_plugnotas', + 'client': '', + 'logDate': '2026-04-02 14:23:46.107170-03:00', + 'msg': 'Webhook repassado ao worker com sucesso - Documento: nfe | Status: ' + 'CONCLUIDO | ID: 69cab45329f21a7de7691961', + 'origin': '', + 'service': 'tecnospeed', + 'system': 'integracao', + 'version': '2.0.1'} +2026-04-02 14:26:07,402 - integracao - INFO - { 'caller': 'ciclo_app', + 'client': '', + 'logDate': '2026-04-02 14:26:07.401683-03:00', + 'msg': 'Listener Webhook encerrando...', + 'origin': '', + 'service': 'tecnospeed', + 'system': 'integracao', + 'version': '2.0.1'} +2026-04-02 14:37:36,191 - integracao - INFO - { 'caller': 'ciclo_app', + 'client': '', + 'logDate': '2026-04-02 14:37:36.190896-03:00', + 'msg': '[local] Configuração lida com sucesso pelo Listener Webhook.', + 'origin': '', + 'service': 'tecnospeed', + 'system': 'integracao', + 'version': '2.0.1'} +2026-04-02 14:37:36,969 - integracao - INFO - { 'caller': 'ciclo_app', + 'client': '', + 'logDate': '2026-04-02 14:37:36.969622-03:00', + 'msg': 'Exchange inicializado com sucesso para o Listener Webhook.', + 'origin': '', + 'service': 'tecnospeed', + 'system': 'integracao', + 'version': '2.0.1'} +2026-04-02 14:37:37,459 - integracao - INFO - { 'caller': 'ciclo_app', + 'client': '', + 'logDate': '2026-04-02 14:37:37.459047-03:00', + 'msg': 'Listener Webhook encerrando...', + 'origin': '', + 'service': 'tecnospeed', + 'system': 'integracao', + 'version': '2.0.1'} +2026-04-02 14:37:42,582 - integracao - INFO - { 'caller': 'ciclo_app', + 'client': '', + 'logDate': '2026-04-02 14:37:42.582792-03:00', + 'msg': '[local] Configuração lida com sucesso pelo Listener Webhook.', + 'origin': '', + 'service': 'tecnospeed', + 'system': 'integracao', + 'version': '2.0.1'} +2026-04-02 14:37:43,391 - integracao - INFO - { 'caller': 'ciclo_app', + 'client': '', + 'logDate': '2026-04-02 14:37:43.391071-03:00', + 'msg': 'Exchange inicializado com sucesso para o Listener Webhook.', + 'origin': '', + 'service': 'tecnospeed', + 'system': 'integracao', + 'version': '2.0.1'} +2026-04-02 14:37:43,782 - integracao - INFO - { 'caller': 'ciclo_app', + 'client': '', + 'logDate': '2026-04-02 14:37:43.781995-03:00', + 'msg': 'Listener Webhook encerrando...', + 'origin': '', + 'service': 'tecnospeed', + 'system': 'integracao', + 'version': '2.0.1'} +2026-04-02 16:03:48,010 - integracao - INFO - { 'caller': 'run', + 'client': '', + 'logDate': '2026-04-02 16:03:48.009993-03:00', + 'msg': 'Conectando ao RabbitMQ', + 'origin': '', + 'service': 'tecnospeed', + 'system': 'integracao', + 'version': '2.0.1'} +2026-04-02 16:03:48,217 - integracao - INFO - { 'caller': 'run', + 'client': '', + 'logDate': '2026-04-02 16:03:48.216535-03:00', + 'msg': "Configuração do sistema carregada - serviceid='local' " + "servicekey='local' origin='local' " + "amqps='amqp://guest:guest@localhost:5672/'", + 'origin': '', + 'service': 'tecnospeed', + 'system': 'integracao', + 'version': '2.0.1'} +2026-04-02 16:12:17,812 - integracao - INFO - { 'caller': 'run', + 'client': '', + 'logDate': '2026-04-02 16:12:17.812369-03:00', + 'msg': 'Conectando ao RabbitMQ', + 'origin': '', + 'service': 'tecnospeed', + 'system': 'integracao', + 'version': '2.0.1'} +2026-04-02 16:12:18,488 - integracao - INFO - { 'caller': 'run', + 'client': '', + 'logDate': '2026-04-02 16:12:18.487913-03:00', + 'msg': "Configuração do sistema carregada - serviceid='local' " + "servicekey='local' origin='local' " + "amqps='amqp://guest:guest@localhost:5672/'", + 'origin': '', + 'service': 'tecnospeed', + 'system': 'integracao', + 'version': '2.0.1'} +2026-04-02 16:12:18,864 - integracao - INFO - { 'caller': 'run', + 'client': '', + 'logDate': '2026-04-02 16:12:18.863889-03:00', + 'msg': 'Erro fatal ao conectar ao RabbitMQ - VERIFY PARAMETERS: ' + 'is_amqp_url: True\n' + ' amqps[amqp://guest:guest@localhost:5672/]\n' + ' origin[local]\n' + ' system[integracao]\n' + ' service[tecnospeed]\n' + ' version[2.0.1\n' + ' client[]', + 'origin': '', + 'service': 'tecnospeed', + 'system': 'integracao', + 'version': '2.0.1'} +2026-04-02 16:12:30,426 - integracao - INFO - { 'caller': 'run', + 'client': '', + 'logDate': '2026-04-02 16:12:30.426681-03:00', + 'msg': 'Conectando ao RabbitMQ', + 'origin': '', + 'service': 'tecnospeed', + 'system': 'integracao', + 'version': '2.0.1'} +2026-04-02 16:12:30,947 - integracao - INFO - { 'caller': 'run', + 'client': '', + 'logDate': '2026-04-02 16:12:30.947199-03:00', + 'msg': "Configuração do sistema carregada - serviceid='local' " + "servicekey='local' origin='local' " + "amqps='amqp://guest:guest@localhost:5672/'", + 'origin': '', + 'service': 'tecnospeed', + 'system': 'integracao', + 'version': '2.0.1'} diff --git a/tecnospeed_rpc/src/logs/integracao_06 b/tecnospeed_rpc/src/logs/integracao_06 new file mode 100644 index 0000000..d64ebb0 --- /dev/null +++ b/tecnospeed_rpc/src/logs/integracao_06 @@ -0,0 +1,56 @@ +2026-04-06 08:25:17,847 - teste_wrapper.integracao - INFO - params: amqps: amqps://iawkzmhh:4iIVXCgLDsLy2oVyVWeV-o6shg-BZ89z@jackal.rmq.cloudamqp.com/iawkzmhh +2026-04-06 08:25:18,327 - teste_wrapper.integracao - INFO - Logger Messages topics: logs.teste_wrapper.integracao. +2026-04-06 08:25:18,361 - teste_wrapper.integracao - INFO - {'caller': 'IC2', + 'client': '', + 'hmac': '862c786853960277c4d518d1eb01143b2fe7782092dbf5403fb61b52127c7829', + 'logDate': '2026-04-06 08:25:18.361486-03:00', + 'msg': "LOGIC2 LOGS [('Testando o novo fo ...]", + 'origin': '', + 'payload': '44102feb05c76aac7b2b', + 'service': 'teste_wrapper', + 'serviceid': 'AF1699F719F1B6C81DE2BD38AB811994E3089B35C8E9314813A770AC7AE25D8D', + 'system': 'integracao', + 'target': '', + 'type': 'logger', + 'version': ''} +2026-04-06 08:25:18,368 - teste_wrapper.integracao - INFO - {'caller': 'logMsg', + 'client': '', + 'hmac': '', + 'logDate': '2026-04-06 08:25:18.367868-03:00', + 'msg': 'Testando o novo formato IC2', + 'origin': '', + 'payload': '', + 'service': 'teste_wrapper', + 'serviceid': 'AF1699F719F1B6C81DE2BD38AB811994E3089B35C8E9314813A770AC7AE25D8D', + 'system': 'integracao', + 'target': '', + 'type': 'logger', + 'version': ''} +2026-04-06 08:28:37,324 - teste_wrapper.integracao - INFO - params: amqps: amqps://iawkzmhh:4iIVXCgLDsLy2oVyVWeV-o6shg-BZ89z@jackal.rmq.cloudamqp.com/iawkzmhh +2026-04-06 08:28:37,834 - teste_wrapper.integracao - INFO - Logger Messages topics: logs.teste_wrapper.integracao. +2026-04-06 08:28:37,855 - teste_wrapper.integracao - INFO - {'caller': 'IC2', + 'client': '', + 'hmac': '841681411efce4ae99ff2602ae79da6284e6c3ca247a27da3a87dedf3771244e', + 'logDate': '2026-04-06 08:28:37.855637-03:00', + 'msg': "LOGIC2 LOGS [('Testando o formato ...]", + 'origin': '', + 'payload': '353fd214129406d78301', + 'service': 'teste_wrapper', + 'serviceid': 'AF1699F719F1B6C81DE2BD38AB811994E3089B35C8E9314813A770AC7AE25D8D', + 'system': 'integracao', + 'target': '', + 'type': 'logger', + 'version': ''} +2026-04-06 08:28:37,865 - teste_wrapper.integracao - INFO - {'caller': 'logMsg', + 'client': '', + 'hmac': '', + 'logDate': '2026-04-06 08:28:37.865799-03:00', + 'msg': 'Testando o formato IC2', + 'origin': '', + 'payload': '', + 'service': 'teste_wrapper', + 'serviceid': 'AF1699F719F1B6C81DE2BD38AB811994E3089B35C8E9314813A770AC7AE25D8D', + 'system': 'integracao', + 'target': '', + 'type': 'logger', + 'version': ''} diff --git a/tecnospeed_rpc/src/logs/integracao_25 b/tecnospeed_rpc/src/logs/integracao_25 new file mode 100644 index 0000000..e69de29 diff --git a/tecnospeed_rpc/src/logs/integracao_26 b/tecnospeed_rpc/src/logs/integracao_26 new file mode 100644 index 0000000..e69de29 diff --git a/tecnospeed_rpc/src/logs/integracao_27 b/tecnospeed_rpc/src/logs/integracao_27 new file mode 100644 index 0000000..f61511b --- /dev/null +++ b/tecnospeed_rpc/src/logs/integracao_27 @@ -0,0 +1,140 @@ +2026-03-27 14:57:57,807 - integracao - INFO - { 'caller': 'run', + 'client': '', + 'logDate': '2026-03-27 14:57:57.807604-03:00', + 'msg': 'Conectando ao RabbitMQ', + 'origin': '', + 'service': 'tecnospeed', + 'system': 'integracao', + 'version': '2.0.1'} +2026-03-27 14:57:58,282 - integracao - INFO - { 'caller': 'run', + 'client': '', + 'logDate': '2026-03-27 14:57:58.282491-03:00', + 'msg': "Configuração do sistema carregada - serviceid='local' " + "servicekey='local' origin='local' " + "amqps='amqp://guest:guest@localhost:5672/'", + 'origin': '', + 'service': 'tecnospeed', + 'system': 'integracao', + 'version': '2.0.1'} +2026-03-27 14:59:07,273 - integracao - INFO - { 'caller': 'callback', + 'client': 'Test', + 'logDate': '2026-03-27 14:59:07.273291-03:00', + 'msg': "Configuração recebida - {'api_key': None, 'x_api_key': " + "'2da392a6-79d2-4304-a8b7-959572c7e44d', 'user_postgres': " + "'postgres', 'pass_postgres': 'postgres', 'host_postgres': " + "'localhost', 'port_postgres': '5432', 'database_postgres': " + "'tecnospeed'}", + 'origin': '', + 'service': 'tecnospeed', + 'system': 'integracao', + 'version': '2.0.1'} +2026-03-27 14:59:08,015 - integracao - INFO - { 'caller': 'callback', + 'client': 'Test', + 'logDate': '2026-03-27 14:59:08.014993-03:00', + 'msg': 'Configuração incompleta', + 'origin': '', + 'service': 'tecnospeed', + 'system': 'integracao', + 'version': '2.0.1'} +2026-03-27 15:00:56,891 - integracao - INFO - { 'caller': 'callback', + 'client': 'Test', + 'logDate': '2026-03-27 15:00:56.890566-03:00', + 'msg': "Configuração recebida - {'api_key': None, 'x_api_key': " + "'2da392a6-79d2-4304-a8b7-959572c7e44d', 'user_postgres': " + "'postgres', 'pass_postgres': 'postgres', 'host_postgres': " + "'localhost', 'port_postgres': '5432', 'database_postgres': " + "'sg_servico_tecnospeed'}", + 'origin': '', + 'service': 'tecnospeed', + 'system': 'integracao', + 'version': '2.0.1'} +2026-03-27 15:00:57,787 - integracao - INFO - { 'caller': 'callback', + 'client': 'Test', + 'logDate': '2026-03-27 15:00:57.787472-03:00', + 'msg': 'Configuração incompleta', + 'origin': '', + 'service': 'tecnospeed', + 'system': 'integracao', + 'version': '2.0.1'} +2026-03-27 15:02:14,102 - integracao - INFO - { 'caller': 'callback', + 'client': 'Test', + 'logDate': '2026-03-27 15:02:14.101859-03:00', + 'msg': "Configuração recebida - {'api_key': None, 'x_api_key': " + "'2da392a6-79d2-4304-a8b7-959572c7e44d', 'user_postgres': " + "'postgres', 'pass_postgres': 'postgres', 'host_postgres': " + "'localhost', 'port_postgres': '5432', 'database_postgres': " + "'tecnospeed'}", + 'origin': '', + 'service': 'tecnospeed', + 'system': 'integracao', + 'version': '2.0.1'} +2026-03-27 15:02:14,792 - integracao - INFO - { 'caller': 'callback', + 'client': 'Test', + 'logDate': '2026-03-27 15:02:14.791942-03:00', + 'msg': 'Configuração incompleta', + 'origin': '', + 'service': 'tecnospeed', + 'system': 'integracao', + 'version': '2.0.1'} +2026-03-27 15:09:33,329 - integracao - INFO - { 'caller': 'run', + 'client': '', + 'logDate': '2026-03-27 15:09:33.329870-03:00', + 'msg': 'Conectando ao RabbitMQ', + 'origin': '', + 'service': 'tecnospeed', + 'system': 'integracao', + 'version': '2.0.1'} +2026-03-27 15:09:33,698 - integracao - INFO - { 'caller': 'run', + 'client': '', + 'logDate': '2026-03-27 15:09:33.698183-03:00', + 'msg': "Configuração do sistema carregada - serviceid='local' " + "servicekey='local' origin='local' " + "amqps='amqp://guest:guest@localhost:5672/'", + 'origin': '', + 'service': 'tecnospeed', + 'system': 'integracao', + 'version': '2.0.1'} +2026-03-27 15:09:39,159 - integracao - INFO - { 'caller': 'callback', + 'client': 'Test', + 'logDate': '2026-03-27 15:09:39.158573-03:00', + 'msg': "Configuração recebida - {'api_key': None, 'x_api_key': " + "'2da392a6-79d2-4304-a8b7-959572c7e44d', 'user_postgres': " + "'postgres', 'pass_postgres': 'postgres', 'host_postgres': " + "'localhost', 'port_postgres': '5432', 'database_postgres': " + "'tecnospeed'}", + 'origin': '', + 'service': 'tecnospeed', + 'system': 'integracao', + 'version': '2.0.1'} +2026-03-27 15:09:44,975 - integracao - INFO - { 'caller': 'callback', + 'client': 'Test', + 'logDate': '2026-03-27 15:09:44.975168-03:00', + 'msg': 'Configuração completa para o cliente Test', + 'origin': '', + 'service': 'tecnospeed', + 'system': 'integracao', + 'version': '2.0.1'} +2026-03-27 15:09:45,763 - integracao - INFO - { 'caller': 'nfe', + 'client': 'Test', + 'logDate': '2026-03-27 15:09:45.763614-03:00', + 'msg': 'Iniciando serviço de NFe - Método: GET', + 'origin': '', + 'service': 'tecnospeed', + 'system': 'integracao', + 'version': '2.0.1'} +2026-03-27 15:09:46,610 - integracao - INFO - { 'caller': 'nfe', + 'client': 'Test', + 'logDate': '2026-03-27 15:09:46.610671-03:00', + 'msg': 'Consultando resumo de NFe', + 'origin': '', + 'service': 'tecnospeed', + 'system': 'integracao', + 'version': '2.0.1'} +2026-03-27 15:09:47,761 - integracao - INFO - { 'caller': 'nfe', + 'client': 'Test', + 'logDate': '2026-03-27 15:09:47.761417-03:00', + 'msg': 'Operação realizada com sucesso', + 'origin': '', + 'service': 'tecnospeed', + 'system': 'integracao', + 'version': '2.0.1'} diff --git a/tecnospeed_rpc/src/tecnospeed_rpc.py b/tecnospeed_rpc/src/tecnospeed_rpc.py new file mode 100644 index 0000000..cce5c97 --- /dev/null +++ b/tecnospeed_rpc/src/tecnospeed_rpc.py @@ -0,0 +1,721 @@ +# IMPORT DAS CLASSES DE TRABALHO +from certificado_digital.certificado_digital import CertificadoDigital +from empresas.empresa import EmpresaController +from NFSe.nfse import NFSeService +from NFSe.Servico.servico import ServicoController +from NFSe.Tomador.tomador import TomadorController +from Relatorios.relatorios import Relatorios +from webhooks.webhooks import Webhooks +from NFe.registroNFe import RegistroNFeService + +# IMPORTS DE MODELS +from Models.config import Config, SystemConfig +from Models.empresa import Empresa +from Models.nfse import NFSe +from Models.servico import Servico +from Models.nfe import NFe +from Models.tomador import Tomador +from Models.payload import validate_payload + +# IMPORTS DE BIBLIOTECAS +from pydantic import ValidationError +import json, sys, time, random, os, uuid + +# IMPORTS DE CLASSES AUXILIARES +from utils.Exchange2 import RPCExchange2, LoggerExchange2, AppIC2Builder +from utils.exchangeModels import RealmType +from utils.banco import Banco + +from utils.redis import RedisClient + +# VERSÃO DO SISTEMA +version = "1.0.0" # Versão inicial do sistema +version = "1.0.1" # Implementando ORM +version = "1.0.2" # Implementando RPC +version = "1.0.3" # Implementando configuração via Redis +version = "2.0.0" # NFe e refatoração da Main +version = "2.0.1" # Refatoração para RPCExchange + + +class Main: + def __init__(self): + self.client = "#" + self.api_key = "" + self.exchange = None + self.banco = None + self.system_config: SystemConfig = None + self.redis = RedisClient(host=os.getenv('REDIS_HOST', 'localhost'), port=6379) + self.service = "tecnospeed" + self.system = "integracao" + self.version = version + self.logger = None + self.url_base = os.getenv('PLUGNOTAS_URL', 'https://api.sandbox.plugnotas.com.br') + self.funcoes = { + "nfe": self.nfe, + "nfse": self.nfse, + "certificado_digital": self.certificado_digital, + "empresa": self.empresa, + "servico": self.servico, + "tomador": self.tomador, + "relatorios": self.relatorios, + "webhooks": self.webhooks + } + + def safe_log(self, realmValue: RealmType, message: tuple): + try: + if self.logger: + self.logger.logIC2(realmValue, message) + except Exception as e: + print(f"[SAFELOG] {realmValue} : {message} | Err: {e}", flush=True) + + # Métodos de serviços orientado a eventos. + + def nfe(self, metodo, dados=None, correlation_id=None): + # Roteador de emissões, baixas e cancelamentos via NFe + id_usr = dados.get('id_usuario', 1) if dados else 1 + try: + self.safe_log(RealmType.LOGS, (f"Iniciando serviço de NFe - Método: {metodo.upper()}",)) + + nfe_service = RegistroNFeService(api_key=self.api_key, + url_base=self.url_base, + banco=self.banco, + logger=self.logger) + + metodo = metodo.lower() + resultado = None + + match metodo: + case "post": + self.safe_log(RealmType.LOGS, ("Cadastrando NFe",)) + nfe = NFe(**dados.get('nfe', dados)) + resultado = nfe_service.cadastrar_nfe(nfe_list=[nfe], data=dados) + + case "get": + self.safe_log(RealmType.LOGS, ("Consultando resumo de NFe",)) + resultado = nfe_service.consultar_resumo(id_nota=dados.get('id')) + + case "download": + self.safe_log(RealmType.LOGS, ("Baixando PDF NFe",)) + resultado = nfe_service.baixar_pdf(id_nota=dados.get('id')) + + case "download_xml": + self.safe_log(RealmType.LOGS, ("Baixando XML NFe",)) + resultado = nfe_service.baixar_xml(id_nota=dados.get('id')) + + case "cancelar": + self.safe_log(RealmType.LOGS, ("Cancelando NFe",)) + resultado = nfe_service.cancelar_nfe(id_nota=dados.get('id'), justificativa=dados.get('justificativa', 'Cancelamento padrão')) + + case "status_cancelamento": + self.safe_log(RealmType.LOGS, ("Consultando status de cancelamento",)) + resultado = nfe_service.consultar_status_cancelamento(id_nota=dados.get('id')) + + case "cce": + self.safe_log(RealmType.LOGS, ("Enviando Carta de Correção NFe",)) + resultado = nfe_service.enviar_cce(id_nota=dados.get('id'), correcao=dados.get('correcao')) + + case "status_cce": + self.safe_log(RealmType.LOGS, ("Consultando status CCe NFe",)) + resultado = nfe_service.consultar_status_cce(id_nota=dados.get('id')) + + case "resumo": + self.safe_log(RealmType.LOGS, ("Consultando resumo de NFe por CNPJ de integracao",)) + resultado = nfe_service.consultar_resumo_por_cnpj_integracao(cnpj=dados.get('cnpj'), id_integracao=dados.get('id_integracao', dados.get('id'))) + + case _: + self.safe_log(RealmType.ERRORS, ("Método não encontrado",)) + self.banco.logar_sistema(id_usuario=id_usr, acao=metodo.upper(), tabela='nfe', status='ERRO', mensagem='Método não encontrado', cliente=self.client, correlation_id=correlation_id) + return json.dumps({"status": "error", "message": "Método não encontrado"}) + + if isinstance(resultado, dict) and "error" in resultado: + msg = resultado["error"] + if "response" in resultado: + msg = f"{msg} | Detalhes: {resultado['response']}" + raise Exception(msg) + + self.safe_log(RealmType.LOGS, ("Operação realizada com sucesso",)) + self.banco.logar_sistema(id_usuario=id_usr, acao=metodo.upper(), tabela='nfe', status='SUCESSO', mensagem='Operação concluída', json=json.dumps(resultado) if resultado else '{}', cliente=self.client, correlation_id=correlation_id) + return self._responder("ok", resultado) + except Exception as e: + self.safe_log(RealmType.ERRORS, (f"Erro ao executar método {metodo} - {e}",)) + self.banco.logar_sistema(id_usuario=id_usr, acao=metodo.upper() if isinstance(metodo, str) else 'NFE', tabela='nfe', status='ERRO', mensagem=str(e), cliente=self.client, correlation_id=correlation_id) + raise e + + def nfse(self, metodo, dados=None, correlation_id=None): + # Roteador de Notas Fiscais de Roteiro/Serviço (NFSe) + id_usr = dados.get('id_usuario', 1) if dados else 1 + try: + self.safe_log(RealmType.LOGS, (f"Iniciando serviço de NFSe - Método: {metodo.upper()}",)) + + nfse_service = NFSeService(api_key=self.api_key, + banco=self.banco, + url_base=self.url_base, + logger=self.logger) + + metodo = metodo.lower() + resultado = None + + match metodo: + case "post": + self.safe_log(RealmType.LOGS, ("Cadastrando NFSe",)) + nfse = NFSe(**dados) + resultado = nfse_service.cadastrar_nfse(nfse=nfse, data=dados) + + case "get": + self.safe_log(RealmType.LOGS, ("Listando NFSe",)) + resultado = nfse_service.buscar_nfse(id_nfse=dados.get('id')) if dados and 'id' in dados else nfse_service.listar_nfse() + + case "download": + self.safe_log(RealmType.LOGS, ("Baixando NFSe",)) + resultado = nfse_service.baixar_nfse(id_nfse=dados.get('id')) + + case "download_xml": + self.safe_log(RealmType.LOGS, ("Baixando XML NFSe",)) + resultado = nfse_service.baixar_xml_nfse(id_nfse=dados.get('id')) + + case "cancelar": + self.safe_log(RealmType.LOGS, ("Cancelando NFSe",)) + resultado = nfse_service.solicitar_cancelamento_nfse(id_nfse=dados.get('id')) + + case "status_cancelamento": + self.safe_log(RealmType.LOGS, ("Consultando status de cancelamento",)) + resultado = nfse_service.consultar_status_cancelamento_nfse(id_nfse=dados.get('id')) + + case "resumo": + self.safe_log(RealmType.LOGS, ("Consultando resumo de NFSe",)) + resultado = nfse_service.consultar_resumo_nfse(id_nfse=dados.get('id'), cnpj=dados.get('cnpj')) + + case "historico": + self.safe_log(RealmType.LOGS, ("Baixando histórico de NFSe",)) + resultado = nfse_service.baixar_histotico_nfse(id_nfse=dados.get('id'), cnpj=dados.get('cnpj')) + + case _: + self.safe_log(RealmType.ERRORS, ("Método não encontrado",)) + self.banco.logar_sistema(id_usuario=id_usr, acao=metodo.upper(), tabela='nfse', status='ERRO', mensagem='Método não encontrado', cliente=self.client, correlation_id=correlation_id) + return json.dumps({"status": "error", "message": "Método não encontrado"}) + + if isinstance(resultado, dict) and "error" in resultado: + raise Exception(json.dumps(resultado)) + + self.safe_log(RealmType.LOGS, ("Operação realizada com sucesso",)) + self.banco.logar_sistema(id_usuario=id_usr, acao=metodo.upper(), tabela='nfse', status='SUCESSO', mensagem='Operação concluída', json=json.dumps(resultado) if resultado else '{}', cliente=self.client, correlation_id=correlation_id) + return self._responder("ok", resultado) + except Exception as e: + self.safe_log(RealmType.ERRORS, (f"Erro ao executar método {metodo} - {e}",)) + self.banco.logar_sistema(id_usuario=id_usr, acao=metodo.upper() if isinstance(metodo, str) else 'NFSE', tabela='nfse', status='ERRO', mensagem=str(e), cliente=self.client, correlation_id=correlation_id) + raise e + + def certificado_digital(self, metodo, dados=None, correlation_id=None): + # Gerenciamento de envio dos certificados A3/A1 + id_usr = dados.get('id_usuario', 1) if dados else 1 + try: + self.safe_log(RealmType.LOGS, (f"Iniciando serviço de Certificado Digital - Método: {metodo.upper()}",)) + + certificado_digital = CertificadoDigital(api_key=self.api_key, banco=self.banco, url_base=self.url_base, logger=self.logger) + + # Log de início da operação para rastreabilidade + self.banco.logar_sistema(id_usuario=id_usr, acao=metodo.upper(), tabela='certificado_digital', status='INICIO', mensagem=f'Iniciando operação de {metodo.upper()}', cliente=self.client, correlation_id=correlation_id) + + metodo = metodo.lower() + resultado = None + + match metodo: + case "post": + self.safe_log(RealmType.LOGS, ("Cadastrando certificado digital",)) + resultado = certificado_digital.post_form_data(data=dados) + case "get": + self.safe_log(RealmType.LOGS, ("Listando certificados digitais",)) + resultado = certificado_digital.get(id=dados.get('id')) if dados and 'id' in dados else certificado_digital.get() + case "put": + self.safe_log(RealmType.LOGS, ("Atualizando certificado digital",)) + resultado = certificado_digital.put_certificado(id=dados.get('id'), data=dados) + case _: + self.safe_log(RealmType.ERRORS, ("Método não encontrado",)) + self.banco.logar_sistema(id_usuario=id_usr, acao=metodo.upper(), tabela='certificado_digital', status='ERRO', mensagem='Método não encontrado', cliente=self.client, correlation_id=correlation_id) + return json.dumps({"status": "error", "message": "Método não encontrado"}) + + if isinstance(resultado, dict) and "error" in resultado: + msg = resultado["error"] + if "response" in resultado: + msg = f"{msg} | Detalhes: {resultado['response']}" + raise Exception(msg) + + self.safe_log(RealmType.LOGS, ("Operação realizada com sucesso",)) + self.banco.logar_sistema(id_usuario=id_usr, acao=metodo.upper(), tabela='certificado_digital', status='SUCESSO', mensagem='Operação concluída', json=json.dumps(resultado) if resultado else '{}', cliente=self.client, correlation_id=correlation_id) + return self._responder("ok", resultado) + + except Exception as e: + self.safe_log(RealmType.ERRORS, (f"Erro ao executar método {metodo} - {e}",)) + self.banco.logar_sistema(id_usuario=id_usr, acao=metodo.upper() if isinstance(metodo, str) else 'CERT', tabela='certificado_digital', status='ERRO', mensagem=str(e), cliente=self.client, correlation_id=correlation_id) + raise e + + def empresa(self, metodo, dados=None, correlation_id=None): + # Intermediador do cadastro e listagem das Empresas emissoras + id_usr = dados.get('id_usuario', 1) if dados else 1 + try: + self.safe_log(RealmType.LOGS, (f"Iniciando serviço de Empresa - Método: {metodo.upper()}",)) + + empresa_controller = EmpresaController( + api_key=self.api_key, + banco=self.banco, + url_base=self.url_base, + logger=self.logger, + client=self.client + ) + + metodo = metodo.lower() + resultado = None + + match metodo: + case "post": + self.safe_log(RealmType.LOGS, ("Cadastrando empresa",)) + if dados and 'incentivoCultural' in dados: + dados['incentivadorCultural'] = dados.pop('incentivoCultural') + empresa = Empresa(**dados) + resultado = empresa_controller.post(empresa=empresa, data=dados, correlation_id=correlation_id) + + case "get": + self.safe_log(RealmType.LOGS, ("Listando empresas",)) + resultado = empresa_controller.get(cnpj=dados.get('cnpj'), data=dados, correlation_id=correlation_id) if dados and 'cnpj' in dados else empresa_controller.get(data=dados, correlation_id=correlation_id) + + case "post_file": + self.safe_log(RealmType.LOGS, ("Realizando upload de logotipo",)) + if not dados or 'file' not in dados or 'cnpj' not in dados: + raise ValueError("Parâmetros 'file' e 'cnpj' são obrigatórios para 'post_file'") + resultado = empresa_controller.post_file(file=dados['file'], cnpj=dados['cnpj'], data=dados, correlation_id=correlation_id) + + case "patch": + self.safe_log(RealmType.LOGS, ("Atualizando certificado da empresa",)) + if not dados or 'cnpj' not in dados or 'certificado' not in dados: + raise ValueError("Parâmetros 'cnpj' e 'certificado' (ID) são obrigatórios para 'patch'") + resultado = empresa_controller.patch_certificado(cnpj=dados['cnpj'], certificado=dados['certificado'], data=dados, correlation_id=correlation_id) + + case _: + self.safe_log(RealmType.ERRORS, ("Método não encontrado",)) + self.banco.logar_sistema(id_usuario=id_usr, acao=metodo.upper(), tabela='empresa', status='ERRO', mensagem='Método não encontrado', cliente=self.client, correlation_id=correlation_id) + return json.dumps({"status": "error", "message": "Método não encontrado"}) + + if isinstance(resultado, dict) and "error" in resultado: + msg = resultado["error"] + if "response" in resultado: + msg = f"{msg} | Detalhes: {resultado['response']}" + raise Exception(msg) + + self.safe_log(RealmType.LOGS, ("Operação realizada com sucesso",)) + self.banco.logar_sistema(id_usuario=id_usr, acao=metodo.upper(), tabela='empresa', status='SUCESSO', mensagem='Operação concluída', json=json.dumps(resultado) if resultado else '{}', cliente=self.client, correlation_id=correlation_id) + return self._responder("ok", resultado) + + except Exception as e: + self.safe_log(RealmType.ERRORS, (f"Erro ao executar método {metodo.upper()} - {str(e)}",)) + self.banco.logar_sistema(id_usuario=id_usr, acao=metodo.upper() if isinstance(metodo, str) else 'EMPRESA', tabela='empresa', status='ERRO', mensagem=str(e), cliente=self.client, correlation_id=correlation_id) + return json.dumps({"status": "error", "message": str(e)}) + + def servico(self, metodo, dados=None, correlation_id=None): + # Intermediador do CRUD de Serviços com configurações de impostos + id_usr = dados.get('id_usuario', 1) if dados else 1 + try: + self.safe_log(RealmType.LOGS, (f"Iniciando serviço de Serviço - Método: {metodo.upper()}",)) + + servico_controller = ServicoController( + api_key=self.api_key, + banco=self.banco, + url_base=self.url_base, + logger=self.logger + ) + + metodo = metodo.lower() + resultado = None + + match metodo: + case "post": + self.safe_log(RealmType.LOGS, ("Cadastrando serviço",)) + try: + servico = Servico(**dados) + except ValidationError as e: + self.safe_log(RealmType.ERRORS, (f"Erro ao validar dados do serviço: {e}",)) + self.banco.logar_sistema(id_usuario=id_usr, acao='POST', tabela='servico', status='ERRO', mensagem=f'Validação: {e}', cliente=self.client, correlation_id=correlation_id) + return json.dumps({"status": "error", "message": str(e)}) + resultado = servico_controller.cadastrar_servico(servico=servico, data=dados) + + case "get": + self.safe_log(RealmType.LOGS, ("Listando serviços",)) + if dados and "id" in dados: + resultado = servico_controller.buscar_servico(id_servico=dados.get('id'), data=dados) + else: + resultado = servico_controller.listar_servicos(data=dados) + + case "put": + self.safe_log(RealmType.LOGS, ("Atualizando serviço",)) + resultado = servico_controller.atualizar_servico(id_servico=dados.get('id'), servico=dados) + + case "delete": + self.safe_log(RealmType.LOGS, ("Deletando serviço",)) + resultado = servico_controller.deletar_servico(id_servico=dados.get('id')) + + case _: + self.safe_log(RealmType.ERRORS, ("Método não encontrado",)) + self.banco.logar_sistema(id_usuario=id_usr, acao=metodo.upper(), tabela='servico', status='ERRO', mensagem='Método não encontrado', cliente=self.client, correlation_id=correlation_id) + return json.dumps({"status": "error", "message": "Método não encontrado"}) + + if isinstance(resultado, dict) and "error" in resultado: + msg = resultado["error"] + if "response" in resultado: + msg = f"{msg} | Detalhes: {resultado['response']}" + raise Exception(msg) + + self.safe_log(RealmType.LOGS, ("Operação realizada com sucesso",)) + self.banco.logar_sistema(id_usuario=id_usr, acao=metodo.upper(), tabela='servico', status='SUCESSO', mensagem='Operação concluída', cliente=self.client, correlation_id=correlation_id) + return self._responder("ok", resultado) + + except Exception as e: + self.safe_log(RealmType.ERRORS, (f"Erro ao executar método {metodo} - {e}",)) + self.banco.logar_sistema(id_usuario=id_usr, acao=metodo.upper() if isinstance(metodo, str) else 'SERVICO', tabela='servico', status='ERRO', mensagem=str(e), cliente=self.client, correlation_id=correlation_id) + raise e + + def tomador(self, metodo, dados=None, correlation_id=None): + # Gerencia clientes/clientes fiscais envolvidos nas transações (Tomadores) + id_usr = dados.get('id_usuario', 1) if dados else 1 + try: + self.safe_log(RealmType.LOGS, (f"Iniciando serviço de Tomador - Método: {metodo.upper()}",)) + + tomador_controller = TomadorController( + api_key=self.api_key, + banco=self.banco, + url_base=self.url_base, + logger=self.logger + ) + + metodo = metodo.lower() + resultado = None + + match metodo: + case "post": + self.safe_log(RealmType.LOGS, ("Cadastrando tomador",)) + tomador_model = Tomador(**dados) + resultado = tomador_controller.cadastrar_tomador(tomador=tomador_model, data=dados) + + case "get": + self.safe_log(RealmType.LOGS, ("Listando tomadores",)) + resultado = tomador_controller.buscar_tomador(cpf_cnpj=dados.get('cpf_cnpj')) if dados and 'cpf_cnpj' in dados else tomador_controller.buscar_tomador() + + case "put": + self.safe_log(RealmType.LOGS, ("Atualizando tomador",)) + tomador_model = Tomador(**dados) + resultado = tomador_controller.atualizar_tomador(cpf_cnpj=dados.get('cpf_cnpj'), tomador=tomador_model) + + case "delete": + self.safe_log(RealmType.LOGS, ("Deletando tomador",)) + resultado = tomador_controller.deletar_tomador(cpf_cnpj=dados.get('cpf_cnpj')) + + case _: + self.safe_log(RealmType.ERRORS, ("Método não encontrado",)) + self.banco.logar_sistema(id_usuario=id_usr, acao=metodo.upper(), tabela='tomador', status='ERRO', mensagem='Método não encontrado', cliente=self.client, correlation_id=correlation_id) + return json.dumps({"status": "error", "message": "Método não encontrado"}) + + if isinstance(resultado, dict) and "error" in resultado: + msg = resultado["error"] + if "response" in resultado: + msg = f"{msg} | Detalhes: {resultado['response']}" + raise Exception(msg) + + self.safe_log(RealmType.LOGS, ("Operação realizada com sucesso",)) + self.banco.logar_sistema(id_usuario=id_usr, acao=metodo.upper(), tabela='tomador', status='SUCESSO', mensagem='Operação concluída', cliente=self.client, correlation_id=correlation_id) + return self._responder("ok", resultado) + except Exception as e: + self.safe_log(RealmType.ERRORS, (f"Erro ao executar método {metodo} - {e}",)) + self.banco.logar_sistema(id_usuario=id_usr, acao=metodo.upper() if isinstance(metodo, str) else 'TOMADOR', tabela='tomador', status='ERRO', mensagem=str(e), cliente=self.client, correlation_id=correlation_id) + raise e + + def relatorios(self, metodo, dados=None, correlation_id=None): + # Extrai planilhas mensais/períodicas e relatórios aglomerados + id_usr = dados.get('id_usuario', 1) if dados else 1 + try: + self.safe_log(RealmType.LOGS, (f"Iniciando serviço de Relatórios - Método: {metodo.upper()}",)) + + relatorios = Relatorios( + url_base=self.url_base, + api_key=self.api_key, + banco=self.banco, + logger=self.logger + ) + metodo = metodo.lower() + resultado = None + + match metodo: + case "nfe_emitidas": + self.safe_log(RealmType.LOGS, ("Relatório de NF-e emitidas",)) + resultado = relatorios.nfe_emitidas() + + case "nfe_por_cnpj": + self.safe_log(RealmType.LOGS, ("Relatório de NF-e por CNPJ",)) + resultado = relatorios.nfe_por_cnpj(cnpj=dados.get('cnpj')) + + case "nfse_emitidas": + self.safe_log(RealmType.LOGS, ("Relatório de NFS-e emitidas",)) + resultado = relatorios.nfse_emitidas( + data_inicial=dados.get('data_inicial'), + data_final=dados.get('data_final') + ) + + case "nfse_por_cnpj": + self.safe_log(RealmType.LOGS, ("Relatório de NFS-e por CNPJ",)) + resultado = relatorios.nfse_por_cnpj( + cnpj=dados.get('cnpj'), + data_inicial=dados.get('data_inicial'), + data_final=dados.get('data_final') + ) + + case _: + self.safe_log(RealmType.ERRORS, ("Método não encontrado",)) + self.banco.logar_sistema(id_usuario=id_usr, acao=metodo.upper(), tabela='relatorios', status='ERRO', mensagem='Método não encontrado', cliente=self.client, correlation_id=correlation_id) + return json.dumps({"status": "error", "message": "Método não encontrado"}) + + if isinstance(resultado, dict) and "error" in resultado: + msg = resultado["error"] + if "response" in resultado: + msg = f"{msg} | Detalhes: {resultado['response']}" + raise Exception(msg) + + self.safe_log(RealmType.LOGS, ("Operação realizada com sucesso",)) + self.banco.logar_sistema(id_usuario=id_usr, acao=metodo.upper(), tabela='relatorios', status='SUCESSO', mensagem='Operação concluída', cliente=self.client, correlation_id=correlation_id) + return self._responder("ok", resultado) + + except Exception as e: + self.safe_log(RealmType.ERRORS, (f"Erro ao executar método {metodo} - {e}",)) + self.banco.logar_sistema(id_usuario=id_usr, acao=metodo.upper() if isinstance(metodo, str) else 'RELATORIOS', tabela='relatorios', status='ERRO', mensagem=str(e), cliente=self.client, correlation_id=correlation_id) + raise e + + def webhooks(self, metodo, dados=None, correlation_id=None): + # Gerencia as rotas externas para qual a PlugNotas envia atualização + id_usr = dados.get('id_usuario', 1) if dados else 1 + try: + self.safe_log(RealmType.LOGS, (f"Iniciando serviço de Webhooks - Método: {metodo.upper()}",)) + + webhooks = Webhooks( + url_base=self.url_base, + api_key=self.api_key, + banco=self.banco, + logger=self.logger + ) + + metodo = metodo.lower() + resultado = None + + match metodo: + case "get": + self.safe_log(RealmType.LOGS, ("Consultando webhooks",)) + resultado = webhooks.consultar_webhooks(cnpj=dados.get('cnpj')) + + case "post": + self.safe_log(RealmType.LOGS, ("Criando webhook",)) + resultado = webhooks.criar_webhook(cnpj=dados.get('cnpj'), url=dados.get('url')) + + case "delete": + self.safe_log(RealmType.LOGS, ("Deletando webhook",)) + resultado = webhooks.deletar_webhook(cnpj=dados.get('cnpj')) + + case "put": + self.safe_log(RealmType.LOGS, ("Atualizando webhook",)) + resultado = webhooks.atualizar_webhook(cnpj=dados.get('cnpj'), url=dados.get('url')) + + case "test": + self.safe_log(RealmType.LOGS, ("Testando webhook",)) + resultado = webhooks.testar_webhook(cnpj=dados.get('cnpj')) + + case _: + self.safe_log(RealmType.ERRORS, ("Método não encontrado",)) + self.banco.logar_sistema(id_usuario=id_usr, acao=metodo.upper(), tabela='webhooks', status='ERRO', mensagem='Método não encontrado', cliente=self.client, correlation_id=correlation_id) + return json.dumps({"status": "error", "message": "Método não encontrado"}) + + if isinstance(resultado, dict) and "error" in resultado: + msg = resultado["error"] + if "response" in resultado: + msg = f"{msg} | Detalhes: {resultado['response']}" + raise Exception(msg) + + self.safe_log(RealmType.LOGS, ("Operação realizada com sucesso",)) + self.banco.logar_sistema(id_usuario=id_usr, acao=metodo.upper(), tabela='webhooks', status='SUCESSO', mensagem='Operação concluída', cliente=self.client, correlation_id=correlation_id) + return self._responder("ok", resultado) + + except Exception as e: + self.safe_log(RealmType.ERRORS, (f"Erro ao executar método {metodo} - {e}",)) + self.banco.logar_sistema(id_usuario=id_usr, acao=metodo.upper() if isinstance(metodo, str) else 'WEBHOOKS', tabela='webhooks', status='ERRO', mensagem=str(e), cliente=self.client, correlation_id=correlation_id) + raise e + + def callback(self, message): + #Descriptografa a msg, e repassa pra função correta (ex: nfe) + try: + dado = json.loads(message) + + hmac = dado.get('hmac') + payload = dado.get('payload') + target = dado.get('target', '').lower() + + if not hmac and not payload and 'metodo' not in dado: + return "{}" + + if hmac and payload: + servicekey = self.system_config.servicekey.ljust(32, '0')[:32] + self.exchange.setCrypto(serviceid=self.system_config.serviceid, + servicekey=servicekey) + + if self.exchange.getPayload(hmac, payload): + payload_dict = json.loads(self.exchange.payload) + + self.client = payload_dict.get('cliente') + self.logger.client = self.client + + redis_key = f"config:{self.client}" + stored_config_str = self.redis.get(redis_key) + stored_config = json.loads(stored_config_str) if stored_config_str else {} + + incoming_config = payload_dict.get('config', {}) + + if incoming_config: + final_config_dict = {**stored_config, **incoming_config} + self.redis.set(redis_key, json.dumps(final_config_dict)) + payload_dict['config'] = final_config_dict + self.safe_log(RealmType.LOGS, (f"Configuração atualizada e salva para o cliente {self.client}",)) + elif stored_config: + payload_dict['config'] = stored_config + self.safe_log(RealmType.LOGS, (f"Usando configuração persistida do Redis para o cliente {self.client}",)) + + try: + if isinstance(payload_dict.get('dados'), dict) and 'incentivoCultural' in payload_dict['dados']: + payload_dict['dados']['incentivadorCultural'] = payload_dict['dados'].pop('incentivoCultural') + + validate_payload(payload_dict) + self.safe_log(RealmType.LOGS, (f"Payload validado: {payload_dict.get('fila')}/{payload_dict.get('metodo')}",)) + except (ValidationError, ValueError) as ve: + self.safe_log(RealmType.ERRORS, (f"Payload inválido: {str(ve)}",)) + print(f"\n[DEBUG ERROR] Payload com validação falha: {str(ve)}\n", flush=True) + return json.dumps({"status": "error", "message": f"Payload inválido: {str(ve)}"}) + + if not payload_dict.get('config'): + self.safe_log(RealmType.ERRORS, (f"Configuração não encontrada para o cliente {self.client}",)) + return json.dumps({"status": "error", "message": "Configuração incompleta e não encontrada no sistema"}) + + config = Config(**payload_dict['config']) + self.safe_log(RealmType.LOGS, (f"Configuração validada para o cliente {self.client}",)) + + api_key_extraida = config.api_key or config.x_api_key + expected_pg_user = os.getenv("POSTGRES_USER", "tecnospeed") + if config.user_postgres != expected_pg_user: + self.safe_log( + RealmType.ERRORS, + (f"Usuário PostgreSQL inválido: {config.user_postgres}. Permitido: {expected_pg_user}",) + ) + return json.dumps({"status": "error", "message": f"Apenas o usuário PostgreSQL '{expected_pg_user}' é permitido"}) + + if api_key_extraida and config.user_postgres and config.pass_postgres and config.host_postgres and config.port_postgres and config.database_postgres: + + self.api_key = api_key_extraida + self.banco = Banco(user=config.user_postgres, + password=config.pass_postgres, + host=config.host_postgres, + port=config.port_postgres, + database=config.database_postgres) + + self.banco.create_tables() + + self.safe_log(RealmType.LOGS, (f"Configuração completa para o cliente {self.client}",)) + if 'url_base' in payload_dict: + self.url_base = payload_dict['url_base'] + + target_fila = str(payload_dict.get('fila', '')).lower() + if target_fila not in self.funcoes: + self.safe_log(RealmType.ERRORS, (f"Fila '{target_fila}' não encontrada",)) + return json.dumps({"status":"error","message":"Fila não encontrada"}) + + corr_id = self.banco.gerar_correlation_id() + self.safe_log(RealmType.LOGS, (f"[{corr_id}] Processando {target_fila}/{payload_dict['metodo']} para cliente {self.client}",)) + retorno = self.funcoes[target_fila](payload_dict['metodo'], payload_dict['dados'], correlation_id=corr_id) + return retorno + + else: + self.safe_log(RealmType.ERRORS, ("Configuração incompleta",)) + return json.dumps({"status":"error","message":"Configuração incompleta"}) + else: + self.safe_log(RealmType.ERRORS, (f"Erro ao decodificar payload - {payload}",)) + return json.dumps({"status":"error","message":"Erro ao decodificar payload"}) + + else: + if self.logger: + self.safe_log(RealmType.ERRORS, (f"Erro ao receber dado (sem hmac/payload) - {dado}",)) + return json.dumps({"status":"error","message":"Erro ao receber dado"}) + + except Exception as e: + if self.logger: + self.safe_log(RealmType.ERRORS, (f"Erro ao executar callback - {e}",)) + return json.dumps({"status":"error","message":str(e)}) + finally: + if hasattr(self, 'banco') and self.banco: + self.banco.close() + + def run(self): + # Ponto de Partida. Conecta no Redis, constrói os parâmetros AMQP (URL) e inicia o loop RabbitMQ + try: + print("[STATUS] Conectando ao RabbitMQ") + + try: + system_config = self.redis.get("system_config") + if system_config: + system_config = json.loads(system_config) + self.system_config = SystemConfig(**system_config) + print(f"[INFO] Configuração do sistema carregada - {self.system_config}") + else: + msg = "Configuração do sistema não encontrada no Redis" + print(f"[ERRO] {msg}") + raise Exception(msg) + except Exception as e: + msg = f"Erro ao conectar ao Redis - {e}" + print(f"[ERRO] {msg}") + raise + + # Inicializa LoggerExchange2 após obter a URL AMQP do Redis + builder = AppIC2Builder(service_name=self.service, versionstr=self.version, origin=self.system_config.origin) + self.logger = LoggerExchange2( + ic2_builder=builder, + amqps=self.system_config.amqps, + system=self.system, + service=self.service, + version=self.version, + origin=self.system_config.origin + ) + self.safe_log(RealmType.LOGS, ("LoggerExchange2 inicializado com sucesso",)) + + self.exchange = RPCExchange2( + amqps=self.system_config.amqps, + origin=self.system_config.origin, + system=self.system, + service=self.service, + version=self.version, + client=self.client, + workfunction=self.callback, + delivery_mode = 2 + ) + self.safe_log(RealmType.LOGS, (f"Consumindo da fila {self.exchange.queue}",)) + self.exchange.start_consuming() + + except Exception as e: + if self.logger: + self.safe_log(RealmType.ERRORS, (f"Erro fatal ao conectar ao RabbitMQ - {e}",)) + else: + print(f"[ERRO] Erro ao conectar ao RabbitMQ - {e}") + time.sleep(5) + sys.exit(1) + + def _responder(self, status, resultado): + # Auxiliar para formatar a resposta RPC criptografada conforme esperado pelo Exchange2 + print(f"[RPC] Preparando resposta criptografada para o cliente (status: {status})", flush=True) + servicekey = self.system_config.servicekey.ljust(32, '0')[:32] + return json.dumps({ + "status": status, + "payload": resultado, # A classe RPCExchange2 vai criptografar este campo + "serviceid": self.system_config.serviceid, + "servicekey": servicekey + }) + +if __name__ == "__main__": + main = Main() + main.run() diff --git a/tecnospeed_rpc/src/tests/__init__.py b/tecnospeed_rpc/src/tests/__init__.py new file mode 100644 index 0000000..65140f2 --- /dev/null +++ b/tecnospeed_rpc/src/tests/__init__.py @@ -0,0 +1 @@ +# tests package diff --git a/tecnospeed_rpc/src/tests/conftest.py b/tecnospeed_rpc/src/tests/conftest.py new file mode 100644 index 0000000..db70125 --- /dev/null +++ b/tecnospeed_rpc/src/tests/conftest.py @@ -0,0 +1,119 @@ +""" +Fixtures compartilhados para os testes E2E do servico_plugnotas. + +Cria um banco SQLite in-memory, mocks do Logger e da Api, +e uma instância pré-configurada da classe Main para cada teste. +""" +import pytest +import json +import sys +from unittest.mock import MagicMock, patch + +mock_pika = MagicMock() +sys.modules["pika"] = mock_pika + +mock_redis_module = MagicMock() +sys.modules["redis"] = mock_redis_module + +from utils.banco import Base, Banco, LogTable +from utils.api import Api + +@pytest.fixture +def banco_teste(): + """Cria um Banco real com SQLite in-memory para cada teste (isolado).""" + from sqlalchemy import create_engine + from sqlalchemy.orm import sessionmaker + + engine = create_engine("sqlite:///:memory:") + Base.metadata.create_all(engine) + Session = sessionmaker(bind=engine) + + banco = Banco.__new__(Banco) + banco.engine = engine + banco.session = Session() + + yield banco + + banco.session.close() + engine.dispose() + +@pytest.fixture +def mock_logger(): + """Logger que grava todas as chamadas de log em uma lista para inspeção.""" + logger = MagicMock() + logger.logs = [] + logger.client = "TestClient" + + def capture_log(realm, msg_tuple): + logger.logs.append({"realm": str(realm), "message": msg_tuple[0] if msg_tuple else ""}) + + logger.logIC2.side_effect = capture_log + return logger + +@pytest.fixture +def mock_api(): + """Api que retorna respostas configuráveis sem fazer chamadas HTTP reais.""" + api = MagicMock(spec=Api) + api.get.return_value = {"status": "ok"} + api.post.return_value = {"documents": [{"id": "PLUG-001"}]} + api.put.return_value = {"status": "ok"} + api.patch.return_value = {"status": "ok"} + api.delete.return_value = {"status": "ok"} + api.post_file.return_value = {"status": "ok"} + api.post_form_data.return_value = {"data": {"id": "CERT-001"}} + api.get_paginated.return_value = {"data": [], "total": 0} + return api + +@pytest.fixture +def mock_exchange(): + """Exchange que simula setPayload sem enviar nada.""" + exchange = MagicMock() + exchange.payload = "" + exchange.payloads_set = [] + + def capture_payload(p): + exchange.payloads_set.append(p) + exchange.payload = p + + exchange.setPayload.side_effect = capture_payload + return exchange + +@pytest.fixture +def main_instance(banco_teste, mock_logger, mock_exchange): + with patch("utils.Exchange2.RPCExchange2"), \ + patch("utils.Exchange2.QueueExchange2"), \ + patch("utils.Exchange2.LoggerExchange2"), \ + patch("utils.redis.RedisClient"): + + from main import Main + + with patch.object(Main, '__init__', lambda self: None): + main = Main() + + main.client = "TestClient" + main.api_key = "test-api-key-fake" + main.exchange = mock_exchange + main.banco = banco_teste + main.system_config = MagicMock() + main.system_config.serviceid = "test-id" + main.system_config.servicekey = "test-key-32-chars-long-12345678" + main.redis = MagicMock() + main.service = "tecnospeed" + main.system = "integracao" + main.version = "2.0.1-test" + main.logger = mock_logger + main.url_base = "https://api.sandbox.plugnotas.com.br" + + main.funcoes = { + "nfe": main.nfe, + "nfse": main.nfse, + "certificado_digital": main.certificado_digital, + "empresa": main.empresa, + "servico": main.servico, + "tomador": main.tomador, + "relatorios": main.relatorios, + "webhooks": main.webhooks, + } + + return main + diff --git a/tecnospeed_rpc/src/tests/debug_payload.py b/tecnospeed_rpc/src/tests/debug_payload.py new file mode 100644 index 0000000..611e9bb --- /dev/null +++ b/tecnospeed_rpc/src/tests/debug_payload.py @@ -0,0 +1,67 @@ +import json +import os +import sys + +# Garante que o Python encontre os módulos do projeto +sys.path.append(os.path.dirname(os.path.dirname(os.path.abspath(__file__)))) + +from Models.payload import validate_payload + +sample_payload = { + "fila": "nfse", + "cliente": "MassiveNFSeTest", + "metodo": "POST", + "dados": { + "id_empresa": "some-uuid", + "id_usuario": 1, + "idIntegracao": "TEST_123", + "prestador": { + "cpfCnpj": "00000000000000" + }, + "tomador": { + "cpfCnpj": "08114280956", + "razaoSocial": "NFSE EMITIDA EM HOMOLOGACAO", + "email": "contato@tecnospeed.com.br", + "endereco": { + "logradouro": "AVENIDA DUQUE DE CAXIAS", + "numero": "882", + "bairro": "CENTRO", + "codigoCidade": "4115200", + "descricaoCidade": "MARINGA", + "estado": "PR", + "cep": "87020025", + "tipoLogradouro": "Avenida" + } + }, + "servico": { + "codigo": "1.01", + "codigoTributacao": "1.01", + "discriminacao": "SERVICO DE TESTE DE CARGA MASSIVA", + "cnae": "6201501", + "iss": { + "tipoTributacao": 1, + "exigibilidade": 1, + "aliquota": 2.0 + }, + "valor": { + "servico": 100.0 + } + } + }, + "config": { + "x_api_key": "2da392a6-79d2-4304-a8b7-959572c7e44d", + "user_postgres": "tecnospeed", + "pass_postgres": "u7QimG7E!SF//JC2c8", + "host_postgres": "banco", + "port_postgres": "5432", + "database_postgres": "tecnospeed" + }, + "url_base": "https://api.sandbox.plugnotas.com.br" +} + +try: + print("Tentando validar payload...") + validated = validate_payload(sample_payload) + print("Sucesso! Payload válido.") +except Exception as e: + print(f"Erro de validação: {e}") diff --git a/tecnospeed_rpc/src/tests/debug_report_api.py b/tecnospeed_rpc/src/tests/debug_report_api.py new file mode 100644 index 0000000..69bc364 --- /dev/null +++ b/tecnospeed_rpc/src/tests/debug_report_api.py @@ -0,0 +1,33 @@ +import requests +import os +import json + +def debug_report(): + api_key = "2da392a6-79d2-4304-a8b7-959572c7e44d" # From src/utils/api.py comment + url = "https://api.sandbox.plugnotas.com.br/nfse/relatorio?from=2026-04-01&to=2026-04-30" + + headers = { + "x-api-key": api_key + } + + print(f"Testing URL: {url}") + response = requests.get(url, headers=headers) + + print(f"Status Code: {response.status_code}") + try: + print(f"Response Body: {json.dumps(response.json(), indent=2)}") + except: + print(f"Response Body: {response.text}") + + # Test with ISO format + url_iso = "https://api.sandbox.plugnotas.com.br/nfse/relatorio?from=2026-04-01T00:00:00&to=2026-04-30T23:59:59" + print(f"\nTesting URL (ISO): {url_iso}") + response = requests.get(url_iso, headers=headers) + print(f"Status Code: {response.status_code}") + try: + print(f"Response Body: {json.dumps(response.json(), indent=2)}") + except: + print(f"Response Body: {response.text}") + +if __name__ == "__main__": + debug_report() diff --git a/tecnospeed_rpc/src/tests/purge_queue.py b/tecnospeed_rpc/src/tests/purge_queue.py new file mode 100644 index 0000000..e5c00c5 --- /dev/null +++ b/tecnospeed_rpc/src/tests/purge_queue.py @@ -0,0 +1,46 @@ +import pika +import os +import sys +import json + +# Garante que o Python encontre os módulos do projeto +sys.path.append(os.path.dirname(os.path.dirname(os.path.abspath(__file__)))) + +from utils.redis import RedisClient + +def purge_queue(): + # 1. Obter URL do RabbitMQ do Redis + redis_host = os.getenv('REDIS_HOST', 'localhost') + redis_client = RedisClient(host=redis_host, port=6379) + try: + system_config_data = redis_client.get("system_config") + if not system_config_data: + print("Erro: system_config não encontrado no Redis.") + return + system_config = json.loads(system_config_data) + amqps_url = system_config.get("amqps") + except Exception as e: + print(f"Erro ao ler configs do Redis: {e}") + return + + if not amqps_url: + print("Erro: AMQPS URL não encontrada na configuração.") + return + + # 2. Conectar e limpar a fila + queue_name = "integracao_tecnospeed" + try: + params = pika.URLParameters(amqps_url) + connection = pika.BlockingConnection(params) + channel = connection.channel() + + # Tenta realizar o purge + result = channel.queue_purge(queue=queue_name) + print(f"Sucesso! Fila [{queue_name}] limpa. Mensagens removidas: {result.method.message_count}") + + connection.close() + except Exception as e: + print(f"Erro ao limpar a fila: {e}") + +if __name__ == "__main__": + purge_queue() diff --git a/tecnospeed_rpc/src/tests/test.py b/tecnospeed_rpc/src/tests/test.py new file mode 100644 index 0000000..257b05d --- /dev/null +++ b/tecnospeed_rpc/src/tests/test.py @@ -0,0 +1,134 @@ +import sys +import os +import json + +sys.path.append(os.path.dirname(os.path.abspath(__file__))) + +from Models.config import SystemConfig +from utils.redis import RedisClient +from utils.Exchange2 import RPCExchange2 + +def main(): + print("Iniciando script de testes da mensageria...") + + # Carregando configuração do Redis + redis_client = RedisClient(host=os.getenv('REDIS_HOST', 'localhost'), port=6379) + try: + system_config_data = redis_client.get("system_config") + if not system_config_data: + print("Erro: system_config não encontrado no Redis. Certifique-se de que a variável existe.") + return + + system_config = SystemConfig(**json.loads(system_config_data)) + print(f"[{system_config.origin}] Configuração do sistema lida com sucesso.") + + except Exception as e: + print(f"Erro ao ler configs do Redis: {e}") + return + + # Inicializando mensageria RPC + try: + amqps_local = system_config.amqps.replace("host.docker.internal", "localhost") + exchange = RPCExchange2( + amqps=amqps_local, + origin=system_config.origin, + system="integracao", + service="tecnospeed", + version="2.0.1", + client="#" + ) + except Exception as e: + print(f"Falha ao inicializar mensageria: {e}") + return + + # Configurando criptografia e payload + servicekey = system_config.servicekey.ljust(32, '0')[:32] + exchange.setCrypto(serviceid=system_config.serviceid, servicekey=servicekey) + + # Payload da mensageria + + payload_interno = { + "fila": "nfse", + "cliente": "Test", + "metodo": "POST", + "dados": { + "idIntegracao": "INTEGRACAO-NFSE-002", + "prestador": { + "cpfCnpj": "08187168000160" + }, + "tomador": { + "cpfCnpj": "08114280956", + "razaoSocial": "João Tomador da Silva", + "inscricaoMunicipal": "isento", + "email": "tomador@email.com", + "endereco": { + "descricaoCidade": "São Paulo", + "cep": "00000000", + "tipoLogradouro": "Rua", + "logradouro": "Dois de Ouro", + "tipoBairro": "Bairro", + "codigoCidade": "3550308", + "complemento": "Apto 13", + "estado": "SP", + "numero": "44", + "bairro": "Centro" + } + }, + "servico": { + "codigo": "1.01", + "codigoTributacao": "1.01", + "discriminacao": "Serviços prestados de tecnologia", + "cnae": "6204000", + "iss": { + "tipoTributacao": 6, + "exigibilidade": 1, + "aliquota": 2.5 + }, + "valor": { + "servico": 1500.50, + "descontoCondicionado": 0.0, + "descontoIncondicionado": 0.0 + } + } + }, + "config": { + "x_api_key": os.getenv('PLUGNOTAS_API_KEY', ''), + "user_postgres": os.getenv('POSTGRES_USER', 'tecnospeed'), + "pass_postgres": os.getenv('POSTGRES_PASSWORD', 'tecnospeed'), + "host_postgres": os.getenv('POSTGRES_HOST', 'localhost'), + "port_postgres": os.getenv('POSTGRES_PORT', '5432'), + "database_postgres": os.getenv('POSTGRES_DB', 'tecnospeed') + }, + "url_base": "https://api.sandbox.plugnotas.com.br" + } + + + # Criptografando e enviando tarefa + exchange.setPayload(json.dumps(payload_interno)) + + print("Disparando chamada RPC para o RabbitMQ...") + # Executando chamada RPC + response = exchange.callRPC("Disparo de testes executado pelo test.py") + print("\n[RAW RPC RESPONSE]:") + print(response) + + # Descriptografando a resposta + try: + resp_obj = json.loads(response) + hmac_resp = resp_obj.get('hmac') + payload_resp = resp_obj.get('payload') + + if hmac_resp and payload_resp: + if exchange.getPayload(hmac_resp, payload_resp): + print("\n[DECRYPTED RPC DATA]:") + print(json.dumps(json.loads(exchange.payload), indent=2)) + else: + print("\n[ERROR]: Failed to decrypt response.") + else: + print("\n[INFO]: Response does not contain an encrypted payload.") + + except Exception as e: + print(f"\n[ERROR]: Error processing response: {e}") + +if __name__ == "__main__": + main() diff --git a/tecnospeed_rpc/src/tests/testArch.py b/tecnospeed_rpc/src/tests/testArch.py new file mode 100644 index 0000000..1fe2d01 --- /dev/null +++ b/tecnospeed_rpc/src/tests/testArch.py @@ -0,0 +1,58 @@ +import pytest +from unittest.mock import patch, MagicMock +from fastapi.testclient import TestClient +from src.listenerEndpoint import app + +client = TestClient(app) + +PAYLOAD_VALIDO = { + "id": "60a7c4f1c9d2a00015...", + "status": "CONCLUIDO", + "documento": "nfe" +} +HEADERS_VALIDOS = {"x-plugnotas-signature": "assinatura_hmac_valida"} + +class TestPlugNotasWebhookEndpoint: + + @patch("src.listenerEndpoint.exchange") + def test_deve_receber_payload_e_enfileirar_com_sucesso(self, mock_exchange): + """Critério: Caminho feliz. Valida, enfileira e responde com sucesso (200).""" + mock_exchange.sendTask.return_value = True + + response = client.post("/listenerPlugnotas", json=PAYLOAD_VALIDO, headers=HEADERS_VALIDOS) + + assert response.status_code == 200 + assert response.json() == {"status": "ok", "message": "Webhook recebido e encaminhado para processamento"} + + mock_exchange.setPayload.assert_called_once() + mock_exchange.sendTask.assert_called_once_with("Webhook PlugNotas Recebido") + + @patch("src.listenerEndpoint.exchange") + def test_deve_rejeitar_payload_invalido_antes_de_processar(self, mock_exchange): + """Critério: Integridade de Dados. Valida campos obrigatórios recebidos do Hook.""" + payload_invalido = {"status": "CONCLUIDO"} # Faltando ID e documento + + response = client.post("/listenerPlugnotas", json=payload_invalido, headers=HEADERS_VALIDOS) + + assert response.status_code == 200 + assert response.json()["status"] == "warning" + assert "Campos faltantes" in response.json()["message"] + + @patch("src.listenerEndpoint.exchange") + def test_deve_retornar_erro_se_internal_error_no_rabbitmq(self, mock_exchange): + """Critério: Resiliência. Retorna 500 em caso de erro ao enviar para fila.""" + # Simulando uma queda ao enviar task + mock_exchange.sendTask.side_effect = Exception("RabbitMQ is down") + + response = client.post("/listenerPlugnotas", json=PAYLOAD_VALIDO, headers=HEADERS_VALIDOS) + + assert response.status_code == 500 + assert response.json()["message"] == "Erro interno no processamento do webhook" + + def test_deve_retornar_erro_se_exchange_nao_inicializado(self): + """Critério: Resiliência. Retorna 500 se o exchange global for nulo.""" + # Sem patch de exchange, então pegará o exchange default (None) em client sem lifespan completo + response = client.post("/listenerPlugnotas", json=PAYLOAD_VALIDO) + + assert response.status_code == 500 + assert response.json()["message"] == "Serviço temporariamente indisponível" \ No newline at end of file diff --git a/tecnospeed_rpc/src/tests/test_1000_empresas.py b/tecnospeed_rpc/src/tests/test_1000_empresas.py new file mode 100644 index 0000000..c24a82a --- /dev/null +++ b/tecnospeed_rpc/src/tests/test_1000_empresas.py @@ -0,0 +1,144 @@ +import os +import sys +import json +import time + +# Garante que o Python encontre os módulos do projeto +sys.path.append(os.path.dirname(os.path.dirname(os.path.abspath(__file__)))) + +from Models.config import SystemConfig +from utils.redis import RedisClient +from utils.Exchange2 import RPCExchange2, QueueExchange2 + +def get_empresa_payload(i): + """Gera um payload de empresa com CNPJ e Razão Social baseados no índice i""" + # Gerando um CNPJ fictício baseado no loop para evitar duplicidade no banco + # Ex: 94074986000001, 94074986000002... + cnpj = f"94074986{str(i).zfill(6)}" + + return { + "id_usuario": 1, + "cpfCnpj": cnpj, + "razaoSocial": f"Varejo de Teste {i} LTDA", + "nomeFantasia": f"Varejo {i}", + "inscricaoMunicipal": "0001", + "inscricaoEstadual": "123456789", + "simplesNacional": True, + "regimeTributario": 1, + "regimeTributarioEspecial": 1, + "incentivadorCultural": False, + "incentivoFiscal": True, + "certificado": "5af59d271f6e8f409178fbf3", + "endereco": { + "codigoPais": "1058", + "descricaoPais": "BRASIL", + "tipoLogradouro": "Avenida", + "logradouro": "Avenida de Teste", + "numero": str(i), + "complemento": "Casa", + "tipoBairro": "Bairro", + "bairro": "Centro", + "codigoCidade": "4115200", + "descricaoCidade": "Maringá", + "estado": "PR", + "cep": "87020025" + }, + "nfe": {"ativo": True, "tipoContrato": 0}, + "nfse": {"ativo": False} + } + +def run_rpc_test(exchange, config_rpc): + """Envia uma única empresa via RPC e aguarda a resposta (bom para debug)""" + print("\n--- INICIANDO TESTE RPC INDIVIDUAL ---") + payload_dados = get_empresa_payload(1) + + full_payload = { + "fila": "empresa", + "metodo": "post", + "cliente": "tray", + "url_base": "https://api.sandbox.plugnotas.com.br", + "config": config_rpc, + "dados": payload_dados + } + + print(f"Enviando empresa {payload_dados['cpfCnpj']} via RPC...") + exchange.setPayload(json.dumps(full_payload)) + resposta = exchange.callRPC(json.dumps(full_payload), timeout=30) + + print("\nRESPOSTA DO SERVIDOR:") + print(resposta) + print("--------------------------------------\n") + +def run_mass_test(exchange, config_rpc, total=1000): + """Envia N empresas via Fila (sem esperar resposta), ideal para teste de carga""" + print(f"\n--- INICIANDO CARGA MASSIVA: {total} Mensagens ---") + start_time = time.time() + + for i in range(1, total + 1): + payload_dados = get_empresa_payload(i) + + full_payload = { + "fila": "empresa", + "metodo": "post", + "cliente": "MassTest", + "url_base": "https://api.sandbox.plugnotas.com.br", + "config": config_rpc, + "dados": payload_dados + } + + exchange.setPayload(json.dumps(full_payload)) + exchange.sendMsg(json.dumps(full_payload), service="tecnospeed") + + if i % 100 == 0: + print(f"Enviadas: {i}/{total}...") + + end_time = time.time() + print(f"Carga concluída em {end_time - start_time:.2f} segundos.") + print("--------------------------------------\n") + +def main(): + # 1. Carrega configurações do Redis + redis_host = os.getenv('REDIS_HOST', 'localhost') + redis_client = RedisClient(host=redis_host, port=6379) + + try: + config_data = redis_client.get("system_config") + if not config_data: + print("Erro: system_config não encontrado no Redis.") + return + sc = SystemConfig(**json.loads(config_data)) + except Exception as e: + print(f"Erro ao carregar configs: {e}") + return + + # Configurações de Banco e API para o Worker + config_rpc = { + "x_api_key": "2da392a6-79d2-4304-a8b7-959572c7e44d", + "user_postgres": "tecnospeed", + "pass_postgres": "u7QimG7E%21SF%2F%2FJC2c8", + "host_postgres": "banco", + "port_postgres": "5432", + "database_postgres": "tecnospeed" + } + + print(f"Conectando ao RabbitMQ de {sc.origin}...") + + # 2. Escolha do Modo de Operação + # Mude para True para testar o retorno (RPC Server) ou False para carga assíncrona (Queue Worker) + MODO_DEBUG_RPC = True + + if MODO_DEBUG_RPC: + # Usa RPCExchange2 para modo Síncrono (Request-Response) + exchange = RPCExchange2(sc.amqps, sc.origin, "integracao", "tecnospeed", "2.1.0", client="") + servicekey = sc.servicekey.ljust(32, '0')[:32] + exchange.setCrypto(sc.serviceid, servicekey) + run_rpc_test(exchange, config_rpc) + else: + # Usa QueueExchange2 para modo Assíncrono (Fire and Forget) + exchange = QueueExchange2(sc.amqps, sc.origin, "integracao", "tecnospeed", "2.1.0", client="#") + servicekey = sc.servicekey.ljust(32, '0')[:32] + exchange.setCrypto(sc.serviceid, servicekey) + run_mass_test(exchange, config_rpc, total=1000) + +if __name__ == "__main__": + main() \ No newline at end of file diff --git a/tecnospeed_rpc/src/tests/test_1000_nfe.py b/tecnospeed_rpc/src/tests/test_1000_nfe.py new file mode 100644 index 0000000..feac5a8 --- /dev/null +++ b/tecnospeed_rpc/src/tests/test_1000_nfe.py @@ -0,0 +1,219 @@ +import sys +import os +import json +import time + +# Adiciona o diretório atual ao path para importar as classes locais +sys.path.append(os.path.dirname(os.path.abspath(__file__))) + +from Models.config import SystemConfig +from utils.redis import RedisClient +from utils.Exchange2 import QueueExchange2 +from utils.banco import Banco, EnterpriseTable +import random + +def main(): + print("Iniciando script de teste de carga: 1000 NFes...", flush=True) + + # 1. Carregando configuração do Redis para obter dados do RabbitMQ + redis_host = os.getenv('REDIS_HOST', 'localhost') + redis_client = RedisClient(host=redis_host, port=6379) + try: + system_config_data = redis_client.get("system_config") + if not system_config_data: + print("Erro: system_config não encontrado no Redis.") + return + + system_config = SystemConfig(**json.loads(system_config_data)) + print(f"Configuração [{system_config.origin}] carregada.", flush=True) + except Exception as e: + print(f"Erro ao ler configs do Redis: {e}") + return + + # 2. Inicializando a conexão com a FILA (RabbitMQ) + try: + print("Tentando conectar ao RabbitMQ...", flush=True) + exchange = QueueExchange2( + amqps=system_config.amqps, + origin=system_config.origin, + system="integracao", + service="tecnospeed", + version="2.1.0", + client="#" + ) + except Exception as e: + print(f"Falha ao iniciar mensageria: {e}") + return + + # 3. Configurando criptografia (necessário para o worker aceitar a mensagem) + servicekey = system_config.servicekey.ljust(32, '0')[:32] + exchange.setCrypto(serviceid=system_config.serviceid, servicekey=servicekey) + + # 4. Buscando empresas válidas no banco para o teste + try: + # Configuração do banco - Força conexão interna se estiver no Docker + # O .env às vezes aponta para o IP externo que não resolve dentro do container + db_host = os.getenv('POSTGRES_HOST', 'postgres') + db_port = int(os.getenv('POSTGRES_PORT', 5432)) + + # Se estivermos DENTRO do docker, o host 'postgres' é o correto. + if db_host != 'postgres': + print(f"Aviso: POSTGRES_HOST no ambiente é {db_host}. Tentando 'postgres' primeiro por estar no Docker...") + db_host = 'postgres' + db_port = 5432 + + print(f"Conectando ao banco em {db_host}:{db_port}...") + + banco = Banco( + host=db_host, + port=db_port, + user=os.getenv('POSTGRES_USER', 'tecnospeed'), + password=os.getenv('POSTGRES_PASSWORD', 'u7QimG7E!SF//JC2c8'), + database=os.getenv('POSTGRES_DB', 'tecnospeed') + ) + print("Executando query na tabela Enterprise...", flush=True) + empresas = banco.session.query(EnterpriseTable.id, EnterpriseTable.cnpj).filter(EnterpriseTable.cnpj != None, EnterpriseTable.cnpj != "").limit(100).all() + lista_empresas = [{"id": emp.id, "cnpj": emp.cnpj} for emp in empresas] + if not lista_empresas: + print("Erro: Nenhuma empresa com CNPJ válido encontrada na tabela 'Enterprise'.", flush=True) + return + print(f"Sucesso: {len(lista_empresas)} empresas válidas encontradas para o teste.", flush=True) + except Exception as e: + print(f"Erro ao conectar ao banco de dados: {e}") + return + + # Base do payload fornecido pelo usuário (omitindo emitente fixo para preencher no loop) + payload_base = { + "nfe": { + "idIntegracao": "", # Será preenchido no loop + "presencial": True, + "consumidorFinal": True, + "natureza": "OPERAÇÃO INTERNA", + "destinatario": { + "cpfCnpj": "08114280956", + "razaoSocial": "NF-E EMITIDA EM HOMOLOGACAO", + "email": "contato@tecnospeed.com.br", + "endereco": { + "logradouro": "AVENIDA DUQUE DE CAXIAS", + "numero": "882", + "bairro": "CENTRO", + "codigoCidade": "4115200", + "descricaoCidade": "MARINGA", + "estado": "PR", + "cep": "87020025" + } + }, + "itens": [ + { + "codigo": "1", + "descricao": "PRODUTO DE TESTE", + "ncm": "06029090", + "cest": "0123456", + "cfop": "5101", + "valorUnitario": { + "comercial": 4.67, + "tributavel": 4.67 + }, + "valor": 4.67, + "tributos": { + "icms": { + "origem": "0", + "cst": "00", + "baseCalculo": { + "modalidadeDeterminacao": 0, + "valor": 0 + }, + "aliquota": 0, + "valor": 0 + }, + "pis": { + "cst": "99", + "baseCalculo": { + "valor": 0, + "quantidade": 0 + }, + "aliquota": 0, + "valor": 0 + }, + "cofins": { + "cst": "07", + "baseCalculo": { + "valor": 0 + }, + "aliquota": 0, + "valor": 0 + } + } + } + ], + "pagamentos": [ + { + "aVista": True, + "meio": "01", + "valor": 4.6 + } + ], + "responsavelTecnico": { + "cpfCnpj": "08187168000160", + "nome": "Tecnospeed", + "email": "contato@tecnospeed.com.br", + "telefone": { + "ddd": "44", + "numero": "30379500" + } + } + } + } + + # Estrutura completa exigida pelo mainQUEUE.py + def get_full_payload(id_num, id_empresa, cnpj_empresa): + # Gerando um timestamp para garantir idIntegracao único + ts = int(time.time()) + payload = json.loads(json.dumps(payload_base)) # Deep copy simples + payload["nfe"]["idIntegracao"] = f"TESTE_{ts}_{id_num}" + payload["nfe"]["emitente"] = { + "cpfCnpj": cnpj_empresa, + "inscricaoEstadual": "ISENTO" + } + + return { + "fila": "nfe", + "cliente": "LoadTest1000", + "metodo": "POST", + "dados": { + "id_empresa": id_empresa, + **payload + }, + "config": { + "x_api_key": "2da392a6-79d2-4304-a8b7-959572c7e44d", + "user_postgres": "tecnospeed", + "pass_postgres": "u7QimG7E!SF//JC2c8", + "host_postgres": "postgres", + "port_postgres": "5432", + "database_postgres": "tecnospeed" + }, + "url_base": "https://api.sandbox.plugnotas.com.br" + } + + print(f"Enviando 1000 mensagens para a fila...") + + start_time = time.time() + + for i in range(1, 1001): + empresa_sorteada = random.choice(lista_empresas) + full_payload = get_full_payload(i, id_empresa=empresa_sorteada["id"], cnpj_empresa=empresa_sorteada["cnpj"]) + # É necessário criptografar o payload antes de cada envio + exchange.setPayload(json.dumps(full_payload)) + exchange.sendMsg(json.dumps(full_payload), service="tecnospeed") + + if i % 100 == 0: + print(f"Enviadas: {i}/1000...", flush=True) + + end_time = time.time() + + print("\nConcluído!") + print(f"Tempo total de envio: {end_time - start_time:.2f} segundos.") + print("Agora você pode acompanhar o processamento nos logs do worker (tecnospeed-queue).") + +if __name__ == "__main__": + main() diff --git a/tecnospeed_rpc/src/tests/test_1000_nfse.py b/tecnospeed_rpc/src/tests/test_1000_nfse.py new file mode 100644 index 0000000..62b6a41 --- /dev/null +++ b/tecnospeed_rpc/src/tests/test_1000_nfse.py @@ -0,0 +1,173 @@ +import sys +import os +import json +import time +import random + +# Adiciona o diretório atual ao path para importar as classes locais +sys.path.append(os.path.dirname(os.path.abspath(__file__))) + +# Adiciona o diretório src ao path para encontrar os Models e Utils +sys.path.append(os.path.dirname(os.path.dirname(os.path.abspath(__file__)))) + +from Models.config import SystemConfig +from utils.redis import RedisClient +from utils.Exchange2 import QueueExchange2 +from utils.banco import Banco, EnterpriseTable + +def main(): + print("Iniciando script de teste de carga: 1000 NFSe...", flush=True) + + # 1. Carregando configuração do Redis para obter dados do RabbitMQ + redis_host = os.getenv('REDIS_HOST', 'localhost') + redis_client = RedisClient(host=redis_host, port=6379) + try: + system_config_data = redis_client.get("system_config") + if not system_config_data: + print("Erro: system_config não encontrado no Redis.") + return + + system_config = SystemConfig(**json.loads(system_config_data)) + print(f"Configuração [{system_config.origin}] carregada.", flush=True) + except Exception as e: + print(f"Erro ao ler configs do Redis: {e}") + return + + # 2. Inicializando a conexão com a FILA (RabbitMQ) + try: + print("Tentando conectar ao RabbitMQ...", flush=True) + exchange = QueueExchange2( + amqps=system_config.amqps, + origin=system_config.origin, + system="integracao", + service="tecnospeed", + version="2.1.0", + client="#" + ) + except Exception as e: + print(f"Falha ao iniciar mensageria: {e}") + return + + # 3. Configurando criptografia (necessário para o worker aceitar a mensagem) + servicekey = system_config.servicekey.ljust(32, '0')[:32] + exchange.setCrypto(serviceid=system_config.serviceid, servicekey=servicekey) + + # 4. Buscando empresas válidas no banco para o teste + try: + db_host = os.getenv('POSTGRES_HOST', 'banco') + db_port = int(os.getenv('POSTGRES_PORT', 5432)) + + # Se estivermos DENTRO do docker e o host configurado for o IP externo ou porta mapeada, forçamos 'banco' e '5432' + if os.path.exists('/.dockerenv') or os.path.exists('/proc/1/cgroup'): + print(f"Aviso: Ambiente Docker detectado. Forçando host 'banco' e porta 5432.") + db_host = 'banco' + db_port = 5432 + + print(f"Conectando ao banco em {db_host}:{db_port}...") + + banco = Banco( + host=db_host, + port=db_port, + user=os.getenv('POSTGRES_USER', 'tecnospeed'), + password=os.getenv('POSTGRES_PASSWORD', 'u7QimG7E!SF//JC2c8'), + database=os.getenv('POSTGRES_DB', 'tecnospeed') + ) + print("Executando query na tabela Enterprise...", flush=True) + empresas = banco.session.query(EnterpriseTable.id, EnterpriseTable.cnpj).filter(EnterpriseTable.cnpj != None, EnterpriseTable.cnpj != "").limit(100).all() + lista_empresas = [{"id": emp.id, "cnpj": emp.cnpj} for emp in empresas] + if not lista_empresas: + print("Erro: Nenhuma empresa com CNPJ válido encontrada na tabela 'Enterprise'.", flush=True) + return + print(f"Sucesso: {len(lista_empresas)} empresas válidas encontradas para o teste.", flush=True) + except Exception as e: + print(f"Erro ao conectar ao banco de dados: {e}") + return + + # Base do payload de NFSe + payload_base = { + "nfse": { + "idIntegracao": "", # Será preenchido no loop + "prestador": { + "cpfCnpj": "" # Será preenchido no loop + }, + "tomador": { + "cpfCnpj": "08114280956", + "razaoSocial": "NFSE EMITIDA EM HOMOLOGACAO", + "email": "contato@tecnospeed.com.br", + "endereco": { + "logradouro": "AVENIDA DUQUE DE CAXIAS", + "numero": "882", + "bairro": "CENTRO", + "codigoCidade": "4115200", + "descricaoCidade": "MARINGA", + "estado": "PR", + "cep": "87020025", + "tipoLogradouro": "Avenida" + } + }, + "servico": { + "codigo": "1.01", + "codigoTributacao": "1.01", + "discriminacao": "SERVICO DE TESTE DE CARGA MASSIVA", + "cnae": "6201501", + "iss": { + "tipoTributacao": 1, + "exigibilidade": 1, + "aliquota": 2.0 + }, + "valor": { + "servico": 100.0 + } + } + } + } + + # Estrutura completa exigida pelo mainQUEUE.py + def get_full_payload(id_num, id_empresa, cnpj_empresa): + ts = int(time.time() * 1000) + payload = json.loads(json.dumps(payload_base)) # Deep copy + payload["nfse"]["idIntegracao"] = f"LOAD_NFSE_{ts}_{id_num}" + payload["nfse"]["prestador"]["cpfCnpj"] = cnpj_empresa + + return { + "fila": "nfse", + "cliente": "MassiveNFSeTest", + "metodo": "POST", + "dados": { + "id_empresa": id_empresa, + "id_usuario": 1, + **payload["nfse"] + }, + "config": { + "x_api_key": "2da392a6-79d2-4304-a8b7-959572c7e44d", + "user_postgres": "tecnospeed", + "pass_postgres": "u7QimG7E!SF//JC2c8", + "host_postgres": "banco", + "port_postgres": "5432", + "database_postgres": "tecnospeed" + }, + "url_base": "https://api.sandbox.plugnotas.com.br" + } + + print(f"Enviando 1000 NFSe para a fila...") + + start_time = time.time() + + for i in range(1, 1001): + empresa_sorteada = random.choice(lista_empresas) + full_payload = get_full_payload(i, id_empresa=empresa_sorteada["id"], cnpj_empresa=empresa_sorteada["cnpj"]) + + exchange.setPayload(json.dumps(full_payload)) + exchange.sendMsg(json.dumps(full_payload), service="tecnospeed") + + if i % 100 == 0: + print(f"Enviadas: {i}/1000...", flush=True) + + end_time = time.time() + + print("\nConcluído!") + print(f"Tempo total de envio: {end_time - start_time:.2f} segundos.") + print("Acompanhe o processamento nos logs do worker (tecnospeed-queue).") + +if __name__ == "__main__": + main() diff --git a/tecnospeed_rpc/src/tests/test_api.py b/tecnospeed_rpc/src/tests/test_api.py new file mode 100644 index 0000000..820ebef --- /dev/null +++ b/tecnospeed_rpc/src/tests/test_api.py @@ -0,0 +1,29 @@ +import json +import sys +import os + +sys.path.append(os.path.dirname(os.path.abspath(__file__))) +from utils.api import Api + +api = Api("2da392a6-79d2-4304-a8b7-959572c7e44d") + +print("\n--- Consultando Certificados ---") +# GET /certificado (Lista todos) +certificados = api.get("https://api.sandbox.plugnotas.com.br/certificado") +print("GET /certificado response:") +print(json.dumps(certificados, indent=2)) + +# Se houver certificados, faz um GET específico no primeiro +if isinstance(certificados, list) and len(certificados) > 0: + first_cert_id = certificados[0].get('id') + print(f"\nConsultando certificado específico (ID: {first_cert_id}):") + spec_cert = api.get(f"https://api.sandbox.plugnotas.com.br/certificado/{first_cert_id}") + print(json.dumps(spec_cert, indent=2)) +elif isinstance(certificados, dict) and "data" in certificados: + # Caso a resposta venha com envelope "data" + data_list = certificados["data"] + if data_list: + first_cert_id = data_list[0].get('id') + print(f"\nConsultando certificado específico (ID: {first_cert_id}):") + spec_cert = api.get(f"https://api.sandbox.plugnotas.com.br/certificado/{first_cert_id}") + print(json.dumps(spec_cert, indent=2)) diff --git a/tecnospeed_rpc/src/tests/test_callback.py b/tecnospeed_rpc/src/tests/test_callback.py new file mode 100644 index 0000000..c019517 --- /dev/null +++ b/tecnospeed_rpc/src/tests/test_callback.py @@ -0,0 +1,95 @@ +import json +import pytest +from unittest.mock import patch, MagicMock + + +class TestRoteamento: + + def test_todas_filas_registradas(self, main_instance): + filas_esperadas = ["nfe", "nfse", "certificado_digital", "empresa", + "servico", "tomador", "relatorios", "webhooks"] + for fila in filas_esperadas: + assert fila in main_instance.funcoes, f"Fila '{fila}' não está registrada" + assert callable(main_instance.funcoes[fila]), f"Fila '{fila}' não é callable" + + def test_fila_inexistente_nao_registrada(self, main_instance): + assert "webhook_callback" not in main_instance.funcoes + assert "fila_fantasma" not in main_instance.funcoes + + +class TestCallbackErros: + """Testa que o callback lida corretamente com payloads inválidos.""" + + def test_callback_json_invalido(self, main_instance): + """Payload que não é JSON válido deve retornar erro.""" + resultado = json.loads(main_instance.callback("isso não é json")) + assert resultado["status"] == "error" + + def test_callback_sem_hmac(self, main_instance): + """Payload sem HMAC deve retornar erro.""" + resultado = json.loads(main_instance.callback(json.dumps({"dados": "algo"}))) + assert resultado["status"] == "error" + + def test_callback_com_hmac_e_payload(self, main_instance, mock_exchange): + """Payload com hmac+payload mas sem crypto configurada deve retornar erro.""" + mock_exchange.getPayload.return_value = False + + main_instance.system_config = MagicMock() + main_instance.system_config.servicekey = "testkey1234567890" + main_instance.system_config.serviceid = "test-service" + + resultado = json.loads(main_instance.callback(json.dumps({ + "hmac": "fake-hmac", + "payload": "fake-encrypted-payload" + }))) + + assert resultado["status"] == "error" + + +class TestBancoIntegracao: + """Testa que os logs são gravados no banco durante as operações.""" + + def test_log_gravado_no_banco(self, main_instance, banco_teste, mock_api): + """Ao executar uma operação, deve gravar log no banco.""" + mock_api.get.return_value = {"id": "PLUG-NFE-001", "status": "CONCLUIDO"} + + with patch("NFe.registroNFe.Api", return_value=mock_api): + main_instance.nfe("get", {"id": "PLUG-NFE-001", "id_usuario": 42}) + + from utils.banco import LogTable + logs = banco_teste.session.query(LogTable).all() + + # Deve ter pelo menos 2 logs: INICIO + SUCESSO + assert len(logs) >= 2 + acoes = [log.acao for log in logs] + assert "GET" in acoes + + # Verifica que o id_usuario foi gravado + assert any(log.id_usuario == 42 for log in logs) + + def test_log_erro_gravado(self, main_instance, banco_teste, mock_api): + """Erros também devem ser gravados no banco.""" + mock_api.get.side_effect = Exception("Timeout na API") + + with patch("NFe.registroNFe.Api", return_value=mock_api): + with pytest.raises(Exception): + main_instance.nfe("get", {"id": "PLUG-NFE-001", "id_usuario": 7}) + + from utils.banco import LogTable + logs = banco_teste.session.query(LogTable).filter_by(status="ERRO").all() + assert len(logs) >= 1 + + +class TestExchangePayload: + """Testa que o exchange.setPayload é chamado com o resultado correto.""" + + def test_payload_set_no_sucesso(self, main_instance, mock_api, mock_exchange): + """Ao concluir com sucesso, setPayload deve ser chamado com o resultado.""" + mock_api.get.return_value = {"id": "PLUG-NFE-001", "status": "CONCLUIDO"} + + with patch("NFe.registroNFe.Api", return_value=mock_api): + main_instance.nfe("get", {"id": "PLUG-NFE-001", "id_usuario": 1}) + + assert mock_exchange.setPayload.called + payload_enviado = json.loads(mock_exchange.payloads_set[-1]) + assert payload_enviado["status"] == "CONCLUIDO" diff --git a/tecnospeed_rpc/src/tests/test_nfe.py b/tecnospeed_rpc/src/tests/test_nfe.py new file mode 100644 index 0000000..771402d --- /dev/null +++ b/tecnospeed_rpc/src/tests/test_nfe.py @@ -0,0 +1,179 @@ +import json +import pytest +from unittest.mock import patch, MagicMock + +class TestNFePost: + + @pytest.fixture + def dados_nfe(self): + return { + "id_usuario": 1, + "id_empresa": 10, + "nfe": { + "idIntegracao": "INT-NFE-001", + "presencial": True, + "consumidorFinal": True, + "natureza": "Venda de mercadoria", + "emitente": {"cpfCnpj": "08187168000160"}, + "destinatario": { + "cpfCnpj": "08114280956", + "razaoSocial": "Destinatário Teste", + "endereco": { + "logradouro": "Rua Teste", + "numero": "100", + "bairro": "Centro", + "codigoCidade": "5201405", + "descricaoCidade": "GOIANIA", + "estado": "GO", + "cep": "74000000" + } + }, + "itens": [{ + "codigo": "001", + "descricao": "Produto Teste", + "ncm": "61091000", + "cfop": "5102", + "valorUnitario": {"comercial": 100.0, "tributavel": 100.0}, + "valor": 100.0, + "tributos": { + "icms": { + "origem": "0", + "cst": "00", + "baseCalculo": {"modalidadeDeterminacao": 0, "valor": 100.0}, + "aliquota": 18.0, + "valor": 18.0 + }, + "pis": { + "cst": "01", + "baseCalculo": {"valor": 100.0, "quantidade": 1}, + "aliquota": 1.65, + "valor": 1.65 + }, + "cofins": { + "cst": "01", + "baseCalculo": {"valor": 100.0}, + "aliquota": 7.6, + "valor": 7.6 + } + } + }], + "pagamentos": [{"aVista": True, "meio": "01", "valor": 100.0}] + } + } + + def test_post_nfe_sucesso(self, main_instance, mock_api, dados_nfe): + mock_api.post.return_value = { + "documents": [{"id": "PLUG-NFE-001"}] + } + + with patch("NFe.registroNFe.Api", return_value=mock_api): + resultado = json.loads(main_instance.nfe("post", dados_nfe)) + + assert resultado["status"] == "ok" + assert resultado["data"]["documents"][0]["id"] == "PLUG-NFE-001" + + def test_post_nfe_erro_api(self, main_instance, mock_api, dados_nfe): + mock_api.post.return_value = {"error": "CPF/CNPJ inválido"} + + with patch("NFe.registroNFe.Api", return_value=mock_api): + with pytest.raises(Exception, match="CPF/CNPJ inválido"): + main_instance.nfe("post", dados_nfe) + +class TestNFeGet: + + def test_get_nfe_resumo(self, main_instance, mock_api): + mock_api.get.return_value = { + "id": "PLUG-NFE-001", + "status": "CONCLUIDO", + "xml": "https://..." + } + + with patch("NFe.registroNFe.Api", return_value=mock_api): + resultado = json.loads(main_instance.nfe("get", {"id": "PLUG-NFE-001", "id_usuario": 1})) + + assert resultado["status"] == "ok" + assert resultado["data"]["status"] == "CONCLUIDO" + +class TestNFeDownload: + """Testa download de PDF e XML de NFe.""" + + def test_download_pdf(self, main_instance, mock_api): + mock_api.get.return_value = {"url": "https://cdn.plugnotas.com.br/nfe/pdf/123"} + + with patch("NFe.registroNFe.Api", return_value=mock_api): + resultado = json.loads(main_instance.nfe("download", {"id": "PLUG-NFE-001", "id_usuario": 1})) + + assert resultado["status"] == "ok" + + def test_download_xml(self, main_instance, mock_api): + mock_api.get.return_value = {"url": "https://cdn.plugnotas.com.br/nfe/xml/123"} + + with patch("NFe.registroNFe.Api", return_value=mock_api): + resultado = json.loads(main_instance.nfe("download_xml", {"id": "PLUG-NFE-001", "id_usuario": 1})) + + assert resultado["status"] == "ok" + +class TestNFeCancelar: + + def test_cancelar_nfe_sucesso(self, main_instance, mock_api): + mock_api.post.return_value = {"id": "PLUG-NFE-001", "status": "CANCELAMENTO_SOLICITADO"} + + with patch("NFe.registroNFe.Api", return_value=mock_api): + resultado = json.loads(main_instance.nfe("cancelar", { + "id": "PLUG-NFE-001", + "justificativa": "Erro de emissão", + "id_usuario": 1 + })) + + assert resultado["status"] == "ok" + + def test_status_cancelamento(self, main_instance, mock_api): + mock_api.get.return_value = {"id": "PLUG-NFE-001", "status": "CANCELADO"} + + with patch("NFe.registroNFe.Api", return_value=mock_api): + resultado = json.loads(main_instance.nfe("status_cancelamento", {"id": "PLUG-NFE-001", "id_usuario": 1})) + + assert resultado["status"] == "ok" + +class TestNFeCCe: + + def test_enviar_cce(self, main_instance, mock_api): + mock_api.post.return_value = {"id": "PLUG-NFE-001", "status": "CCE_ENVIADA"} + + with patch("NFe.registroNFe.Api", return_value=mock_api): + resultado = json.loads(main_instance.nfe("cce", { + "id": "PLUG-NFE-001", + "correcao": "Corrigir endereço do destinatário", + "id_usuario": 1 + })) + + assert resultado["status"] == "ok" + + def test_status_cce(self, main_instance, mock_api): + mock_api.get.return_value = {"id": "PLUG-NFE-001", "status": "CCE_AUTORIZADA"} + + with patch("NFe.registroNFe.Api", return_value=mock_api): + resultado = json.loads(main_instance.nfe("status_cce", {"id": "PLUG-NFE-001", "id_usuario": 1})) + + assert resultado["status"] == "ok" + +class TestNFeResumo: + + def test_resumo_por_cnpj(self, main_instance, mock_api): + mock_api.get.return_value = {"id": "PLUG-NFE-001", "chave": "3524..."} + + with patch("NFe.registroNFe.Api", return_value=mock_api): + resultado = json.loads(main_instance.nfe("resumo", { + "cnpj": "08187168000160", + "id_integracao": "INT-001", + "id_usuario": 1 + })) + + assert resultado["status"] == "ok" + +class TestNFeInvalido: + + def test_metodo_invalido(self, main_instance): + resultado = json.loads(main_instance.nfe("inexistente", {"id_usuario": 1})) + assert resultado["status"] == "error" + assert "não encontrado" in resultado["message"].lower() diff --git a/tecnospeed_rpc/src/tests/test_nfse.py b/tecnospeed_rpc/src/tests/test_nfse.py new file mode 100644 index 0000000..be1045a --- /dev/null +++ b/tecnospeed_rpc/src/tests/test_nfse.py @@ -0,0 +1,146 @@ +""" +Testes E2E para o serviço NFSe. +Testa: POST, GET, DOWNLOAD, DOWNLOAD_XML, CANCELAR, +STATUS_CANCELAMENTO, RESUMO, HISTORICO. +""" +import json +import pytest +from unittest.mock import patch, MagicMock + + +@pytest.fixture +def dados_nfse(): + return { + "id_usuario": 1, + "id_empresa": 10, + "idIntegracao": "INT-NFSE-001", + "prestador": {"cpfCnpj": "08187168000160"}, + "tomador": { + "cpfCnpj": "08114280956", + "razaoSocial": "Tomador Teste", + "inscricaoMunicipal": "isento", + "email": "tomador@teste.com", + "endereco": { + "descricaoCidade": "São Paulo", + "cep": "01000000", + "tipoLogradouro": "Rua", + "logradouro": "Teste", + "tipoBairro": "Bairro", + "codigoCidade": "3550308", + "complemento": "", + "estado": "SP", + "numero": "100", + "bairro": "Centro" + } + }, + "servico": { + "codigo": "1.01", + "codigoTributacao": "1.01", + "discriminacao": "Serviços de tecnologia", + "cnae": "6204000", + "iss": {"tipoTributacao": 6, "exigibilidade": 1, "aliquota": 2.5}, + "valor": {"servico": 1500.0, "descontoCondicionado": 0, "descontoIncondicionado": 0} + } + } + + +class TestNFSePost: + def test_post_nfse_sucesso(self, main_instance, mock_api, dados_nfse): + mock_api.post.return_value = {"documents": [{"id": "PLUG-NFSE-001"}]} + + with patch("NFSe.nfse.Api", return_value=mock_api): + resultado = json.loads(main_instance.nfse("post", dados_nfse)) + + assert resultado["status"] == "ok" + assert resultado["data"]["documents"][0]["id"] == "PLUG-NFSE-001" + + def test_post_nfse_erro_api(self, main_instance, mock_api, dados_nfse): + mock_api.post.return_value = {"error": "CNPJ do prestador não cadastrado"} + + with patch("NFSe.nfse.Api", return_value=mock_api): + with pytest.raises(Exception): + main_instance.nfse("post", dados_nfse) + + +class TestNFSeGet: + def test_get_nfse_por_id(self, main_instance, mock_api): + mock_api.get.return_value = {"id": "PLUG-NFSE-001", "status": "CONCLUIDO"} + + with patch("NFSe.nfse.Api", return_value=mock_api): + resultado = json.loads(main_instance.nfse("get", {"id": "PLUG-NFSE-001", "id_usuario": 1})) + + assert resultado["status"] == "ok" + + def test_listar_nfse(self, main_instance, banco_teste): + """Listar NFSe sem ID deve buscar do banco.""" + resultado = json.loads(main_instance.nfse("get", {"id_usuario": 1})) + assert resultado["status"] == "ok" + + +class TestNFSeDownload: + def test_download_pdf(self, main_instance, mock_api): + mock_api.get.return_value = {"pdf_url": "https://..."} + + with patch("NFSe.nfse.Api", return_value=mock_api): + resultado = json.loads(main_instance.nfse("download", {"id": "PLUG-NFSE-001", "id_usuario": 1})) + + assert resultado["status"] == "ok" + + def test_download_xml(self, main_instance, mock_api): + mock_api.get.return_value = {"xml_url": "https://..."} + + with patch("NFSe.nfse.Api", return_value=mock_api): + resultado = json.loads(main_instance.nfse("download_xml", {"id": "PLUG-NFSE-001", "id_usuario": 1})) + + assert resultado["status"] == "ok" + + +class TestNFSeCancelar: + def test_cancelar_nfse(self, main_instance, mock_api): + mock_api.post.return_value = {"status": "CANCELAMENTO_SOLICITADO"} + + with patch("NFSe.nfse.Api", return_value=mock_api): + resultado = json.loads(main_instance.nfse("cancelar", {"id": "PLUG-NFSE-001", "id_usuario": 1})) + + assert resultado["status"] == "ok" + + def test_status_cancelamento(self, main_instance, mock_api): + mock_api.get.return_value = {"status": "CANCELADO"} + + with patch("NFSe.nfse.Api", return_value=mock_api): + resultado = json.loads(main_instance.nfse("status_cancelamento", {"id": "PLUG-NFSE-001", "id_usuario": 1})) + + assert resultado["status"] == "ok" + + +class TestNFSeResumo: + def test_consultar_resumo(self, main_instance, mock_api): + mock_api.get.return_value = {"id": "PLUG-NFSE-001", "numero": "12345"} + + with patch("NFSe.nfse.Api", return_value=mock_api): + resultado = json.loads(main_instance.nfse("resumo", { + "id": "PLUG-NFSE-001", + "cnpj": "08187168000160", + "id_usuario": 1 + })) + + assert resultado["status"] == "ok" + + +class TestNFSeHistorico: + def test_historico_nfse(self, main_instance, mock_api): + mock_api.get.return_value = [{"data": "2026-01-01", "status": "CONCLUIDO"}] + + with patch("NFSe.nfse.Api", return_value=mock_api): + resultado = json.loads(main_instance.nfse("historico", { + "id": "PLUG-NFSE-001", + "id_usuario": 1 + })) + + assert resultado["status"] == "ok" + + +class TestNFSeInvalido: + def test_metodo_invalido(self, main_instance): + resultado = json.loads(main_instance.nfse("inexistente", {"id_usuario": 1})) + assert resultado["status"] == "error" diff --git a/tecnospeed_rpc/src/tests/test_queue.py b/tecnospeed_rpc/src/tests/test_queue.py new file mode 100644 index 0000000..9bc6adb --- /dev/null +++ b/tecnospeed_rpc/src/tests/test_queue.py @@ -0,0 +1,164 @@ +import sys +import os +import json +import time + +sys.path.append(os.path.dirname(os.path.abspath(__file__))) + +from Models.config import SystemConfig +from utils.redis import RedisClient +from utils.Exchange2 import QueueExchange2 + +def main(): + print("Iniciando script de testes da FILA (Queue Exchange)...") + + # Carregando configuração do Redis + redis_host = os.getenv('REDIS_HOST', 'localhost') + redis_client = RedisClient(host=redis_host, port=6379) + try: + system_config_data = redis_client.get("system_config") + if not system_config_data: + print("Erro: system_config não encontrado no Redis.") + return + + system_config = SystemConfig(**json.loads(system_config_data)) + print(f"[{system_config.origin}] Configuração carregada.") + except Exception as e: + print(f"Erro ao ler configs: {e}") + return + + # Inicializando mensageria de FILA + try: + # Nota: O QueueExchange2 envia para 'system_service_client' + exchange = QueueExchange2( + amqps=system_config.amqps, + origin=system_config.origin, + system="integracao", + service="tecnospeed", + version="2.1.0", + client="#" # '#' para bater com a fila padrão do worker (evita erro de validação length) + ) + except Exception as e: + print(f"Falha ao iniciar mensageria: {e}") + return + + # Configurando criptografia + servicekey = system_config.servicekey.ljust(32, '0')[:32] + exchange.setCrypto(serviceid=system_config.serviceid, servicekey=servicekey) + + # Payload (mesmo do teste RPC) + payload_interno = { + "fila": "empresa", + "cliente": "TestQueue", + "metodo": "POST", + "dados": { + "endereco" : { + "codigoPais": "1058", + "descricaoPais": "Brasil", + "tipoLogradouro": "Rua", + "logradouro": "T55", + "numero": "882", + "complemento": "Quadra99 Lote 11/14 Sala 1507", + "tipoBairro": "Zona", + "bairro": "Setor Bueno", + "codigoCidade": "5208707", + "descricaoCidade": "Goiania", + "estado": "GO", + "cep": "74215170" + }, + "nfse" : { + "config" : { + "rps" : { + "lote" : 12, + "numeracao" : [ + { + "numero" : 2, + "serie" : "CTBZ", + "numeracaoAtual" : 2 + } + ] + }, + "email" : { + "envio" : False + }, + "producao" : True, + "envioLimitado" : False, + "integracoes" : [] + }, + "ativo" : True, + "tipoContrato" : 0 + }, + "nfe" : { + "config" : { + "dfe" : { + "primeiroNsu" : True + }, + "email" : { + "envio" : False + }, + "integracoes" : { + "plugStorage" : { + "ativo" : False + } + }, + "producao" : False, + "impressaoFcp" : False, + "impressaoPartilha" : True, + "versaoManual" : "6.0", + "versaoEsquema" : "pl_009", + "numeracao" : [] + }, + "ativo" : False + }, + "nfce" : { + "config" : { + "email" : { + "envio" : False + }, + "integracoes" : { + "plugStorage" : { + "ativo" : False + } + }, + "producao" : False, + "versaoManual" : "6.0", + "versaoEsquema" : "pl_009", + "numeracao" : [] + }, + "ativo" : False + }, + "incentivoFiscal" : False, + "incentivadorCultural" : False, + "cpfCnpj" : "14358405000100", + "inscricaoMunicipal" : "1085328365", + "certificado" : "668698592698d7831c758d52", + "razaoSocial" : "Teste da SIlva do teste", + "simplesNacional" : True, + "regimeTributario" : 1, + "regimeTributarioEspecial" : 6 + }, + "config": { + "x_api_key": "2da392a6-79d2-4304-a8b7-959572c7e44d", + "user_postgres": "tecnospeed", + "pass_postgres": "u7QimG7E!SF//JC2c8", + "host_postgres": "postgres", + "port_postgres": "5432", + "database_postgres": "tecnospeed" + }, + "url_base": "https://api.sandbox.plugnotas.com.br" + } + + # Criptografando e enviando + exchange.setPayload(json.dumps(payload_interno)) + + print("\n--- DEBUG ENVIO ---") + print(f"Alvo (targetQueue): integracao_tecnospeed") + print(f"Config Enviada: {payload_interno['config']['host_postgres']}:{payload_interno['config']['port_postgres']}") + print("-------------------\n") + + print("Disparando mensagem para a FILA...") + exchange.sendMsg(json.dumps(payload_interno), service="tecnospeed") + print("Mensagem enviada com sucesso. Verifique os logs do worker mainQUEUE.py") + +if __name__ == "__main__": + main() diff --git a/tecnospeed_rpc/src/tests/test_servicos.py b/tecnospeed_rpc/src/tests/test_servicos.py new file mode 100644 index 0000000..1228a2a --- /dev/null +++ b/tecnospeed_rpc/src/tests/test_servicos.py @@ -0,0 +1,328 @@ +import json +import pytest +from unittest.mock import patch, MagicMock +from utils.banco import EmpresaTable, ServicoTable, TomadorTable + +class TestEmpresaPost: + @pytest.fixture + def dados_empresa(self): + return { + "id_usuario": 1, + "endereco": { + "codigoPais": "1058", "descricaoPais": "BRASIL", + "tipoLogradouro": "Rua", "logradouro": "Teste", "numero": "123", + "complemento": "", "tipoBairro": "Bairro", "bairro": "Centro", + "codigoCidade": "5201405", "descricaoCidade": "GOIANIA", + "estado": "GO", "cep": "74000000" + }, + "nfse": { + "config": {"rps": {"lote": 1, "numeracao": [1]}, "email": {"envio": True}, + "producao": False, "envioLimitado": False, "integracoes": []}, + "ativo": True, "tipoContrato": 0 + }, + "nfe": { + "config": {"dfe": {"primeiroNsu": False}, "email": {"envio": False}, + "integracoes": {}, "producao": False, "impressaoFcp": False, + "impressaoPartilha": False, "versaoManual": "6.0", + "versaoEsquema": "PL_009_V4", "numeracao": [1]}, + "ativo": False + }, + "nfce": { + "config": {"email": {"envio": False}, "integracoes": {}, "producao": False, + "versaoManual": "6.0", "versaoEsquema": "PL_009_V4", "numeracao": [1]}, + "ativo": False + }, + "incentivoFiscal": False, "incentivadorCultural": False, + "cpfCnpj": "17956143000174", "inscricaoMunicipal": "12345", + "certificado": "CERT-UUID", "razaoSocial": "Empresa Teste SA", + "simplesNacional": True, "regimeTributario": 1, "regimeTributarioEspecial": 0 + } + + def test_post_empresa_sucesso(self, main_instance, mock_api, dados_empresa): + mock_api.post.return_value = {"_id": "PLUG-EMP-001", "cpfCnpj": "17956143000174"} + + with patch("empresas.empresa.Api", return_value=mock_api): + resultado = json.loads(main_instance.empresa("post", dados_empresa)) + + assert resultado["status"] == "ok" + + def test_post_empresa_erro(self, main_instance, mock_api, dados_empresa): + mock_api.post.return_value = { + "error": "Erro", + "response": {"error": {"message": "CNPJ já cadastrado", "data": []}} + } + + with patch("empresas.empresa.Api", return_value=mock_api): + resultado = json.loads(main_instance.empresa("post", dados_empresa)) + + assert resultado["status"] == "error" + + +class TestEmpresaGet: + def test_get_empresa_por_cnpj(self, main_instance, mock_api): + mock_api.get.return_value = {"_id": "PLUG-EMP-001", "cpfCnpj": "17956143000174", "razaoSocial": "Teste SA"} + + with patch("empresas.empresa.Api", return_value=mock_api): + resultado = json.loads(main_instance.empresa("get", {"cnpj": "17956143000174", "id_usuario": 1})) + + assert resultado["status"] == "ok" + + def test_listar_empresas(self, main_instance, mock_api): + mock_api.get_paginated.return_value = {"data": [{"cpfCnpj": "111", "razaoSocial": "Emp A"}], "total": 1} + + with patch("empresas.empresa.Api", return_value=mock_api): + resultado = json.loads(main_instance.empresa("get", {"id_usuario": 1})) + + assert resultado["status"] == "ok" + + +class TestEmpresaInvalido: + def test_metodo_invalido(self, main_instance): + resultado = json.loads(main_instance.empresa("inexistente", {"id_usuario": 1})) + assert resultado["status"] == "error" + + +class TestCertificadoPost: + @pytest.fixture + def dados_certificado(self): + return { + "id_usuario": 1, + "id_empresa": 10, + "arquivo": "CONTEUDO_PFX_BASE64", + "senha": "12345678", + "expirationAt": "2027-12-31" + } + + def test_post_certificado_sucesso(self, main_instance, mock_api, dados_certificado): + mock_api.post_form_data.return_value = {"data": {"id": "CERT-001"}} + + with patch("certificado_digital.certificado_digital.Api", return_value=mock_api): + resultado = json.loads(main_instance.certificado_digital("post", dados_certificado)) + + assert resultado["status"] == "ok" + assert resultado["data"] == "CERT-001" + + +class TestCertificadoGet: + def test_get_certificado_por_id(self, main_instance, mock_api): + mock_api.get.return_value = {"id": "CERT-001", "status": "VALIDO"} + + with patch("certificado_digital.certificado_digital.Api", return_value=mock_api): + resultado = json.loads(main_instance.certificado_digital("get", {"id": "CERT-001", "id_usuario": 1})) + + assert resultado["status"] == "ok" + + def test_listar_certificados(self, main_instance, mock_api): + mock_api.get.return_value = [{"id": "CERT-001"}, {"id": "CERT-002"}] + + with patch("certificado_digital.certificado_digital.Api", return_value=mock_api): + resultado = json.loads(main_instance.certificado_digital("get", {"id_usuario": 1})) + + assert resultado["status"] == "ok" + + +class TestCertificadoInvalido: + def test_metodo_invalido(self, main_instance): + resultado = json.loads(main_instance.certificado_digital("inexistente", {"id_usuario": 1})) + assert resultado["status"] == "error" + +class TestServicoPost: + """Testa criação de Serviço na API + banco.""" + + @pytest.fixture + def dados_servico_completo(self): + """Payload completo do Model Servico com todos campos obrigatórios.""" + return { + "id_usuario": 1, + "codigo": "1.01", + "idIntegracao": "SERV-INT-001", + "discriminacao": "Consultoria em TI", + "codigoTributacao": "1.01", + "cnae": "6204000", + "codigoCidadeIncidencia": "3550308", + "descricaoCidadeIncidencia": "São Paulo", + "unidade": "UN", + "quantidade": 1, + "iss": { + "tipoTributacao": 6, "exigibilidade": 1, + "retido": False, "aliquota": 2.5, "valor": 37.5, + "valorRetido": 0, "processoSuspensao": "" + }, + "obra": {"art": "", "codigo": ""}, + "valor": { + "servico": 1500.0, "baseCalculo": 1500.0, "deducoes": 0, + "descontoCondicionado": 0, "descontoIncondicionado": 0, + "liquido": 1500.0, "unitario": 1500.0, "valorAproximadoTributos": 0 + }, + "deducao": {"tipo": 0, "descricao": ""}, + "retencao": { + "pis": {"aliquota": 0, "valor": 0}, + "cofins": {"aliquota": 0, "valor": 0}, + "csll": {"aliquota": 0, "valor": 0}, + "inss": {"aliquota": 0, "valor": 0}, + "irrf": {"aliquota": 0, "valor": 0}, + "outrasRetencoes": 0, + "cpp": {"aliquota": 0, "valor": 0} + }, + "tributavel": True, + "ibpt": { + "simplificado": {"aliquota": 8.65}, + "detalhado": {"aliquota": {"municipal": 2.5, "estadual": 0, "federal": 6.15}} + }, + "responsavelRetencao": "" + } + + def test_post_servico_sucesso(self, main_instance, mock_api, dados_servico_completo): + mock_api.post.return_value = {"data": {"id": "SERV-001"}} + + with patch("NFSe.Servico.servico.Api", return_value=mock_api): + resultado = json.loads(main_instance.servico("post", dados_servico_completo)) + + assert resultado["status"] == "ok" + + +class TestServicoGet: + def test_listar_servicos(self, main_instance, banco_teste): + """Deve retornar lista vazia quando não há serviços.""" + resultado = json.loads(main_instance.servico("get", {"id_usuario": 1})) + assert resultado["status"] == "ok" + + def test_buscar_servico_por_id(self, main_instance, mock_api): + mock_api.get.return_value = {"id": "SERV-001", "discriminacao": "Consultoria"} + + with patch("NFSe.Servico.servico.Api", return_value=mock_api): + resultado = json.loads(main_instance.servico("get", {"id": "SERV-001", "id_usuario": 1})) + + assert resultado["status"] == "ok" + + +class TestServicoInvalido: + def test_metodo_invalido(self, main_instance): + resultado = json.loads(main_instance.servico("inexistente", {"id_usuario": 1})) + assert resultado["status"] == "error" + +class TestTomadorPost: + @pytest.fixture + def dados_tomador(self): + return { + "id_usuario": 1, + "cpfCnpj": "08114280956", + "inscricaoMunicipal": "isento", + "inscricaoEstadual": "", + "inscricaoSuframa": "", + "indicadorInscricaoEstadual": 9, + "razaoSocial": "Tomador Teste LTDA", + "nomeFantasia": "Tomador Teste", + "endereco": { + "tipoLogradouro": "Rua", "logradouro": "Teste", "numero": "100", + "complemento": "", "tipoBairro": "Bairro", "bairro": "Centro", + "codigoPais": "1058", "descricaoPais": "BRASIL", + "codigoCidade": "3550308", "descricaoCidade": "São Paulo", + "estado": "SP", "cep": "01000000" + }, + "telefone": {"ddd": "11", "numero": "999999999"}, + "email": "tomador@teste.com", + "orgaoPublico": False + } + + def test_post_tomador_sucesso(self, main_instance, mock_api, dados_tomador): + mock_api.post.return_value = {"id": "TOM-001", "cpfCnpj": "08114280956"} + + with patch("NFSe.Tomador.tomador.Api", return_value=mock_api): + resultado = json.loads(main_instance.tomador("post", dados_tomador)) + + assert resultado["status"] == "ok" + + +class TestTomadorGet: + def test_buscar_tomador(self, main_instance, mock_api): + mock_api.get.return_value = {"cpfCnpj": "08114280956", "razaoSocial": "Tomador Teste"} + + with patch("NFSe.Tomador.tomador.Api", return_value=mock_api): + resultado = json.loads(main_instance.tomador("get", {"cpf_cnpj": "08114280956", "id_usuario": 1})) + + assert resultado["status"] == "ok" + + +class TestTomadorInvalido: + def test_metodo_invalido(self, main_instance): + resultado = json.loads(main_instance.tomador("inexistente", {"id_usuario": 1})) + assert resultado["status"] == "error" + +class TestRelatorios: + def test_nfe_emitidas(self, main_instance, mock_api): + mock_api.get_paginated.return_value = {"data": [{"id": "NFE-1"}], "total": 1} + + with patch("Relatorios.relatorios.Api", return_value=mock_api): + resultado = json.loads(main_instance.relatorios("nfe_emitidas", {"id_usuario": 1})) + + assert resultado["status"] == "ok" + + def test_nfe_por_cnpj(self, main_instance, mock_api): + mock_api.get_paginated.return_value = {"data": [], "total": 0} + + with patch("Relatorios.relatorios.Api", return_value=mock_api): + resultado = json.loads(main_instance.relatorios("nfe_por_cnpj", {"cnpj": "08187168000160", "id_usuario": 1})) + + assert resultado["status"] == "ok" + + def test_nfse_emitidas(self, main_instance, mock_api): + mock_api.get_paginated.return_value = {"data": [], "total": 0} + + with patch("Relatorios.relatorios.Api", return_value=mock_api): + resultado = json.loads(main_instance.relatorios("nfse_emitidas", { + "data_inicial": "2026-01-01", "data_final": "2026-12-31", "id_usuario": 1 + })) + + assert resultado["status"] == "ok" + + def test_nfse_por_cnpj(self, main_instance, mock_api): + mock_api.get_paginated.return_value = {"data": [], "total": 0} + + with patch("Relatorios.relatorios.Api", return_value=mock_api): + resultado = json.loads(main_instance.relatorios("nfse_por_cnpj", { + "cnpj": "08187168000160", + "data_inicial": "2026-01-01", "data_final": "2026-12-31", + "id_usuario": 1 + })) + + assert resultado["status"] == "ok" + + def test_metodo_invalido(self, main_instance): + resultado = json.loads(main_instance.relatorios("inexistente", {"id_usuario": 1})) + assert resultado["status"] == "error" + +class TestWebhooks: + def test_consultar_webhooks(self, main_instance, mock_api): + mock_api.get.return_value = {"url": " ", "method": "POST"} + + with patch("webhooks.webhooks.Api", return_value=mock_api): + main_instance.webhooks("get", {"cnpj": "08187168000160", "id_usuario": 1}) + + def test_criar_webhook(self, main_instance, mock_api): + mock_api.post.return_value = {"status": "ok"} + + with patch("webhooks.webhooks.Api", return_value=mock_api): + main_instance.webhooks("post", {"cnpj": "08187168000160", "url": " ", "id_usuario": 1}) + + def test_deletar_webhook(self, main_instance, mock_api): + mock_api.delete.return_value = {"status": "ok"} + + with patch("webhooks.webhooks.Api", return_value=mock_api): + main_instance.webhooks("delete", {"cnpj": "08187168000160", "id_usuario": 1}) + + def test_atualizar_webhook(self, main_instance, mock_api): + mock_api.put.return_value = {"status": "ok"} + + with patch("webhooks.webhooks.Api", return_value=mock_api): + main_instance.webhooks("put", {"cnpj": "08187168000160", "url": " ", "id_usuario": 1}) + + def test_testar_webhook(self, main_instance, mock_api): + mock_api.post.return_value = {"status": "ok", "message": "Webhook testado"} + + with patch("webhooks.webhooks.Api", return_value=mock_api): + main_instance.webhooks("test", {"cnpj": "08187168000160", "id_usuario": 1}) + + def test_metodo_invalido(self, main_instance): + resultado = json.loads(main_instance.webhooks("inexistente", {"id_usuario": 1})) + assert resultado["status"] == "error" diff --git a/tecnospeed_rpc/src/tests/test_simplified_payload.py b/tecnospeed_rpc/src/tests/test_simplified_payload.py new file mode 100644 index 0000000..d40c494 --- /dev/null +++ b/tecnospeed_rpc/src/tests/test_simplified_payload.py @@ -0,0 +1,80 @@ +import os +import sys +import json +import time + +sys.path.append(os.path.dirname(os.path.dirname(os.path.abspath(__file__)))) + +from Models.config import SystemConfig +from utils.redis import RedisClient +from utils.Exchange2 import RPCExchange2 + +def run_rpc_test(): + redis_host = os.getenv('REDIS_HOST', 'localhost') + redis_client = RedisClient(host=redis_host, port=6379) + try: + config_data = redis_client.get("system_config") + sc = SystemConfig(**json.loads(config_data)) + except Exception as e: + print(f"Erro ao carregar configs: {e}") + return + + config_rpc = { + "x_api_key": "2da392a6-79d2-4304-a8b7-959572c7e44d", + "user_postgres": "tecnospeed", + "pass_postgres": "u7QimG7E%21SF%2F%2FJC2c8", + "host_postgres": "banco", + "port_postgres": "5432", + "database_postgres": "tecnospeed" + } + + exchange = RPCExchange2(sc.amqps, sc.origin, "integracao", "tecnospeed", "2.1.0", client="#") + servicekey = sc.servicekey.ljust(32, '0')[:32] + exchange.setCrypto(sc.serviceid, servicekey) + + # Payload "simplificado" do front-end + full_payload = { + "fila": "empresa", + "metodo": "POST", + "cliente": "tray", + "url_base": "https://api.sandbox.plugnotas.com.br", + "config": config_rpc, + "dados": { + "cpfCnpj": "94074986000198", + "razaoSocial": "Tech nux", + "nomeFantasia": "Tech nux ltda", + "inscricaoMunicipal": "0001", + "simplesNacional": True, + "regimeTributario": 1, + "regimeTributarioEspecial": 1, + "incentivoCultural": False, + "incentivoFiscal": True, + "certificado": "cert-mock-01", + "endereco": { + "codigoPais": "1058", + "descricaoPais": "BRASIL", + "tipoLogradouro": "Avenida", + "logradouro": "Avenida", + "numero": "123", + "complemento": "Casa", + "tipoBairro": "Bairro", + "bairro": "Centro", + "codigoCidade": "5200175", + "descricaoCidade": "ÁGUA FRIA DE GOIÁS", + "estado": "GO", + "cep": "68907-460" + }, + "nfe": { "ativo": True }, + "nfse": { "ativo": False } + } + } + + print("Enviando payload via RPC...") + exchange.setPayload(json.dumps(full_payload)) + resposta = exchange.callRPC(json.dumps(full_payload), timeout=30) + + print("\nRESPOSTA DO SERVIDOR (RPC):") + print(resposta) + +if __name__ == "__main__": + run_rpc_test() diff --git a/tecnospeed_rpc/src/tests/test_upload_certificado.py b/tecnospeed_rpc/src/tests/test_upload_certificado.py new file mode 100644 index 0000000..c3c7b9d --- /dev/null +++ b/tecnospeed_rpc/src/tests/test_upload_certificado.py @@ -0,0 +1,88 @@ +import sys +import os +import json +import time +from dotenv import load_dotenv + +# Carrega variáveis de ambiente +load_dotenv() + +# Adiciona o diretório atual ao path para importar as classes locais +sys.path.append(os.path.dirname(os.path.abspath(__file__))) + +from Models.config import SystemConfig +from utils.redis import RedisClient +from utils.Exchange2 import QueueExchange2 + +# Dados do Certificado (Preenchidos dinamicamente) +PFX_BASE64 = " " +PFX_PASSWORD = str(sys.argv[1]) if len(sys.argv) > 1 else " " + +def main(): + print(f"Iniciando teste de upload de certificado para senha: {PFX_PASSWORD}") + + # 1. Carregando configuração do Redis + redis_host = os.getenv('REDIS_HOST', 'localhost') + redis_client = RedisClient(host=redis_host, port=6379) + try: + system_config_data = redis_client.get("system_config") + if not system_config_data: + print("Erro: system_config não encontrado no Redis.") + return + + system_config = SystemConfig(**json.loads(system_config_data)) + except Exception as e: + print(f"Erro ao ler configs do Redis: {e}") + return + + # 2. Inicializando QueueExchange2 + try: + exchange = QueueExchange2( + amqps=system_config.amqps, + origin=system_config.origin, + system="integracao", + service="tecnospeed", + version="1.0.0", + client="#" + ) + + servicekey = system_config.servicekey.ljust(32, '0')[:32] + exchange.setCrypto(serviceid=system_config.serviceid, servicekey=servicekey) + except Exception as e: + print(f"Falha ao iniciar mensageria: {e}") + return + + # 3. Montando o payload interno + internal_payload = { + "fila": "certificado_digital", + "cliente": "UploadTest", + "metodo": "POST", + "dados": { + "arquivo": PFX_BASE64, + "senha": PFX_PASSWORD, + "id_empresa": "c19d874447b1kxlapg1xv783", + "id_usuario": 1, + "expirationAt": "2027-04-13" + }, + "config": { + "x_api_key": os.getenv("PLUGNOTAS_API_KEY"), + "user_postgres": os.getenv("POSTGRES_USER", "tecnospeed"), + "pass_postgres": os.getenv("POSTGRES_PASSWORD", "u7QimG7E!SF//JC2c8"), + "host_postgres": "postgres", + "port_postgres": "5432", + "database_postgres": os.getenv("POSTGRES_DB", "tecnospeed") + }, + "url_base": "https://api.sandbox.plugnotas.com.br" + } + + # 4. Enviando com criptografia (hmac/payload) + try: + msg_json = json.dumps(internal_payload) + exchange.setPayload(msg_json) + exchange.sendMsg(msg_json, service="tecnospeed") + print("Mensagem enviada com sucesso (criptografada)!") + except Exception as e: + print(f"Erro ao enviar mensagem: {e}") + +if __name__ == "__main__": + main() diff --git a/tecnospeed_rpc/src/utils/Exchange2.py b/tecnospeed_rpc/src/utils/Exchange2.py new file mode 100644 index 0000000..3b9d56b --- /dev/null +++ b/tecnospeed_rpc/src/utils/Exchange2.py @@ -0,0 +1,1047 @@ +import json, time, sys, inspect, os +import hmac, hashlib, uuid, re + +import pika, pprint +from datetime import datetime +from pytz import timezone +from cryptography.hazmat.primitives.ciphers import Cipher, algorithms, modes +from cryptography.hazmat.primitives import padding +from cryptography.hazmat.backends import default_backend + +from queue import Queue, Full, Empty +from threading import Thread, Event +from abc import ABC, abstractmethod +from typing import Sequence, Any +import logging + +from utils.exchangeModels import * + +versao = "v1.0.0 14/12/2023 - Baltazar - Prefetch count" +versao = "v1.1.0 06/02/2024 - MJ. - Detecta Serviço rabbitmq ausente no inicio" +versao = "v1.2.0 20/02/2024 - MJ. - Possibilita uma taksqueue por serviço" +versao = "v1.3.0 06/03/2024 - MJ. - Verboso apenas nos erros" +versao = "v1.5.0 07/03/2024 - MJ. - External Rabbitmq Connection" +versao = "v1.6.0 26/06/2024 - MJ. - Conversao dos parametros host, service e client para lowercase" +versao = "v1.7.0 26/07/2024 - MJ. - Prepacao para utilizar Cloudamqp ou Rabbitmq local amqps/system" +versao = "v2.0.0 16/08/2024 - MJ. - Nova classe RPC e restruturação de Exchange2 e Tasks" +versao = "v2.1.0 21/11/2024 - MJ. - Clareza no log e tratamento de erros" +versao = "v2.2.0 31/01/2025 - MJ. - Remoção de alguns logs e melhorias de performance" +versao = "v2.3.0 12/02/2025 - MJ. - Melhorias na conexão com o Rabbitmq" +versao = "v2.4.0 14/02/2025 - MJ. - Check amqp url parameters" +versao = "v2.4.1 19/02/2025 - MJ. - Correcao prefetch_count=1" +versao = "v2.5.1 13/05/2025 - MJ. - Payload até 128kb - ajuste documentação e visual" +versao = "v2.5.2 01/10/2025 - MJ. - Evita especificar um client vazio" +versao = "v3" +versaostr = f"{versao}0.0 16/01/2026 S.D.G. - SG_IC2 LoggerExchange" + +def is_amqp_url(url): + pattern = r"^amqps?:\/\/([a-zA-Z0-9_\-\.]+):([^@]+)@([a-zA-Z0-9_\-\.]+)(:[0-9]+)?(\/[a-zA-Z0-9_\-\.]*)?$" + + return bool(re.match(pattern, url)) + +# QUEUE EXCHANGE - Apoia no comunicação inter-serviços de um sistema +# EXCHANGE TIPO DIRECT +# NAO DURAVEL - EXCLUSIVO - AUTODELETE +# SE NAO HOUVER QUEM RECEBA A MSG A MSG SERÁ PERDIDA -> O ENVIO FUNCIONA MESMO QUE NINGUEM EXISTA PARA RECEBER +class QueueExchange2: + def __init__( self, + amqps:str, + origin:str, + system:str, + service:str, + version:str, + client:str = '#', + callback: object = None, + delivery_mode = 1): + + paramOK = len(service) and len(system) and len(amqps) and len(origin) and len(version) and len(client) + if not (paramOK) or not is_amqp_url(amqps): + if not is_amqp_url(amqps): + msg = f"NOT A VALID AMQP URL: {amqps}" + else: + msg = f"VERIFY PARAMETERS: is_amqp_url: {is_amqp_url(amqps)}\n amqps[{amqps}]\n origin[{origin}]\n system[{system}]\n service[{service}]\n version[{version}\n client[{client}]" + raise Exception(msg) + + self.delivery = delivery_mode + self.payload = "" + self.hmac = "" + self.version = version + self.serviceid = "" + self.type = "msg" + self.origin = origin.lower() + self.client = client.lower() + self.service = service.lower() + self.system = system.lower() + self.client = client + self.params = pika.URLParameters(amqps) + self.callback = callback + self.queue = f"{self.system}_{service}_{self.client}" if client != '#' else f"{self.system}_{service}" + if callback is not None: + try: + self.connection = pika.BlockingConnection(self.params) + self.channelIn = self.connection.channel() # start main channel + self.channelIn.exchange_declare (exchange = f"{self.system}.direct", exchange_type = 'direct') + self.channelIn.queue_declare (queue = self.queue, durable = True) + self.channelIn.queue_bind (queue = self.queue, exchange = f"{self.system}.direct", routing_key= self.queue) + self.channelIn.basic_qos (prefetch_count=1) + self.consumerTag = self.channelIn.basic_consume (queue = self.queue, + auto_ack = False, + on_message_callback = self.callback) + except Exception as error: + raise(error) + else: + conexao = pika.BlockingConnection(self.params) #testa conexao + conexao.close() + + def cancel(self): + self.channelIn.basic_cancel(self.consumerTag) + + # METODOS INTERNOS _ + # MODELO MSG QUE TRAFEGA ENTRE SERVIÇOS E CONTROLLER + """ + MSG JSON CONTROLADOR + data: + { + "salesContractId": , + "client": "bambui", + "service": "SALES_CONTRACT_CREATED" + } + + message: + { ..., + "serviceid": , + "payload": aes-256 , + "hmac": hmac , + "type": "work", + ... + } + """ + def _setMsg(self, msg:str, caller:str): + now = datetime.now(timezone('America/Sao_Paulo')) + message =msg.strip('"') + + self.Msg = { + "system": self.system, + "target": self.target, + "service": self.service, + "serviceid": self.serviceid, + "payload": self.payload, + "hmac": self.hmac, + "type": self.type, + "version": self.version, + "origin": self.origin, + "client": self.client, + "caller": caller, + "logDate": f"{now}", + "msg": f"{message}" + } + + # Envia uma mensagem para um serviço associado ao sistema + # controller é o nome padrão do serviço, caso nao seja informado + def sendMsg(self, message: str, service:str = 'controller'): + trials = 5 + waitTime = 12 + messageSent = False + caller = inspect.stack()[1].function + if caller == '': caller = 'main' + + self.target = service + self._setMsg(message, caller) + targetQueue = f"{self.system}_{self.target}_{self.client}" if self.client != '#' else f"{self.system}_{self.target}" + while not messageSent and trials: + try: + connection = pika.BlockingConnection(self.params) + channelOut = connection.channel() # start a channel + #channelOut.exchange_declare(exchange = f"{self.system}.direct", exchange_type = 'direct') + #channelOut.queue_declare (queue= targetQueue, durable= True) + #channelOut.queue_bind (queue= targetQueue, exchange= f"{self.system}.direct", routing_key= targetQueue) + channelOut.basic_publish( + exchange = f"{self.system}.direct", + routing_key = targetQueue, + body = json.dumps(self.Msg), + properties = pika.BasicProperties(content_type = 'text/plain', delivery_mode= self.delivery)) + messageSent = True + connection.close() + except Exception as ex: + print(f"EXCH2: TROUBLE CNX RABBITMQ REMAIN ATTEMPT: {trials}") + print(ex, flush=True) + sys.stdout.flush() + time.sleep(waitTime) + trials -= 1 + if messageSent: + self.payload = "" + self.hmac = "" + else: + print(f"EXCH2: ERROR MESSAGE NOT SENT TO [{targetQueue}]", flush=True) + + def start_consuming(self): + if self.callback is not None: + self.consumerTag = self.channelIn.start_consuming() + + def setCrypto(self, serviceid:str, servicekey:str): + self.serviceid = serviceid + self.servicekey = servicekey + self.Crypto = Payload(serviceid, servicekey) + + def setPayload(self, data:str): + try: + self.hmac = "" + self.payload = "" + self.hmac, self.payload = self.Crypto.setPayload(data) + except Exception as ex: + print(f"EXCH2: CRYPTO ERROR: {ex}", flush=True) + + def getPayload(self, hmac:str, safeData:str): + try: + self.hmac = "" + self.payload = "" + self.hmac, self.payload = self.Crypto.getPayload(safeData) + except Exception as ex: + print(ex) + return False + + return hmac == self.hmac + +# CRIPTOGRAFIA DO PAYLOAD +class Payload: + def __init__(self, serviceid:str, servicekey:str): + self.serviceid = serviceid + self.servicekey = servicekey + + + def setPayload(self, data:str): + data = data.encode('utf-8') + tamanho = len(data) + if tamanho > 1024 * 1024 * 10 or tamanho <= 0: + raise Exception("EXCHANGE2 Payload setPayload: Invalid Payload size") + + hmacData= hmac.new(self.serviceid.encode('utf-8'), data, 'sha256').hexdigest() + iv = os.urandom(16) + padder = padding.PKCS7(128).padder() + datarw = padder.update(data) + padder.finalize() + cipher = Cipher(algorithms.AES(self.servicekey.encode('utf-8')), modes.CBC(iv)) + encrypt = cipher.encryptor() + safeData = encrypt.update(datarw) + encrypt.finalize() + payload = (iv + safeData) + + return (hmacData, payload.hex()) + + def getPayload(self, safeData:str): + safeData = bytes.fromhex(safeData) + iv = safeData[:16] + cipher = Cipher(algorithms.AES(self.servicekey.encode('utf-8')), modes.CBC(iv)) + decrypt = cipher.decryptor() + rawdata = decrypt.update(safeData[16:]) + decrypt.finalize() + if len(rawdata)%16 != 0: + raise Exception("EXCHANGE2 Payload getPayload: Invalid Payload size block {0}".format(len(rawdata))) + + unpadder = padding.PKCS7(128).unpadder() + data = unpadder.update(rawdata) + unpadder.finalize() + hmacData = hmac.new(self.serviceid.encode('utf-8'), data, 'sha256').hexdigest() + + return (hmacData, data.decode('utf-8')) + +# TASK EXCHANGE +# PERMITE CRIAR UM DISTRIBUIDOR DE TAREFAS +# O DISTRIBUIDOR DEVE RECEBER UMA FUNCAO DE CALLBACK +class TaskExchange2: + def __init__(self, + amqps:str, + origin:str, + system:str, + service:str, + version:str, + client:str = '#', callback: object = None, delivery_mode:int = 2): + paramOK = len(service) and len(system) and len(amqps) and len(origin) and len(version) + if not (paramOK) or not is_amqp_url(amqps): + if not is_amqp_url(amqps): + msg = f"NOT A VALID AMQP URL {amqps}" + else: + msg = f"VERIFY PARAMETERS: is_amqp_url: {is_amqp_url(amqps)}\n amqps[{amqps}]\n origin[{origin}]\n system[{system}]\n service[{service}]\n version[{version}]" + raise Exception(msg) + + self.delivery = delivery_mode + self.payload = "" + self.hmac = "" + self.version = version + self.serviceid = "" + self.type = "task" + self.origin = origin.lower() + self.service = service.lower() + self.system = system.lower() + self.client = client.lower() + self.params = pika.URLParameters(amqps) + self.callback = callback + self.queue = f'{self.system}_{self.service}_tasks_{client}' if client != '#' else f'{self.system}_{self.service}_tasks' + print(f"EXCH2: TASK EXCHANGE: [{self.queue}] [{amqps}]", flush=True) + if callback is not None: + self.connection = pika.BlockingConnection(self.params) + self.channelIn = self.connection.channel() # start main channel + self.channelIn.exchange_declare (exchange = f"{self.system}.direct", exchange_type = 'direct') + self.channelIn.queue_declare (queue = self.queue, durable = True) + self.channelIn.queue_bind (queue = self.queue, exchange = f"{self.system}.direct", routing_key= self.queue) + self.channelIn.basic_qos (prefetch_count=1) + self.consumerTag = self.channelIn.basic_consume (queue = self.queue, + auto_ack = False, + on_message_callback = self.callback) + else: + conexao = pika.BlockingConnection(self.params) #testa conexao + conexao.close() + def _setTask(self, msg:str, caller:str): + now = datetime.now(timezone('America/Sao_Paulo')) + message =msg.strip('"') + + self.Msg = { + "system": self.system, + "target": self.target, + "service": self.service, + "serviceid": self.serviceid, + "payload": self.payload, + "hmac": self.hmac, + "type": self.type, + "version": self.version, + "origin": self.origin, + "client": self.client, + "caller": caller, + "logDate": f"{now}", + "msg": f"{message}" + } + + def cancel(self): + self.channelIn.basic_cancel(self.consumerTag) + + def sendTask(self, message:str): + trials = 5 + waitTime = 12 + messageSent = False + caller = 'TaskExchange2' + self.target = 'workers' + self._setTask(message, caller) + + while not messageSent and trials: + try: + connection = pika.BlockingConnection(self.params) + channelOut = connection.channel() # start a channel + channelOut.exchange_declare(exchange = f"{self.system}.direct", exchange_type = 'direct') + channelOut.queue_declare (queue = self.queue, durable = True) + channelOut.queue_bind (queue = self.queue, exchange = f"{self.system}.direct", routing_key= self.queue) + + channelOut.basic_publish (exchange = f"{self.system}.direct", + routing_key = self.queue, + body = json.dumps(self.Msg), + properties = pika.BasicProperties(content_type = 'text/plain', + delivery_mode= self.delivery)) + messageSent = True + connection.close() + except Exception as ex: + print(f"EXCH2: TROUBLE CNX RABBITMQ REMAIN ATTEMPT: {trials}") + print(ex, flush=True) + time.sleep(waitTime) + trials -= 1 + if not messageSent: + print(f"EXCH2: ERROR TASK NOT SENT TO: {self.queue}", flush=True) + + def start_consuming(self): + if self.callback is not None: + print(f"EXCH2: SEND TASKS TO: [{self.queue}]", flush=True) + self.consumerTag = self.channelIn.start_consuming() + + def setCrypto(self, serviceid:str, servicekey:str): + self.serviceid = serviceid + self.servicekey = servicekey + self.Crypto = Payload(serviceid, servicekey) + + def setPayload(self, data:str): + try: + self.hmac = "" + self.payload = "" + self.hmac, self.payload = self.Crypto.setPayload(data) + except Exception as ex: + print(f"EXCH2: CRYPTO ERROR: {ex}", flush=True) + + def getPayload(self, hmac:str, safeData:str): + try: + self.hmac = "" + self.payload = "" + self.hmac, self.payload = self.Crypto.getPayload(safeData) + except Exception as ex: + print(f"EXCH2: CRYPTO ERROR: {ex}", flush=True) + return False + + return hmac == self.hmac + +# RPC - QUANDO FOR UTILIZAR COMO ENDPOINT +# FORNECER FUNCAO DE TRABALHO NA INICIALIZACAO +# COMO RPC Server: FILA ONDE RECEBERA AS TAREFAS => SISTEM_SERVICE_RPC ou SISTEM_SERVICE_RPC_CLIENT +# COMO RPC Client: UTILIZA callRPC +class RPCExchange2: + def __init__(self, + amqps:str, # ENDERECO URL RABBITMQ - CLOUDAMQP + origin:str, # NOME DO HOST ONDE O SCRIPT ESTÁ RODANDO + system:str, # NOME DO SISTEMA QUE O SERVIÇO FAZ PARTE + service:str, # NOME DO SERVIÇO DENTRO DO SISTEMA + version:str, # VERSÃO DO SERVIÇO + client:str = '#', # CASO ESPECIFICO A UM CLIENTE, COMO NO CASO DO SG_ETL + workfunction=None, # FUNCAO DE TRABALHO PARA O USO COMO RCP Server, i.e. para substituir um endpoint fastapi + delivery_mode=2 # VAI PERSISTIR AS MENSAGENS NO MODO 2 + ): + paramOK = len(service) and len(system) and len(amqps) and len(origin) and len(version) + + if not (paramOK) or not is_amqp_url(amqps): + if not is_amqp_url(amqps): + msg = f"RPCExchange2: NOT A VALID AMQP URL: {amqps}" + else: + msg = f"RPCExchange2: VERIFY PARAMETERS: is_amqp_url: {is_amqp_url(amqps)}\n amqps[{amqps}]\n origin[{origin}]\n system[{system}]\n service[{service}]\n version[{version}]" + raise Exception(msg) + + self.callback = workfunction + self.delivery = delivery_mode + self.payload = "" + self.hmac = "" + self.version = version + self.serviceid = "" + self.servicekey = "" + self.correlation_id = "" + self.consumerTag = None + self.type = "rpc" + self.origin = origin.lower() + self.service = service.lower() + self.system = system.lower() + self.client = client.lower() + self.params = pika.URLParameters(amqps) + # Server ouve nessa fila - Cliente publica nesta fila + self.queue = f'{self.system}_{self.service}_rpc_{client}' if client != '#' else f'{self.system}_{self.service}_rpc' + self.connection = pika.BlockingConnection(self.params) + self.channel = self.connection.channel() # start main channel + self.channel.exchange_declare (exchange = f"{self.system}.direct", exchange_type = 'direct') + + if (workfunction is not None): # FUNCIONAMENTO COMO RPC SERVER + self.target = 'rpcclt' # SINALIZA O DESTINO DA MSG SER UM RPC CLIENT + self.channel.queue_declare (queue = self.queue, exclusive = False, durable=True, auto_delete = True) + self.channel.queue_bind (exchange = f"{self.system}.direct", queue = self.queue, routing_key = self.queue) + self.channel.basic_qos (prefetch_count = 1) + self.consumerTag = self.channel.basic_consume(queue = self.queue, + auto_ack = True, + on_message_callback = self._RPCResp) # de lá chamamos self.callback + else: # FUNCIONAMENTO COMO RPC CLIENT + self.target = 'rpcsrv' # SINALIZA O DESTINO DA MSG SER UM RPC SERVER + + # PREPARA QUEUE DE RETORNO + result = self.channel.queue_declare(queue='', exclusive=True, auto_delete=True) + self.callback_queue = result.method.queue + self.channel.queue_bind(exchange = f"{self.system}.direct", + queue = self.callback_queue, + routing_key = self.callback_queue) + + self.channel.basic_consume( + queue = self.callback_queue, + on_message_callback = self._on_response, + auto_ack = True + ) + #print("EXCH2: RPC CLT - QUEUE:", self.callback_queue, flush=True) + + # RPC - QUANDO FOR UTILIZAR COMO ENDPOINT + def _RPCResp(self, ch, method, props, body): + #ch.basic_ack(delivery_tag = method.delivery_tag) + message = body.decode("utf-8") + reply_to = getattr(props, 'reply_to', None) + correlation_id = getattr(props, 'correlation_id', None) + resposta = "{}" # SEM WORKFUNCTION + # ENVIA A MENSAGEM AO CALLBACK + self.payload = "" + self.hmac = "" + try: + if self.callback is not None: + resposta = self.callback(message) + rspObj = json.loads(resposta) + # para enviar um payload criptografado na resposta ao caller + # retorne um objeto contendo serviceid, servicekey e payload + # essas chaves serao retiradas do objeto e o restante da informacao + # irá no chave msg + if 'payload' in rspObj and 'serviceid' in rspObj and 'servicekey' in rspObj: + self.setCrypto(rspObj['serviceid'], rspObj['servicekey']) + self.setPayload(json.dumps(rspObj['payload'])) + rspObj.pop('payload', None) + rspObj.pop('serviceid', None) + rspObj.pop('servicekey', None) + resposta = json.dumps(rspObj) + + + except Exception as ex: + erro = f"ERROR CALLBACK: {str(ex)}" + print(erro, flush=True) + resposta = json.dumps({"status": "error", "message": erro}) + + # ENVIA RESPOSTA AO CALLER + msg = json.loads(message) + msg = msg['msg'] + #print(f"RPC <== {msg}", flush=True) + #print(f"RPC ==> {resposta}", flush=True) + try: + if reply_to: + self.target = "rpcclt" + self._setJSON(resposta, 'RPCResp') + + ch.basic_publish(exchange = f"{self.system}.direct", + routing_key = reply_to, + body = json.dumps(self.Msg), + properties = pika.BasicProperties(content_type = 'application/json', + delivery_mode = self.delivery, + correlation_id = correlation_id)) + else: + print(f"RPCExchange2: NO TARGET FOUND: {resposta}", props, flush=True) + + except Exception as ex: + print(ex) + resposta = f"RPCExchange2: ERRO MSG CALLER: {str(ex)}" + raise Exception(resposta) + + + def _setJSON(self, msg:str, caller:str): + now = datetime.now(timezone('America/Sao_Paulo')) + message =msg.strip('"') + + self.Msg = { + "system": self.system, + "target": self.target, + "service": self.service, + "serviceid": self.serviceid, + "payload": self.payload, + "hmac": self.hmac, + "type": self.type, + "correlation_id": self.correlation_id, + "version": self.version, + "origin": self.origin, + "client": self.client, + "caller": caller, + "logDate": f"{now}", + "msg": f"{message}" + } + self.hmac = "" + self.payload = "" + + + def _on_response(self, ch, method, properties, body): + try: + if self.correlation_id == properties.correlation_id: + self.response = body.decode("utf-8") + except Exception as ex: + print(f"RPCExchange2: RPC ERROR: {ex}", flush=True) + sys.exit(0) + + return self.response + + # ENVIA A MENSAGEM AO CALLBACK + # RPC CLIENT CHAMA O RPC SERVER + def callRPC(self, message:str, timeout:int=10): + # ENVIA A MENSAGEM + self.response = None + self.correlation_id = str(uuid.uuid4()) + self._setJSON(message, 'callRPC') # COLOCA A message DENTRO DA CHAVE msg em self.Msg + + self.channel.exchange_declare(exchange = f"{self.system}.direct", exchange_type = 'direct') + # self.channel.queue_declare (queue= self.queue, durable=True, exclusive=False, auto_delete=True) + # self.channel.queue_bind (queue= self.queue, exchange= f"{self.system}.direct", routing_key= self.queue) + + self.channel.basic_publish (exchange = f"{self.system}.direct", + routing_key = self.queue, + body = json.dumps(self.Msg), + properties = pika.BasicProperties(content_type = 'application/json', + delivery_mode = self.delivery, + reply_to = self.callback_queue, + correlation_id= self.correlation_id)) + # AGUARDA O RETORNO + while self.response is None: + self.connection.process_data_events(time_limit=timeout) + return self.response + + def cancel(self): + print(f"RPCExchange2: CONSUMER TAG CANCELED: {self.consumerTag}", flush=True) + self.channel.basic_cancel(self.consumerTag) + + def start_consuming(self): + if self.callback is not None: + print(f"RPCExchange2: {self.service.upper()} RPC SRV QUEUE: [{self.queue}]", flush=True) + self.channel.start_consuming() + + def setCrypto(self, serviceid:str, servicekey:str): + self.serviceid = serviceid + self.servicekey = servicekey + self.Crypto = Payload(serviceid, servicekey) + + def setPayload(self, data:str): + print(f"RPCExchange2: SET PAYLOAD", flush=True) + try: + self.hmac = "" + self.payload = "" + self.hmac, self.payload = self.Crypto.setPayload(data) + except Exception as ex: + print(ex) + + def getPayload(self, hmac:str, safeData:str): + print(f"RPCExchange2: GET PAYLOAD", flush=True) + try: + self.hmac = "" + self.payload = "" + self.hmac, self.payload = self.Crypto.getPayload(safeData) + except Exception as ex: + print(ex) + return False + + return hmac == self.hmac + +# CADA APLICAÇÃO CRIA UMA CLASSE QUE ESTENDE IC2Builder IMPLEMENTANDO +# OS METODOS ABSTRATOS DE CADA REALMTYPE +class IC2Builder(ABC): + @abstractmethod + def _setLogs(self, message: tuple) -> InfoConnector: ... + @abstractmethod + def _setErrors(self, message: tuple) -> InfoConnector: ... + @abstractmethod + def _setOperational(self, message: tuple) -> InfoConnector: ... + @abstractmethod + def _setStatus(self, message: tuple) -> InfoConnector: ... + @abstractmethod + def _setMetrics(self, message: tuple) -> InfoConnector: ... + + def build(self, realm: RealmType, message: Sequence[Any]) -> InfoConnector: + handlers = { + RealmType.LOGS: self._setLogs, + RealmType.ERRORS: self._setErrors, + RealmType.OPERATIONAL: self._setOperational, + RealmType.STATUS: self._setStatus, + RealmType.METRICS: self._setMetrics, + } + + try: + return handlers[realm](message) + except KeyError: + raise ValueError(f"RealmType não suportado: {realm}") + +class LoggerExchange2: + def __enter__(self): return self + def __exit__(self, exc_type, exc, tb): self.close() + + def __init__(self, + ic2_builder: IC2Builder, + amqps : str, + system : str, + service : str, + version : str, + origin : str = "", + client : str = "#", + logType : str = 'logs', + logLocal : bool = False, + callback : object = None): + self.callback = callback + self.ic2_builder = ic2_builder + self.system = system.lower() + self.target = "" + self.service = service.lower() + self.serviceid = "AF1699F719F1B6C81DE2BD38AB811994E3089B35C8E9314813A770AC7AE25D8D" + self.servicekey = "KoG704lAafBA7Yafj8jyg5lAHfarrQ12" + self.payload = "" + self.hmac = "" + self.type = "logger" + self.version = version + self.origin = origin + self.client = client.lower() + self.params = pika.URLParameters(amqps) + self.params.socket_timeout = 15 + self.params.heartbeat = 60 + self.logType = logType.lower() + self.logTypeSvd = logType.lower() + self.logLocal = logLocal + self.pp = pprint.PrettyPrinter() + self.diaMes = datetime.now().strftime("%d") # PARA SABER SE ATUALIZA OU NÃO O ARQUIVO DE LOG LOCAL + self.flush = True + self.base_routeKey = f"{self.service}.{self.system}" + self.connection = None + self.channel = None + self.logging = logging.getLogger(self.base_routeKey) + self.Crypto = Payload(self.serviceid, self.servicekey) + self.exchange_name = 'info_connector.topic' + + try: + self._connect() + except Exception as err: + self.logging.error(f"ERROR CONNECTION {err}") + raise + + if self.logLocal: self.setLogLocalName() + + # --- async logging --- + self.queue_size = 1000 + self.queue = Queue(maxsize=self.queue_size) + self.stop_event = Event() + self.worker_thread = Thread( + target = self._worker, + name = "logger-worker", + daemon = True + ) + self.worker_thread.start() + + self.logging.info(f"params: {self.params} amqps: {amqps}") + if self.callback is not None: + try: + self.queueIC2 = "info_connector_collector" + self.channelIn = self.connection.channel() + self.channelIn.queue_declare (queue = self.queueIC2, exclusive=True, durable=True) + self.channelIn.queue_bind (queue = self.queueIC2, exchange = self.exchange_name, routing_key = "#") + self.channelIn.basic_qos (prefetch_count = 1) + self.consumerTag = self.channelIn.basic_consume (queue = self.queueIC2, + auto_ack = False, + on_message_callback = self.callback) + self.logging.info("COLLECTING TOPIC MESSAGES VHOST info_connector_host EXCHANGE info_connector.topic KEY #") + except Exception as error: + raise(error) + else: + conexao = pika.BlockingConnection(self.params) #testa conexao + conexao.close() + self.logging.info(f"Logger Messages topics: {self.logType}.{self.base_routeKey}.{self.client}") + + + # PERSISTENCIA LOGS MESSGERIA + def _worker(self): + while not self.stop_event.is_set() or not self.queue.empty(): + try: + payload, client = self.queue.get(timeout=0.5) + except Empty: + continue + + try: + self.sendMsgWork(payload, client) + except Exception as e: + # última linha de defesa + self.logging.error(f"ASYNC LOGGER FAILURE: {e}") + if self.logLocal: + self.logging.info(payload) + finally: + self.queue.task_done() + + # PERSISTENCIA CONEXAO AMQPS + def _connect(self): + """Conecta ou reconecta ao RabbitMQ.""" + if self.connection and self.connection.is_open: + return + try: + if self.connection: + self.connection.close() + except Exception: + pass + try: + self.connection = pika.BlockingConnection(self.params) + self.channel = self.connection.channel() + self.channel.exchange_declare(exchange = self.exchange_name, + exchange_type = 'topic', + auto_delete = False) + + self.logging.info("Conexão ao RabbitMQ estabelecida.") + except pika.exceptions.AMQPConnectionError as e: + self.logging.error(f"Falha na conexão: {e}") + raise + + def _setMsg(self, msg:str, caller:str): + now = datetime.now(timezone('America/Sao_Paulo')) # melhoria: acrescentar timezone info no system_config + return { + "system" : self.system, + "target" : self.target, + "service" : self.service, + "serviceid": self.serviceid, + "payload" : self.payload, + "hmac" : self.hmac, + "type" : self.type, + "version" : self.version, + "origin" : self.origin, + "client" : self.client, + "caller" : caller, + "logDate" : f"{now}", + "msg" : msg + } + + def _sendMsg(self, msg: str, caller: str, *args): + # Avalia arquivo de logs + if self.logLocal: + current_dia = datetime.now().strftime("%d") + if current_dia != self.diaMes: + self.diaMes = current_dia + self.setLogLocalName() + + message = msg.strip() + + try: + if args and isinstance(args[0], (list, tuple)): + for item in args[0]: + msgArg = pprint.pformat(item).replace("'", "").strip() + message = f"{message}, {msgArg}" + + Msg = self._setMsg(message, caller) + payload = json.dumps(Msg) + + # console / docker logs continuam síncronos + self.pp.pprint(f"{Msg['logDate'][0:21]} {Msg['msg']}") + + # log local imediato (garantia mínima) + if self.logLocal: + Msg['payload'] = Msg['payload'][:20] + self.logging.info(self.pp.pformat(Msg)) + + # envio assíncrono + try: + self.queue.put_nowait((payload, self.client)) + except Full: + # fila cheia → degrada com dignidade + self.logging.warning("LOGGER QUEUE FULL — DROPPING MESSAGE") + + if self.flush: + sys.stdout.flush() + + except Exception as e: + erro = f"LOGGER ERROR: {e}" + self.logging.error(erro) + print(erro, flush=True) + + + def _sendMsgLocal(self, msg: str, caller: str, *args): + # Avalia arquivo de logs + if self.logLocal: + current_dia = datetime.now().strftime("%d") + if current_dia != self.diaMes: + self.diaMes = current_dia + self.setLogLocalName() + + message = msg.strip() + + try: + if args and isinstance(args[0], (list, tuple)): + for item in args[0]: + msgArg = pprint.pformat(item).replace("'", "").strip() + message = f"{message}, {msgArg}" + + Msg = self._setMsg(message, caller) + payload = json.dumps(Msg) + + # console / docker logs continuam síncronos + self.pp.pprint(f"{Msg['logDate'][0:21]} {Msg['msg']}") + + # log local imediato (garantia mínima) + if self.logLocal: + Msg['payload'] = Msg['payload'][:20] + self.logging.info(self.pp.pformat(Msg)) + + except Exception as e: + erro = f"LOGGER ERROR: {e}" + self.logging.error(erro) + print(erro, flush=True) + + def start_consuming(self): + if self.callback is not None: + self.consumerTag = self.channelIn.start_consuming() + + def close(self): + # sinaliza parada + self.stop_event.set() + + # aguarda flush da fila (timeout defensivo) + try: + self.queue.join() + except Exception: + pass + + if self.worker_thread.is_alive(): + self.worker_thread.join(timeout=2) + + if self.channel: + self.channel.close() + if self.connection: + self.connection.close() + + def setLogLocalName(self): + self.logLocalName = 'logs/'+ self.system.split()[0] + '_' + self.diaMes + formatter = logging.Formatter("%(asctime)s - %(name)s - %(levelname)s - %(message)s") + fhandler = logging.FileHandler(self.logLocalName) + fhandler.setLevel(logging.INFO) + fhandler.setFormatter(formatter) + self.logging.propagate = False + self.logging.setLevel(logging.INFO) + if not any(isinstance(h, logging.FileHandler) for h in self.logging.handlers): + self.logging.addHandler(fhandler) + + # Funcionamento: caso nao seja possivel enviar a msg, tentar nova conexao + # e envio da msg original mais uma vez + def sendMsgWork(self, message:str, client:str = "#"): + messageSent = False + trials = 5 + waitTime = 5 + while not messageSent and trials > 0: + self._connect() + # Cliente informado - MAIOR PRECEDENCIA + if client != "#": + routeKey = self.base_routeKey + f".{client.lower()}" + elif self.client != "#": + routeKey = self.base_routeKey + f".{self.client.lower()}" + else: + routeKey = self.base_routeKey + + try: + routing_key = f"{self.logType}.{routeKey}" + self.channel.basic_publish( + exchange = "info_connector.topic", + routing_key = routing_key, + body = message, + properties = pika.BasicProperties( + content_type = 'text/plain', + delivery_mode= pika.DeliveryMode.Transient)) + messageSent = True + except (pika.exceptions.ConnectionClosed, pika.exceptions.ChannelClosed, pika.exceptions.AMQPConnectionError) as ex: + self.logging.warning(f"TROUBLE CNX LOGGER REMAIN ATTEMPT: {trials} {ex}") + self._connect() + except Exception as ex: + self.logging.error(f"LOGGER ERROR: {ex}") + self.logging.warning(str(self.params)) + finally: + if not messageSent: + trials -= 1 + time.sleep(waitTime) + waitTime *= 2 + + if not messageSent: + self.logging.error(f"LOGGER ERROR: {message}") + + # Para quando desejar trocar o header do topico + # Salva o tópico padrão especificado no momento da inicialização + def setLogType(self, logType:str): + if not logType or len(logType) > 25: + return + self.logTypeSvd = self.logType + self.logType = logType.lower() + + # Volta ao header topico padrao + def clrLogType(self): + self.logType = self.logTypeSvd + + def log_logs(self, msg: str): + self.logIC2(RealmType.LOGS, (msg,)) + + def log_errors(self, msg: str): + self.logIC2(RealmType.ERRORS, (msg,)) + + def logIC2(self, realm: RealmType, msg: tuple = ()): + ic_obj = self.ic2_builder.build(realm, msg) + payload = ic_obj.model_dump_json() + self.setPayload(data=payload) + self.setLogType(logType=realm.value) + self._sendMsg(f"LOGIC2 {realm.value.upper()} [{str(msg)[:20]} ...]", caller="IC2") + self.clrLogType() + self.clrPayload() + + def logMsgError(self, msg:str, *args): + self.setLogType('error.logs') + caller = inspect.stack()[1].function + if caller == '': caller = 'main' + + self._sendMsgLocal(msg, caller, args) + self.clrLogType() + + # LOGS GERAIS + # INFORM logs...# + # Log geral de acordo com o valor logType + def logMsg (self, msg: str, *args): + caller = inspect.stack()[1].function + if caller == '': caller = 'main' + Msg = f"{msg}" + self._sendMsgLocal(Msg, caller, args) + + # FUNÇÕES PARA CRYPTOGRAFIA + def clrPayload(self): + self.hmac = "" + self.payload = "" + + def setPayload(self, data:str): + try: + self.hmac = "" + self.payload = "" + self.hmac, self.payload = self.Crypto.setPayload(data) + except Exception as ex: + print(f"EXCH2: CRYPTO ERROR: {ex}", flush=True) + + def getPayload(self, hmac:str, safeData:str): + try: + self.hmac = "" + self.payload = "" + self.hmac, self.payload = self.Crypto.getPayload(safeData) + except Exception as ex: + print(f"EXCH2: CRYPTO ERROR: {ex}", flush=True) + return False + + return hmac == self.hmac + +class AppIC2Builder(IC2Builder): + def __init__(self, service_name: str, versionstr: str, origin: str): + self.service_name = service_name + self.versionstr = versionstr + self.origin = origin + + def _get_common_fields(self): + now = datetime.now().strftime("%Y-%m-%d %H:%M:%S") + return { + "log_date": now, + "service_date": now, + "service": self.service_name, + "versionstr": self.versionstr, + "origin": self.origin + } + + def _setLogs(self, message: tuple) -> InfoConnector: + msg_str = str(message[0]) if len(message) > 0 else "" + ent_id = str(message[1]) if len(message) > 1 else "0" + ent_name = str(message[2]) if len(message) > 2 else "Unknown" + tenant = str(message[3]) if len(message) > 3 else "#" + + ic_msg = ICLogs(**self._get_common_fields(), enterprise_id=ent_id, enterprise_name=ent_name, message=msg_str, tenant=tenant) + return InfoConnector(realm=RealmType.LOGS, messageIC=ic_msg) + + def _setErrors(self, message: tuple) -> InfoConnector: + msg_str = str(message[0]) if len(message) > 0 else "" + ent_id = str(message[1]) if len(message) > 1 else "0" + ent_name = str(message[2]) if len(message) > 2 else "Unknown" + tenant = str(message[3]) if len(message) > 3 else "#" + + ic_msg = ICErrors(**self._get_common_fields(), enterprise_id=ent_id, enterprise_name=ent_name, message=msg_str, tenant=tenant) + return InfoConnector(realm=RealmType.ERRORS, messageIC=ic_msg) + + def _setOperational(self, message: tuple) -> InfoConnector: + acao = str(message[0]) if len(message) > 0 else "" + tabela = str(message[1]) if len(message) > 1 else "" + id_usuario = str(message[2]) if len(message) > 2 else "0" + id_registro = str(message[3]) if len(message) > 3 else "0" + status_str = str(message[4]).upper() if len(message) > 4 else "INICIO" + mensagem = str(message[5]) if len(message) > 5 else "" + json_data = str(message[6]) if len(message) > 6 else "{}" + + if status_str == 'SUCESSO': op_status = OpStatus.OK + elif status_str == 'ERRO': op_status = OpStatus.ERROR + else: op_status = OpStatus.ACTIVE + + common = self._get_common_fields() + ic_msg = ICOperational( + log_date=common["log_date"], service_date=common["service_date"], service_name=self.service_name, + versionstr=self.versionstr, origin=self.origin, enterprise_id=id_usuario, enterprise_name=f"User {id_usuario}", + status=op_status, step_status=acao, message=f"{mensagem} | Tabela: {tabela} | ID_Reg: {id_registro} | Data: {json_data}" + ) + return InfoConnector(realm=RealmType.OPERATIONAL, messageIC=ic_msg) + + def _setStatus(self, message: tuple) -> InfoConnector: + common = self._get_common_fields() + ic_msg = ICStatus( service=self.service_name, versionstr=self.versionstr, system_name="integracao", + startup_date=common["log_date"], access_date=common["log_date"], origin=self.origin ) + return InfoConnector(realm=RealmType.STATUS, messageIC=ic_msg) + + def _setMetrics(self, message: tuple) -> InfoConnector: + metric_name = str(message[0]) if len(message) > 0 else "" + value = str(message[1]) if len(message) > 1 else "0" + unit = str(message[2]) if len(message) > 2 else "" + + now = datetime.now().strftime("%Y-%m-%d %H:%M:%S") + ic_msg = ICMetrics( metric_date=now, metric=metric_name, metric_name=metric_name, value=value, unit=unit, tenant="#" ) + return InfoConnector(realm=RealmType.METRICS, messageIC=ic_msg) diff --git a/tecnospeed_rpc/src/utils/api.py b/tecnospeed_rpc/src/utils/api.py new file mode 100644 index 0000000..e7c48ce --- /dev/null +++ b/tecnospeed_rpc/src/utils/api.py @@ -0,0 +1,158 @@ +import requests + +# Wrapper base para realizar requests (GET/POST/PUT/DELETE) à API da PlugNotas de forma padronizada +class Api: + def __init__(self, api_key): + self.api_key = api_key + + def get(self, url): + try: + headers = { + "x-api-key": self.api_key + } + response = requests.get(url, headers=headers, timeout=30) + response.raise_for_status() + return response.json() + except requests.exceptions.RequestException as e: + resp_data = {} + if 'response' in locals() and response is not None: + try: + resp_data = response.json() + except ValueError: + resp_data = response.text + return {"error": str(e), "response": resp_data} + + def get_paginated(self, url, max_pages=100): + try: + headers = { + "x-api-key": self.api_key + } + all_data = [] + current_url = url + page = 0 + + while current_url and page < max_pages: + separator = "&" if "?" in current_url else "?" + request_url = current_url if page == 0 else current_url + + response = requests.get(request_url, headers=headers, timeout=30) + response.raise_for_status() + result = response.json() + + if isinstance(result, list): + all_data.extend(result) + break + elif isinstance(result, dict): + page_data = result.get("data", []) + if isinstance(page_data, list): + all_data.extend(page_data) + elif page_data: + all_data.append(page_data) + + hash_proxima = result.get("hashProximaPagina") + if hash_proxima: + separator = "&" if "?" in url else "?" + current_url = f"{url}{separator}hashProximaPagina={hash_proxima}" + page += 1 + else: + break + else: + break + + return {"data": all_data, "total_paginas": page + 1, "total_registros": len(all_data)} + except requests.exceptions.RequestException as e: + resp_data = {} + if 'response' in locals() and response is not None: + try: + resp_data = response.json() + except ValueError: + resp_data = response.text + return {"error": str(e), "response": resp_data} + + def post(self, url, data): + try: + headers = { + "Content-Type": "application/json", + "x-api-key": self.api_key + } + response = requests.post(url, headers=headers, json=data, timeout=30) + response.raise_for_status() + return response.json() + except requests.exceptions.RequestException as e: + resp_data = {} + if 'response' in locals() and response is not None: + try: + resp_data = response.json() + except ValueError: + resp_data = response.text + return {"error": str(e), "response": resp_data} + + def put(self, url, data): + try: + headers = { + "x-api-key": self.api_key + } + response = requests.put(url, headers=headers, json=data, timeout=30) + response.raise_for_status() + return response.json() + except requests.exceptions.RequestException as e: + return {"error": str(e)} + + def patch(self, url, data): + try: + headers = { + "x-api-key": self.api_key + } + response = requests.patch(url, headers=headers, json=data, timeout=30) + response.raise_for_status() + return response.json() + except requests.exceptions.RequestException as e: + return {"error": str(e)} + + def delete(self, url): + try: + headers = { + "x-api-key": self.api_key + } + response = requests.delete(url, headers=headers, timeout=30) + response.raise_for_status() + return response.json() + except requests.exceptions.RequestException as e: + return {"error": str(e)} + + def post_file(self, url, file): + try: + headers = { + "x-api-key": self.api_key + } + response = requests.post(url, headers=headers, files=file, timeout=60) + response.raise_for_status() + return response.json() + except requests.exceptions.RequestException as e: + return {"error": str(e)} + + def post_form_data(self, url, data): + try: + headers = { + "x-api-key": self.api_key + } + response = requests.post(url, headers=headers, files=data, timeout=60) + response.raise_for_status() + return response.json() + except requests.exceptions.RequestException as e: + return {"error": str(e)} + + def put_form_data(self, url, data): + try: + headers = { + "x-api-key": self.api_key + } + response = requests.put(url, headers=headers, files=data, timeout=60) + response.raise_for_status() + return response.json() + except requests.exceptions.RequestException as e: + return {"error": str(e)} + +# api = Api("2da392a6-79d2-4304-a8b7-959572c7e44d") +# response = api.get("https://api.sandbox.plugnotas.com.br/certificado") +# print(response) diff --git a/tecnospeed_rpc/src/utils/banco.py b/tecnospeed_rpc/src/utils/banco.py new file mode 100644 index 0000000..3a01938 --- /dev/null +++ b/tecnospeed_rpc/src/utils/banco.py @@ -0,0 +1,243 @@ +from datetime import datetime +import uuid +from sqlalchemy import Column, Integer, String, DateTime, Text, create_engine, Boolean +from sqlalchemy.orm import declarative_base, sessionmaker +from sqlalchemy.exc import SQLAlchemyError + +Base = declarative_base() + +class CertificadoDigitalTable(Base): + __tablename__ = 'certificado_digital' + id = Column(Integer, primary_key=True, autoincrement=True) + id_tecnospeed = Column(String(50), nullable=False) + certificado = Column(Text, nullable=False) + createdAt = Column(DateTime, default=datetime.now) + updatedAt = Column(DateTime, default=datetime.now, onupdate=datetime.now) + deletedAt = Column(DateTime, nullable=True) + expirationAt = Column(DateTime, nullable=False) + senha = Column(String(50), nullable=False) + status = Column(Boolean, nullable=False) + id_empresa = Column(String(50), nullable=True) + id_usuario = Column(Integer, nullable=True) + cnpj = Column(String(50), nullable=True) + email = Column(String(255), nullable=True) + +class EnterpriseTable(Base): + __tablename__ = 'Enterprise' + id = Column(String(50), primary_key=True) + nome_fantasia = Column(String(255), nullable=True) # Antigo 'name' + razao_social = Column(String(255), nullable=True) + cnpj = Column(String(50), nullable=False) + banco = Column(Text, nullable=True) + email = Column(String(255), nullable=True) + createdAt = Column(DateTime, default=datetime.now) + updatedAt = Column(DateTime, default=datetime.now, onupdate=datetime.now) + deletedAt = Column(DateTime, nullable=True) + status = Column(Boolean, nullable=False, default=True) + +class LogoTable(Base): + __tablename__ = 'logo' + id = Column(Integer, primary_key=True, autoincrement=True) + logo = Column(Text, nullable=False) + id_empresa = Column(String(50), nullable=False) + createdAt = Column(DateTime, default=datetime.now) + updatedAt = Column(DateTime, default=datetime.now, onupdate=datetime.now) + deletedAt = Column(DateTime, nullable=True) + status = Column(Boolean, nullable=False) + +class LogTable(Base): + __tablename__ = 'log' + id = Column(Integer, primary_key=True, autoincrement=True) + id_usuario = Column(Integer, nullable=False) + createdAt = Column(DateTime, default=datetime.now) + updatedAt = Column(DateTime, default=datetime.now, onupdate=datetime.now) + deletedAt = Column(DateTime, nullable=True) + acao = Column(String(50), nullable=False) + tabela = Column(String(50), nullable=False) + id_registro = Column(String(50), nullable=False) + json = Column(Text, nullable=False) + status = Column(String(20), nullable=False, default='INICIO') + mensagem = Column(Text, nullable=False, default='') + cliente = Column(String(100), nullable=True) + correlation_id = Column(String(50), nullable=True) + +class ServicoTable(Base): + __tablename__ = 'servico' + id = Column(Integer, primary_key=True, autoincrement=True) + id_tecnospeed = Column(String(50), nullable=False) + descricao = Column(Text, nullable=False) + id_usuario = Column(Integer, nullable=False) + createdAt = Column(DateTime, default=datetime.now) + updatedAt = Column(DateTime, default=datetime.now, onupdate=datetime.now) + deletedAt = Column(DateTime, nullable=True) + status = Column(Boolean, nullable=False) + +class TomadorTable(Base): + __tablename__ = 'tomador' + id = Column(Integer, primary_key=True, autoincrement=True) + id_tecnospeed = Column(String(50), nullable=False) + nome = Column(String(50), nullable=False) + cpf_cnpj = Column(String(50), nullable=False) + email = Column(String(50), nullable=False) + telefone = Column(String(50), nullable=False) + id_usuario = Column(Integer, nullable=False) + createdAt = Column(DateTime, default=datetime.now) + updatedAt = Column(DateTime, default=datetime.now, onupdate=datetime.now) + deletedAt = Column(DateTime, nullable=True) + status = Column(Boolean, nullable=False) + +class NFSeTable(Base): + __tablename__ = 'nfse' + id = Column(Integer, primary_key=True, autoincrement=True) + id_tecnospeed = Column(String(50), nullable=False) + id_empresa = Column(String(50), nullable=False) + id_usuario = Column(Integer, nullable=False) + createdAt = Column(DateTime, default=datetime.now) + updatedAt = Column(DateTime, default=datetime.now, onupdate=datetime.now) + deletedAt = Column(DateTime, nullable=True) + status = Column(Boolean, nullable=False) + +class NFeTable(Base): + __tablename__ = 'nfe' + id = Column(Integer, primary_key=True, autoincrement=True) + id_tecnospeed = Column(String(50), nullable=False) + id_empresa = Column(String(50), nullable=False) + id_usuario = Column(Integer, nullable=False) + createdAt = Column(DateTime, default=datetime.now) + updatedAt = Column(DateTime, default=datetime.now, onupdate=datetime.now) + deletedAt = Column(DateTime, nullable=True) + status = Column(Boolean, nullable=False) + +# Cache de motores criados pelo SQLAlchemy por URL de conexao +_engines = {} + +# Abstração de conexão base e de persistência de instâncias de metadados relacionais usando PostgreSQL +class Banco: + def __init__(self, host, port, user, password, database): + conn_str = f'postgresql://{user}:{password}@{host}:{port}/{database}' + if conn_str not in _engines: + _engines[conn_str] = create_engine( + conn_str, + pool_pre_ping=True, + pool_recycle=3600 + ) + self.engine = _engines[conn_str] + self.Session = sessionmaker(bind=self.engine) + self.session = self.Session() + + def close(self): + """Encerra a sessão SQLAlchemy atual liberando do pooler.""" + if hasattr(self, 'session') and self.session is not None: + self.session.close() + + def create_tables(self): + """Cria as tabelas no banco de dados.""" + try: + Base.metadata.create_all(self.engine) + except SQLAlchemyError as e: + print(f"Erro ao criar tabelas (SQLAlchemy): {e}") + except Exception as e: + print(f"Erro ao criar tabelas: {e}") + + def add(self, obj): + """Adiciona um novo registro no banco.""" + try: + self.session.add(obj) + self.session.commit() + return obj + except SQLAlchemyError as e: + self.session.rollback() + print(f"Erro ao adicionar registro (SQLAlchemy): {e}") + raise e + except Exception as e: + self.session.rollback() + print(f"Erro ao adicionar registro: {e}") + raise e + + def get(self, model, obj_id): + """Busca um registro pelo ID.""" + try: + return self.session.query(model).get(obj_id) + except SQLAlchemyError as e: + self.session.rollback() + print(f"Erro ao buscar registro (SQLAlchemy): {e}") + raise e + except Exception as e: + self.session.rollback() + print(f"Erro ao buscar registro: {e}") + raise e + + def get_all(self, model): + """Retorna todos os registros de um modelo.""" + try: + return self.session.query(model).all() + except SQLAlchemyError as e: + print(f"Erro ao buscar registros (SQLAlchemy): {e}") + raise e + except Exception as e: + print(f"Erro ao buscar registros: {e}") + raise e + + def update(self, obj): + """Atualiza um registro no banco.""" + try: + self.session.commit() + return obj + except SQLAlchemyError as e: + self.session.rollback() + print(f"Erro ao atualizar registro (SQLAlchemy): {e}") + raise e + except Exception as e: + self.session.rollback() + print(f"Erro ao atualizar registro: {e}") + raise e + + def delete(self, obj): + """Deleta um registro do banco.""" + try: + self.session.delete(obj) + self.session.commit() + return True + except SQLAlchemyError as e: + self.session.rollback() + print(f"Erro ao deletar registro (SQLAlchemy): {e}") + raise e + except Exception as e: + self.session.rollback() + print(f"Erro ao deletar registro: {e}") + raise e + + def rollback(self): + """Desfaz as alterações pendentes.""" + try: + self.session.rollback() + except SQLAlchemyError as e: + print(f"Erro ao desfazer alterações (SQLAlchemy): {e}") + except Exception as e: + print(f"Erro ao desfazer alterações: {e}") + + def logar_sistema(self, id_usuario, acao, tabela, id_registro=0, json="{}", status="INICIO", mensagem="", cliente=None, correlation_id=None): + """Registra uma ação no log do sistema.""" + try: + log = LogTable( + id_usuario=id_usuario, + acao=acao, + tabela=tabela, + id_registro=id_registro, + json=json, + status=status, + mensagem=mensagem, + cliente=cliente, + correlation_id=correlation_id + ) + self.add(log) + except SQLAlchemyError as e: + print(f"Erro ao logar ação (SQLAlchemy): {e}") + except Exception as e: + print(f"Erro ao logar ação: {e}") + + @staticmethod + def gerar_correlation_id(): + """Gera um ID único para rastrear toda a cadeia de uma operação.""" + return str(uuid.uuid4())[:8] + diff --git a/tecnospeed_rpc/src/utils/enviaListerner.py b/tecnospeed_rpc/src/utils/enviaListerner.py new file mode 100644 index 0000000..6db4635 --- /dev/null +++ b/tecnospeed_rpc/src/utils/enviaListerner.py @@ -0,0 +1,35 @@ +import requests +import json +import os + +class enviaListerner: + + def __init__(self, logger=None): + self.url = os.getenv("LISTENER_INTERNAL_URL", "http://endpoint_listener:8000/listenerPlugnotas") + self.logger = logger + + def notificar_emissao(self, id_tecnospeed: str, documento: str, cnpj: str, status: str = "PROCESSANDO"): + """ + Envia um POST para o listener local para sincronização imediata. + """ + payload = { + "id": id_tecnospeed, + "status": status, + "documento": documento, + "emitente": cnpj + } + + try: + response = requests.post(self.url, json=payload, timeout=5) + + if self.logger: + if response.status_code in [200, 201]: + self.logger.log_logs(f"Notificação interna enviada com sucesso - ID: {id_tecnospeed} | Status: {status}") + else: + self.logger.log_errors(f"Falha na notificação interna - Status: {response.status_code} | Resposta: {response.text}") + + return response.status_code in [200, 201] + except Exception as e: + if self.logger: + self.logger.log_errors(f"Erro ao tentar notificar listener interno: {e}") + return False diff --git a/tecnospeed_rpc/src/utils/exchangeModels.py b/tecnospeed_rpc/src/utils/exchangeModels.py new file mode 100644 index 0000000..1e19116 --- /dev/null +++ b/tecnospeed_rpc/src/utils/exchangeModels.py @@ -0,0 +1,127 @@ + +__version__ = "1.0.0 - 18/02/2025" # VERSAO PARA TODOS OS SISTEMAS TRANSMITINDO PARA O SERVIÇO LOGGER_CONTROLLER DO SG_IC2 +__version__ = "1.0.1 - 15/01/2026 - S.D.G. - Ajustes" +__version__ = "1.0.2 - 23/01/2026 - S.D.G. - ForwardCompatibleModel & SerializeAsAny" +from pydantic import BaseModel, field_validator, ValidationInfo, Field, ConfigDict, SerializeAsAny +from enum import StrEnum +from typing import Union, Any, Optional + +class ForwardCompatibleModel(BaseModel): + """ + Base para todos os modelos que permite campos extras. + O Pydantic V2 gerencia automaticamente o dicionário 'model_extra'. + """ + model_config = ConfigDict(extra='allow') + +class RealmType(StrEnum): + LOGS = "LOGS" + METRICS = "METRICS" + STATUS = "STATUS" + ERRORS = "ERRORS" + OPERATIONAL = "OPERATIONAL" + +class OpStatus(StrEnum): + ACTIVE = "ACTIVE" + OK = "OK" + ERROR = "ERROR" + +class ICLogs(ForwardCompatibleModel): + log_date : str + service_date : str + service : str + versionstr : str + origin : str + enterprise_id : str + enterprise_name : str + message : str + tenant : Optional[str] = Field(default='#', description="Tenant ou '#' para global") + +class ICErrors(ForwardCompatibleModel): + log_date : str + service_date : str + service : str + versionstr : str + origin : str + enterprise_id : str + enterprise_name : str + message : str + tenant : Optional[str] = Field(default='#', description="Tenant ou '#' para global") + +class ICMetrics(ForwardCompatibleModel): + metric_date : str + metric : str + metric_name : str + value : str + unit : str + tenant : Optional[str] = Field(default='#', description="Tenant ou '#' para global") + +class ICOperational(ForwardCompatibleModel): + log_date : str # YYYY-MM-DD HH:MM:S + service_date : str # YYYY-MM-DD HH:MM:S + service_name : str + versionstr : str + origin : str + enterprise_id : str + enterprise_name : str + status : OpStatus + step_status : str + message : str + +class ICStatus(ForwardCompatibleModel): + service : str + versionstr : str + system_name : str + startup_date : str + access_date : str + origin : str + sid : Optional[str] = "" + task_count : Optional[int] = 0 + cpu : Optional[float] = 0.0 + memory : Optional[float] = 0.0 + disk : Optional[float] = 0.0 + network : Optional[float] = 0.0 + +# Modelo polimórfico gerador de metadados JSON para o Exchange de Logs do Sistema Inteirador +class InfoConnector(ForwardCompatibleModel): + realm: RealmType + + # SerializeAsAny garante que se 'messageIC' for uma subclasse (como 'Operation'), + # os campos extras da subclasse (como 'reserva_id') aparecerão no JSON final. + messageIC: SerializeAsAny[Union[ICLogs, ICErrors, ICMetrics, ICStatus, ICOperational]] + + @field_validator('messageIC', mode='before') + @classmethod + def validate_schema(cls, v: Any, info: ValidationInfo) -> Any: + realm = info.data.get("realm") + if realm is None: + # Se realm não existe, o próprio Pydantic lançará erro de validação do campo 'realm' + return v + + # Mapeamento de Realm para Classe Esperada + target_map = { + RealmType.LOGS : ICLogs, + RealmType.ERRORS : ICErrors, + RealmType.METRICS : ICMetrics, + RealmType.STATUS : ICStatus, + RealmType.OPERATIONAL: ICOperational, + } + + expected_class = target_map.get(realm) + if not expected_class: + raise ValueError(f"Invalid realm: {realm}") + + # Se for um dicionário (vindo de um JSON), validamos contra a classe mapeada + if isinstance(v, dict): + return expected_class.model_validate(v) + + # Se já for um objeto instanciado (como no seu Builder), verificamos a herança + if not isinstance(v, expected_class): + raise ValueError(f"Expected {expected_class.__name__} for realm {realm}, got {type(v).__name__}") + + return v + + model_config = ConfigDict( + extra="allow", + frozen=False, + str_strip_whitespace=True + ) \ No newline at end of file diff --git a/tecnospeed_rpc/src/utils/redis.py b/tecnospeed_rpc/src/utils/redis.py new file mode 100644 index 0000000..a712eb1 --- /dev/null +++ b/tecnospeed_rpc/src/utils/redis.py @@ -0,0 +1,107 @@ +import redis + +# Fornece mecanismos de Caching e Persistência Volátil/PubSub na aplicação usando Redis +class RedisClient: + def __init__(self, host='localhost', port=6379, db=0): + """ + Inicializa o cliente Redis com as configurações fornecidas. + """ + self.client = redis.Redis(host=host, port=port, db=db, decode_responses=True) + + def set(self, key, value): + """ + Define um valor para uma chave no Redis. + """ + self.client.set(key, value) + + def setex(self, key, value, ex): + """ + Define um valor para uma chave no Redis com um tempo de expiração em segundos. + """ + self.client.set(key, value, ex) + + def get(self, key): + """ + Obtém o valor de uma chave no Redis. + """ + return self.client.get(key) + + def get_keys(self, pattern="*"): + """ + Obtém todas as chaves que correspondem a um padrão. + """ + return self.client.keys(pattern) + + def get_all(self): + """ + Obtém todas as chaves e seus valores no Redis. + """ + keys = self.client.keys('*') + return {key: self.client.get(key) for key in keys} + + def delete(self, key): + """ + Remove uma chave do Redis. + """ + self.client.delete(key) + + def exists(self, key): + """ + Verifica se uma chave existe no Redis. + Retorna True se a chave existir, caso contrário, False. + """ + return self.client.exists(key) > 0 + + def close(self): + """ + Fecha a conexão com o Redis. + """ + self.client.close() + + def set_expiration(self, key, value, seconds): + """ + Define um valor para uma chave e configura um tempo de expiração em segundos. + """ + self.client.set(key, value, ex=seconds) + + def increment(self, key, amount=1): + """ + Incrementa o valor de uma chave numérica no Redis. + """ + return self.client.incr(key, amount) + + def decrement(self, key, amount=1): + """ + Decrementa o valor de uma chave numérica no Redis. + """ + return self.client.decr(key, amount) + + def hset(self, name, key, value): + """ + Define um campo em um hash no Redis. + """ + self.client.hset(name, key, value) + + def hget(self, name, key): + """ + Obtém o valor de um campo em um hash no Redis. + """ + return self.client.hget(name, key) + + def hgetall(self, name): + """ + Obtém todos os campos e valores de um hash no Redis. + """ + return self.client.hgetall(name) + + def expire(self, key, seconds): + """ + Define um tempo de expiração para uma chave existente. + """ + self.client.expire(key, seconds) + + def ttl(self, key): + """ + Obtém o tempo restante de vida (TTL) de uma chave. + """ + return self.client.ttl(key) \ No newline at end of file diff --git a/tecnospeed_rpc/src/webhooks/webhooks.py b/tecnospeed_rpc/src/webhooks/webhooks.py new file mode 100644 index 0000000..4106a1c --- /dev/null +++ b/tecnospeed_rpc/src/webhooks/webhooks.py @@ -0,0 +1,64 @@ +from utils.api import Api +from utils.banco import Banco +from utils.Exchange2 import LoggerExchange2 +from utils.exchangeModels import RealmType +import json + +# Serviço responsável por vincular rotas (webhooks) por CNPJ onde a PlugNotas enviará notificações +class Webhooks: + def __init__(self, url_base, api_key, banco:Banco, logger: LoggerExchange2 = None): + self.api = Api(api_key) + self.url = f"{url_base}/empresa/" + self.banco = banco + self.logger = logger + + def _log(self, realm: RealmType, msg: str): + if self.logger: + self.logger.logIC2(realm, (msg,)) + + def consultar_webhooks(self, cnpj): + try: + resposta = self.api.get(f"{self.url}{cnpj}/webhook") + self._log(RealmType.OPERATIONAL, f"Webhook consultado - CNPJ: {cnpj}") + return resposta + except Exception as e: + self._log(RealmType.ERRORS, f"Erro ao consultar webhook: {e}") + return {"error": str(e)} + + def criar_webhook(self, cnpj, url): + try: + resposta = self.api.post(f"{self.url}{cnpj}/webhook", {"url": url}) + self._log(RealmType.OPERATIONAL, f"Webhook criado - CNPJ: {cnpj}") + return resposta + except Exception as e: + self._log(RealmType.ERRORS, f"Erro ao criar webhook: {e}") + return {"error": str(e)} + + def deletar_webhook(self, cnpj): + try: + resposta = self.api.delete(f"{self.url}{cnpj}/webhook") + self._log(RealmType.OPERATIONAL, f"Webhook deletado - CNPJ: {cnpj}") + return resposta + except Exception as e: + self._log(RealmType.ERRORS, f"Erro ao deletar webhook: {e}") + return {"error": str(e)} + + def atualizar_webhook(self, cnpj, url): + try: + resposta = self.api.put(f"{self.url}{cnpj}/webhook", {"url": url,"method": "POST"}) + self._log(RealmType.OPERATIONAL, f"Webhook atualizado - CNPJ: {cnpj}") + return resposta + except Exception as e: + self._log(RealmType.ERRORS, f"Erro ao atualizar webhook: {e}") + return {"error": str(e)} + + def testar_webhook(self, cnpj): + try: + resposta = self.api.post(f"{self.url}{cnpj}/webhook/verify", {}) + self._log(RealmType.OPERATIONAL, f"Webhook testado - CNPJ: {cnpj}") + return resposta + except Exception as e: + self._log(RealmType.ERRORS, f"Erro ao testar webhook: {e}") + return {"error": str(e)} + + \ No newline at end of file diff --git a/utils/Exchange2.py b/utils/Exchange2.py new file mode 100644 index 0000000..d6af010 --- /dev/null +++ b/utils/Exchange2.py @@ -0,0 +1,1041 @@ +import json, time, sys, inspect, os +import hmac, hashlib, uuid, re + +import pika, pprint +from datetime import datetime +from pytz import timezone +from cryptography.hazmat.primitives.ciphers import Cipher, algorithms, modes +from cryptography.hazmat.primitives import padding +from cryptography.hazmat.backends import default_backend + +from queue import Queue, Full, Empty +from threading import Thread, Event +from abc import ABC, abstractmethod +from typing import Sequence, Any +import logging + +from utils.exchangeModels import * + +versao = "v1.0.0 14/12/2023 - Baltazar - Prefetch count" +versao = "v1.1.0 06/02/2024 - MJ. - Detecta Serviço rabbitmq ausente no inicio" +versao = "v1.2.0 20/02/2024 - MJ. - Possibilita uma taksqueue por serviço" +versao = "v1.3.0 06/03/2024 - MJ. - Verboso apenas nos erros" +versao = "v1.5.0 07/03/2024 - MJ. - External Rabbitmq Connection" +versao = "v1.6.0 26/06/2024 - MJ. - Conversao dos parametros host, service e client para lowercase" +versao = "v1.7.0 26/07/2024 - MJ. - Prepacao para utilizar Cloudamqp ou Rabbitmq local amqps/system" +versao = "v2.0.0 16/08/2024 - MJ. - Nova classe RPC e restruturação de Exchange2 e Tasks" +versao = "v2.1.0 21/11/2024 - MJ. - Clareza no log e tratamento de erros" +versao = "v2.2.0 31/01/2025 - MJ. - Remoção de alguns logs e melhorias de performance" +versao = "v2.3.0 12/02/2025 - MJ. - Melhorias na conexão com o Rabbitmq" +versao = "v2.4.0 14/02/2025 - MJ. - Check amqp url parameters" +versao = "v2.4.1 19/02/2025 - MJ. - Correcao prefetch_count=1" +versao = "v2.5.1 13/05/2025 - MJ. - Payload até 128kb - ajuste documentação e visual" +versao = "v2.5.2 01/10/2025 - MJ. - Evita especificar um client vazio" +versao = "v3" +versaostr = f"{versao}0.0 16/01/2026 S.D.G. - SG_IC2 LoggerExchange" + +def is_amqp_url(url): + pattern = r"^amqps?:\/\/([a-zA-Z0-9_\-\.]+):([^@]+)@([a-zA-Z0-9_\-\.]+)(:[0-9]+)?(\/[a-zA-Z0-9_\-\.]*)?$" + + return bool(re.match(pattern, url)) + +# QUEUE EXCHANGE - Apoia no comunicação inter-serviços de um sistema +# EXCHANGE TIPO DIRECT +# NAO DURAVEL - EXCLUSIVO - AUTODELETE +# SE NAO HOUVER QUEM RECEBA A MSG A MSG SERÁ PERDIDA -> O ENVIO FUNCIONA MESMO QUE NINGUEM EXISTA PARA RECEBER +class QueueExchange2: + def __init__( self, + amqps:str, + origin:str, + system:str, + service:str, + version:str, + client:str = '#', + callback: object = None, + delivery_mode = 1): + + paramOK = len(service) and len(system) and len(amqps) and len(origin) and len(version) and len(client) + if not (paramOK) or not is_amqp_url(amqps): + if not is_amqp_url(amqps): + msg = f"NOT A VALID AMQP URL: {amqps}" + else: + msg = f"VERIFY PARAMETERS: is_amqp_url: {is_amqp_url(amqps)}\n amqps[{amqps}]\n origin[{origin}]\n system[{system}]\n service[{service}]\n version[{version}\n client[{client}]" + raise Exception(msg) + + self.delivery = delivery_mode + self.payload = "" + self.hmac = "" + self.version = version + self.serviceid = "" + self.type = "msg" + self.origin = origin.lower() + self.client = client.lower() + self.service = service.lower() + self.system = system.lower() + self.client = client + self.params = pika.URLParameters(amqps) + self.callback = callback + self.queue = f"{self.system}_{service}_{self.client}" if client != '#' else f"{self.system}_{service}" + if callback is not None: + try: + self.connection = pika.BlockingConnection(self.params) + self.channelIn = self.connection.channel() # start main channel + self.channelIn.exchange_declare (exchange = f"{self.system}.direct", exchange_type = 'direct') + self.channelIn.queue_declare (queue = self.queue, durable = True) + self.channelIn.queue_bind (queue = self.queue, exchange = f"{self.system}.direct", routing_key= self.queue) + self.channelIn.basic_qos (prefetch_count=1) + self.consumerTag = self.channelIn.basic_consume (queue = self.queue, + auto_ack = False, + on_message_callback = self.callback) + except Exception as error: + raise(error) + else: + conexao = pika.BlockingConnection(self.params) #testa conexao + conexao.close() + + def cancel(self): + self.channelIn.basic_cancel(self.consumerTag) + + # METODOS INTERNOS _ + # MODELO MSG QUE TRAFEGA ENTRE SERVIÇOS E CONTROLLER + """ + MSG JSON CONTROLADOR + data: + { + "salesContractId": , + "client": "bambui", + "service": "SALES_CONTRACT_CREATED" + } + + message: + { ..., + "serviceid": , + "payload": aes-256 , + "hmac": hmac , + "type": "work", + ... + } + """ + def _setMsg(self, msg:str, caller:str): + now = datetime.now(timezone('America/Sao_Paulo')) + message =msg.strip('"') + + self.Msg = { + "system": self.system, + "target": self.target, + "service": self.service, + "serviceid": self.serviceid, + "payload": self.payload, + "hmac": self.hmac, + "type": self.type, + "version": self.version, + "origin": self.origin, + "client": self.client, + "caller": caller, + "logDate": f"{now}", + "msg": f"{message}" + } + + # Envia uma mensagem para um serviço associado ao sistema + # controller é o nome padrão do serviço, caso nao seja informado + def sendMsg(self, message: str, service:str = 'controller'): + trials = 5 + waitTime = 12 + messageSent = False + caller = inspect.stack()[1].function + if caller == '': caller = 'main' + + self.target = service + self._setMsg(message, caller) + targetQueue = f"{self.system}_{self.target}_{self.client}" if self.client != '#' else f"{self.system}_{self.target}" + while not messageSent and trials: + try: + connection = pika.BlockingConnection(self.params) + channelOut = connection.channel() # start a channel + #channelOut.exchange_declare(exchange = f"{self.system}.direct", exchange_type = 'direct') + #channelOut.queue_declare (queue= targetQueue, durable= True) + #channelOut.queue_bind (queue= targetQueue, exchange= f"{self.system}.direct", routing_key= targetQueue) + channelOut.basic_publish( + exchange = f"{self.system}.direct", + routing_key = targetQueue, + body = json.dumps(self.Msg), + properties = pika.BasicProperties(content_type = 'text/plain', delivery_mode= self.delivery)) + messageSent = True + connection.close() + except Exception as ex: + print(f"EXCH2: TROUBLE CNX RABBITMQ REMAIN ATTEMPT: {trials}") + print(ex, flush=True) + sys.stdout.flush() + time.sleep(waitTime) + trials -= 1 + if messageSent: + self.payload = "" + self.hmac = "" + else: + print(f"EXCH2: ERROR MESSAGE NOT SENT TO [{targetQueue}]", flush=True) + + def start_consuming(self): + if self.callback is not None: + self.consumerTag = self.channelIn.start_consuming() + + def setCrypto(self, serviceid:str, servicekey:str): + self.serviceid = serviceid + self.servicekey = servicekey + self.Crypto = Payload(serviceid, servicekey) + + def setPayload(self, data:str): + try: + self.hmac = "" + self.payload = "" + self.hmac, self.payload = self.Crypto.setPayload(data) + except Exception as ex: + print(f"EXCH2: CRYPTO ERROR: {ex}", flush=True) + + def getPayload(self, hmac:str, safeData:str): + try: + self.hmac = "" + self.payload = "" + self.hmac, self.payload = self.Crypto.getPayload(safeData) + except Exception as ex: + print(ex) + return False + + return hmac == self.hmac + +# CRIPTOGRAFIA DO PAYLOAD +class Payload: + def __init__(self, serviceid:str, servicekey:str): + self.serviceid = serviceid + self.servicekey = servicekey + + + def setPayload(self, data:str): + data = data.encode('utf-8') + tamanho = len(data) + if tamanho > 1024 * 1024 * 10 or tamanho <= 0: + raise Exception("EXCHANGE2 Payload setPayload: Invalid Payload size") + + hmacData= hmac.new(self.serviceid.encode('utf-8'), data, 'sha256').hexdigest() + iv = os.urandom(16) + padder = padding.PKCS7(128).padder() + datarw = padder.update(data) + padder.finalize() + cipher = Cipher(algorithms.AES(self.servicekey.encode('utf-8')), modes.CBC(iv)) + encrypt = cipher.encryptor() + safeData = encrypt.update(datarw) + encrypt.finalize() + payload = (iv + safeData) + + return (hmacData, payload.hex()) + + def getPayload(self, safeData:str): + safeData = bytes.fromhex(safeData) + iv = safeData[:16] + cipher = Cipher(algorithms.AES(self.servicekey.encode('utf-8')), modes.CBC(iv)) + decrypt = cipher.decryptor() + rawdata = decrypt.update(safeData[16:]) + decrypt.finalize() + if len(rawdata)%16 != 0: + raise Exception("EXCHANGE2 Payload getPayload: Invalid Payload size block {0}".format(len(rawdata))) + + unpadder = padding.PKCS7(128).unpadder() + data = unpadder.update(rawdata) + unpadder.finalize() + hmacData = hmac.new(self.serviceid.encode('utf-8'), data, 'sha256').hexdigest() + + return (hmacData, data.decode('utf-8')) + +# TASK EXCHANGE +# PERMITE CRIAR UM DISTRIBUIDOR DE TAREFAS +# O DISTRIBUIDOR DEVE RECEBER UMA FUNCAO DE CALLBACK +class TaskExchange2: + def __init__(self, + amqps:str, + origin:str, + system:str, + service:str, + version:str, + client:str = '#', callback: object = None, delivery_mode:int = 2): + paramOK = len(service) and len(system) and len(amqps) and len(origin) and len(version) + if not (paramOK) or not is_amqp_url(amqps): + if not is_amqp_url(amqps): + msg = f"NOT A VALID AMQP URL {amqps}" + else: + msg = f"VERIFY PARAMETERS: is_amqp_url: {is_amqp_url(amqps)}\n amqps[{amqps}]\n origin[{origin}]\n system[{system}]\n service[{service}]\n version[{version}]" + raise Exception(msg) + + self.delivery = delivery_mode + self.payload = "" + self.hmac = "" + self.version = version + self.serviceid = "" + self.type = "task" + self.origin = origin.lower() + self.service = service.lower() + self.system = system.lower() + self.client = client.lower() + self.params = pika.URLParameters(amqps) + self.callback = callback + self.queue = f'{self.system}_{self.service}_tasks_{client}' if client != '#' else f'{self.system}_{self.service}_tasks' + print(f"EXCH2: TASK EXCHANGE: [{self.queue}] [{amqps}]", flush=True) + if callback is not None: + self.connection = pika.BlockingConnection(self.params) + self.channelIn = self.connection.channel() # start main channel + self.channelIn.exchange_declare (exchange = f"{self.system}.direct", exchange_type = 'direct') + self.channelIn.queue_declare (queue = self.queue, durable = True) + self.channelIn.queue_bind (queue = self.queue, exchange = f"{self.system}.direct", routing_key= self.queue) + self.channelIn.basic_qos (prefetch_count=1) + self.consumerTag = self.channelIn.basic_consume (queue = self.queue, + auto_ack = False, + on_message_callback = self.callback) + else: + conexao = pika.BlockingConnection(self.params) #testa conexao + conexao.close() + def _setTask(self, msg:str, caller:str): + now = datetime.now(timezone('America/Sao_Paulo')) + message =msg.strip('"') + + self.Msg = { + "system": self.system, + "target": self.target, + "service": self.service, + "serviceid": self.serviceid, + "payload": self.payload, + "hmac": self.hmac, + "type": self.type, + "version": self.version, + "origin": self.origin, + "client": self.client, + "caller": caller, + "logDate": f"{now}", + "msg": f"{message}" + } + + def cancel(self): + self.channelIn.basic_cancel(self.consumerTag) + + def sendTask(self, message:str): + trials = 5 + waitTime = 12 + messageSent = False + caller = 'TaskExchange2' + self.target = 'workers' + self._setTask(message, caller) + + while not messageSent and trials: + try: + connection = pika.BlockingConnection(self.params) + channelOut = connection.channel() # start a channel + channelOut.exchange_declare(exchange = f"{self.system}.direct", exchange_type = 'direct') + channelOut.queue_declare (queue = self.queue, durable = True) + channelOut.queue_bind (queue = self.queue, exchange = f"{self.system}.direct", routing_key= self.queue) + + channelOut.basic_publish (exchange = f"{self.system}.direct", + routing_key = self.queue, + body = json.dumps(self.Msg), + properties = pika.BasicProperties(content_type = 'text/plain', + delivery_mode= self.delivery)) + messageSent = True + connection.close() + except Exception as ex: + print(f"EXCH2: TROUBLE CNX RABBITMQ REMAIN ATTEMPT: {trials}") + print(ex, flush=True) + time.sleep(waitTime) + trials -= 1 + if not messageSent: + print(f"EXCH2: ERROR TASK NOT SENT TO: {self.queue}", flush=True) + + def start_consuming(self): + if self.callback is not None: + print(f"EXCH2: SEND TASKS TO: [{self.queue}]", flush=True) + self.consumerTag = self.channelIn.start_consuming() + + def setCrypto(self, serviceid:str, servicekey:str): + self.serviceid = serviceid + self.servicekey = servicekey + self.Crypto = Payload(serviceid, servicekey) + + def setPayload(self, data:str): + try: + self.hmac = "" + self.payload = "" + self.hmac, self.payload = self.Crypto.setPayload(data) + except Exception as ex: + print(f"EXCH2: CRYPTO ERROR: {ex}", flush=True) + + def getPayload(self, hmac:str, safeData:str): + try: + self.hmac = "" + self.payload = "" + self.hmac, self.payload = self.Crypto.getPayload(safeData) + except Exception as ex: + print(f"EXCH2: CRYPTO ERROR: {ex}", flush=True) + return False + + return hmac == self.hmac + +# RPC - QUANDO FOR UTILIZAR COMO ENDPOINT +# FORNECER FUNCAO DE TRABALHO NA INICIALIZACAO +# COMO RPC Server: FILA ONDE RECEBERA AS TAREFAS => SISTEM_SERVICE_RPC ou SISTEM_SERVICE_RPC_CLIENT +# COMO RPC Client: UTILIZA callRPC +class RPCExchange2: + def __init__(self, + amqps:str, # ENDERECO URL RABBITMQ - CLOUDAMQP + origin:str, # NOME DO HOST ONDE O SCRIPT ESTÁ RODANDO + system:str, # NOME DO SISTEMA QUE O SERVIÇO FAZ PARTE + service:str, # NOME DO SERVIÇO DENTRO DO SISTEMA + version:str, # VERSÃO DO SERVIÇO + client:str = '#', # CASO ESPECIFICO A UM CLIENTE, COMO NO CASO DO SG_ETL + workfunction=None, # FUNCAO DE TRABALHO PARA O USO COMO RCP Server, i.e. para substituir um endpoint fastapi + delivery_mode=2 # VAI PERSISTIR AS MENSAGENS NO MODO 2 + ): + paramOK = len(service) and len(system) and len(amqps) and len(origin) and len(version) + + if not (paramOK) or not is_amqp_url(amqps): + if not is_amqp_url(amqps): + msg = f"RPCExchange2: NOT A VALID AMQP URL: {amqps}" + else: + msg = f"RPCExchange2: VERIFY PARAMETERS: is_amqp_url: {is_amqp_url(amqps)}\n amqps[{amqps}]\n origin[{origin}]\n system[{system}]\n service[{service}]\n version[{version}]" + raise Exception(msg) + + self.callback = workfunction + self.delivery = delivery_mode + self.payload = "" + self.hmac = "" + self.version = version + self.serviceid = "" + self.servicekey = "" + self.correlation_id = "" + self.consumerTag = None + self.type = "rpc" + self.origin = origin.lower() + self.service = service.lower() + self.system = system.lower() + self.client = client.lower() + self.params = pika.URLParameters(amqps) + # Server ouve nessa fila - Cliente publica nesta fila + self.queue = f'{self.system}_{self.service}_rpc_{client}' if client != '#' else f'{self.system}_{self.service}_rpc' + self.connection = pika.BlockingConnection(self.params) + self.channel = self.connection.channel() # start main channel + self.channel.exchange_declare (exchange = f"{self.system}.direct", exchange_type = 'direct') + + if (workfunction is not None): # FUNCIONAMENTO COMO RPC SERVER + self.target = 'rpcclt' # SINALIZA O DESTINO DA MSG SER UM RPC CLIENT + self.channel.queue_declare (queue = self.queue, exclusive = False, durable=True, auto_delete = True) + self.channel.queue_bind (exchange = f"{self.system}.direct", queue = self.queue, routing_key = self.queue) + self.channel.basic_qos (prefetch_count = 1) + self.consumerTag = self.channel.basic_consume(queue = self.queue, + auto_ack = True, + on_message_callback = self._RPCResp) # de lá chamamos self.callback + else: # FUNCIONAMENTO COMO RPC CLIENT + self.target = 'rpcsrv' # SINALIZA O DESTINO DA MSG SER UM RPC SERVER + + # PREPARA QUEUE DE RETORNO + result = self.channel.queue_declare(queue='', exclusive=True, auto_delete=True) + self.callback_queue = result.method.queue + self.channel.queue_bind(exchange = f"{self.system}.direct", + queue = self.callback_queue, + routing_key = self.callback_queue) + + self.channel.basic_consume( + queue = self.callback_queue, + on_message_callback = self._on_response, + auto_ack = True + ) + #print("EXCH2: RPC CLT - QUEUE:", self.callback_queue, flush=True) + + # RPC - QUANDO FOR UTILIZAR COMO ENDPOINT + def _RPCResp(self, ch, method, props, body): + #ch.basic_ack(delivery_tag = method.delivery_tag) + message = body.decode("utf-8") + reply_to = getattr(props, 'reply_to', None) + correlation_id = getattr(props, 'correlation_id', None) + resposta = "{}" # SEM WORKFUNCTION + # ENVIA A MENSAGEM AO CALLBACK + self.payload = "" + self.hmac = "" + try: + if self.callback is not None: + resposta = self.callback(message) + rspObj = json.loads(resposta) + # para enviar um payload criptografado na resposta ao caller + # retorne um objeto contendo serviceid, servicekey e payload + # essas chaves serao retiradas do objeto e o restante da informacao + # irá no chave msg + if 'payload' in rspObj and 'serviceid' in rspObj and 'servicekey' in rspObj: + self.setCrypto(rspObj['serviceid'], rspObj['servicekey']) + self.setPayload(json.dumps(rspObj['payload'])) + rspObj.pop('payload', None) + rspObj.pop('serviceid', None) + rspObj.pop('servicekey', None) + resposta = json.dumps(rspObj) + + + except Exception as ex: + erro = f"ERROR CALLBACK: {str(ex)}" + print(erro, flush=True) + resposta = json.dumps({"status": "error", "message": erro}) + + # ENVIA RESPOSTA AO CALLER + msg = json.loads(message) + msg = msg['msg'] + #print(f"RPC <== {msg}", flush=True) + #print(f"RPC ==> {resposta}", flush=True) + try: + if reply_to: + self.target = "rpcclt" + self._setJSON(resposta, 'RPCResp') + + ch.basic_publish(exchange = f"{self.system}.direct", + routing_key = reply_to, + body = json.dumps(self.Msg), + properties = pika.BasicProperties(content_type = 'application/json', + delivery_mode = self.delivery, + correlation_id = correlation_id)) + else: + print(f"RPCExchange2: NO TARGET FOUND: {resposta}", props, flush=True) + + except Exception as ex: + print(ex) + resposta = f"RPCExchange2: ERRO MSG CALLER: {str(ex)}" + raise Exception(resposta) + + + def _setJSON(self, msg:str, caller:str): + now = datetime.now(timezone('America/Sao_Paulo')) + message =msg.strip('"') + + self.Msg = { + "system": self.system, + "target": self.target, + "service": self.service, + "serviceid": self.serviceid, + "payload": self.payload, + "hmac": self.hmac, + "type": self.type, + "correlation_id": self.correlation_id, + "version": self.version, + "origin": self.origin, + "client": self.client, + "caller": caller, + "logDate": f"{now}", + "msg": f"{message}" + } + self.hmac = "" + self.payload = "" + + + def _on_response(self, ch, method, properties, body): + try: + if self.correlation_id == properties.correlation_id: + self.response = body.decode("utf-8") + except Exception as ex: + print(f"RPCExchange2: RPC ERROR: {ex}", flush=True) + sys.exit(0) + + return self.response + + # ENVIA A MENSAGEM AO CALLBACK + # RPC CLIENT CHAMA O RPC SERVER + def callRPC(self, message:str, timeout:int=10): + # ENVIA A MENSAGEM + self.response = None + self.correlation_id = str(uuid.uuid4()) + self._setJSON(message, 'callRPC') # COLOCA A message DENTRO DA CHAVE msg em self.Msg + + self.channel.exchange_declare(exchange = f"{self.system}.direct", exchange_type = 'direct') + self.channel.queue_declare (queue= self.queue, durable=True, exclusive=False, auto_delete=True) + self.channel.queue_bind (queue= self.queue, exchange= f"{self.system}.direct", routing_key= self.queue) + + self.channel.basic_publish (exchange = f"{self.system}.direct", + routing_key = self.queue, + body = json.dumps(self.Msg), + properties = pika.BasicProperties(content_type = 'application/json', + delivery_mode = self.delivery, + reply_to = self.callback_queue, + correlation_id= self.correlation_id)) + # AGUARDA O RETORNO + while self.response is None: + self.connection.process_data_events(time_limit=timeout) + return self.response + + def cancel(self): + print(f"RPCExchange2: CONSUMER TAG CANCELED: {self.consumerTag}", flush=True) + self.channel.basic_cancel(self.consumerTag) + + def start_consuming(self): + if self.callback is not None: + print(f"RPCExchange2: {self.service.upper()} RPC SRV QUEUE: [{self.queue}]", flush=True) + self.channel.start_consuming() + + def setCrypto(self, serviceid:str, servicekey:str): + self.serviceid = serviceid + self.servicekey = servicekey + self.Crypto = Payload(serviceid, servicekey) + + def setPayload(self, data:str): + print(f"RPCExchange2: SET PAYLOAD", flush=True) + try: + self.hmac = "" + self.payload = "" + self.hmac, self.payload = self.Crypto.setPayload(data) + except Exception as ex: + print(ex) + + def getPayload(self, hmac:str, safeData:str): + print(f"RPCExchange2: GET PAYLOAD", flush=True) + try: + self.hmac = "" + self.payload = "" + self.hmac, self.payload = self.Crypto.getPayload(safeData) + except Exception as ex: + print(ex) + return False + + return hmac == self.hmac + +# CADA APLICAÇÃO CRIA UMA CLASSE QUE ESTENDE IC2Builder IMPLEMENTANDO +# OS METODOS ABSTRATOS DE CADA REALMTYPE +class IC2Builder(ABC): + @abstractmethod + def _setLogs(self, message: tuple) -> InfoConnector: ... + @abstractmethod + def _setErrors(self, message: tuple) -> InfoConnector: ... + @abstractmethod + def _setOperational(self, message: tuple) -> InfoConnector: ... + @abstractmethod + def _setStatus(self, message: tuple) -> InfoConnector: ... + @abstractmethod + def _setMetrics(self, message: tuple) -> InfoConnector: ... + + def build(self, realm: RealmType, message: Sequence[Any]) -> InfoConnector: + handlers = { + RealmType.LOGS: self._setLogs, + RealmType.ERRORS: self._setErrors, + RealmType.OPERATIONAL: self._setOperational, + RealmType.STATUS: self._setStatus, + RealmType.METRICS: self._setMetrics, + } + + try: + return handlers[realm](message) + except KeyError: + raise ValueError(f"RealmType não suportado: {realm}") + +class LoggerExchange2: + def __enter__(self): return self + def __exit__(self, exc_type, exc, tb): self.close() + + def __init__(self, + ic2_builder: IC2Builder, + amqps : str, + system : str, + service : str, + version : str, + origin : str = "", + client : str = "#", + logType : str = 'logs', + logLocal : bool = False, + callback : object = None): + self.callback = callback + self.ic2_builder = ic2_builder + self.system = system.lower() + self.target = "" + self.service = service.lower() + self.serviceid = "AF1699F719F1B6C81DE2BD38AB811994E3089B35C8E9314813A770AC7AE25D8D" + self.servicekey = "KoG704lAafBA7Yafj8jyg5lAHfarrQ12" + self.payload = "" + self.hmac = "" + self.type = "logger" + self.version = version + self.origin = origin + self.client = client.lower() + self.params = pika.URLParameters(amqps) + self.params.socket_timeout = 15 + self.params.heartbeat = 60 + self.logType = logType.lower() + self.logTypeSvd = logType.lower() + self.logLocal = logLocal + self.pp = pprint.PrettyPrinter() + self.diaMes = datetime.now().strftime("%d") # PARA SABER SE ATUALIZA OU NÃO O ARQUIVO DE LOG LOCAL + self.flush = True + self.base_routeKey = f"{self.service}.{self.system}" + self.connection = None + self.channel = None + self.logging = logging.getLogger(self.base_routeKey) + self.Crypto = Payload(self.serviceid, self.servicekey) + self.exchange_name = 'info_connector.topic' + + try: + self._connect() + except Exception as err: + self.logging.error(f"ERROR CONNECTION {err}") + raise + + if self.logLocal: self.setLogLocalName() + + # --- async logging --- + self.queue_size = 1000 + self.queue = Queue(maxsize=self.queue_size) + self.stop_event = Event() + self.worker_thread = Thread( + target = self._worker, + name = "logger-worker", + daemon = True + ) + self.worker_thread.start() + + self.logging.info(f"params: {self.params} amqps: {amqps}") + if self.callback is not None: + try: + self.queueIC2 = "info_connector_collector" + self.channelIn = self.connection.channel() + self.channelIn.queue_declare (queue = self.queueIC2, exclusive=True, durable=True) + self.channelIn.queue_bind (queue = self.queueIC2, exchange = self.exchange_name, routing_key = "#") + self.channelIn.basic_qos (prefetch_count = 1) + self.consumerTag = self.channelIn.basic_consume (queue = self.queueIC2, + auto_ack = False, + on_message_callback = self.callback) + self.logging.info("COLLECTING TOPIC MESSAGES VHOST info_connector_host EXCHANGE info_connector.topic KEY #") + except Exception as error: + raise(error) + else: + conexao = pika.BlockingConnection(self.params) #testa conexao + conexao.close() + self.logging.info(f"Logger Messages topics: {self.logType}.{self.base_routeKey}.{self.client}") + + + # PERSISTENCIA LOGS MESSGERIA + def _worker(self): + while not self.stop_event.is_set() or not self.queue.empty(): + try: + payload, client = self.queue.get(timeout=0.5) + except Empty: + continue + + try: + self.sendMsgWork(payload, client) + except Exception as e: + # última linha de defesa + self.logging.error(f"ASYNC LOGGER FAILURE: {e}") + if self.logLocal: + self.logging.info(payload) + finally: + self.queue.task_done() + + # PERSISTENCIA CONEXAO AMQPS + def _connect(self): + """Conecta ou reconecta ao RabbitMQ.""" + if self.connection and self.connection.is_open: + return + try: + if self.connection: + self.connection.close() + except Exception: + pass + try: + self.connection = pika.BlockingConnection(self.params) + self.channel = self.connection.channel() + self.channel.exchange_declare(exchange = self.exchange_name, + exchange_type = 'topic', + auto_delete = False) + + self.logging.info("Conexão ao RabbitMQ estabelecida.") + except pika.exceptions.AMQPConnectionError as e: + self.logging.error(f"Falha na conexão: {e}") + raise + + def _setMsg(self, msg:str, caller:str): + now = datetime.now(timezone('America/Sao_Paulo')) # melhoria: acrescentar timezone info no system_config + return { + "system" : self.system, + "target" : self.target, + "service" : self.service, + "serviceid": self.serviceid, + "payload" : self.payload, + "hmac" : self.hmac, + "type" : self.type, + "version" : self.version, + "origin" : self.origin, + "client" : self.client, + "caller" : caller, + "logDate" : f"{now}", + "msg" : msg + } + + def _sendMsg(self, msg: str, caller: str, *args): + # Avalia arquivo de logs + if self.logLocal: + current_dia = datetime.now().strftime("%d") + if current_dia != self.diaMes: + self.diaMes = current_dia + self.setLogLocalName() + + message = msg.strip() + + try: + if args and isinstance(args[0], (list, tuple)): + for item in args[0]: + msgArg = pprint.pformat(item).replace("'", "").strip() + message = f"{message}, {msgArg}" + + Msg = self._setMsg(message, caller) + payload = json.dumps(Msg) + + # console / docker logs continuam síncronos + self.pp.pprint(f"{Msg['logDate'][0:21]} {Msg['msg']}") + + # log local imediato (garantia mínima) + if self.logLocal: + Msg['payload'] = Msg['payload'][:20] + self.logging.info(self.pp.pformat(Msg)) + + # envio assíncrono + try: + self.queue.put_nowait((payload, self.client)) + except Full: + # fila cheia → degrada com dignidade + self.logging.warning("LOGGER QUEUE FULL — DROPPING MESSAGE") + + if self.flush: + sys.stdout.flush() + + except Exception as e: + erro = f"LOGGER ERROR: {e}" + self.logging.error(erro) + print(erro, flush=True) + + + def _sendMsgLocal(self, msg: str, caller: str, *args): + # Avalia arquivo de logs + if self.logLocal: + current_dia = datetime.now().strftime("%d") + if current_dia != self.diaMes: + self.diaMes = current_dia + self.setLogLocalName() + + message = msg.strip() + + try: + if args and isinstance(args[0], (list, tuple)): + for item in args[0]: + msgArg = pprint.pformat(item).replace("'", "").strip() + message = f"{message}, {msgArg}" + + Msg = self._setMsg(message, caller) + payload = json.dumps(Msg) + + # console / docker logs continuam síncronos + self.pp.pprint(f"{Msg['logDate'][0:21]} {Msg['msg']}") + + # log local imediato (garantia mínima) + if self.logLocal: + Msg['payload'] = Msg['payload'][:20] + self.logging.info(self.pp.pformat(Msg)) + + except Exception as e: + erro = f"LOGGER ERROR: {e}" + self.logging.error(erro) + print(erro, flush=True) + + def start_consuming(self): + if self.callback is not None: + self.consumerTag = self.channelIn.start_consuming() + + def close(self): + # sinaliza parada + self.stop_event.set() + + # aguarda flush da fila (timeout defensivo) + try: + self.queue.join() + except Exception: + pass + + if self.worker_thread.is_alive(): + self.worker_thread.join(timeout=2) + + if self.channel: + self.channel.close() + if self.connection: + self.connection.close() + + def setLogLocalName(self): + self.logLocalName = 'logs/'+ self.system.split()[0] + '_' + self.diaMes + formatter = logging.Formatter("%(asctime)s - %(name)s - %(levelname)s - %(message)s") + fhandler = logging.FileHandler(self.logLocalName) + fhandler.setLevel(logging.INFO) + fhandler.setFormatter(formatter) + self.logging.propagate = False + self.logging.setLevel(logging.INFO) + if not any(isinstance(h, logging.FileHandler) for h in self.logging.handlers): + self.logging.addHandler(fhandler) + + # Funcionamento: caso nao seja possivel enviar a msg, tentar nova conexao + # e envio da msg original mais uma vez + def sendMsgWork(self, message:str, client:str = "#"): + messageSent = False + trials = 5 + waitTime = 5 + while not messageSent and trials > 0: + self._connect() + # Cliente informado - MAIOR PRECEDENCIA + if client != "#": + routeKey = self.base_routeKey + f".{client.lower()}" + elif self.client != "#": + routeKey = self.base_routeKey + f".{self.client.lower()}" + else: + routeKey = self.base_routeKey + + try: + routing_key = f"{self.logType}.{routeKey}" + self.channel.basic_publish( + exchange = "info_connector.topic", + routing_key = routing_key, + body = message, + properties = pika.BasicProperties( + content_type = 'text/plain', + delivery_mode= pika.DeliveryMode.Transient)) + messageSent = True + except (pika.exceptions.ConnectionClosed, pika.exceptions.ChannelClosed, pika.exceptions.AMQPConnectionError) as ex: + self.logging.warning(f"TROUBLE CNX LOGGER REMAIN ATTEMPT: {trials} {ex}") + self._connect() + except Exception as ex: + self.logging.error(f"LOGGER ERROR: {ex}") + self.logging.warning(str(self.params)) + finally: + if not messageSent: + trials -= 1 + time.sleep(waitTime) + waitTime *= 2 + + if not messageSent: + self.logging.error(f"LOGGER ERROR: {message}") + + # Para quando desejar trocar o header do topico + # Salva o tópico padrão especificado no momento da inicialização + def setLogType(self, logType:str): + if not logType or len(logType) > 25: + return + self.logTypeSvd = self.logType + self.logType = logType.lower() + + # Volta ao header topico padrao + def clrLogType(self): + self.logType = self.logTypeSvd + + def logIC2(self, realm: RealmType, msg: tuple = ()): + ic_obj = self.ic2_builder.build(realm, msg) + payload = ic_obj.model_dump_json() + self.setPayload(data=payload) + self.setLogType(logType=realm.value) + self._sendMsg(f"LOGIC2 {realm.value.upper()} [{str(msg)[:20]} ...]", caller="IC2") + self.clrLogType() + self.clrPayload() + + def logMsgError(self, msg:str, *args): + self.setLogType('error.logs') + caller = inspect.stack()[1].function + if caller == '': caller = 'main' + + self._sendMsgLocal(msg, caller, args) + self.clrLogType() + + # LOGS GERAIS + # INFORM logs...# + # Log geral de acordo com o valor logType + def logMsg (self, msg: str, *args): + caller = inspect.stack()[1].function + if caller == '': caller = 'main' + Msg = f"{msg}" + self._sendMsgLocal(Msg, caller, args) + + # FUNÇÕES PARA CRYPTOGRAFIA + def clrPayload(self): + self.hmac = "" + self.payload = "" + + def setPayload(self, data:str): + try: + self.hmac = "" + self.payload = "" + self.hmac, self.payload = self.Crypto.setPayload(data) + except Exception as ex: + print(f"EXCH2: CRYPTO ERROR: {ex}", flush=True) + + def getPayload(self, hmac:str, safeData:str): + try: + self.hmac = "" + self.payload = "" + self.hmac, self.payload = self.Crypto.getPayload(safeData) + except Exception as ex: + print(f"EXCH2: CRYPTO ERROR: {ex}", flush=True) + return False + + return hmac == self.hmac + +class AppIC2Builder(IC2Builder): + def __init__(self, service_name: str, versionstr: str, origin: str): + self.service_name = service_name + self.versionstr = versionstr + self.origin = origin + + def _get_common_fields(self): + now = datetime.now().strftime("%Y-%m-%d %H:%M:%S") + return { + "log_date": now, + "service_date": now, + "service": self.service_name, + "versionstr": self.versionstr, + "origin": self.origin + } + + def _setLogs(self, message: tuple) -> InfoConnector: + msg_str = str(message[0]) if len(message) > 0 else "" + ent_id = str(message[1]) if len(message) > 1 else "0" + ent_name = str(message[2]) if len(message) > 2 else "Unknown" + tenant = str(message[3]) if len(message) > 3 else "#" + + ic_msg = ICLogs(**self._get_common_fields(), enterprise_id=ent_id, enterprise_name=ent_name, message=msg_str, tenant=tenant) + return InfoConnector(realm=RealmType.LOGS, messageIC=ic_msg) + + def _setErrors(self, message: tuple) -> InfoConnector: + msg_str = str(message[0]) if len(message) > 0 else "" + ent_id = str(message[1]) if len(message) > 1 else "0" + ent_name = str(message[2]) if len(message) > 2 else "Unknown" + tenant = str(message[3]) if len(message) > 3 else "#" + + ic_msg = ICErrors(**self._get_common_fields(), enterprise_id=ent_id, enterprise_name=ent_name, message=msg_str, tenant=tenant) + return InfoConnector(realm=RealmType.ERRORS, messageIC=ic_msg) + + def _setOperational(self, message: tuple) -> InfoConnector: + acao = str(message[0]) if len(message) > 0 else "" + tabela = str(message[1]) if len(message) > 1 else "" + id_usuario = str(message[2]) if len(message) > 2 else "0" + id_registro = str(message[3]) if len(message) > 3 else "0" + status_str = str(message[4]).upper() if len(message) > 4 else "INICIO" + mensagem = str(message[5]) if len(message) > 5 else "" + json_data = str(message[6]) if len(message) > 6 else "{}" + + if status_str == 'SUCESSO': op_status = OpStatus.OK + elif status_str == 'ERRO': op_status = OpStatus.ERROR + else: op_status = OpStatus.ACTIVE + + common = self._get_common_fields() + ic_msg = ICOperational( + log_date=common["log_date"], service_date=common["service_date"], service_name=self.service_name, + versionstr=self.versionstr, origin=self.origin, enterprise_id=id_usuario, enterprise_name=f"User {id_usuario}", + status=op_status, step_status=acao, message=f"{mensagem} | Tabela: {tabela} | ID_Reg: {id_registro} | Data: {json_data}" + ) + return InfoConnector(realm=RealmType.OPERATIONAL, messageIC=ic_msg) + + def _setStatus(self, message: tuple) -> InfoConnector: + common = self._get_common_fields() + ic_msg = ICStatus( service=self.service_name, versionstr=self.versionstr, system_name="integracao", + startup_date=common["log_date"], access_date=common["log_date"], origin=self.origin ) + return InfoConnector(realm=RealmType.STATUS, messageIC=ic_msg) + + def _setMetrics(self, message: tuple) -> InfoConnector: + metric_name = str(message[0]) if len(message) > 0 else "" + value = str(message[1]) if len(message) > 1 else "0" + unit = str(message[2]) if len(message) > 2 else "" + + now = datetime.now().strftime("%Y-%m-%d %H:%M:%S") + ic_msg = ICMetrics( metric_date=now, metric=metric_name, metric_name=metric_name, value=value, unit=unit, tenant="#" ) + return InfoConnector(realm=RealmType.METRICS, messageIC=ic_msg) diff --git a/utils/api.py b/utils/api.py new file mode 100644 index 0000000..e7c48ce --- /dev/null +++ b/utils/api.py @@ -0,0 +1,158 @@ +import requests + +# Wrapper base para realizar requests (GET/POST/PUT/DELETE) à API da PlugNotas de forma padronizada +class Api: + def __init__(self, api_key): + self.api_key = api_key + + def get(self, url): + try: + headers = { + "x-api-key": self.api_key + } + response = requests.get(url, headers=headers, timeout=30) + response.raise_for_status() + return response.json() + except requests.exceptions.RequestException as e: + resp_data = {} + if 'response' in locals() and response is not None: + try: + resp_data = response.json() + except ValueError: + resp_data = response.text + return {"error": str(e), "response": resp_data} + + def get_paginated(self, url, max_pages=100): + try: + headers = { + "x-api-key": self.api_key + } + all_data = [] + current_url = url + page = 0 + + while current_url and page < max_pages: + separator = "&" if "?" in current_url else "?" + request_url = current_url if page == 0 else current_url + + response = requests.get(request_url, headers=headers, timeout=30) + response.raise_for_status() + result = response.json() + + if isinstance(result, list): + all_data.extend(result) + break + elif isinstance(result, dict): + page_data = result.get("data", []) + if isinstance(page_data, list): + all_data.extend(page_data) + elif page_data: + all_data.append(page_data) + + hash_proxima = result.get("hashProximaPagina") + if hash_proxima: + separator = "&" if "?" in url else "?" + current_url = f"{url}{separator}hashProximaPagina={hash_proxima}" + page += 1 + else: + break + else: + break + + return {"data": all_data, "total_paginas": page + 1, "total_registros": len(all_data)} + except requests.exceptions.RequestException as e: + resp_data = {} + if 'response' in locals() and response is not None: + try: + resp_data = response.json() + except ValueError: + resp_data = response.text + return {"error": str(e), "response": resp_data} + + def post(self, url, data): + try: + headers = { + "Content-Type": "application/json", + "x-api-key": self.api_key + } + response = requests.post(url, headers=headers, json=data, timeout=30) + response.raise_for_status() + return response.json() + except requests.exceptions.RequestException as e: + resp_data = {} + if 'response' in locals() and response is not None: + try: + resp_data = response.json() + except ValueError: + resp_data = response.text + return {"error": str(e), "response": resp_data} + + def put(self, url, data): + try: + headers = { + "x-api-key": self.api_key + } + response = requests.put(url, headers=headers, json=data, timeout=30) + response.raise_for_status() + return response.json() + except requests.exceptions.RequestException as e: + return {"error": str(e)} + + def patch(self, url, data): + try: + headers = { + "x-api-key": self.api_key + } + response = requests.patch(url, headers=headers, json=data, timeout=30) + response.raise_for_status() + return response.json() + except requests.exceptions.RequestException as e: + return {"error": str(e)} + + def delete(self, url): + try: + headers = { + "x-api-key": self.api_key + } + response = requests.delete(url, headers=headers, timeout=30) + response.raise_for_status() + return response.json() + except requests.exceptions.RequestException as e: + return {"error": str(e)} + + def post_file(self, url, file): + try: + headers = { + "x-api-key": self.api_key + } + response = requests.post(url, headers=headers, files=file, timeout=60) + response.raise_for_status() + return response.json() + except requests.exceptions.RequestException as e: + return {"error": str(e)} + + def post_form_data(self, url, data): + try: + headers = { + "x-api-key": self.api_key + } + response = requests.post(url, headers=headers, files=data, timeout=60) + response.raise_for_status() + return response.json() + except requests.exceptions.RequestException as e: + return {"error": str(e)} + + def put_form_data(self, url, data): + try: + headers = { + "x-api-key": self.api_key + } + response = requests.put(url, headers=headers, files=data, timeout=60) + response.raise_for_status() + return response.json() + except requests.exceptions.RequestException as e: + return {"error": str(e)} + +# api = Api("2da392a6-79d2-4304-a8b7-959572c7e44d") +# response = api.get("https://api.sandbox.plugnotas.com.br/certificado") +# print(response) diff --git a/utils/banco.py b/utils/banco.py new file mode 100644 index 0000000..b51c9a6 --- /dev/null +++ b/utils/banco.py @@ -0,0 +1,241 @@ +from datetime import datetime +import uuid +from sqlalchemy import Column, Integer, String, DateTime, Text, create_engine, Boolean +from sqlalchemy.orm import declarative_base, sessionmaker +from sqlalchemy.exc import SQLAlchemyError + +Base = declarative_base() + +class CertificadoDigitalTable(Base): + __tablename__ = 'certificado_digital' + id = Column(Integer, primary_key=True, autoincrement=True) + id_tecnospeed = Column(String(50), nullable=False) + certificado = Column(Text, nullable=False) + createdAt = Column(DateTime, default=datetime.now) + updatedAt = Column(DateTime, default=datetime.now, onupdate=datetime.now) + deletedAt = Column(DateTime, nullable=True) + expirationAt = Column(DateTime, nullable=False) + senha = Column(String(50), nullable=False) + status = Column(Boolean, nullable=False) + id_empresa = Column(String(50), nullable=False) + id_usuario = Column(Integer, nullable=False) + email = Column(String(255), nullable=True) + +class EnterpriseTable(Base): + __tablename__ = 'Enterprise' + id = Column(String(50), primary_key=True) + name = Column(String(255), nullable=False) + cnpj = Column(String(50), nullable=False) + banco = Column(Text, nullable=True) + email = Column(String(255), nullable=True) + createdAt = Column(DateTime, default=datetime.now) + updatedAt = Column(DateTime, default=datetime.now, onupdate=datetime.now) + deletedAt = Column(DateTime, nullable=True) + status = Column(Boolean, nullable=False, default=True) + +class LogoTable(Base): + __tablename__ = 'logo' + id = Column(Integer, primary_key=True, autoincrement=True) + logo = Column(Text, nullable=False) + id_empresa = Column(String(50), nullable=False) + createdAt = Column(DateTime, default=datetime.now) + updatedAt = Column(DateTime, default=datetime.now, onupdate=datetime.now) + deletedAt = Column(DateTime, nullable=True) + status = Column(Boolean, nullable=False) + +class LogTable(Base): + __tablename__ = 'log' + id = Column(Integer, primary_key=True, autoincrement=True) + id_usuario = Column(Integer, nullable=False) + createdAt = Column(DateTime, default=datetime.now) + updatedAt = Column(DateTime, default=datetime.now, onupdate=datetime.now) + deletedAt = Column(DateTime, nullable=True) + acao = Column(String(50), nullable=False) + tabela = Column(String(50), nullable=False) + id_registro = Column(String(50), nullable=False) + json = Column(Text, nullable=False) + status = Column(String(20), nullable=False, default='INICIO') + mensagem = Column(Text, nullable=False, default='') + cliente = Column(String(100), nullable=True) + correlation_id = Column(String(50), nullable=True) + +class ServicoTable(Base): + __tablename__ = 'servico' + id = Column(Integer, primary_key=True, autoincrement=True) + id_tecnospeed = Column(String(50), nullable=False) + descricao = Column(Text, nullable=False) + id_usuario = Column(Integer, nullable=False) + createdAt = Column(DateTime, default=datetime.now) + updatedAt = Column(DateTime, default=datetime.now, onupdate=datetime.now) + deletedAt = Column(DateTime, nullable=True) + status = Column(Boolean, nullable=False) + +class TomadorTable(Base): + __tablename__ = 'tomador' + id = Column(Integer, primary_key=True, autoincrement=True) + id_tecnospeed = Column(String(50), nullable=False) + nome = Column(String(50), nullable=False) + cpf_cnpj = Column(String(50), nullable=False) + email = Column(String(50), nullable=False) + telefone = Column(String(50), nullable=False) + id_usuario = Column(Integer, nullable=False) + createdAt = Column(DateTime, default=datetime.now) + updatedAt = Column(DateTime, default=datetime.now, onupdate=datetime.now) + deletedAt = Column(DateTime, nullable=True) + status = Column(Boolean, nullable=False) + +class NFSeTable(Base): + __tablename__ = 'nfse' + id = Column(Integer, primary_key=True, autoincrement=True) + id_tecnospeed = Column(String(50), nullable=False) + id_empresa = Column(String(50), nullable=False) + id_usuario = Column(Integer, nullable=False) + createdAt = Column(DateTime, default=datetime.now) + updatedAt = Column(DateTime, default=datetime.now, onupdate=datetime.now) + deletedAt = Column(DateTime, nullable=True) + status = Column(Boolean, nullable=False) + +class NFeTable(Base): + __tablename__ = 'nfe' + id = Column(Integer, primary_key=True, autoincrement=True) + id_tecnospeed = Column(String(50), nullable=False) + id_empresa = Column(String(50), nullable=False) + id_usuario = Column(Integer, nullable=False) + createdAt = Column(DateTime, default=datetime.now) + updatedAt = Column(DateTime, default=datetime.now, onupdate=datetime.now) + deletedAt = Column(DateTime, nullable=True) + status = Column(Boolean, nullable=False) + +# Cache de motores criados pelo SQLAlchemy por URL de conexao +_engines = {} + +# Abstração de conexão base e de persistência de instâncias de metadados relacionais usando PostgreSQL +class Banco: + def __init__(self, host, port, user, password, database): + conn_str = f'postgresql://{user}:{password}@{host}:{port}/{database}' + if conn_str not in _engines: + _engines[conn_str] = create_engine( + conn_str, + pool_pre_ping=True, + pool_recycle=3600 + ) + self.engine = _engines[conn_str] + self.Session = sessionmaker(bind=self.engine) + self.session = self.Session() + + def close(self): + """Encerra a sessão SQLAlchemy atual liberando do pooler.""" + if hasattr(self, 'session') and self.session is not None: + self.session.close() + + def create_tables(self): + """Cria as tabelas no banco de dados.""" + try: + Base.metadata.create_all(self.engine) + except SQLAlchemyError as e: + print(f"Erro ao criar tabelas (SQLAlchemy): {e}") + except Exception as e: + print(f"Erro ao criar tabelas: {e}") + + def add(self, obj): + """Adiciona um novo registro no banco.""" + try: + self.session.add(obj) + self.session.commit() + return obj + except SQLAlchemyError as e: + self.session.rollback() + print(f"Erro ao adicionar registro (SQLAlchemy): {e}") + raise e + except Exception as e: + self.session.rollback() + print(f"Erro ao adicionar registro: {e}") + raise e + + def get(self, model, obj_id): + """Busca um registro pelo ID.""" + try: + return self.session.query(model).get(obj_id) + except SQLAlchemyError as e: + self.session.rollback() + print(f"Erro ao buscar registro (SQLAlchemy): {e}") + raise e + except Exception as e: + self.session.rollback() + print(f"Erro ao buscar registro: {e}") + raise e + + def get_all(self, model): + """Retorna todos os registros de um modelo.""" + try: + return self.session.query(model).all() + except SQLAlchemyError as e: + print(f"Erro ao buscar registros (SQLAlchemy): {e}") + raise e + except Exception as e: + print(f"Erro ao buscar registros: {e}") + raise e + + def update(self, obj): + """Atualiza um registro no banco.""" + try: + self.session.commit() + return obj + except SQLAlchemyError as e: + self.session.rollback() + print(f"Erro ao atualizar registro (SQLAlchemy): {e}") + raise e + except Exception as e: + self.session.rollback() + print(f"Erro ao atualizar registro: {e}") + raise e + + def delete(self, obj): + """Deleta um registro do banco.""" + try: + self.session.delete(obj) + self.session.commit() + return True + except SQLAlchemyError as e: + self.session.rollback() + print(f"Erro ao deletar registro (SQLAlchemy): {e}") + raise e + except Exception as e: + self.session.rollback() + print(f"Erro ao deletar registro: {e}") + raise e + + def rollback(self): + """Desfaz as alterações pendentes.""" + try: + self.session.rollback() + except SQLAlchemyError as e: + print(f"Erro ao desfazer alterações (SQLAlchemy): {e}") + except Exception as e: + print(f"Erro ao desfazer alterações: {e}") + + def logar_sistema(self, id_usuario, acao, tabela, id_registro=0, json="{}", status="INICIO", mensagem="", cliente=None, correlation_id=None): + """Registra uma ação no log do sistema.""" + try: + log = LogTable( + id_usuario=id_usuario, + acao=acao, + tabela=tabela, + id_registro=id_registro, + json=json, + status=status, + mensagem=mensagem, + cliente=cliente, + correlation_id=correlation_id + ) + self.add(log) + except SQLAlchemyError as e: + print(f"Erro ao logar ação (SQLAlchemy): {e}") + except Exception as e: + print(f"Erro ao logar ação: {e}") + + @staticmethod + def gerar_correlation_id(): + """Gera um ID único para rastrear toda a cadeia de uma operação.""" + return str(uuid.uuid4())[:8] + diff --git a/utils/enviaListerner.py b/utils/enviaListerner.py new file mode 100644 index 0000000..42ba65c --- /dev/null +++ b/utils/enviaListerner.py @@ -0,0 +1,35 @@ +import requests +import json +import os + +class enviaListerner: + + def __init__(self, logger=None): + self.url = os.getenv("LISTENER_INTERNAL_URL", "http://webhook_listener:8000/listenerPlugnotas") + self.logger = logger + + def notificar_emissao(self, id_tecnospeed: str, documento: str, cnpj: str, status: str = "PROCESSANDO"): + """ + Envia um POST para o listener local para sincronização imediata. + """ + payload = { + "id": id_tecnospeed, + "status": status, + "documento": documento, + "emitente": cnpj + } + + try: + response = requests.post(self.url, json=payload, timeout=5) + + if self.logger: + if response.status_code in [200, 201]: + self.logger.log_logs(f"Notificação interna enviada com sucesso - ID: {id_tecnospeed} | Status: {status}") + else: + self.logger.log_errors(f"Falha na notificação interna - Status: {response.status_code} | Resposta: {response.text}") + + return response.status_code in [200, 201] + except Exception as e: + if self.logger: + self.logger.log_errors(f"Erro ao tentar notificar listener interno: {e}") + return False diff --git a/utils/exchangeModels.py b/utils/exchangeModels.py new file mode 100644 index 0000000..1e19116 --- /dev/null +++ b/utils/exchangeModels.py @@ -0,0 +1,127 @@ + +__version__ = "1.0.0 - 18/02/2025" # VERSAO PARA TODOS OS SISTEMAS TRANSMITINDO PARA O SERVIÇO LOGGER_CONTROLLER DO SG_IC2 +__version__ = "1.0.1 - 15/01/2026 - S.D.G. - Ajustes" +__version__ = "1.0.2 - 23/01/2026 - S.D.G. - ForwardCompatibleModel & SerializeAsAny" +from pydantic import BaseModel, field_validator, ValidationInfo, Field, ConfigDict, SerializeAsAny +from enum import StrEnum +from typing import Union, Any, Optional + +class ForwardCompatibleModel(BaseModel): + """ + Base para todos os modelos que permite campos extras. + O Pydantic V2 gerencia automaticamente o dicionário 'model_extra'. + """ + model_config = ConfigDict(extra='allow') + +class RealmType(StrEnum): + LOGS = "LOGS" + METRICS = "METRICS" + STATUS = "STATUS" + ERRORS = "ERRORS" + OPERATIONAL = "OPERATIONAL" + +class OpStatus(StrEnum): + ACTIVE = "ACTIVE" + OK = "OK" + ERROR = "ERROR" + +class ICLogs(ForwardCompatibleModel): + log_date : str + service_date : str + service : str + versionstr : str + origin : str + enterprise_id : str + enterprise_name : str + message : str + tenant : Optional[str] = Field(default='#', description="Tenant ou '#' para global") + +class ICErrors(ForwardCompatibleModel): + log_date : str + service_date : str + service : str + versionstr : str + origin : str + enterprise_id : str + enterprise_name : str + message : str + tenant : Optional[str] = Field(default='#', description="Tenant ou '#' para global") + +class ICMetrics(ForwardCompatibleModel): + metric_date : str + metric : str + metric_name : str + value : str + unit : str + tenant : Optional[str] = Field(default='#', description="Tenant ou '#' para global") + +class ICOperational(ForwardCompatibleModel): + log_date : str # YYYY-MM-DD HH:MM:S + service_date : str # YYYY-MM-DD HH:MM:S + service_name : str + versionstr : str + origin : str + enterprise_id : str + enterprise_name : str + status : OpStatus + step_status : str + message : str + +class ICStatus(ForwardCompatibleModel): + service : str + versionstr : str + system_name : str + startup_date : str + access_date : str + origin : str + sid : Optional[str] = "" + task_count : Optional[int] = 0 + cpu : Optional[float] = 0.0 + memory : Optional[float] = 0.0 + disk : Optional[float] = 0.0 + network : Optional[float] = 0.0 + +# Modelo polimórfico gerador de metadados JSON para o Exchange de Logs do Sistema Inteirador +class InfoConnector(ForwardCompatibleModel): + realm: RealmType + + # SerializeAsAny garante que se 'messageIC' for uma subclasse (como 'Operation'), + # os campos extras da subclasse (como 'reserva_id') aparecerão no JSON final. + messageIC: SerializeAsAny[Union[ICLogs, ICErrors, ICMetrics, ICStatus, ICOperational]] + + @field_validator('messageIC', mode='before') + @classmethod + def validate_schema(cls, v: Any, info: ValidationInfo) -> Any: + realm = info.data.get("realm") + if realm is None: + # Se realm não existe, o próprio Pydantic lançará erro de validação do campo 'realm' + return v + + # Mapeamento de Realm para Classe Esperada + target_map = { + RealmType.LOGS : ICLogs, + RealmType.ERRORS : ICErrors, + RealmType.METRICS : ICMetrics, + RealmType.STATUS : ICStatus, + RealmType.OPERATIONAL: ICOperational, + } + + expected_class = target_map.get(realm) + if not expected_class: + raise ValueError(f"Invalid realm: {realm}") + + # Se for um dicionário (vindo de um JSON), validamos contra a classe mapeada + if isinstance(v, dict): + return expected_class.model_validate(v) + + # Se já for um objeto instanciado (como no seu Builder), verificamos a herança + if not isinstance(v, expected_class): + raise ValueError(f"Expected {expected_class.__name__} for realm {realm}, got {type(v).__name__}") + + return v + + model_config = ConfigDict( + extra="allow", + frozen=False, + str_strip_whitespace=True + ) \ No newline at end of file diff --git a/utils/redis.py b/utils/redis.py new file mode 100644 index 0000000..a712eb1 --- /dev/null +++ b/utils/redis.py @@ -0,0 +1,107 @@ +import redis + +# Fornece mecanismos de Caching e Persistência Volátil/PubSub na aplicação usando Redis +class RedisClient: + def __init__(self, host='localhost', port=6379, db=0): + """ + Inicializa o cliente Redis com as configurações fornecidas. + """ + self.client = redis.Redis(host=host, port=port, db=db, decode_responses=True) + + def set(self, key, value): + """ + Define um valor para uma chave no Redis. + """ + self.client.set(key, value) + + def setex(self, key, value, ex): + """ + Define um valor para uma chave no Redis com um tempo de expiração em segundos. + """ + self.client.set(key, value, ex) + + def get(self, key): + """ + Obtém o valor de uma chave no Redis. + """ + return self.client.get(key) + + def get_keys(self, pattern="*"): + """ + Obtém todas as chaves que correspondem a um padrão. + """ + return self.client.keys(pattern) + + def get_all(self): + """ + Obtém todas as chaves e seus valores no Redis. + """ + keys = self.client.keys('*') + return {key: self.client.get(key) for key in keys} + + def delete(self, key): + """ + Remove uma chave do Redis. + """ + self.client.delete(key) + + def exists(self, key): + """ + Verifica se uma chave existe no Redis. + Retorna True se a chave existir, caso contrário, False. + """ + return self.client.exists(key) > 0 + + def close(self): + """ + Fecha a conexão com o Redis. + """ + self.client.close() + + def set_expiration(self, key, value, seconds): + """ + Define um valor para uma chave e configura um tempo de expiração em segundos. + """ + self.client.set(key, value, ex=seconds) + + def increment(self, key, amount=1): + """ + Incrementa o valor de uma chave numérica no Redis. + """ + return self.client.incr(key, amount) + + def decrement(self, key, amount=1): + """ + Decrementa o valor de uma chave numérica no Redis. + """ + return self.client.decr(key, amount) + + def hset(self, name, key, value): + """ + Define um campo em um hash no Redis. + """ + self.client.hset(name, key, value) + + def hget(self, name, key): + """ + Obtém o valor de um campo em um hash no Redis. + """ + return self.client.hget(name, key) + + def hgetall(self, name): + """ + Obtém todos os campos e valores de um hash no Redis. + """ + return self.client.hgetall(name) + + def expire(self, key, seconds): + """ + Define um tempo de expiração para uma chave existente. + """ + self.client.expire(key, seconds) + + def ttl(self, key): + """ + Obtém o tempo restante de vida (TTL) de uma chave. + """ + return self.client.ttl(key) \ No newline at end of file