From 343b30a19f8670f21ab42390b849e375c96b0e57 Mon Sep 17 00:00:00 2001 From: Baltazar Barbosa Date: Fri, 10 Jul 2026 16:28:18 -0300 Subject: [PATCH] =?UTF-8?q?fix:=20corre=C3=A7=C3=A3o=20no=20modelo=20tecno?= =?UTF-8?q?speed?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .gitignore | 216 ++++++++ Dockerfile | 34 ++ README.md | 75 +++ buildImages.sh | 96 ++++ docker-compose.yml | 20 + envio_cliente.json | 28 ++ modelo_envio_plugnotas.json | 88 ++++ modelo_webhook_plugnotas.json | 7 + payload_nfe_tray.json | 16 + pedido_completo.json | 262 ++++++++++ requirements.txt | 21 + src/Exchange2.py | 1 + src/HostMetrics.py | 1 + src/api/publicas.py | 15 + src/api/tray.py | 104 ++++ src/appIC2Builder.py | 124 +++++ src/controller.py | 655 +++++++++++++++++++++++++ src/create_tables.py | 5 + src/db/banco.py | 284 +++++++++++ src/exchangeModels.py | 1 + src/models/plugnotas/services_model.py | 102 ++++ src/models/recebimento.py | 19 + src/models/sienge/services_models.py | 117 +++++ src/send_queue.py | 26 + src/send_rpc.py | 37 ++ src/services/cliente.py | 85 ++++ src/services/nfe.py | 198 ++++++++ src/services/titulo_receber.py | 51 ++ src/teste.py | 201 ++++++++ src/testeNfe.py | 14 + webhook.json | 6 + 31 files changed, 2909 insertions(+) create mode 100644 .gitignore create mode 100644 Dockerfile create mode 100644 README.md create mode 100644 buildImages.sh create mode 100644 docker-compose.yml create mode 100644 envio_cliente.json create mode 100644 modelo_envio_plugnotas.json create mode 100644 modelo_webhook_plugnotas.json create mode 100644 payload_nfe_tray.json create mode 100644 pedido_completo.json create mode 100644 requirements.txt create mode 120000 src/Exchange2.py create mode 120000 src/HostMetrics.py create mode 100644 src/api/publicas.py create mode 100644 src/api/tray.py create mode 100644 src/appIC2Builder.py create mode 100644 src/controller.py create mode 100644 src/create_tables.py create mode 100644 src/db/banco.py create mode 120000 src/exchangeModels.py create mode 100644 src/models/plugnotas/services_model.py create mode 100644 src/models/recebimento.py create mode 100644 src/models/sienge/services_models.py create mode 100644 src/send_queue.py create mode 100644 src/send_rpc.py create mode 100644 src/services/cliente.py create mode 100644 src/services/nfe.py create mode 100644 src/services/titulo_receber.py create mode 100644 src/teste.py create mode 100644 src/testeNfe.py create mode 100644 webhook.json diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..38e7a9b --- /dev/null +++ b/.gitignore @@ -0,0 +1,216 @@ +# Byte-compiled / optimized / DLL files +__pycache__/ +*.py[codz] +*$py.class + +# C extensions +*.so + +# Distribution / packaging +.Python +build/ +develop-eggs/ +dist/ +downloads/ +eggs/ +.eggs/ +lib/ +lib64/ +parts/ +sdist/ +var/ +wheels/ +share/python-wheels/ +*.egg-info/ +.installed.cfg +*.egg +MANIFEST + +# PyInstaller +# Usually these files are written by a python script from a template +# before PyInstaller builds the exe, so as to inject date/other infos into it. +*.manifest +*.spec + +# Installer logs +pip-log.txt +pip-delete-this-directory.txt + +# Unit test / coverage reports +htmlcov/ +.tox/ +.nox/ +.coverage +.coverage.* +.cache +nosetests.xml +coverage.xml +*.cover +*.py.cover +.hypothesis/ +.pytest_cache/ +cover/ + +# Translations +*.mo +*.pot + +# Django stuff: +*.log +local_settings.py +db.sqlite3 +db.sqlite3-journal + +# Flask stuff: +instance/ +.webassets-cache + +# Scrapy stuff: +.scrapy + +# Sphinx documentation +docs/_build/ + +# PyBuilder +.pybuilder/ +target/ + +# Jupyter Notebook +.ipynb_checkpoints + +# IPython +profile_default/ +ipython_config.py + +# pyenv +# For a library or package, you might want to ignore these files since the code is +# intended to run in multiple environments; otherwise, check them in: +# .python-version + +# pipenv +# According to pypa/pipenv#598, it is recommended to include Pipfile.lock in version control. +# However, in case of collaboration, if having platform-specific dependencies or dependencies +# having no cross-platform support, pipenv may install dependencies that don't work, or not +# install all needed dependencies. +#Pipfile.lock + +# UV +# Similar to Pipfile.lock, it is generally recommended to include uv.lock in version control. +# This is especially recommended for binary packages to ensure reproducibility, and is more +# commonly ignored for libraries. +#uv.lock + +# poetry +# Similar to Pipfile.lock, it is generally recommended to include poetry.lock in version control. +# This is especially recommended for binary packages to ensure reproducibility, and is more +# commonly ignored for libraries. +# https://python-poetry.org/docs/basic-usage/#commit-your-poetrylock-file-to-version-control +#poetry.lock +#poetry.toml + +# pdm +# Similar to Pipfile.lock, it is generally recommended to include pdm.lock in version control. +# pdm recommends including project-wide configuration in pdm.toml, but excluding .pdm-python. +# https://pdm-project.org/en/latest/usage/project/#working-with-version-control +#pdm.lock +#pdm.toml +.pdm-python +.pdm-build/ + +# pixi +# Similar to Pipfile.lock, it is generally recommended to include pixi.lock in version control. +#pixi.lock +# Pixi creates a virtual environment in the .pixi directory, just like venv module creates one +# in the .venv directory. It is recommended not to include this directory in version control. +.pixi + +# PEP 582; used by e.g. github.com/David-OConnor/pyflow and github.com/pdm-project/pdm +__pypackages__/ + +# Celery stuff +celerybeat-schedule +celerybeat.pid + +# SageMath parsed files +*.sage.py + +# Environments +.env +.envrc +.venv +env/ +venv/ +ENV/ +env.bak/ +venv.bak/ +system_config.json +inserts_teste.sql + +# Spyder project settings +.spyderproject +.spyproject + +# Rope project settings +.ropeproject + +# mkdocs documentation +/site + +# mypy +.mypy_cache/ +.dmypy.json +dmypy.json + +# Pyre type checker +.pyre/ + +# pytype static type analyzer +.pytype/ + +# Cython debug symbols +cython_debug/ + +# PyCharm +# JetBrains specific template is maintained in a separate JetBrains.gitignore that can +# be found at https://github.com/github/gitignore/blob/main/Global/JetBrains.gitignore +# and can be added to the global gitignore or merged into this file. For a more nuclear +# option (not recommended) you can uncomment the following to ignore the entire idea folder. +#.idea/ + +# Abstra +# Abstra is an AI-powered process automation framework. +# Ignore directories containing user credentials, local state, and settings. +# Learn more at https://abstra.io/docs +.abstra/ + +# Visual Studio Code +# Visual Studio Code specific template is maintained in a separate VisualStudioCode.gitignore +# that can be found at https://github.com/github/gitignore/blob/main/Global/VisualStudioCode.gitignore +# and can be added to the global gitignore or merged into this file. However, if you prefer, +# you could uncomment the following to ignore the entire vscode folder +# .vscode/ + +# Ruff stuff: +.ruff_cache/ + +# PyPI configuration file +.pypirc + +# Cursor +# Cursor is an AI-powered code editor. `.cursorignore` specifies files/directories to +# exclude from AI features like autocomplete and code analysis. Recommended for sensitive data +# refer to https://docs.cursor.com/context/ignore-files +.cursorignore +.cursorindexingignore + +# Marimo +marimo/_static/ +marimo/_lsp/ +__marimo__/ + +# Diretorio de classes acessorias +Model/ +utils/ +logs/ +src/models/sg_tray_models/ +src/models/servico_titulos_receber/ diff --git a/Dockerfile b/Dockerfile new file mode 100644 index 0000000..d33962f --- /dev/null +++ b/Dockerfile @@ -0,0 +1,34 @@ +FROM python:3.12-slim AS builder +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 build-essential && \ + locale-gen en_US.UTF-8 && \ + apt-get clean && apt-get autoremove -y && rm -rf /var/lib/apt/lists/* && \ + pip install --no-cache-dir unidecode + +WORKDIR /app +COPY requirements.txt . +RUN pip install --no-cache-dir -r requirements.txt + +FROM python:3.12-slim +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 && \ + apt-get clean && apt-get autoremove -y && rm -rf /var/lib/apt/lists/* + +WORKDIR /app +COPY --from=builder /usr/local/lib/python3.12/site-packages /usr/local/lib/python3.12/site-packages +COPY --from=builder /usr/local/bin /usr/local/bin + +RUN mkdir /app/logs +RUN mkdir /app/controller +COPY src . +COPY utils/Exchange2.py utils/exchangeModels.py utils/HostMetrics.py /app +# COPY Model/sg_tray_models/services_model.py /app/models/sg_tray_models/ +# COPY Model/servico_titulos_receber/contract_Tittles.py /app/models/servico_titulos_receber/ +# COPY Model/servico_titulos_receber/tittle_model.py /app/models/servico_titulos_receber/ + +CMD ["python", "controller.py"] \ No newline at end of file diff --git a/README.md b/README.md new file mode 100644 index 0000000..a26e387 --- /dev/null +++ b/README.md @@ -0,0 +1,75 @@ +# Innovar AMR Tray Controller + +Microserviço responsável por atuar como controlador de integrações entre a plataforma de e-commerce Tray, o emissor de NFe Plugnotas e o ERP Sienge. + +## Visão Geral + +Este projeto consome mensagens de uma fila RabbitMQ (originadas de webhooks) e orquestra a comunicação entre diferentes sistemas para automatizar o fluxo de vendas: + +1. **Tray (E-commerce):** + - Recebe webhooks de criação (`insert`) e atualização (`update`) de pedidos. + - Busca dados completos do pedido na API da Tray. + - Atualiza dados fiscais (NFe) no pedido dentro da Tray após faturamento. + +2. **Plugnotas (Notas Fiscais):** + - Envia dados do pedido para emissão de Nota Fiscal Eletrônica. + - Recebe webhooks informando a conclusão da emissão da NFe, extraindo a chave de acesso, o número e os produtos a partir do XML retornado (base64). + +3. **Sienge (ERP - Integração via Filas/RPC):** + - **Clientes:** Envia requisições (RPC) para criação de clientes no Sienge quando um novo pedido é gerado. + - **Títulos a Receber:** Envia requisições (RPC) para geração de títulos financeiros no Sienge após a emissão da NFe. + +## Fluxo de Processamento + +### 1. Webhook Tray (`Recebimento`) +- **Ação `insert`:** O controlador identifica o novo pedido, cria/sincroniza o cliente no ERP Sienge e atualiza o ID do cliente no banco de dados local. +- **Ação `update`:** Caso o pedido passe para o status "A ENVIAR", o serviço envia os dados mapeados para a fila de emissão de NFe, sinalizando no banco local que o envio ocorreu. + +### 2. Webhook Plugnotas (`RecebimentoPlugnotas`) +- Ao receber a confirmação de que a NFe foi gerada com status "CONCLUIDO": + - Decodifica e extrai as informações pertinentes da nota a partir do XML da NFe. + - Envia a ordem para criar o título a receber no ERP (Sienge). + - Atualiza a nota fiscal (chave, link, xml) diretamente no painel da Tray. + - Grava o status atualizado no banco de dados local. + +## Tecnologias e Bibliotecas + +- **Python 3** +- **Bibliotecas Principais:** Pydantic (validação de dados), SQLAlchemy/Psycopg2 (banco de dados), Redis, Requests, Pika (RabbitMQ). +- **Mensageria:** RabbitMQ para filas assíncronas e RPC, facilitado pelas bibliotecas base da equipe (`Exchange2`). +- **Banco de Dados:** PostgreSQL (Persistência de estado dos pedidos, status de integrações e logs). +- **Cache / Configuração:** Redis (Armazenamento e busca das credenciais e configurações de ambiente em tempo real). +- **Infraestrutura:** Docker & Docker Compose. + +## Como Executar + +### Pré-requisitos +- Docker e Docker Compose instalados no ambiente de execução. +- Acesso aos repositórios Git base (`Exchange2` e `SoftGOModel`) pois o script de build fará o clone de dependências proprietárias. +- Instâncias do Redis, RabbitMQ e PostgreSQL acessíveis na rede (`sg_base_network`). + +### Passos para Build e Deploy + +1. Clone o repositório e navegue até a pasta raiz. +2. Execute o script de build para baixar as dependências e criar a imagem Docker: + ```bash + chmod +x buildImages.sh + ./buildImages.sh + ``` +3. Inicie o container com o Docker Compose: + ```bash + docker-compose up -d + ``` + +## Variáveis de Ambiente e Configurações + +O container do `controller` possui poucas configurações no `docker-compose.yml`, pois o sistema busca suas credenciais principais (banco, AMQP, endpoints, chaves de API da Tray, etc.) diretamente de uma chave JSON no **Redis** (`innovar_amr_tray-system_config-innovar`). +Certifique-se de que essa chave JSON esteja corretamente populada no Redis para que o microserviço consiga inicializar. + +## Estrutura de Diretórios + +- `src/controller.py`: Ponto de entrada do serviço e consumidor principal da fila RabbitMQ. +- `src/services/`: Regras de negócio, transformações e mapeamentos (Tray para Sienge, Tray para NFe). +- `src/models/`: Definições Pydantic para validação das estruturas de dados. +- `src/api/`: Comunicação HTTP direta com APIs externas. +- `src/db/`: Instância e métodos de comunicação com o PostgreSQL local. diff --git a/buildImages.sh b/buildImages.sh new file mode 100644 index 0000000..698201d --- /dev/null +++ b/buildImages.sh @@ -0,0 +1,96 @@ +#!/bin/bash + +version=':v1' # VERSIONAMENTO MACRO - TODOS SERVIÇOS NA MESMA VERSÃO SÃO COMPATÍVEIS ENTRE SI + +nome_diretorio="innovar_amr_tray" #$(basename "$PWD") +system="${nome_diretorio,,}" +SYSTEM="${system^^}" + +# 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 + + # PROJETOS DE BASE + 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 SOFTGO_AUTH_MIDDLEWARE + cd .. + # remove .git e .gitignore do Model + cd Model + find . \( -name ".git" -o -name ".gitignore" \) -exec rm -rf {} + + cd .. + } + +for arg in "$@"; do + if [ -n "$arg" ] && [ "$arg" == "git" ]; then + echo "VERSAO ATUAL 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 ${system} ${version}" + docker image ls -q -f "reference=${system}-*${version}" | xargs -I {} docker image rm -f {} + docker system prune -af + fi +done + +# EDITE DE ACORDO COM OS SERVIÇOS QUE VAI IMPLEMENTAR +# NOME DO DIRETORIO É O NOME DO SISTEMA E CADA DIRETORIO UM SERVICO +# CASO UM UNICO SERVIÇO MANTENHA O MODELO PARA PODERMOS IMPLEMENTAR UM DEPLOY AUTOMATICO NO FUTURO +#==> MODELO +echo "GERAR IMAGENS ${system} ${version}" + +service="controller" +echo $service +echo "COPIAR ARQUIVOS PARA BUILD ${system} ${version}" +cp -r Model/sg_tray_models ./src/models/sg_tray_models/ +cp -r Model/servico_titulos_receber ./src/models/servico_titulos_receber/ + +docker build -t ${system}-${service}${version} . +rm -rf ./src/models/sg_tray_models/* +rm -rf ./src/models/servico_titulos_receber/* + +cd ./src + +ln -sf ../utils/Exchange2.py . +ln -sf ../utils/exchangeModels.py . +ln -sf ../utils/HostMetrics.py . +ln -sf ../Model/sg_tray_models/services_model.py ./models/sg_tray_models/services_model.py +ln -sf ../Model/servico_titulos_receber/contract_Tittles.py ./models/servico_titulos_receber/contract_Tittles.py +ln -sf ../Model/servico_titulos_receber/tittle_model.py ./models/servico_titulos_receber/tittle_model.py + +cd .. diff --git a/docker-compose.yml b/docker-compose.yml new file mode 100644 index 0000000..99c4a0e --- /dev/null +++ b/docker-compose.yml @@ -0,0 +1,20 @@ +networks: + local: + sg_base_network: + name: sg_base_network + external: true + +services: + controller: + image: innovar_amr_tray-controller:v1 + #restart: always + volumes: + - ./logs/controller:/app/logs + - /etc/hostname:/app/hostname + - /proc:/app/host-proc + - /metrics:/app/metrics + environment: + - "TZ=America/Sao_Paulo" + networks: + - local + - sg_base_network \ No newline at end of file diff --git a/envio_cliente.json b/envio_cliente.json new file mode 100644 index 0000000..73e7f4b --- /dev/null +++ b/envio_cliente.json @@ -0,0 +1,28 @@ +{ + "config": { + "usuario": "api_user", + "senha": "api_password", + "subdominio": "empresa_exemplo" + }, + "cliente": { + "naturalPersonData": { + "name": "João da Silva", + "cpf": "12345678901", + "email": "joao@exemplo.com", + "birthDate": "1990-01-01", + "civilStatus": "Casado", + "profession": "Engenheiro" + }, + "addresses": [ + { + "type": "RES", + "streetName": "Rua Exemplo", + "number": "123", + "neighborhood": "Centro", + "city": "São Paulo", + "state": "SP", + "zipCode": "01001000" + } + ] + } +} \ No newline at end of file diff --git a/modelo_envio_plugnotas.json b/modelo_envio_plugnotas.json new file mode 100644 index 0000000..77eea08 --- /dev/null +++ b/modelo_envio_plugnotas.json @@ -0,0 +1,88 @@ +{ + "fila": "nfe", + "metodo": "post", + "cliente": "{{cliente}}", + "dados": { + "idIntegracao": "NFe_TESTE_001", + "presencial": true, + "consumidorFinal": true, + "natureza": "OPERAÇÃO INTERNA", + "emitente": { + "cpfCnpj": "00000000000000", + "inscricaoEstadual": "ISENTO" + }, + "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.67 + } + ], + "responsavelTecnico": { + "cpfCnpj": "08187168000160", + "nome": "Tecnospeed", + "email": "contato@tecnospeed.com.br", + "telefone": { + "ddd": "44", + "numero": "30379500" + } + } + } +} \ No newline at end of file diff --git a/modelo_webhook_plugnotas.json b/modelo_webhook_plugnotas.json new file mode 100644 index 0000000..14322bf --- /dev/null +++ b/modelo_webhook_plugnotas.json @@ -0,0 +1,7 @@ +{ + "id": "69f8b4062a48c20329bbd441", + "status": "CONCLUIDO", + "documento": "nfe", + "emitente": "1", + "idIntegracao": "PE_16527", + "xml_base64":"PD94bWwgdmVyc2lvbj0iMS4wIiBlbmNvZGluZz0iVVRGLTgiPz48bmZlUHJvYyB2ZXJzYW89IjQuMDAiIHhtbG5zPSJodHRwOi8vd3d3LnBvcnRhbGZpc2NhbC5pbmYuYnIvbmZlIj48TkZlIHhtbG5zPSJodHRwOi8vd3d3LnBvcnRhbGZpc2NhbC5pbmYuYnIvbmZlIj48aW5mTkZlIElkPSJORmU0MTIwMDMwODE4NzE2ODAwMDE2MDU1ODA0MDAwNzkzMjcyMTAwMDAwMDAxNSIgdmVyc2FvPSI0LjAwIj48aWRlPjxjVUY+NDE8L2NVRj48Y05GPjAwMDAwMDAxPC9jTkY+PG5hdE9wPlZFTkRBIERFIE1FUkNBRE9SSUEgQURRLiBERSBURVJDRUlSTyAtIFBGIEUgUEogTkFPIENPTlRSSUJVSTwvbmF0T3A+PG1vZD41NTwvbW9kPjxzZXJpZT44MDQ8L3NlcmllPjxuTkY+NzkzMjcyPC9uTkY+PGRoRW1pPjIwMjAtMDMtMDVUMDc6NTA6NDItMDM6MDA8L2RoRW1pPjxkUHJldkVudHJlZ2E+MjAyMC0wMy0xMDwvZFByZXZFbnRyZWdhPjx0cE5GPjE8L3RwTkY+PGlkRGVzdD4xPC9pZERlc3Q+PGNNdW5GRz40MTE1MjAwPC9jTXVuRkc+PHRwSW1wPjE8L3RwSW1wPjx0cEVtaXM+MTwvdHBFbWlzPjxjRFY+NTwvY0RWPjx0cEFtYj4yPC90cEFtYj48ZmluTkZlPjE8L2Zpbk5GZT48aW5kRmluYWw+MTwvaW5kRmluYWw+PGluZFByZXM+MTwvaW5kUHJlcz48cHJvY0VtaT4wPC9wcm9jRW1pPjx2ZXJQcm9jPjU8L3ZlclByb2M+PC9pZGU+PGVtaXQ+PENOUEo+MDgxODcxNjgwMDAxNjA8L0NOUEo+PHhOb21lPlRlc3RlIE5PTUU8L3hOb21lPjx4RmFudD50ZXN0ZSBGQU5ULjwveEZhbnQ+PGVuZGVyRW1pdD48eExncj5SVUEgRE8gUE9WTzwveExncj48bnJvPjcxMTwvbnJvPjx4QmFpcnJvPmphcmRpbSB0ZXN0ZTwveEJhaXJybz48Y011bj40MTE1MjAwPC9jTXVuPjx4TXVuPk1BUklOR0E8L3hNdW4+PFVGPlBSPC9VRj48Q0VQPjc5NDE1MDAwPC9DRVA+PGNQYWlzPjEwNTg8L2NQYWlzPjx4UGFpcz5CUkFTSUw8L3hQYWlzPjxmb25lPjQ0MzIyMjIyMjI8L2ZvbmU+PC9lbmRlckVtaXQ+PElFPjkwNDQwMTY2ODg8L0lFPjxDUlQ+MzwvQ1JUPjwvZW1pdD48ZGVzdD48Q05QSj4xODU4MjIwNjAwMDEzMjwvQ05QSj48eE5vbWU+TkYtRSBFTUlUSURBIEVNIEFNQklFTlRFIERFIEhPTU9MT0dBQ0FPIC0gU0VNIFZBTE9SIEZJU0NBTDwveE5vbWU+PGVuZGVyRGVzdD48eExncj5SVUEgRE8gQ0VOVFJPPC94TGdyPjxucm8+ODk3PC9ucm8+PHhCYWlycm8+Q0VOVFJBTDwveEJhaXJybz48Y011bj40MTE1MjAwPC9jTXVuPjx4TXVuPk1BUklOR0E8L3hNdW4+PFVGPlBSPC9VRj48Q0VQPjc5NDE1MDAwPC9DRVA+PGNQYWlzPjEwNTg8L2NQYWlzPjx4UGFpcz5CUkFTSUw8L3hQYWlzPjxmb25lPjQ0NTU1NTU1NTwvZm9uZT48L2VuZGVyRGVzdD48aW5kSUVEZXN0PjE8L2luZElFRGVzdD48SUU+OTA3MTI1MjQ2NjwvSUU+PC9kZXN0PjxkZXQgbkl0ZW09IjEiPjxwcm9kPjxjUHJvZD4wOTk5PC9jUHJvZD48Y0VBTj5TRU0gR1RJTjwvY0VBTj48eFByb2Q+TUVMQU88L3hQcm9kPjxOQ00+MTEwODEyMDA8L05DTT48Q0VTVD4wMTIzNDU2PC9DRVNUPjxpbmRFc2NhbGE+UzwvaW5kRXNjYWxhPjxDRk9QPjUxMDI8L0NGT1A+PHVDb20+Q1g8L3VDb20+PHFDb20+MTwvcUNvbT48dlVuQ29tPjAuMDEwMDwvdlVuQ29tPjx2UHJvZD4wLjAxPC92UHJvZD48Y0VBTlRyaWI+U0VNIEdUSU48L2NFQU5UcmliPjx1VHJpYj5DWDwvdVRyaWI+PHFUcmliPjE8L3FUcmliPjx2VW5UcmliPjAuMDEwMDwvdlVuVHJpYj48aW5kVG90PjE8L2luZFRvdD48cmFzdHJvPjxuTG90ZT4xMjM8L25Mb3RlPjxxTG90ZT41NTY1LjAwMDwvcUxvdGU+PGRGYWI+MjAxNy0wNy0yMzwvZEZhYj48ZFZhbD4yMDE4LTA3LTIzPC9kVmFsPjwvcmFzdHJvPjwvcHJvZD48aW1wb3N0bz48SUNNUz48SUNNUzAwPjxvcmlnPjA8L29yaWc+PENTVD4wMDwvQ1NUPjxtb2RCQz4wPC9tb2RCQz48dkJDPjAuMDE8L3ZCQz48cElDTVM+MTIuMDA8L3BJQ01TPjx2SUNNUz4wLjAxPC92SUNNUz48L0lDTVMwMD48L0lDTVM+PFBJUz48UElTQWxpcT48Q1NUPjAxPC9DU1Q+PHZCQz4wLjAxPC92QkM+PHBQSVM+MS42NTwvcFBJUz48dlBJUz4wLjAwPC92UElTPjwvUElTQWxpcT48L1BJUz48Q09GSU5TPjxDT0ZJTlNBbGlxPjxDU1Q+MDE8L0NTVD48dkJDPjAuMDE8L3ZCQz48cENPRklOUz43LjYwPC9wQ09GSU5TPjx2Q09GSU5TPjAuMDE8L3ZDT0ZJTlM+PC9DT0ZJTlNBbGlxPjwvQ09GSU5TPjxJQlNDQlM+PENTVD4xMDE8L0NTVD48Y0NsYXNzVHJpYj5jbGFzc2lmaWNhY2FvPC9jQ2xhc3NUcmliPjxnSUJTQ0JTPjx2QkM+MC4wMTwvdkJDPjxnSUJTVUY+PHBJQlNVRj4wLjEwPC9wSUJTVUY+PHZJQlNVRj4wLjAwPC92SUJTVUY+PC9nSUJTVUY+PGdJQlNNdW4+PHBJQlNNdW4+MC4wMDwvcElCU011bj48dklCU011bj4wLjAwPC92SUJTTXVuPjwvZ0lCU011bj48dklCUz4wLjAwPC92SUJTPjxnQ0JTPjxwQ0JTPjAuOTA8L3BDQlM+PHZDQlM+MC4wMDwvdkNCUz48L2dDQlM+PC9nSUJTQ0JTPjwvSUJTQ0JTPjwvaW1wb3N0bz48dkl0ZW0+MC4wMTwvdkl0ZW0+PC9kZXQ+PHRvdGFsPjxJQ01TVG90Pjx2QkM+MC4wMTwvdkJDPjx2SUNNUz4wLjAxPC92SUNNUz48dklDTVNEZXNvbj4wLjAwPC92SUNNU0Rlc29uPjx2RkNQVUZEZXN0PjAuMDA8L3ZGQ1BVRkRlc3Q+PHZJQ01TVUZEZXN0PjAuMDA8L3ZJQ01TVUZEZXN0Pjx2SUNNU1VGUmVtZXQ+MC4wMDwvdklDTVNVRlJlbWV0Pjx2RkNQPjAuMDA8L3ZGQ1A+PHZCQ1NUPjAuMDA8L3ZCQ1NUPjx2U1Q+MC4wMDwvdlNUPjx2RkNQU1Q+MC4wMDwvdkZDUFNUPjx2RkNQU1RSZXQ+MC4wMDwvdkZDUFNUUmV0Pjx2UHJvZD4wLjAxPC92UHJvZD48dkZyZXRlPjAuMDA8L3ZGcmV0ZT48dlNlZz4wLjAwPC92U2VnPjx2RGVzYz4wLjAwPC92RGVzYz48dklJPjAuMDA8L3ZJST48dklQST4wLjAwPC92SVBJPjx2SVBJRGV2b2w+MC4wMDwvdklQSURldm9sPjx2UElTPjAuMDA8L3ZQSVM+PHZDT0ZJTlM+MC4wMTwvdkNPRklOUz48dk91dHJvPjAuMDA8L3ZPdXRybz48dk5GPjAuMDE8L3ZORj48L0lDTVNUb3Q+PElCU0NCU1RvdD48dkJDSUJTQ0JTPjAuMDE8L3ZCQ0lCU0NCUz48Z0lCUz48Z0lCU1VGPjx2RGlmPjAuMDA8L3ZEaWY+PHZEZXZUcmliPjAuMDA8L3ZEZXZUcmliPjx2SUJTVUY+MC4wMDwvdklCU1VGPjwvZ0lCU1VGPjxnSUJTTXVuPjx2RGlmPjAuMDA8L3ZEaWY+PHZEZXZUcmliPjAuMDA8L3ZEZXZUcmliPjx2SUJTTXVuPjAuMDA8L3ZJQlNNdW4+PC9nSUJTTXVuPjx2SUJTPjAuMDA8L3ZJQlM+PHZDcmVkUHJlcz4wLjAwPC92Q3JlZFByZXM+PHZDcmVkUHJlc0NvbmRTdXM+MC4wMDwvdkNyZWRQcmVzQ29uZFN1cz48L2dJQlM+PGdDQlM+PHZEaWY+MC4wMDwvdkRpZj48dkRldlRyaWI+MC4wMDwvdkRldlRyaWI+PHZDQlM+MC4wMDwvdkNCUz48dkNyZWRQcmVzPjAuMDA8L3ZDcmVkUHJlcz48dkNyZWRQcmVzQ29uZFN1cz4wLjAwPC92Q3JlZFByZXNDb25kU3VzPjwvZ0NCUz48L0lCU0NCU1RvdD48dk5GVG90PjAuMDE8L3ZORlRvdD48L3RvdGFsPjx0cmFuc3A+PG1vZEZyZXRlPjM8L21vZEZyZXRlPjwvdHJhbnNwPjxjb2JyPjxmYXQ+PG5GYXQ+MjAwMDwvbkZhdD48dk9yaWc+MTAwMC4wMDwvdk9yaWc+PHZEZXNjPjEwMC4wMDwvdkRlc2M+PHZMaXE+OTAwLjAwPC92TGlxPjwvZmF0PjwvY29icj48cGFnPjxkZXRQYWc+PHRQYWc+MDE8L3RQYWc+PHZQYWc+MC4wMTwvdlBhZz48L2RldFBhZz48L3BhZz48aW5mQWRpYz48aW5mQ3BsPlBhcmEgaW1wcmltaXIgdW1hIHF1ZWJyYSBkZSBsaW5oYSwgfCBkaWdpdGUgdW0gcGlwZTwvaW5mQ3BsPjwvaW5mQWRpYz48aW5mUmVzcFRlYz48Q05QSj4wODE4NzE2ODAwMDE2MDwvQ05QSj48eENvbnRhdG8+Tm9tZSB0ZXN0ZTwveENvbnRhdG8+PGVtYWlsPmVtYWlsLnRlc3RlQHRlc3RlLmNvbTwvZW1haWw+PGZvbmU+NDQ2NjY2NjY2NjwvZm9uZT48L2luZlJlc3BUZWM+PC9pbmZORmU+PFNpZ25hdHVyZSB4bWxucz0iaHR0cDovL3d3dy53My5vcmcvMjAwMC8wOS94bWxkc2lnIyI+PFNpZ25lZEluZm8+PENhbm9uaWNhbGl6YXRpb25NZXRob2QgQWxnb3JpdGhtPSJodHRwOi8vd3d3LnczLm9yZy9UUi8yMDAxL1JFQy14bWwtYzE0bi0yMDAxMDMxNSIvPjxTaWduYXR1cmVNZXRob2QgQWxnb3JpdGhtPSJodHRwOi8vd3d3LnczLm9yZy8yMDAwLzA5L3htbGRzaWcjcnNhLXNoYTEiLz48UmVmZXJlbmNlIFVSST0iI05GZTQxMjAwMzA4MTg3MTY4MDAwMTYwNTU4MDQwMDA3OTMyNzIxMDAwMDAwMDE1Ij48VHJhbnNmb3Jtcz48VHJhbnNmb3JtIEFsZ29yaXRobT0iaHR0cDovL3d3dy53My5vcmcvMjAwMC8wOS94bWxkc2lnI2VudmVsb3BlZC1zaWduYXR1cmUiLz48VHJhbnNmb3JtIEFsZ29yaXRobT0iaHR0cDovL3d3dy53My5vcmcvVFIvMjAwMS9SRUMteG1sLWMxNG4tMjAwMTAzMTUiLz48L1RyYW5zZm9ybXM+PERpZ2VzdE1ldGhvZCBBbGdvcml0aG09Imh0dHA6Ly93d3cudzMub3JnLzIwMDAvMDkveG1sZHNpZyNzaGExIi8+PERpZ2VzdFZhbHVlPmx0THlaZVRDOS9tN3hsMEhqdGRvVFBMbm5BOD08L0RpZ2VzdFZhbHVlPjwvUmVmZXJlbmNlPjwvU2lnbmVkSW5mbz48U2lnbmF0dXJlVmFsdWU+aU1NN0EyU3hyUUZiS0FEaDgwcWhVWStlS1RvbmEwMDZjY3d1TlJQWmxocUY0VTUycDJEZEFKVTVoSGVveVJOTnRmN1V5Yk1NYkhTMnl0ZlRYZmhXajlrZG9HemZmeUhxbEl2TFhVSHc4a1ZxM21xZXdyR0FPa3c5MUUvVm9vSis0dWJsTW85Y21LZnJGdnZ4M0ZIRTJuN2ZUQURNVkJlMXZaU1VuWjFFRmNuQTRvVDNCRGZVanJaOGdxeXlRSmU3elFWV1pqL1ZZUDRhd1NwSko2WkszNzZXTDJ2RmhSTUtyQmFBUlIzUDBiaEFFL0tSN0k1bDdtZ3dodUlTRHNXM1BGWEhrcWdiejc0WHpjaWpnbWpQRVZKL0tTV0R0OGsyVWV6dTV2TzZxeVplU3YvZlltQXMrR0s0em1YWFg0RkNJQ0VydzZQRk1vbFNWVGdoemN1MmNnPT08L1NpZ25hdHVyZVZhbHVlPjxLZXlJbmZvPjxYNTA5RGF0YT48WDUwOUNlcnRpZmljYXRlPk1JSUh6ekNDQmJlZ0F3SUJBZ0lJYll3WkJDVlRob0l3RFFZSktvWklodmNOQVFFTEJRQXdnWWt4Q3pBSkJnTlZCQVlUQWtKU01STXdFUVlEVlFRS0V3cEpRMUF0UW5KaGMybHNNVFF3TWdZRFZRUUxFeXRCZFhSdmNtbGtZV1JsSUVObGNuUnBabWxqWVdSdmNtRWdVbUZwZWlCQ2NtRnphV3hsYVhKaElIWXlNUkl3RUFZRFZRUUxFd2xCUXlCVFQweFZWRWt4R3pBWkJnTlZCQU1URWtGRElGTlBURlZVU1NCTmRXeDBhWEJzWVRBZUZ3MHhPVEEwTWpreE16TXlOVGRhRncweU1EQTBNall5TURVeU1EQmFNSUhsTVFzd0NRWURWUVFHRXdKQ1VqRVRNQkVHQTFVRUNoTUtTVU5RTFVKeVlYTnBiREUwTURJR0ExVUVDeE1yUVhWMGIzSnBaR0ZrWlNCRFpYSjBhV1pwWTJGa2IzSmhJRkpoYVhvZ1FuSmhjMmxzWldseVlTQjJNakVTTUJBR0ExVUVDeE1KUVVNZ1UwOU1WVlJKTVJzd0dRWURWUVFMRXhKQlF5QlRUMHhWVkVrZ1RYVnNkR2x3YkdFeEZ6QVZCZ05WQkFzVERqRTBNalU1TXpRNE1EQXdNVEF5TVJvd0dBWURWUVFMRXhGRFpYSjBhV1pwWTJGa2J5QlFTaUJCTVRFbE1DTUdBMVVFQXhNY1ZFVkRUazlUVUVWRlJDQlRRVG93T0RFNE56RTJPREF3TURFMk1EQ0NBU0l3RFFZSktvWklodmNOQVFFQkJRQURnZ0VQQURDQ0FRb0NnZ0VCQUk4Q3d3OHlqazUyQ1phais2MWR6cmRQckhXS0svTCtIMGxQczdjenBwayt2S0RDUFZLYy9xRVVqbXN6cDBEb1IzVFVLbVY0NEVzSDVpKzhGeVMzeU43YlloTFZNcVRaN1RHYmU4Y0N4YWluMnh0ekZkbGVINVlQWityTEkwemovMFpaem0zYTNHMG1BQ3Jxc2tjamozUS9yWGpnZlNNR2tnRzlNRVpJSy8yZk9xbWZtdlpvNHA2Mm9UK21QS3NwdzZDVjFtc0RJRFF6UzFjcXd5VHB3RlRGZ3dSOFVrOXJzZiszUm5Pcks0TEdpR01FdmRGTFVDQ29oOUE5bzN6Y2N3TDV2VHVkeE1rZHdPOW04bmtDM3B2bDFRKzdVTmxrZDRIak5OMzc5WXM4YVB4aUo3KzdnV0JyZHkxandtU2IrTjNwSWFwcElZMHRycDFTc2RYVmZPMENBd0VBQWFPQ0F0c3dnZ0xYTUZRR0NDc0dBUVVGQndFQkJFZ3dSakJFQmdnckJnRUZCUWN3QW9ZNGFIUjBjRG92TDJOalpDNWhZM052YkhWMGFTNWpiMjB1WW5JdmJHTnlMMkZqTFhOdmJIVjBhUzF0ZFd4MGFYQnNZUzEyTVM1d04ySXdIUVlEVlIwT0JCWUVGTUpuWWlZOW9aVkdkdzM2UGZrWGRsWi90K0x4TUFrR0ExVWRFd1FDTUFBd0h3WURWUjBqQkJnd0ZvQVVOYTR4RlBaZTBucFBXUDQwcUJwbmx3ckVtd2N3WGdZRFZSMGdCRmN3VlRCVEJnWmdUQUVDQVNZd1NUQkhCZ2dyQmdFRkJRY0NBUlk3YUhSMGNITTZMeTlqWTJRdVlXTnpiMngxZEdrdVkyOXRMbUp5TDJSdlkzTXZaSEJqTFdGakxYTnZiSFYwYVMxdGRXeDBhWEJzWVM1d1pHWXdnZDRHQTFVZEh3U0IxakNCMHpBK29EeWdPb1k0YUhSMGNEb3ZMMk5qWkM1aFkzTnZiSFYwYVM1amIyMHVZbkl2YkdOeUwyRmpMWE52YkhWMGFTMXRkV3gwYVhCc1lTMTJNUzVqY213d1A2QTlvRHVHT1doMGRIQTZMeTlqWTJReUxtRmpjMjlzZFhScExtTnZiUzVpY2k5c1kzSXZZV010YzI5c2RYUnBMVzExYkhScGNHeGhMWFl4TG1OeWJEQlFvRTZnVElaS2FIUjBjRG92TDNKbGNHOXphWFJ2Y21sdkxtbGpjR0p5WVhOcGJDNW5iM1l1WW5JdmJHTnlMMEZEVTA5TVZWUkpMMkZqTFhOdmJIVjBhUzF0ZFd4MGFYQnNZUzEyTVM1amNtd3dEZ1lEVlIwUEFRSC9CQVFEQWdYZ01CMEdBMVVkSlFRV01CUUdDQ3NHQVFVRkJ3TUNCZ2dyQmdFRkJRY0RCRENCd3dZRFZSMFJCSUc3TUlHNGdSOWxjbWxyWlM1aGJHMWxhV1JoUUhSbFkyNXZjM0JsWldRdVkyOXRMbUp5b0NFR0JXQk1BUU1Db0JnVEZrVlNTVXRGSUV4RlNWUkZJRVJGSUVGTVRVVkpSRUdnR1FZRllFd0JBd09nRUJNT01EZ3hPRGN4Tmpnd01EQXhOakNnUGdZRllFd0JBd1NnTlJNek1UUXhNREU1TnpnNU9UQTBPVE15TlRrd05EQXdNREF3TURBd01EQXdNREF3TURBd01EWTNPVEEyTXpRNVUwVlRVRkJTb0JjR0JXQk1BUU1Ib0E0VEREQXdNREF3TURBd01EQXdNREFOQmdrcWhraUc5dzBCQVFzRkFBT0NBZ0VBY2FoVTVCcmk2MWhGei9tMnVXVk42Q3RZaXRFRFFxa0lLcVVJbllVNVJzWjRISEx1bXFYdDNpTWpLdHd4TllUd0VwM1U0NlBtMnp5MEpkY0hXSmFJSWRIUGdVYmxVMTZXODZ6TGx6OXYxa0RiclVwNFJBa1dPVHF1OXQwMkRmUmpqazlXMkFmREdLaWJPNEVvNU54V1FEYndhek9vQU9iNThXWnp1YTdxVGVJMEo5S2lqUnpEN05Ka0lyQWVuVktBMmMxdWhRQmwrSy93UzZVMkx6TEhqK2ZhQ0IzYWlpVWNDNElUaUlXZ2dCa2E3QUgwaElaY1JTNmpXTENqdlZLZDV3QndDamh2QVc0Y1FlbjZZb0cvUk5sY3daclY0VkhJV2xnU05RZTVWL0RybG0ybjl6ZmgzZzIxdUIzSGx3bzk4K0J5U2FBVDdRMFpBWjRXNyttT3ZQSUk3Smg5MTVXZFc4cmtUU3JaVi9vRllBYTZxUEwxRzN6eDF3TFdmdGR0Q0hHUHYrMXRHclRIQ0oxY3c3bFRhbU9hbnQ1U0ZyV3hnZFFnMEVVTElteDFUZ3lzVlVZWTNXY0xtWWlhSGpGSjVhblZoN2s2RlB4UDVpUENhM3R2bk84dXVEbGYyMUMvdzFqT2NhTG90cEdDYnlEUEZhTnB5VzVVb2Q2c1F4OU84NlFWR055WkQ0Q2hWS2dWeWJMa0JkNkY5SU4yQUZNQ2F2dUxtVDhWSmZocHZ0dmk1VitXTkduM0FUaDBPaFkzOGN4a3VtcG1TT0swaFZuNFpSVURiOHJUTXVKVyt6RFdFR1Y2TUtYNVpiaml0amNlVlVnZ0ttbjNNNE5jUWFtckFnL1d2cGFrTEU3K2c0MTZCQXFIc3hWZ1RTNGpHNFUySENoRzRDUlNab3M9PC9YNTA5Q2VydGlmaWNhdGU+PC9YNTA5RGF0YT48L0tleUluZm8+PC9TaWduYXR1cmU+PC9ORmU+PHByb3RORmUgdmVyc2FvPSI0LjAwIj48aW5mUHJvdD48dHBBbWI+MjwvdHBBbWI+PHZlckFwbGljPlBSLXY0XzVfODwvdmVyQXBsaWM+PGNoTkZlPjQxMjAwMzA4MTg3MTY4MDAwMTYwNTU4MDQwMDA3OTMyNzIxMDAwMDAwMDE1PC9jaE5GZT48ZGhSZWNidG8+MjAyMC0wMy0wNVQwODoxNTowMC0wMzowMDwvZGhSZWNidG8+PG5Qcm90PjE0MTIwMDAwMDIzNzMyMzwvblByb3Q+PGRpZ1ZhbD5sdEx5WmVUQzkvbTd4bDBIanRkb1RQTG5uQTg9PC9kaWdWYWw+PGNTdGF0PjEwMDwvY1N0YXQ+PHhNb3Rpdm8+QXV0b3JpemFkbyBvIHVzbyBkYSBORi1lPC94TW90aXZvPjwvaW5mUHJvdD48L3Byb3RORmU+PC9uZmVQcm9jPg0K"} \ No newline at end of file diff --git a/payload_nfe_tray.json b/payload_nfe_tray.json new file mode 100644 index 0000000..7f6d9ed --- /dev/null +++ b/payload_nfe_tray.json @@ -0,0 +1,16 @@ +{ + "ProductCfop": [ + { + "cfop": "5102", + "product_id": "0999", + "variation_id": "MELAO" + } + ], + "issue_date": "2020-03-05", + "key": "41200308187168000160558040007932721000000015", + "link": null, + "number": "793272", + "serie": "804", + "value": "0.01", + "xml_danfe": "4100000001VENDA DE MERCADORIA ADQ. DE TERCEIRO - PF E PJ NAO CONTRIBUI558047932722020-03-05T07:50:42-03:002020-03-1011411520011521110508187168000160Teste NOMEteste FANT.RUA DO POVO711jardim teste4115200MARINGAPR794150001058BRASIL44322222229044016688318582206000132NF-E EMITIDA EM AMBIENTE DE HOMOLOGACAO - SEM VALOR FISCALRUA DO CENTRO897CENTRAL4115200MARINGAPR794150001058BRASIL445555555190712524660999SEM GTINMELAO110812000123456S5102CX10.01000.01SEM GTINCX10.010011235565.0002017-07-232018-07-2300000.0112.000.01010.011.650.00010.017.600.01101classificacao0.010.100.000.000.000.000.900.000.010.010.010.000.000.000.000.000.000.000.000.000.010.000.000.000.000.000.000.000.010.000.010.010.000.000.000.000.000.000.000.000.000.000.000.000.000.000.01320001000.00100.00900.00010.01Para imprimir uma quebra de linha, | digite um pipe08187168000160Nome testeemail.teste@teste.com4466666666ltLyZeTC9/m7xl0HjtdoTPLnnA8=iMM7A2SxrQFbKADh80qhUY+eKTona006ccwuNRPZlhqF4U52p2DdAJU5hHeoyRNNtf7UybMMbHS2ytfTXfhWj9kdoGzffyHqlIvLXUHw8kVq3mqewrGAOkw91E/VooJ+4ublMo9cmKfrFvvx3FHE2n7fTADMVBe1vZSUnZ1EFcnA4oT3BDfUjrZ8gqyyQJe7zQVWZj/VYP4awSpJJ6ZK376WL2vFhRMKrBaARR3P0bhAE/KR7I5l7mgwhuISDsW3PFXHkqgbz74XzcijgmjPEVJ/KSWDt8k2Uezu5vO6qyZeSv/fYmAs+GK4zmXXX4FCICErw6PFMolSVTghzcu2cg==MIIHzzCCBbegAwIBAgIIbYwZBCVThoIwDQYJKoZIhvcNAQELBQAwgYkxCzAJBgNVBAYTAkJSMRMwEQYDVQQKEwpJQ1AtQnJhc2lsMTQwMgYDVQQLEytBdXRvcmlkYWRlIENlcnRpZmljYWRvcmEgUmFpeiBCcmFzaWxlaXJhIHYyMRIwEAYDVQQLEwlBQyBTT0xVVEkxGzAZBgNVBAMTEkFDIFNPTFVUSSBNdWx0aXBsYTAeFw0xOTA0MjkxMzMyNTdaFw0yMDA0MjYyMDUyMDBaMIHlMQswCQYDVQQGEwJCUjETMBEGA1UEChMKSUNQLUJyYXNpbDE0MDIGA1UECxMrQXV0b3JpZGFkZSBDZXJ0aWZpY2Fkb3JhIFJhaXogQnJhc2lsZWlyYSB2MjESMBAGA1UECxMJQUMgU09MVVRJMRswGQYDVQQLExJBQyBTT0xVVEkgTXVsdGlwbGExFzAVBgNVBAsTDjE0MjU5MzQ4MDAwMTAyMRowGAYDVQQLExFDZXJ0aWZpY2FkbyBQSiBBMTElMCMGA1UEAxMcVEVDTk9TUEVFRCBTQTowODE4NzE2ODAwMDE2MDCCASIwDQYJKoZIhvcNAQEBBQADggEPADCCAQoCggEBAI8Cww8yjk52CZaj+61dzrdPrHWKK/L+H0lPs7czppk+vKDCPVKc/qEUjmszp0DoR3TUKmV44EsH5i+8FyS3yN7bYhLVMqTZ7TGbe8cCxain2xtzFdleH5YPZ+rLI0zj/0ZZzm3a3G0mACrqskcjj3Q/rXjgfSMGkgG9MEZIK/2fOqmfmvZo4p62oT+mPKspw6CV1msDIDQzS1cqwyTpwFTFgwR8Uk9rsf+3RnOrK4LGiGMEvdFLUCCoh9A9o3zccwL5vTudxMkdwO9m8nkC3pvl1Q+7UNlkd4HjNN379Ys8aPxiJ7+7gWBrdy1jwmSb+N3pIappIY0trp1SsdXVfO0CAwEAAaOCAtswggLXMFQGCCsGAQUFBwEBBEgwRjBEBggrBgEFBQcwAoY4aHR0cDovL2NjZC5hY3NvbHV0aS5jb20uYnIvbGNyL2FjLXNvbHV0aS1tdWx0aXBsYS12MS5wN2IwHQYDVR0OBBYEFMJnYiY9oZVGdw36PfkXdlZ/t+LxMAkGA1UdEwQCMAAwHwYDVR0jBBgwFoAUNa4xFPZe0npPWP40qBpnlwrEmwcwXgYDVR0gBFcwVTBTBgZgTAECASYwSTBHBggrBgEFBQcCARY7aHR0cHM6Ly9jY2QuYWNzb2x1dGkuY29tLmJyL2RvY3MvZHBjLWFjLXNvbHV0aS1tdWx0aXBsYS5wZGYwgd4GA1UdHwSB1jCB0zA+oDygOoY4aHR0cDovL2NjZC5hY3NvbHV0aS5jb20uYnIvbGNyL2FjLXNvbHV0aS1tdWx0aXBsYS12MS5jcmwwP6A9oDuGOWh0dHA6Ly9jY2QyLmFjc29sdXRpLmNvbS5ici9sY3IvYWMtc29sdXRpLW11bHRpcGxhLXYxLmNybDBQoE6gTIZKaHR0cDovL3JlcG9zaXRvcmlvLmljcGJyYXNpbC5nb3YuYnIvbGNyL0FDU09MVVRJL2FjLXNvbHV0aS1tdWx0aXBsYS12MS5jcmwwDgYDVR0PAQH/BAQDAgXgMB0GA1UdJQQWMBQGCCsGAQUFBwMCBggrBgEFBQcDBDCBwwYDVR0RBIG7MIG4gR9lcmlrZS5hbG1laWRhQHRlY25vc3BlZWQuY29tLmJyoCEGBWBMAQMCoBgTFkVSSUtFIExFSVRFIERFIEFMTUVJREGgGQYFYEwBAwOgEBMOMDgxODcxNjgwMDAxNjCgPgYFYEwBAwSgNRMzMTQxMDE5Nzg5OTA0OTMyNTkwNDAwMDAwMDAwMDAwMDAwMDAwMDY3OTA2MzQ5U0VTUFBSoBcGBWBMAQMHoA4TDDAwMDAwMDAwMDAwMDANBgkqhkiG9w0BAQsFAAOCAgEAcahU5Bri61hFz/m2uWVN6CtYitEDQqkIKqUInYU5RsZ4HHLumqXt3iMjKtwxNYTwEp3U46Pm2zy0JdcHWJaIIdHPgUblU16W86zLlz9v1kDbrUp4RAkWOTqu9t02DfRjjk9W2AfDGKibO4Eo5NxWQDbwazOoAOb58WZzua7qTeI0J9KijRzD7NJkIrAenVKA2c1uhQBl+K/wS6U2LzLHj+faCB3aiiUcC4ITiIWggBka7AH0hIZcRS6jWLCjvVKd5wBwCjhvAW4cQen6YoG/RNlcwZrV4VHIWlgSNQe5V/Drlm2n9zfh3g21uB3Hlwo98+BySaAT7Q0ZAZ4W7+mOvPII7Jh915WdW8rkTSrZV/oFYAa6qPL1G3zx1wLWftdtCHGPv+1tGrTHCJ1cw7lTamOant5SFrWxgdQg0EULImx1TgysVUYY3WcLmYiaHjFJ5anVh7k6FPxP5iPCa3tvnO8uuDlf21C/w1jOcaLotpGCbyDPFaNpyW5Uod6sQx9O86QVGNyZD4ChVKgVybLkBd6F9IN2AFMCavuLmT8VJfhpvtvi5V+WNGn3ATh0OhY38cxkumpmSOK0hVn4ZRUDb8rTMuJW+zDWEGV6MKX5ZbjitjceVUggKmn3M4NcQamrAg/WvpakLE7+g416BAqHsxVgTS4jG4U2HChG4CRSZos=2PR-v4_5_8412003081871680001605580400079327210000000152020-03-05T08:15:00-03:00141200000237323ltLyZeTC9/m7xl0HjtdoTPLnnA8=100Autorizado o uso da NF-e" +} \ No newline at end of file diff --git a/pedido_completo.json b/pedido_completo.json new file mode 100644 index 0000000..f2602ad --- /dev/null +++ b/pedido_completo.json @@ -0,0 +1,262 @@ +{ + "Order": { + "status": "A ENVIAR VINDI", + "id": "12797", + "date": "2025-10-01", + "hour": "09:07:57", + "customer_id": "11531", + "partial_total": "69.90", + "taxes": "0.00", + "discount": "0.00", + "point_sale": "PARTICULAR", + "shipment": "Retirada", + "shipment_value": "0.00", + "original_shipment_value": "0.00", + "shipment_date": "", + "delivered": "", + "delivered_status": "", + "shipping_cancelled": "0", + "store_note": "", + "customer_note": "", + "partner_id": "", + "discount_coupon": "", + "client_ip": "", + "payment_method_rate": "0.00", + "installment": "0", + "value_1": "0.00", + "sending_code": "", + "sending_date": "0000-00-00", + "billing_address": "", + "delivery_time": "", + "payment_method_id": "", + "payment_method": "PIX", + "session_id": "0BBB15A404B6BA1", + "total": "69.90", + "payment_date": "2025-10-01", + "access_code": "834E9A51CFF9D99", + "shipment_integrator": "", + "modified": "2025-10-30 10:22:38", + "printed": "", + "interest": "0.00", + "cart_additional_values_discount": "0.00", + "cart_additional_values_increase": "0.00", + "id_quotation": "", + "estimated_delivery_date": "2025-10-01", + "is_traceable": "0", + "external_code": "", + "tracking_url": "", + "has_payment": "1", + "has_shipment": "0", + "has_invoice": "0", + "delivery_date": "", + "dc_id": "", + "dc_order_origin": "", + "total_comission_user": "0.00", + "total_comission": "0.00", + "OrderStatus": { + "id": "995", + "default": "0", + "type": "open", + "show_backoffice": "1", + "allow_edit_order": "1", + "description": "", + "status": "A ENVIAR VINDI", + "show_status_central": "1", + "background": "#60e163", + "display_name": "A enviar Vindi", + "font_color": "#000000" + }, + "PickupLocation": [], + "cost": "0.00", + "app_id": "0", + "urls": { + "payment": "https://1225878.commercesuite.com.br/loja/pagamento.php?loja=1225878&pedido=834E9A51CFF9D99" + }, + "store_segment": "Animais & petshop", + "payment_method_type": "", + "Customer": { + "cnpj": "", + "newsletter": "0", + "created": "2025-10-01 08:05:12", + "terms": "0000-00-00 00:00:00", + "id": "11531", + "name": "Henrique Crespilho Ferro", + "registration_date": "2025-10-01", + "rg": "", + "cpf": "04650716004", + "phone": "14991239292", + "cellphone": "14991239292", + "birth_date": "", + "gender": "0", + "email": "gazelle30064@aminating.com", + "nickname": "", + "token": "834E9A51CFF9D99", + "total_orders": "0", + "observation": "", + "type": "0", + "foreign": "0", + "company_name": "", + "state_inscription": "", + "reseller": "", + "discount": "0.00", + "blocked": "0", + "credit_limit": "0.00", + "indicator_id": "0", + "profile_customer_id": "1", + "last_sending_newsletter": "0000-00-00", + "last_purchase": "2025-10-01", + "last_visit": "2025-10-01", + "last_modification": "0000-00-00 00:00:00", + "address": "Av. Dr. Quinzinho", + "zip_code": "17210-110", + "number": "511", + "complement": "Estacionamento Jaú Shopping", + "neighborhood": "Vila Assis", + "city": "Jaú", + "state": "SP", + "country": "Brasil", + "modified": "2025-10-01 09:07:57", + "count_orders": "0", + "Extensions": { + "Profile": { + "id": "1", + "name": "Padrao", + "approves_registration": "0" + }, + "Profiles": [ + { + "id": "1", + "price_list_id": "0", + "name": "Padrao", + "approves_registration": "0", + "show_price": "0", + "theme_id": "0", + "selected": "1" + } + ] + }, + "CustomerAddresses": [ + { + "CustomerAddress": { + "id": "9285", + "customer_id": "11531", + "address": "Av. Dr. Quinzinho", + "number": "511", + "complement": "Estacionamento Jaú Shopping", + "neighborhood": "Vila Assis", + "city": "Jaú", + "state": "SP", + "zip_code": "17210-110", + "country": "Brasil", + "type": "1", + "active": "1", + "description": "", + "recipient": "", + "type_delivery": "1", + "not_list": "0" + } + } + ] + }, + "ProductsSold": [ + { + "ProductsSold": { + "product_kit_id": "0", + "product_kit_id_kit": "0", + "id_campaign": "", + "product_id": "63", + "quantity": "1", + "id": "16113", + "order_id": "12797", + "name": "MMM Calça de Malha Foil Preta Brilhante
Cor PRETO
TAMANHO 12 (Disponibilidade: Imediata)
", + "original_name": "", + "virtual_product": "0", + "ean": "", + "availability_days": "", + "availability": "", + "Sku": [], + "price": "69.90", + "cost_price": "0.00", + "original_price": "69.90", + "weight": "136", + "weight_cubic": "110", + "brand": "kamylus INV", + "model": "", + "reference": "", + "length": "24", + "width": "22", + "height": "1", + "variant_id": "1132951", + "additional_information": "", + "text_variant": "", + "warranty": "", + "bought_together_id": "0", + "ncm": "", + "included_items": "", + "release_date": "", + "commissioner_value": "", + "comissao": "0.00", + "is_giveaway_by_coupon": "0", + "ProductSoldImage": [], + "Category": [], + "is_giveaway": "0", + "BoughtTogether": [], + "ProductSoldPackage": [], + "ProductSoldCard": [], + "url": { + "http": "", + "https": "" + }, + "Discount": [], + "Stock": { + "id": "1", + "name": "Loja" + } + } + } + ], + "OrderInvoice": [], + "Payment": [ + { + "Payment": { + "created": "2025-10-01 09:24:13", + "modified": "2025-10-01 09:24:12", + "id": "2691", + "order_id": "12797", + "payment_method_id": "", + "method": "PIX", + "payment_place": "", + "value": "66.41", + "date": "2025-10-01", + "note": "Pagamento realizado com sucesso utilizando ASSAS com o metodo de pagamento PIX", + "unique_number": "" + } + } + ], + "MlOrder": [], + "MarketplaceOrder": [], + "OrderTransactions": [], + "OrderInvoiceAmount": { + "reference": "2025-10-01", + "value": "0.70", + "tax_type": "%", + "tax_value": "1.00", + "point_sale": "PARTICULAR" + }, + "OtherInvoiceAmounts": [], + "ExtraTabs": [], + "OrderChilds": [], + "interest_paid_by": "seller", + "DistributionCenter": { + "status": "pending", + "type": "main_order", + "Package": "" + }, + "PaymentMethodMessage": [], + "partner_name": "", + "Transaction": "" + }, + "Extensions": [], + "User": [], + "Confirmation": [] +} \ No newline at end of file diff --git a/requirements.txt b/requirements.txt new file mode 100644 index 0000000..6567bef --- /dev/null +++ b/requirements.txt @@ -0,0 +1,21 @@ +annotated-types==0.7.0 +certifi==2026.2.25 +cffi==2.0.0 +charset-normalizer==3.4.7 +cryptography==46.0.7 +cryptograpy==0.0.0 +greenlet==3.5.0 +idna==3.11 +pika==1.3.2 +psutil==7.2.2 +psycopg2-binary==2.9.12 +pycparser==3.0 +pydantic==2.13.2 +pydantic_core==2.46.2 +pytz==2026.1.post1 +redis==7.4.0 +requests==2.33.1 +SQLAlchemy==2.0.49 +typing-inspection==0.4.2 +typing_extensions==4.15.0 +urllib3==2.6.3 diff --git a/src/Exchange2.py b/src/Exchange2.py new file mode 120000 index 0000000..b1a3cf2 --- /dev/null +++ b/src/Exchange2.py @@ -0,0 +1 @@ +../utils/Exchange2.py \ No newline at end of file diff --git a/src/HostMetrics.py b/src/HostMetrics.py new file mode 120000 index 0000000..4d70729 --- /dev/null +++ b/src/HostMetrics.py @@ -0,0 +1 @@ +../utils/HostMetrics.py \ No newline at end of file diff --git a/src/api/publicas.py b/src/api/publicas.py new file mode 100644 index 0000000..e0b2ad1 --- /dev/null +++ b/src/api/publicas.py @@ -0,0 +1,15 @@ +import requests +import time + +def busca_cep(cep: str) -> dict: + while True: + try: + url = f"https://viacep.com.br/ws/{cep}/json/" + response = requests.get(url) + if response.status_code == 200: + return response.json() + else: + time.sleep(0.1) + except Exception as e: + time.sleep(0.1) + diff --git a/src/api/tray.py b/src/api/tray.py new file mode 100644 index 0000000..f720fbd --- /dev/null +++ b/src/api/tray.py @@ -0,0 +1,104 @@ +from models.sg_tray_models.services_model import TrayOrderRootResponse +from appIC2Builder import SystemConfig + +import requests +import redis +import json + +class ApiTray: + def __init__(self, system_config: SystemConfig, redis_client: redis.Redis): + self.consumer_key = system_config.tray_consumer_key + self.consumer_secret = system_config.tray_consumer_secret + self.code = system_config.tray_code + self.url = system_config.tray_url + self.redis_client = redis_client + self.service = system_config.service + self.system = system_config.system + + def auth(self) -> str: + try: + print("\n") + print("Autenticando na API da Tray...", flush=True) + response = requests.post(url = f"{self.url}/web_api/auth", + headers = {"Content-Type": "application/x-www-form-urlencoded"}, + data = f"consumer_key={self.consumer_key}&consumer_secret={self.consumer_secret}&code={self.code}") + if response.status_code in [ 200, 201 ]: + print("Autenticado com sucesso!", flush=True) + return response.json() + else: + print(f"Erro ao autenticar na Tray: {response.status_code} | {response.text}", flush=True) + raise Exception(response.text) + except Exception as e: + print(f"Error connecting to Tray: {e}", flush=True) + raise e + + def refresh_token(self) -> str: + try: + print("\n") + print("Atualizando token de acesso da Tray...", flush=True) + response = requests.get(url = f"{self.url}/web_api/auth?access_token={self.get_token()}") + if response.status_code in [ 200, 201 ]: + print("Token atualizado com sucesso!", flush=True) + return response.json() + else: + print(f"Erro ao atualizar token na Tray: {response.status_code} | {response.text}", flush=True) + raise Exception(response.text) + except Exception as e: + print(f"Error connecting to Tray: {e}", flush=True) + raise e + + def get_token(self) -> str: + try: + chave = f"{self.system}_{self.service}_auth" + if self.redis_client.exists(chave): + auth = json.loads(self.redis_client.get(chave)) + return auth['access_token'] + + # Busca o token e guarda no redis com cache de 2h e 55 minutos + token = self.auth() + self.redis_client.set(chave, json.dumps(token), ex=(2*3600)) + return token['access_token'] + except Exception as e: + raise e + + def get_pedido_completo(self, id_pedido: int) -> TrayOrderRootResponse: + try: + response = requests.get(url = f"{self.url}/web_api/orders/{id_pedido}/complete?access_token={self.get_token()}") + if response.status_code == 200: + print(f"Pedido {id_pedido} obtido com sucesso!", flush=True) + return TrayOrderRootResponse(**response.json()) + else: + print(f"Erro ao obter pedido {id_pedido} na Tray: {response.text}", flush=True) + raise Exception(response.text) + except Exception as e: + print(f"Error connecting to Tray: {e}", flush=True) + raise e + + def inserir_nfe_pedido(self, id_pedido: int, nfe: dict) -> None: + try: + response = requests.post(url = f"{self.url}/web_api/orders/{id_pedido}/invoices?access_token={self.get_token()}", + headers = {"Content-Type": "application/json"}, + data = json.dumps(nfe)) + if response.status_code == 200: + print(f"NFE do pedido {id_pedido} atualizada com sucesso!", flush=True) + else: + print(f"Erro ao atualizar NFE do pedido {id_pedido} na Tray: {response.text}", flush=True) + raise Exception(response.text) + except Exception as e: + print(f"Error connecting to Tray: {e}", flush=True) + raise e + + def inserir_produto_tray(self, produto: dict) -> int: + try: + response = requests.post(url = f"{self.url}/web_api/products?access_token={self.get_token()}", + headers = {"Content-Type": "application/json"}, + data = json.dumps(produto)) + if response.status_code in [ 200, 201 ]: + print(f"Produto {produto['name']} inserido com sucesso!", flush=True) + return response.json()['id'] + else: + print(f"Erro ao inserir produto {produto['name']} na Tray: {response.text}", flush=True) + raise Exception(response.text) + except Exception as e: + print(f"Error connecting to Tray: {e}", flush=True) + raise e \ No newline at end of file diff --git a/src/appIC2Builder.py b/src/appIC2Builder.py new file mode 100644 index 0000000..08c35cf --- /dev/null +++ b/src/appIC2Builder.py @@ -0,0 +1,124 @@ +from datetime import datetime as dt + +from typing import Tuple, Any + +from exchangeModels import * +from Exchange2 import IC2Builder + +class SystemConfig(BaseModel): + amqps : str + ic2_url : str + rabbitmq : str + system : str + service : str + client : str + enterprise_id : str + enterprise_name : str + servicekey : str + serviceid : str + tray_consumer_key : str + tray_consumer_secret: str + tray_code : str + tray_url : str + db_host : str + db_port : int + db_user : str + db_password : str + db_name : str + + +# AQUI PODEMOS ESTENDER OS REALMS TYPE DE ACORDO COM A APLICAÇÃO +# Dados cria o objeto que será passado para o IC2Builder na inicialização +class Dados(BaseModel): + service_date : str + startup_date : str + task_id : str + service : str + versionstr : str + origin : str + enterprise_id : str + enterprise_name : str + system : str + source_key : int | None = 0 + destination_key : int | None = 0 + +# ESPECIALIZAÇÃO REALM OPERATIONAL - AQUI EXEMPLO PARA MIDDLEWARE DE VENDAS +class Operation(ICOperational): + source_key : int = 0 + destination_key : int = 0 + + +class AppIC2Builder(IC2Builder): + def __init__(self, dados: object): # AQUI dados TEM INFORMACOES EM TEMPO DE INICIALIZAÇÃO + self.dados = dados + + def _setLogs(self, message: tuple) -> InfoConnector: # AQUI message TEM INFORMACOES EM TEMPO DE EXECUÇÃO + msg, *extra = message + return InfoConnector( + realm = RealmType.LOGS, + messageIC = ICLogs( + log_date = dt.now().strftime("%Y-%m-%d %H:%M:%S"), + service_date = self.dados.service_date, + service = self.dados.service, + versionstr = self.dados.versionstr, + origin = self.dados.origin, + enterprise_id = self.dados.enterprise_id, + enterprise_name = self.dados.enterprise_name, + message = msg) + ) + + def _setErrors(self, message: Tuple[str]) -> object: # AQUI message TEM INFORMACOES EM TEMPO DE EXECUÇÃO + return InfoConnector( + realm = RealmType.ERRORS, + messageIC = ICErrors( + log_date = dt.now().strftime("%Y-%m-%d %H:%M:%S"), + service_date = self.dados.service_date, + service = self.dados.service, + versionstr = self.dados.versionstr, + origin = self.dados.origin, + enterprise_id = self.dados.enterprise_id, + enterprise_name = self.dados.enterprise_name, + message = message[0]) + ) + + def _setOperational(self, message:Tuple[str,str,OpStatus]) -> object: # AQUI message TEM INFORMACOES EM TEMPO DE EXECUÇÃO + return InfoConnector( + realm = RealmType.OPERATIONAL, + messageIC = Operation( + log_date = dt.now().strftime("%Y-%m-%d %H:%M:%S"), + service_date = self.dados.service_date, # DATA DE INICIO DO TASK/SERVIÇO + service_name = self.dados.service, # NOME DO SERVIÇO + task_id = self.dados.task_id, # ID DO TASK + startup_date = self.dados.startup_date, # DATA DE INICIO DO TASK/SERVIÇO + versionstr = self.dados.versionstr, + origin = self.dados.origin, + enterprise_id = self.dados.enterprise_id, + enterprise_name = self.dados.enterprise_name, + source_key = self.dados.source_key, + destination_key = self.dados.destination_key, + step_status = message[0], # step_status + message = message[1], # msg + status = message[2] # status + ) + ) + + def _setStatus(self, message=Tuple[(int, float, float, float, float)]) -> object: # AQUI message TEM INFORMACOES EM TEMPO DE EXECUÇÃO + return InfoConnector( + realm = RealmType.STATUS, + messageIC = ICStatus( + service = self.dados.service, + versionstr = self.dados.versionstr, + system_name = self.dados.enterprise_name, + startup_date = self.dados.startup_date, + access_date = dt.now().strftime("%Y-%m-%d %H:%M:%S"), + origin = self.dados.origin, + sid = self.dados.enterprise_id, + task_count = message[0], + cpu = message[1], + memory = message[2], + disk = message[3], + network = message[4]) + ) + + def _setMetrics(self, message=Tuple[()]) -> object: # AQUI message TEM INFORMACOES EM TEMPO DE EXECUÇÃO + pass \ No newline at end of file diff --git a/src/controller.py b/src/controller.py new file mode 100644 index 0000000..de1bd7e --- /dev/null +++ b/src/controller.py @@ -0,0 +1,655 @@ +from datetime import datetime as dt +# pyrefly: ignore [missing-import] +from pydantic import ValidationError, TypeAdapter +from json.decoder import JSONDecodeError + +from Exchange2 import QueueExchange2, LoggerExchange2 +from exchangeModels import * +from HostMetrics import * +from appIC2Builder import * + +from services.cliente import ClienteService +from services.nfe import NfeService +from services.titulo_receber import TituloReceberService + +from db.banco import Banco + +from models.recebimento import Recebimento, RecebimentoPlugnotas +from models.sg_tray_models.services_model import TrayOrderRootResponse + +from api.tray import ApiTray + +from send_rpc import SendRPCService +from send_queue import SendQueueService + +import xml.etree.ElementTree as ET +import base64 +import redis +import json +import time +import sys + +# Versão do script e histórico básico +version = "1" +versionstr = f"v{version}.0.0 - BALT - 24/04/2026 - VERSÃO INICIAL" + +system = "innovar_amr_tray" +service = "controller" +tenant = "innovar" +startup_date = dt.now().strftime("%Y-%m-%d %H:%M:%S") +TaskCount = 0 +metrics = HostMetrics() +stats = metrics.get_metrics() +# Obtém o nome do ambiente/container +try: + with open("/app/hostname") as f: + origin= f.read().strip() +except Exception as e: + print(f"Montar o arquivo hostname no compose: {e}", flush=True) + time.sleep(5) + sys.exit(0) + + +############################################################## < Inicio logica > ############################################################## + +def buscar_dados_tray(id_pedido_tray) -> TrayOrderRootResponse | None: + step_status = "BUSCANDO PEDIDO NO TRAY" + logger.logIC2(realm=RealmType.OPERATIONAL,msg=(step_status,"INICIADO",OpStatus.ACTIVE)) + try: + api_tray = ApiTray(system_config=system_config, redis_client=rd) + except Exception as e: + message = f"Erro ao inicializar ApiTray: {e}" + logger.logIC2(realm=RealmType.OPERATIONAL,msg=(step_status, message, OpStatus.ERROR)) + return None + + try: + pedido = api_tray.get_pedido_completo(id_pedido_tray) + logger.logIC2(realm=RealmType.OPERATIONAL,msg=(step_status, f"Pedido encontrado: {pedido}", OpStatus.OK)) + return pedido + except Exception as e: + message = f"Erro ao buscar pedido no Tray: {e}" + logger.logIC2(realm=RealmType.OPERATIONAL,msg=(step_status, message, OpStatus.ERROR)) + return None + +def criar_cliente(pedido: TrayOrderRootResponse, banco: Banco) -> int | None: + step_status = "CRIANDO CLIENTE NO SIENGE" + logger.logIC2(realm=RealmType.OPERATIONAL,msg=(step_status,"INICIADO",OpStatus.ACTIVE)) + try: + cliente_service = ClienteService(config=system_config, redis_client=rd) + except Exception as e: + message = f"Erro ao inicializar ClienteService: {e}" + logger.logIC2(realm=RealmType.OPERATIONAL,msg=(step_status, message, OpStatus.ERROR)) + logger.logMsgError(f"Erro ao inicializar ClienteService: {e}") + return False + + logger.logIC2(realm=RealmType.OPERATIONAL,msg=(step_status,"BUSCANDO CONFIGURAÇÃO DO CLIENTE",OpStatus.ACTIVE)) + try: + config_tenant = banco.get_config_cliente(tenant=tenant) + if not config_tenant: + message = f"Configuração do cliente '{tenant}' não encontrada no banco de dados." + logger.logIC2(realm=RealmType.OPERATIONAL,msg=(step_status, message, OpStatus.ERROR)) + return False + except Exception as e: + message = f"Erro ao obter configuração do cliente: {e}" + logger.logIC2(realm=RealmType.OPERATIONAL,msg=(step_status, message, OpStatus.ERROR)) + return False + + logger.logIC2(realm=RealmType.OPERATIONAL,msg=(step_status,"MAPPING DOS DADOS",OpStatus.ACTIVE)) + try: + payload = cliente_service._mapper_tray_to_sienge_cliente(pedido, config_tenant) + except Exception as e: + print(f"Erro ao mapear cliente: {e}",flush=True) + message = f"Erro ao mapear dados do cliente: {e}" + logger.logIC2(realm=RealmType.OPERATIONAL,msg=(step_status, message, OpStatus.ERROR)) + return False + + logger.logIC2(realm=RealmType.OPERATIONAL,msg=(step_status,"BUSCANDO CREDENCIAIS DO INTEGRADOR",OpStatus.ACTIVE)) + try: + integrador = banco.get_credenciais_servicos_integradores(integrador="cliente") + if not integrador: + message = f"Credenciais para integrador 'cliente' não encontradas no banco de dados." + logger.logIC2(realm=RealmType.OPERATIONAL,msg=(step_status, message, OpStatus.ERROR)) + return False + except Exception as e: + message = f"Erro ao obter credenciais do integrador: {e}" + logger.logIC2(realm=RealmType.OPERATIONAL,msg=(step_status, message, OpStatus.ERROR)) + return False + + logger.logIC2(realm=RealmType.OPERATIONAL,msg=(step_status,"ENVIANDO DADOS PARA FILA DE CLIENTE (RPC)",OpStatus.ACTIVE)) + try: + rpc = SendRPCService( + system = integrador.system, + service = integrador.service, + amqps = integrador.amqps, + origin = origin, + serviceid = integrador.serviceid, + servicekey = integrador.servicekey + ) + print(payload, flush=True) + response = rpc.send_rpc(payload=payload) + + payload = response.get('payload') + hmac = response.get('hmac') + if not payload or not hmac: + message = f"Resposta RPC inválida: {response}" + logger.logIC2(realm=RealmType.OPERATIONAL,msg=(step_status, message, OpStatus.ERROR)) + return False + response_data = rpc.get_payload(hmac, payload) + logger.logIC2(realm=RealmType.OPERATIONAL,msg=(step_status, f"Cliente criado: {response_data.get('customer_id')}", OpStatus.OK)) + return response_data.get('customer_id') + except Exception as e: + message = f"Erro ao enviar dados para fila de cliente: {e}" + logger.logIC2(realm=RealmType.OPERATIONAL,msg=(step_status, message, OpStatus.ERROR)) + return False + +def criar_titulo(pedido: TrayOrderRootResponse, banco: Banco, chave_nfe: str, numero_nfe: str) -> int | None: + step_status = "CRIANDO TÍTULO A RECEBER NO SIENGE" + logger.logIC2(realm=RealmType.OPERATIONAL,msg=(step_status,"INICIADO",OpStatus.ACTIVE)) + try: + titulo_service = TituloReceberService(config=system_config) + except Exception as e: + message = f"Erro ao inicializar TituloReceberService: {e}" + logger.logIC2(realm=RealmType.OPERATIONAL,msg=(step_status, message, OpStatus.ERROR)) + return False + + logger.logIC2(realm=RealmType.OPERATIONAL,msg=(step_status,"BUSCANDO CONFIGURAÇÃO DO CLIENTE",OpStatus.ACTIVE)) + try: + config_tenant = banco.get_config_cliente(tenant=tenant) + if not config_tenant: + message = f"Configuração do cliente '{tenant}' não encontrada no banco de dados." + logger.logIC2(realm=RealmType.OPERATIONAL,msg=(step_status, message, OpStatus.ERROR)) + return False + except Exception as e: + message = f"Erro ao obter configuração do cliente: {e}" + logger.logIC2(realm=RealmType.OPERATIONAL,msg=(step_status, message, OpStatus.ERROR)) + return False + + logger.logIC2(realm=RealmType.OPERATIONAL,msg=(step_status,"BUSCANDO APROPRIAÇÃO FINANCEIRA",OpStatus.ACTIVE)) + try: + apropriacao = banco.get_lista_apropriacao_financeira(tenant=tenant) + if not apropriacao: + message = f"Apropriação financeira para tenant '{tenant}' não encontrada no banco de dados." + logger.logIC2(realm=RealmType.OPERATIONAL,msg=(step_status, message, OpStatus.ERROR)) + return False + except Exception as e: + message = f"Erro ao obter apropriação financeira: {e}" + logger.logIC2(realm=RealmType.OPERATIONAL,msg=(step_status, message, OpStatus.ERROR)) + return False + + logger.logIC2(realm=RealmType.OPERATIONAL,msg=(step_status,"BUSCANDO PEDIDO",OpStatus.ACTIVE)) + try: + pedido_db = banco.get_pedido(id_pedido_tray=pedido.Order.id) + if not pedido_db: + message = f"Pedido '{pedido.id}' não encontrado no banco de dados." + logger.logIC2(realm=RealmType.OPERATIONAL,msg=(step_status, message, OpStatus.ERROR)) + return False + except Exception as e: + message = f"Erro ao obter pedido: {e}" + logger.logIC2(realm=RealmType.OPERATIONAL,msg=(step_status, message, OpStatus.ERROR)) + return False + + logger.logIC2(realm=RealmType.OPERATIONAL,msg=(step_status,"MAPPING DOS DADOS",OpStatus.ACTIVE)) + try: + payload = titulo_service.map_tray_order_to_bill_data(pedido, config_tenant, apropriacao, pedido_db, chave_nfe, numero_nfe) + except Exception as e: + message = f"Erro ao mapear dados: {e}" + logger.logIC2(realm=RealmType.OPERATIONAL,msg=(step_status, message, OpStatus.ERROR)) + return False + + logger.logIC2(realm=RealmType.OPERATIONAL,msg=(step_status,"BUSCANDO CREDENCIAIS DO INTEGRADOR",OpStatus.ACTIVE)) + try: + integrador = banco.get_credenciais_servicos_integradores(integrador="titulo-cr") + if not integrador: + message = f"Credenciais para integrador 'titulo-cr' não encontradas no banco de dados." + logger.logIC2(realm=RealmType.OPERATIONAL,msg=(step_status, message, OpStatus.ERROR)) + return False + except Exception as e: + message = f"Erro ao obter credenciais do integrador: {e}" + logger.logIC2(realm=RealmType.OPERATIONAL,msg=(step_status, message, OpStatus.ERROR)) + return False + + logger.logIC2(realm=RealmType.OPERATIONAL,msg=(step_status,"ENVIANDO DADOS PARA FILA DE TÍTULO A RECEBER (RPC)",OpStatus.ACTIVE)) + try: + rpc = SendRPCService( + system = integrador.system, + service = integrador.service, + amqps = integrador.amqps, + origin = origin, + serviceid = integrador.serviceid, + servicekey = integrador.servicekey + ) + response = rpc.send_rpc(payload=payload.model_dump(exclude_none=True)) + + payload = response.get('payload') + hmac = response.get('hmac') + if not payload or not hmac: + message = f"Resposta RPC inválida: {response}" + logger.logIC2(realm=RealmType.OPERATIONAL,msg=(step_status, message, OpStatus.ERROR)) + return False + response_data = rpc.get_payload(hmac, payload) + logger.logIC2(realm=RealmType.OPERATIONAL,msg=(step_status, f"Título a receber criado: {response_data.get('id_titulo')}", OpStatus.OK)) + return response_data.get('id_titulo') + except Exception as e: + print(f"Erro ao enviar dados para título a receber: {e}", flush=True) + message = f"Erro ao enviar dados para fila de título a receber: {e}" + logger.logIC2(realm=RealmType.OPERATIONAL,msg=(step_status, message, OpStatus.ERROR)) + return False + +def criar_nfe(pedido: TrayOrderRootResponse, banco: Banco) -> bool: + step_status = "NFe - CRIACAO" + logger.logIC2(realm=RealmType.OPERATIONAL,msg=(step_status,"INICIADO",OpStatus.ACTIVE)) + try: + nfe = NfeService(system_config, rd) + except Exception as e: + message = f"Erro ao inicializar NFeService: {e}" + logger.logIC2(realm=RealmType.OPERATIONAL,msg=(step_status, message, OpStatus.ERROR)) + return False + + logger.logIC2(realm=RealmType.OPERATIONAL,msg=(step_status,"BUSCANDO CONFIGURAÇÃO DO CLIENTE",OpStatus.ACTIVE)) + try: + config_tenant = banco.get_config_cliente(tenant=tenant) + if not config_tenant: + message = f"Configuração do cliente '{tenant}' não encontrada no banco de dados." + logger.logIC2(realm=RealmType.OPERATIONAL,msg=(step_status, message, OpStatus.ERROR)) + return False + except Exception as e: + message = f"Erro ao obter configuração do cliente: {e}" + logger.logIC2(realm=RealmType.OPERATIONAL,msg=(step_status, message, OpStatus.ERROR)) + return False + + logger.logIC2(realm=RealmType.OPERATIONAL,msg=(step_status,"MAPPING DOS DADOS",OpStatus.ACTIVE)) + try: + payload = nfe._mapper_pedido_tray_to_nfe(pedido, config_tenant) + except Exception as e: + message = f"Erro ao mapear dados: {e}" + logger.logIC2(realm=RealmType.OPERATIONAL,msg=(step_status, message, OpStatus.ERROR)) + return False + + logger.logIC2(realm=RealmType.OPERATIONAL,msg=(step_status,"BUSCANDO CREDENCIAIS DO INTEGRADOR",OpStatus.ACTIVE)) + try: + integrador = banco.get_credenciais_servicos_integradores(integrador="nfe") + if not integrador: + message = f"Credenciais para integrador 'nfe' não encontradas no banco de dados." + logger.logIC2(realm=RealmType.OPERATIONAL,msg=(step_status, message, OpStatus.ERROR)) + return False + except Exception as e: + message = f"Erro ao obter credenciais do integrador: {e}" + logger.logIC2(realm=RealmType.OPERATIONAL,msg=(step_status, message, OpStatus.ERROR)) + return False + + logger.logIC2(realm=RealmType.OPERATIONAL,msg=(step_status,"ENVIANDO DADOS PARA FILA DE NFE (QUEUE)",OpStatus.ACTIVE)) + try: + queue = SendQueueService( + system = integrador.system, + service = integrador.service, + amqps = integrador.amqps, + origin = origin, + serviceid = integrador.serviceid, + servicekey = integrador.servicekey + ) + queue.send_queue(payload=payload.model_dump()) + logger.logIC2(realm=RealmType.OPERATIONAL,msg=(step_status, "Dados enviados para fila de NFe", OpStatus.OK)) + return True + except Exception as e: + message = f"Erro ao enviar dados para fila de NFe: {e}" + logger.logIC2(realm=RealmType.OPERATIONAL,msg=(step_status, message, OpStatus.ERROR)) + return False + +def atualizar_nfe_tray(id_pedido_tray: str, xml: str, banco: Banco) -> bool: + step_status = "NFe - ATUALIZACAO TRAY" + logger.logIC2(realm=RealmType.OPERATIONAL,msg=(step_status,"Localizando dados no XML",OpStatus.ACTIVE)) + try: + ns = {"nfe": "http://www.portalfiscal.inf.br/nfe"} + root = ET.fromstring(xml) + + chave = root.find(".//nfe:chNFe", ns) + data_emissao = root.find(".//nfe:dhEmi", ns) + serie = root.find(".//nfe:serie", ns) + value = root.find(".//nfe:vNF", ns) + numero = root.find(".//nfe:nNF", ns) + + chave = chave.text if chave is not None else None + data_emissao = data_emissao.text if data_emissao is not None else None + serie = serie.text if serie is not None else None + value = value.text if value is not None else None + numero = numero.text if numero is not None else None + + if not all([chave, data_emissao, serie, value, numero]): + message = "Dados necessários não encontrados no XML" + logger.logIC2(realm=RealmType.OPERATIONAL,msg=(step_status, message, OpStatus.ERROR)) + return False + itens = root.findall(".//nfe:det", ns) + produtos = [] + for item in itens: + cProd = item.find(".//nfe:cProd", ns) + xProd = item.find(".//nfe:xProd", ns) + cfop = item.find(".//nfe:CFOP", ns) + produtos.append({ + "product_id" : cProd.text if cProd is not None else None, + "variation_id": xProd.text if xProd is not None else None, + "cfop" : cfop.text if cfop is not None else None + }) + + payload = { + "issue_date" : data_emissao[:10], + "number" : numero, + "serie" : serie, + "value" : value, + "key" : chave, + "link" : None, + "xml_danfe" : xml, + "ProductCfop": produtos + } + except Exception as e: + message = f"Erro ao extrair dados do XML da NFe: {e}" + logger.logIC2(realm=RealmType.OPERATIONAL,msg=(step_status, message, OpStatus.ERROR)) + return False + + step_status = "NFe - ATUALIZACAO TRAY" + logger.logIC2(realm=RealmType.OPERATIONAL,msg=(step_status,"Enviando dados para atualização do pedido no Tray",OpStatus.ACTIVE)) + try: + api_tray = ApiTray(system_config=system_config, redis_client=rd) + api_tray.inserir_nfe_pedido(id_pedido=id_pedido_tray, nfe=payload) + logger.logIC2(realm=RealmType.OPERATIONAL,msg=(step_status, "Dados enviados para atualização do pedido no Tray", OpStatus.OK)) + # return True + except Exception as e: + message = f"Erro ao enviar dados para atualização do pedido no Tray: {e}" + logger.logIC2(realm=RealmType.OPERATIONAL,msg=(step_status, message, OpStatus.ERROR)) + # return False + + step_status = "ATUALIZANDO NFe NO BANCO" + logger.logIC2(realm=RealmType.OPERATIONAL,msg=(step_status,"INICIADO",OpStatus.ACTIVE)) + try: + banco.update_pedido_nfe(id_pedido_tray = id_pedido_tray, status="Faturado", nfe_gerada = True,nfe_numero=numero,nfe_serie=serie) + logger.logIC2(realm=RealmType.OPERATIONAL,msg=(step_status, "NFe atualizada no banco de dados", OpStatus.OK)) + return True + except Exception as e: + message = f"Erro ao atualizar NFe no banco de dados: {e}" + logger.logIC2(realm=RealmType.OPERATIONAL,msg=(step_status, message, OpStatus.ERROR)) + return False + +############################################################## < Fim logica > ############################################################## + +def sendStatus(logger: LoggerExchange2): + global TaskCount, stats + TaskCount += 1 + stats = metrics.get_metrics() + cpu_p, cores, load_1, load_5, load_15, m_total, m_used, m_free, m_pct, net_up, net_down, net_total, disk_usage = stats + logger.logIC2(realm = RealmType.STATUS, msg = (TaskCount, cpu_p, m_pct, disk_usage, net_total)) + +def tratar_recebimento_webhook_tray(recebimento: Recebimento): + logger.logMsg(f"Tratando recebimento do webhook Tray: {recebimento}") + if recebimento.data.scope_name != "order": + logger.logMsg(f"Escopo desconhecido: {recebimento.data.scope_name}") + return + + logger.ic2_builder.dados.task_id = recebimento.data.scope_id + + dados_tray = buscar_dados_tray(recebimento.data.scope_id) + if not dados_tray: + logger.logMsgError("Erro ao buscar dados do pedido no Tray. Verifique os logs para mais detalhes.") + return + + step_status = "INICIANDO BANCO DE DADOS" + logger.logIC2(realm=RealmType.OPERATIONAL,msg=(step_status, "Iniciando conexão com o banco de dados", OpStatus.ACTIVE)) + try: + banco = Banco(database = system_config.db_name, + user = system_config.db_user, + password = system_config.db_password, + host = system_config.db_host, + port = system_config.db_port) + logger.logIC2(realm=RealmType.OPERATIONAL,msg=(step_status, "Banco de dados inicializado com sucesso", OpStatus.OK)) + except Exception as e: + message = f"Erro ao conectar ao banco de dados: {e}" + logger.logIC2(realm=RealmType.OPERATIONAL,msg=(step_status, message, OpStatus.ERROR)) + return + + step_status = "INSERINDO PEDIDO NO BANCO CASO NÃO EXISTA" + logger.logIC2(realm=RealmType.OPERATIONAL,msg=(step_status, "Iniciando inserção do pedido no banco de dados caso não exista", OpStatus.ACTIVE)) + try: + banco.upsert_pedido(id_pedido_tray=recebimento.data.scope_id, status=recebimento.data.act, tenant=tenant) + logger.logIC2(realm=RealmType.OPERATIONAL,msg=(step_status, "Pedido inserido no banco de dados com sucesso", OpStatus.OK)) + except Exception as e: + message = f"Erro ao inserir pedido no banco de dados: {e}" + logger.logIC2(realm=RealmType.OPERATIONAL,msg=(step_status, message, OpStatus.ERROR)) + return + + match recebimento.data.act: + case "insert": + cliente_id = criar_cliente(dados_tray, banco) + if not cliente_id: + return + step_status = "ATUALIZANDO CLIENTE NO PEDIDO (DB)" + logger.logIC2(RealmType.OPERATIONAL,msg=(step_status, "Iniciando atualização do cliente no pedido", OpStatus.ACTIVE)) + try: + banco.update_pedido_cliente(id_pedido_tray=recebimento.data.scope_id, codigo_cliente=cliente_id) + logger.logIC2(RealmType.OPERATIONAL,msg=(step_status, "Cliente atualizado no pedido com sucesso", OpStatus.OK)) + except Exception as e: + message = f"Erro ao atualizar cliente no pedido: {e}" + logger.logIC2(RealmType.OPERATIONAL,msg=(step_status, message, OpStatus.ERROR)) + return + + case "update": + if dados_tray.Order.status.upper() != "A ENVIAR": + logger.logMsgError(f"Status do pedido diferente do esperado {dados_tray.Order.status.upper()}") + + nfe = criar_nfe(dados_tray, banco) + if not nfe: + return + + step_status = "ATUALIZANDO ENVIADO PLUGNOTAS NO PEDIDO (DB)" + logger.logIC2(RealmType.OPERATIONAL,msg=(step_status, "Atualizando pedido no banco", OpStatus.ACTIVE)) + try: + banco.update_pedido_enviado_plugnotas(id_pedido_tray=recebimento.data.scope_id, enviado_plugnotas=True) + logger.logIC2(RealmType.OPERATIONAL,msg=(step_status, "enviado_plugnotas atualizado no banco", OpStatus.OK)) + except Exception as e: + message = f"Erro ao atualizar enviado_plugnotas no pedido: {e}" + logger.logIC2(RealmType.OPERATIONAL,msg=(step_status, message, OpStatus.ERROR)) + return + case _: + logger.logMsg(f"Ação desconhecida: {recebimento.data.act}") + return + + time.sleep(0.5) + return True + +def tratar_webhook_plugnotas(recebimento: RecebimentoPlugnotas): + + print(f"WebHook recebido do PlugNotas: {recebimento}", flush=True) + if recebimento.status.upper() != "CONCLUIDO": + logger.logMsgError(f"Status do webhook do PlugNotas diferente do esperado: {recebimento.status}") + return + + id_pedido_tray = recebimento.idIntegracao.split("_")[1] + + dados_tray = buscar_dados_tray(id_pedido_tray) + if not dados_tray: + logger.logMsgError(f"Erro ao buscar dados do pedido no Tray: {id_pedido_tray}") + return + + step_status = "INICIANDO BANCO DE DADOS" + logger.logIC2(realm=RealmType.OPERATIONAL,msg=(step_status, "Iniciando conexão com o banco de dados", OpStatus.ACTIVE)) + try: + banco = Banco(database = system_config.db_name, + user = system_config.db_user, + password = system_config.db_password, + host = system_config.db_host, + port = system_config.db_port) + logger.logIC2(realm=RealmType.OPERATIONAL,msg=(step_status, "Banco de dados inicializado com sucesso", OpStatus.OK)) + except Exception as e: + message = f"Erro ao conectar ao banco de dados: {e}" + logger.logIC2(realm=RealmType.OPERATIONAL,msg=(step_status, message, OpStatus.ERROR)) + return + + step_status = "BUSCANDO CHAVE DE ACESSO DA NFE" + logger.logIC2(realm=RealmType.OPERATIONAL,msg=(step_status, "Buscando chave de acesso da NFe no banco de dados", OpStatus.ACTIVE)) + try: + xml = base64.b64decode(recebimento.xml_base64).decode() + ns = {"nfe": "http://www.portalfiscal.inf.br/nfe"} + root = ET.fromstring(xml) + chave_nfe = root.find(".//nfe:chNFe", ns) + numero = root.find(".//nfe:nNF", ns) + if chave_nfe is not None and numero is not None: + chave_nfe = chave_nfe.text + numero_nfe = numero.text + logger.logIC2(realm=RealmType.OPERATIONAL,msg=(step_status, f"Chave de acesso da NFe encontrada: {chave_nfe}", OpStatus.ACTIVE)) + else: + message = "Chave de acesso da NFe não encontrada no XML" + logger.logIC2(realm=RealmType.OPERATIONAL,msg=(step_status, message, OpStatus.ERROR)) + return + except Exception as e: + message = f"Erro ao buscar chave de acesso da NFe: {e}" + logger.logIC2(realm=RealmType.OPERATIONAL,msg=(step_status, message, OpStatus.ERROR)) + return + + titulo = criar_titulo(dados_tray, banco, chave_nfe, numero_nfe) + if not titulo: + return + + step_status = "ATUALIZANDO TITULO NO PEDIDO NO BANCO" + logger.logIC2(realm=RealmType.OPERATIONAL,msg=(step_status, "Iniciando atualização do titulo no pedido no banco", OpStatus.ACTIVE)) + try: + banco.update_pedido_titulo(id_pedido_tray=id_pedido_tray, titulo_gerado=titulo) + logger.logIC2(realm=RealmType.OPERATIONAL,msg=(step_status, "Titulo atualizado no pedido no banco com sucesso", OpStatus.OK)) + except Exception as e: + print(f"Erro ao atualizar titulo no pedido no banco: {e}", flush=True) + message = f"Erro ao atualizar titulo no pedido no banco: {e}" + logger.logIC2(realm=RealmType.OPERATIONAL,msg=(step_status, message, OpStatus.ERROR)) + return + + step_status = "ATUALIZANDO NFE NO TRAY" + logger.logIC2(realm=RealmType.OPERATIONAL,msg=(step_status, "Iniciando atualização da NFe no Tray", OpStatus.ACTIVE)) + if not atualizar_nfe_tray(id_pedido_tray, xml): + logger.logMsgError("Erro ao atualizar NFe no Tray. Verifique os logs para mais detalhes.") + return + logger.logIC2(realm=RealmType.OPERATIONAL,msg=(step_status, "NFe atualizada no Tray com sucesso", OpStatus.OK)) + + + +def callback(ch, method, properties, body): + # Controlador de mensagens do RabbitMQ: decodifica a mensagem e inicia o processo de clonagem. + global logger + sendStatus(logger) + logger.logMsg(f"Mensagem recebida: {body}") + # Confirma o recebimento da mensagem (ACK) para remover da fila + ch.basic_ack(delivery_tag=method.delivery_tag) + + dado = json.loads(body) + hmac = dado['hmac'] + payload = dado['payload'] + + if not hmac or not payload: + logger.logMsg("HMAC ou Payload inválido") + return + + exchange.setCrypto(serviceid=system_config.serviceid, servicekey=system_config.servicekey) + + if not exchange.getPayload(hmac, payload): + logger.logMsg("Falha ao descifrar payload") + return + + payload_dict = json.loads(exchange.payload) + if not payload_dict: + logger.logMsg("Payload inválido") + return + + # Verifica qual modelo de recebimento é compativel - + PayloadModel = Recebimento | RecebimentoPlugnotas + adapter = TypeAdapter(PayloadModel) + + try: + logger.logMsg(f"Validando payload recebido: {payload_dict}") + recebimento = adapter.validate_python(payload_dict) + if isinstance(recebimento, Recebimento): + logger.logMsg("Payload validado como Recebimento do webhook Tray") + tratar_recebimento_webhook_tray(recebimento) + elif isinstance(recebimento, RecebimentoPlugnotas): + logger.logMsg("Payload validado como Recebimento do webhook PlugNotas") + tratar_webhook_plugnotas(recebimento) + else: + logger.logMsgError("Modelo de recebimento fora do padrão") + return + except Exception as e: + logger.logMsgError(f"Erro ao converter recebimento \n {recebimento} \n {e}") + return + + + +# --- Lógica de Inicialização do Script --- + +# CONFIGURAÇÃO SERVIÇO REDIS +try: + rd = redis.Redis(host='redis', port=6379, decode_responses=True) + key = f"{system}-system_config-{tenant}" + if not rd.exists(key): + msg = f"Key '{key}' not found in Redis." + print(msg, flush=True) + time.sleep(5) + sys.exit(0) +except Exception as e: + print(f"Error connecting to Redis: A {e}", flush=True) + time.sleep(5) + sys.exit(0) +# VALIDA JSON +try: + configRaw = rd.get(key) + configJS = json.loads(configRaw) +except JSONDecodeError as e: + print(f"Error connecting to Redis: B {e}", flush=True) + time.sleep(5) + sys.exit(0) +except Exception as e: + print(f"Error REDIS GET KEY {key}: {e}", flush=True) + time.sleep(5) + sys.exit(0) +# VALIDA QUALIDADE DO JSON +try: + system_config = SystemConfig(**configJS) +except ValidationError as e: + print(f"Error connecting to Redis: C {e}", flush=True) + time.sleep(5) + sys.exit(0) + +# Estruturas de dados para logging e telemetria +dados = Dados( + service_date = dt.now().strftime("%Y-%m-%d %H:%M:%S"), # INICIO DO TRABALHO - FICA FIXO ATÉ FINAL DO TRABALHO + startup_date = startup_date, + task_id = "00000000", #secrets.token_hex(8).upper(), + service = service, + versionstr = versionstr, + origin = origin, + enterprise_id = system_config.enterprise_id, + enterprise_name = system_config.enterprise_name, + system = system +) +logger = LoggerExchange2( + ic2_builder = AppIC2Builder(dados), + amqps = system_config.ic2_url, + system = system_config.system, + service = service, + version = versionstr, + origin = origin, + client = tenant, + logType = "logs", + logLocal = True +) +try: + exchange = QueueExchange2( + amqps = system_config.amqps, + origin = origin, + system = system, + service = service, + version = versionstr, + # client = tenant, + callback= callback) +except Exception as e: + print(f"Erro conectando a fila: {e}", flush=True) + time.sleep(5) + sys.exit(0) + +# Inicialização operacional +logger.logMsg(f"{system.upper()} - SERVIÇO DE RESERVAS v{version}") + + +# Inicia o consumo perpétuo da fila +exchange.start_consuming() diff --git a/src/create_tables.py b/src/create_tables.py new file mode 100644 index 0000000..3c18a5c --- /dev/null +++ b/src/create_tables.py @@ -0,0 +1,5 @@ +from db.banco import Banco + +db = Banco(database="innovar_amr_tray_controller",port=5432, user="softgo", password="[PASSWORD]", host="[IP_ADDRESS]") + +db.create_tables() \ No newline at end of file diff --git a/src/db/banco.py b/src/db/banco.py new file mode 100644 index 0000000..aaff754 --- /dev/null +++ b/src/db/banco.py @@ -0,0 +1,284 @@ +from datetime import datetime, timezone +from sqlalchemy import Column, DateTime, Integer, Numeric, String, Text, Boolean, create_engine +from sqlalchemy.exc import SQLAlchemyError +from sqlalchemy.orm import declarative_base, sessionmaker + +Base = declarative_base() + +class CredenciaisServicosIntegradores(Base): + __tablename__ = 'credenciais_servicos_integradores' + __table_args__ = {'schema': 'controller'} + + id_credenciais_servicos_integradores = Column(Integer, primary_key=True) + integrador = Column(String, nullable=False) + amqps = Column(String, nullable=False) + service = Column(String, nullable=False) + system = Column(String, nullable=False) + serviceid = Column(String, nullable=False) + servicekey = Column(String, nullable=False) + +class ConfigTenant(Base): + __tablename__ = 'config_clientes' + __table_args__ = {'schema': 'controller'} + + id_config_cliente = Column(Integer, primary_key=True) + tenant = Column(String, nullable=False) + cpf_cnpj = Column(String, nullable=False) + empresa_integracao = Column(String, nullable=False) + indexador_integracao = Column(String, nullable=False) + inscricao_estadual = Column(String) + usuario_sienge = Column(String, nullable=False) + token_sienge = Column(String, nullable=False) + oauth = Column(Boolean, nullable=False) + +class ApropriacaoFinanceira(Base): + __tablename__ = 'apropriacao_financeira' + __table_args__ = {'schema': 'controller'} + + id_apropriacao_financeira = Column(Integer, primary_key=True) + tenant = Column(String, nullable=False) + centro_custo = Column(Integer, nullable=False) + plano_financeiro = Column(String, nullable=False) + percentual_apropriacao = Column(Numeric, nullable=False) + +class Pedido(Base): + __tablename__ = 'pedido' + __table_args__ = {'schema': 'controller'} + + id_pedido_tray = Column(Integer, primary_key=True) + tenant = Column(String, nullable=False) + status = Column(String, nullable=False) + codigo_cliente = Column(String) + enviado_plugnotas = Column(Boolean, nullable=False) + nfe_gerada = Column(Boolean, nullable=False) + numero_nfe = Column(String) + serie_nfe = Column(String) + titulo_gerado = Column(Integer) + data_criacao = Column(DateTime) + data_atualizacao = Column(DateTime) + +class Banco: + def __init__(self, host, port, user, password, database): + try: + self.engine = create_engine( + f"postgresql+psycopg2://{user}:{password}@{host}:{port}/{database}", + echo = False, + future = True, + pool_pre_ping = True, + pool_recycle = 1800, + logging_name = "TrayController", + ) + self.Session = sessionmaker(bind=self.engine) + self.session = self.Session() + except Exception as e: + print(f"Erro ao conectar ao banco de dados: {e}") + raise e + except SQLAlchemyError as e: + print(f"Erro ao conectar ao banco de dados: {e}") + raise e + finally: + self.session.close() + + def create_tables(self): + try: + Base.metadata.create_all(self.engine) + except Exception as e: + print(f"Erro ao criar tabelas: {e}") + except SQLAlchemyError as e: + print(f"Erro ao criar tabelas: {e}") + finally: + self.session.close() + +# MÉTODOS PARA GERENCIAMENTO DE PEDIDOS - AQUI PODEMOS IMPLEMENTAR MÉTODOS PARA INSERIR, ATUALIZAR E CONSULTAR PEDIDOS NO BANCO DE DADOS + + def get_pedido(self, id_pedido_tray): + try: + pedido = self.session.query(Pedido).filter(Pedido.id_pedido_tray == id_pedido_tray).first() + return pedido + except Exception as e: + print(f"Erro ao buscar pedido: {e}") + finally: + self.session.close() + + def get_all_pedidos(self): + try: + pedidos = self.session.query(Pedido).all() + return pedidos + except Exception as e: + print(f"Erro ao buscar pedidos: {e}") + finally: + self.session.close() + + def upsert_pedido(self, id_pedido_tray, status, tenant): + try: + pedido = self.session.query(Pedido).filter(Pedido.id_pedido_tray == id_pedido_tray).first() + if not pedido: + pedido = Pedido( + id_pedido_tray = id_pedido_tray, + status = status, + tenant = tenant, + enviado_plugnotas = False, + nfe_gerada = False, + data_criacao = datetime.now(timezone.utc) + ) + self.session.add(pedido) + else: + pedido.status = status + pedido.data_atualizacao = datetime.now(timezone.utc) + self.session.commit() + except Exception as e: + print(f"Erro ao inserir pedido: {e}", flush=True) + finally: + self.session.close() + + def update_pedido_cliente(self, id_pedido_tray, codigo_cliente): + try: + pedido = self.session.query(Pedido).filter(Pedido.id_pedido_tray == id_pedido_tray).first() + if pedido: + print(f"Atualizando pedido {id_pedido_tray} com cliente {codigo_cliente}", flush=True) + pedido.codigo_cliente = str(codigo_cliente) + pedido.data_atualizacao = datetime.now(timezone.utc) + self.session.commit() + except Exception as e: + print(f"Erro ao atualizar pedido: {e}", flush=True) + finally: + self.session.close() + + def update_pedido_enviado_plugnotas(self, id_pedido_tray, enviado_plugnotas): + try: + pedido = self.session.query(Pedido).filter(Pedido.id_pedido_tray == id_pedido_tray).first() + if pedido: + pedido.enviado_plugnotas = enviado_plugnotas + pedido.data_atualizacao = datetime.now(timezone.utc) + self.session.commit() + except Exception as e: + print(f"Erro ao atualizar pedido: {e}", flush=True) + finally: + self.session.close() + + def update_pedido_nfe(self, id_pedido_tray, status, nfe_gerada, nfe_numero, nfe_serie): + try: + pedido = self.session.query(Pedido).filter(Pedido.id_pedido_tray == id_pedido_tray).first() + if pedido: + pedido.status = status + pedido.nfe_gerada = nfe_gerada + pedido.nfe_numero = nfe_numero + pedido.nfe_serie = nfe_serie + pedido.data_atualizacao = datetime.now(timezone.utc) + self.session.commit() + except Exception as e: + print(f"Erro ao atualizar pedido: {e}", flush=True ) + finally: + self.session.close() + + def update_pedido_titulo(self, id_pedido_tray, titulo_gerado): + try: + pedido = self.session.query(Pedido).filter(Pedido.id_pedido_tray == id_pedido_tray).first() + if pedido: + pedido.titulo_gerado = titulo_gerado + pedido.data_atualizacao = datetime.now(timezone.utc) + self.session.commit() + except Exception as e: + print(f"Erro ao atualizar pedido: {e}", flush=True) + finally: + self.session.close() + +# MÉTODOS PARA GERENCIAMENTO DE CONFIGURAÇÕES - AQUI PODEMOS IMPLEMENTAR MÉTODOS PARA INSERIR, ATUALIZAR E CONSULTAR CONFIGURAÇÕES DE CLIENTES NO BANCO DE DADOS + def get_config_cliente(self, tenant): + try: + config = self.session.query(ConfigTenant).filter(ConfigTenant.tenant == tenant).first() + return config + except Exception as e: + print(f"Erro ao buscar configuração do cliente: {e}") + finally: + self.session.close() + + def insert_config_cliente(self, tenant, cpf_cnpj, empresa_integracao, indexador_integracao, inscricao_estadual, usuario_sienge, token_sienge, oauth): + try: + config = ConfigTenant( + tenant = tenant, + cpf_cnpj = cpf_cnpj, + empresa_integracao = empresa_integracao, + indexador_integracao = indexador_integracao, + inscricao_estadual = inscricao_estadual, + usuario_sienge = usuario_sienge, + token_sienge = token_sienge, + oauth = oauth + ) + self.session.add(config) + self.session.commit() + except Exception as e: + print(f"Erro ao inserir configuração do cliente: {e}") + finally: + self.session.close() + + def update_config_cliente(self, tenant, cpf_cnpj, empresa_integracao, indexador_integracao, inscricao_estadual, usuario_sienge, token_sienge, oauth): + try: + config = self.session.query(ConfigTenant).filter(ConfigTenant.tenant == tenant).first() + if config: + config.cpf_cnpj = cpf_cnpj + config.empresa_integracao = empresa_integracao + config.indexador_integracao = indexador_integracao + config.inscricao_estadual = inscricao_estadual + config.usuario_sienge = usuario_sienge + config.token_sienge = token_sienge + config.oauth = oauth + self.session.commit() + except Exception as e: + print(f"Erro ao atualizar configuração do cliente: {e}") + finally: + self.session.close() + +# MÉTODOS PARA GERENCIAMENTO DE APROPRIAÇÕES FINANCEIRAS - AQUI PODEMOS IMPLEMENTAR MÉTODOS PARA INSERIR, ATUALIZAR E CONSULTAR APROPRIAÇÕES FINANCEIRAS NO BANCO DE DADOS + def get_lista_apropriacao_financeira(self, tenant): + try: + apropriacao = self.session.query(ApropriacaoFinanceira).filter( + ApropriacaoFinanceira.tenant == tenant + ).all() + return apropriacao + except Exception as e: + print(f"Erro ao buscar apropriação financeira: {e}") + finally: + self.session.close() + + def insert_apropriacao_financeira(self, tenant, centro_custo, plano_financeiro, percentual_apropriacao): + try: + apropriacao = ApropriacaoFinanceira( + tenant = tenant, + centro_custo = centro_custo, + plano_financeiro = plano_financeiro, + percentual_apropriacao = percentual_apropriacao + ) + self.session.add(apropriacao) + self.session.commit() + except Exception as e: + print(f"Erro ao inserir apropriação financeira: {e}") + finally: + self.session.close() + + def update_apropriacao_financeira(self, tenant, centro_custo, plano_financeiro, percentual_apropriacao): + try: + apropriacao = self.session.query(ApropriacaoFinanceira).filter( + ApropriacaoFinanceira.tenant == tenant, + ApropriacaoFinanceira.centro_custo == centro_custo + ).first() + if apropriacao: + apropriacao.plano_financeiro = plano_financeiro + apropriacao.percentual_apropriacao = percentual_apropriacao + self.session.commit() + except Exception as e: + print(f"Erro ao atualizar apropriação financeira: {e}") + finally: + self.session.close() + +# MÉTODOS PARA GERENCIAMENTO DE CREDENCIAIS - AQUI PODEMOS IMPLEMENTAR MÉTODOS PARA INSERIR, ATUALIZAR E CONSULTAR CREDENCIAIS DE SERVIÇOS INTEGRADORES NO BANCO DE DADOS + def get_credenciais_servicos_integradores(self, integrador): + try: + credenciais = self.session.query(CredenciaisServicosIntegradores).filter( + CredenciaisServicosIntegradores.integrador == integrador + ).first() + return credenciais + except Exception as e: + print(f"Erro ao buscar credenciais de serviços integradores: {e}") + finally: + self.session.close() \ No newline at end of file diff --git a/src/exchangeModels.py b/src/exchangeModels.py new file mode 120000 index 0000000..a677dc6 --- /dev/null +++ b/src/exchangeModels.py @@ -0,0 +1 @@ +../utils/exchangeModels.py \ No newline at end of file diff --git a/src/models/plugnotas/services_model.py b/src/models/plugnotas/services_model.py new file mode 100644 index 0000000..59f43b7 --- /dev/null +++ b/src/models/plugnotas/services_model.py @@ -0,0 +1,102 @@ +from pydantic import BaseModel + +class EnderecoNfe(BaseModel): + logradouro : str | None = None + numero : str | None = None + bairro : str | None = None + codigoCidade : str | None = None + tipoLogradouro : str | None = None + estado : str | None = None + cep : str | None = None + +class EmitenteNfe(BaseModel): + cpfCnpj : str | None = None + inscricaoEstadual: str | None = None + +class DestinatarioNfe(BaseModel): + cpfCnpj : str | None = None + razaoSocial: str | None = None + email : str | None = None + endereco : EnderecoNfe + +class ValorUnitarioNfe(BaseModel): + comercial : float | None = None + tributavel: float | None = None + +class BaseCalculoNfe(BaseModel): + modalidadeDeterminacao: int | None = None + valor : float | None = None + quantidade : float | None = None + +class IcmsNfe(BaseModel): + origem : str | None = None + cst : str | None = None + baseCalculo: BaseCalculoNfe + aliquota : float | None = None + valor : float | None = None + +class PisNfe(BaseModel): + cst : str | None = None + baseCalculo: BaseCalculoNfe + aliquota : float | None = None + valor : float | None = None + +class CofinsNfe(BaseModel): + cst : str | None = None + baseCalculo: BaseCalculoNfe + aliquota : float | None = None + valor : float | None = None + +class TributosNfe(BaseModel): + icms : IcmsNfe + pis : PisNfe + cofins: CofinsNfe + +class ItensNfe(BaseModel): + codigo : str | None = None + descricao : str | None = None + ncm : str | None = None + cest : str | None = None + cfop : str | None = None + valorUnitario: ValorUnitarioNfe + valor : float | None = None + tributos : TributosNfe + +class PagamentosNfe(BaseModel): + aVista: bool | None = None + meio : str | None = None + valor : float | None = None + +class TelefoneNfe(BaseModel): + ddd : str | None = None + numero: str | None = None + +class ResponsavelTecnicoNfe(BaseModel): + cpfCnpj : str | None = None + nome : str | None = None + email : str | None = None + telefone: TelefoneNfe | None = None + +class DadosNfe(BaseModel): + idIntegracao : str | None = None + presencial : bool | None = None + consumidorFinal : bool | None = None + natureza : str | None = None + emitente : EmitenteNfe + destinatario : DestinatarioNfe + itens : list[ItensNfe] + pagamentos : list[PagamentosNfe] + responsavelTecnico: ResponsavelTecnicoNfe | None = None + +class FilaRetorno(BaseModel): + system : str | None = None + service : str | None = None + serviceid : str | None = None + servicekey: str | None = None + +class EmitirNfe(BaseModel): + fila_retorno: FilaRetorno + fila : str | None = None + metodo : str | None = None + cliente : str | None = None + dados : DadosNfe \ No newline at end of file diff --git a/src/models/recebimento.py b/src/models/recebimento.py new file mode 100644 index 0000000..902eeeb --- /dev/null +++ b/src/models/recebimento.py @@ -0,0 +1,19 @@ +from pydantic import BaseModel +from models.sg_tray_models.services_model import TrayWebhookPayload + +class Metadata(BaseModel): + cliente : str + origem : str + timestamp: str + +class Recebimento(BaseModel): + metadata: Metadata + data : TrayWebhookPayload + +class RecebimentoPlugnotas(BaseModel): + id : str | None = None + status : str | None = None + documento : str | None = None + idIntegracao : str | None = None + emitente : str | None = None + xml_base64 : str | None = None \ No newline at end of file diff --git a/src/models/sienge/services_models.py b/src/models/sienge/services_models.py new file mode 100644 index 0000000..94943d9 --- /dev/null +++ b/src/models/sienge/services_models.py @@ -0,0 +1,117 @@ +from pydantic import BaseModel + +class Addresses(BaseModel): + type : str | None = None + streetName : str | None = None + number : str | None = None + complement : str | None = None + neighborhood : str | None = None + cityId : int | None = None + city : str | None = None + state : str | None = None + zipCode : str | None = None + +class Phones(BaseModel): + number : str | None = None + main : bool | None = None + type : str | None = None + note : str | None = None + idd : str | None = None + + +class AddressesSpouse(BaseModel): + city : str | None = None + complement : str | None = None + neighborhood : str | None = None + number : str | None = None + streetName : str | None = None + zipCode : str | None = None + +class Spouse(BaseModel) : + foreigner : str | None = None + internationalId : str | None = None + cpf : str | None = None + name : str | None = None + email : str | None = None + sex : str | None = None + civilStatus : str | None = None + birthDate : str | None = None + numberIdentityCard : str | None = None + issueDateIdentityCard: str | None = None + profession : str | None = None + nationality : str | None = None + birthPlace : str | None = None + fatherName : str | None = None + motherName : str | None = None + cellphoneNumber : str | None = None + businessPhone : str | None = None + company : str | None = None + addresses : AddressesSpouse | None = None + +class NaturalPersonData(BaseModel): + name : str | None = None + email : str | None = None + birthDate : str | None = None + birthPlace : str | None = None + civilStatus : str | None = None + cpf : str | None = None + mailingAddress : str | None = None + licenseNumber : str | None = None + licenseIssuingBody : str | None = None + licenseIssueDate : str | None = None + fatherName : str | None = None + sex : str | None = None + issueDateIdentityCard : str | None = None + matrimonialRegime : str | None = None + marriageDate : str | None = None + issuingBody : str | None = None + nationality : str | None = None + numberIdentityCard : str | None = None + motherName : str | None = None + profession : str | None = None + spouse : Spouse | None = None + +class Agents(BaseModel): + id : int | None = None + +class LegalPersonData(BaseModel): + name : str | None = None + email : str | None = None + cityRegistrationNumber : str | None = None + cnaeNumber : str | None = None + cnpj : str | None = None + contactName : str | None = None + creaNumber : str | None = None + establishmentDate : str | None = None + fantasyName : str | None = None + note : str | None = None + site : str | None = None + shareCapital : float | None = None + stateRegistrationNumber : str | None = None + technicalManager : str | None = None + agents : list[Agents] | None = None + +class FamilyIncome(BaseModel): + kinsName : str | None = None + kinship : str | None = None + incomeValue : float | None = None + observation : str | None = None + +class PostCustomerSienge(BaseModel): + personType : str | None = None + foreigner : str | None = None + typeId : int | None = None + internationalId : str | None = None + subtypeIds : list[int] | None = None + addresses : list[Addresses] | None = None + phones : list[Phones] | None = None + naturalPersonData : NaturalPersonData | None = None + legalPersonData : LegalPersonData | None= None + familyIncome : list[FamilyIncome] | None= None + +class Config(BaseModel): + subdominio: str | None = None + usuario : str + senha : str + tenant : str | None = None + cliente : str | None = None \ No newline at end of file diff --git a/src/send_queue.py b/src/send_queue.py new file mode 100644 index 0000000..bd8175a --- /dev/null +++ b/src/send_queue.py @@ -0,0 +1,26 @@ +import json + +from Exchange2 import QueueExchange2 + +version = "1.0.0" # Define the version of your service + +class SendQueueService: + def __init__(self,system: str, service: str, amqps: str, origin: str, serviceid: str, servicekey: str): + try: + self.service = service + self.exchange = QueueExchange2(amqps = amqps, + origin = origin, + system = system, + service = service, + version = version) + self.exchange.setCrypto(serviceid, servicekey) + except Exception as e: + raise e + + def send_queue(self, payload: dict): + try: + self.exchange.setPayload(json.dumps(payload)) + self.exchange.sendMsg(message=json.dumps({"queue": "OK"}), service=self.service) + return {"status": "success", "message": "Task sent successfully"} + except Exception as e: + return {"status": "error", "message": str(e)} \ No newline at end of file diff --git a/src/send_rpc.py b/src/send_rpc.py new file mode 100644 index 0000000..18ad615 --- /dev/null +++ b/src/send_rpc.py @@ -0,0 +1,37 @@ +import json + +from Exchange2 import RPCExchange2 + +version = "1.0.0" # Define the version of your service + +class SendRPCService: + def __init__(self, system: str, service: str, amqps: str, origin: str, serviceid: str, servicekey: str): + try: + self.max_timeout = 15 + self.exchange = RPCExchange2(amqps = amqps, + origin = origin, + system = system, + service = service, + version = version) + print(f"Conectando ao RabbitMQ em {amqps}...", flush=True) + print(f"system: {system}, service: {service}, origin: {origin}", flush=True) + self.exchange.setCrypto(serviceid, servicekey) + except Exception as e: + raise e + + def send_rpc(self, payload: dict): + try: + self.exchange.setPayload(json.dumps(payload)) + response = self.exchange.callRPC(message=json.dumps({"task": "OK"}), timeout=self.max_timeout) + return json.loads(response) + except Exception as e: + raise e + + def get_payload(self, hmac, payload): + try: + if self.exchange.getPayload(hmac, payload): + return json.loads(self.exchange.payload) + else: + raise ValueError("Falha ao descifrar payload") + except Exception as e: + raise e \ No newline at end of file diff --git a/src/services/cliente.py b/src/services/cliente.py new file mode 100644 index 0000000..5fe9fa0 --- /dev/null +++ b/src/services/cliente.py @@ -0,0 +1,85 @@ +from models.sienge.services_models import * +from models.sg_tray_models.services_model import TrayOrderRootResponse +from appIC2Builder import SystemConfig +from api.tray import ApiTray +from db.banco import ConfigTenant + +from pydantic import ValidationError + +import redis + +class ClienteService: + def __init__(self, config : SystemConfig, redis_client : redis.Redis): + self.config = config + self.api_tray = ApiTray(redis_client=redis_client, system_config=config) + + def _mapper_tray_to_sienge_cliente(self, pedido : TrayOrderRootResponse, config_tenant: ConfigTenant) -> PostCustomerSienge: + + enderecos : list[Addresses] = [] + try: + for endereco in pedido.Order.Customer.CustomerAddresses: + enderecos.append(Addresses( + cityId = None, + city = endereco.CustomerAddress.city, + state = endereco.CustomerAddress.state, + neighborhood = endereco.CustomerAddress.neighborhood, + number = endereco.CustomerAddress.number, + complement = endereco.CustomerAddress.complement, + streetName = endereco.CustomerAddress.address, + zipCode = endereco.CustomerAddress.zip_code, + type = 'C' if endereco.CustomerAddress.description == "Comercial" else 'R' + )) + except ValidationError as e: + print(f"Erro ao mapear endereços: {e}",flush=True) + raise e + + try: + telefones = [Phones( + type = "CE", + number = pedido.Order.Customer.cellphone, + main = True + )] + except ValidationError as e: + print(f"Erro ao mapear telefones: {e}",flush=True) + raise e + + try: + if pedido.Order.Customer.cpf is not None and pedido.Order.Customer.cpf != "": + cliente = PostCustomerSienge( + addresses = enderecos, + phones = telefones, + naturalPersonData = NaturalPersonData( + name = pedido.Order.Customer.name, + cpf = pedido.Order.Customer.cpf, + email = pedido.Order.Customer.email, + birthDate = pedido.Order.Customer.birth_date, + sex = 'M' if pedido.Order.Customer.gender == "0" else 'F', + numberIdentityCard=pedido.Order.Customer.rg + ) + ) + elif pedido.Order.Customer.cnpj is not None and pedido.Order.Customer.cnpj != "": + cliente = PostCustomerSienge( + addresses = enderecos, + phones = telefones, + legalPersonData = LegalPersonData( + name = pedido.Order.Customer.company_name, + email = pedido.Order.Customer.email, + cnpj = pedido.Order.Customer.cnpj, + stateRegistrationNumber = pedido.Order.Customer.state_inscription + ) + ) + else: + raise ValueError("Cliente sem CPF ou CNPJ") + return { + "config": { + "subdominio": "s8psasistemas", #config_tenant.tenant if not config_tenant.oauth else None, + "usuario" : config_tenant.usuario_sienge, + "senha" : config_tenant.token_sienge, + "tenant" : config_tenant.tenant if config_tenant.oauth else None, + "cliente" : config_tenant.tenant + }, + "cliente": cliente.model_dump() + } + except ValidationError as e: + print(f"Erro ao mapear cliente: {e}",flush=True) + raise e \ No newline at end of file diff --git a/src/services/nfe.py b/src/services/nfe.py new file mode 100644 index 0000000..8d4b115 --- /dev/null +++ b/src/services/nfe.py @@ -0,0 +1,198 @@ +from models.plugnotas.services_model import DadosNfe +from models.sg_tray_models.services_model import TrayOrderRootResponse +from models.plugnotas.services_model import * +from appIC2Builder import SystemConfig + +from db.banco import ConfigTenant +from api.tray import ApiTray +from api.publicas import busca_cep + +import redis + +class NfeService: + def __init__(self, config : SystemConfig, redis : redis.Redis): + self.config = config + self.api_tray = ApiTray(redis_client=redis, system_config=config) + self.meios_pagamento_plugnotas = { + "01" : "Dinheiro", + "02" : "Cheque", + "03" : "Cartão de Crédito", + "04" : "Cartão de Débito", + "05" : "Cartão da Loja (Private Label), Crediário Digital, Outros Crediários", + "10" : "Vale Alimentação", + "11" : "Vale Refeição", + "12" : "Vale Presente", + "13" : "Vale Combustível", + "14" : "Duplicata Mercantil", + "15" : "Boleto Bancário", + "16" : "Depósito Bancário", + "17" : "Pagamento Instantâneo (PIX) - Dinâmico", + "18" : "TED (Transferência Eletrônica Disponível)", + "19" : "Programa de fidelidade, Cashback, Crédito Virtual", + "20" : "Pagamento Instantâneo (PIX) - Estático", + "21" : "Crédito em Loja", + "22" : "Pagamento Eletrônico não Informado - falha de hardware do sistema emissor", + "23" : "Pagamento Instantâneo (PIX) - Automático", + "24" : "TEF - (Book Transfer)", + "90" : "Sem pagamento", + "91" : "Pagamento Posterior", + "99" : "Outros" + } + + def localiza_tipo_logradouro(self, logradouro : str) -> str: + match logradouro.split(" ")[0].upper(): + case "AVENIDA": + return "Avenida" + case "ALAMEDA": + return "Alameda" + case "CHACARA": + return "Chácara" + case "COLONIA": + return "Colônia" + case "CONDOMINIO": + return "Condomínio" + case "EQNP": + return "Eqnp" + case "ESTANCIA": + return "Estância" + case "ESTRADA": + return "Estrada" + case "FAZENDA": + return "Fazenda" + case "PRACA": + return "Praça" + case "PROLONGAMENTO": + return "Prolongamento" + case "RODOVIA": + return "Rodovia" + case "RUA": + return "Rua" + case "SITIO": + return "Sítio" + case "TRAVESSA": + return "Travessa" + case "VICINAL": + return "Vicinal" + case _: + return "Rua" + + def localiza_meio_pagamento(self, pagamento : str) -> str: + for key, value in self.meios_pagamento_plugnotas.items(): + if pagamento.upper() in value.upper(): + return key + return "99" + + def _mapper_pedido_tray_to_nfe(self, pedido_tray : TrayOrderRootResponse, config_tenant : ConfigTenant) -> EmitirNfe: + try: + emitente_nfe = EmitenteNfe( + cpfCnpj = config_tenant.cpf_cnpj, + inscricaoEstadual = config_tenant.inscricao_estadual + ) + + cep_info = busca_cep(pedido_tray.Order.Customer.zip_code) + + if 'erro' in cep_info: + raise Exception(f"Erro ao buscar CEP: {cep_info['erro']}") + + endereco_destinatario = EnderecoNfe( + bairro = cep_info['bairro'], + cep = cep_info['cep'], + codigoCidade = cep_info['ibge'], + estado = cep_info['uf'], + logradouro = cep_info['logradouro'], + numero = pedido_tray.Order.Customer.number, + tipoLogradouro = self.localiza_tipo_logradouro(cep_info['logradouro']) + ) + + destinatario_nfe = DestinatarioNfe( + cpfCnpj = pedido_tray.Order.Customer.cpf if pedido_tray.Order.Customer.cpf else pedido_tray.Order.Customer.cnpj, + razaoSocial = pedido_tray.Order.Customer.name, + email = pedido_tray.Order.Customer.email, + endereco = endereco_destinatario + ) + itens_nfe = list[ItensNfe]() + + for item in pedido_tray.Order.ProductsSold: + item_nfe = ItensNfe( + codigo = item.ProductsSold.product_id, + descricao = item.ProductsSold.name, + ncm = item.ProductsSold.ncm, + cfop = "", # Implementar + valorUnitario = ValorUnitarioNfe( + comercial = item.ProductsSold.price, + tributavel = item.ProductsSold.price + ), + valor = item.ProductsSold.price, + tributos = TributosNfe( + icms=IcmsNfe( + origem = "0", # Implementar + cst = "00", # Implementar + baseCalculo=BaseCalculoNfe( + modalidadeDeterminacao = 0, + valor = 0, + quantidade = 0 + ), + aliquota = 0, + valor = 0 + ), + pis=PisNfe( + cst="99", # Implementar + baseCalculo=BaseCalculoNfe( + modalidadeDeterminacao = 0, + valor = 0, + quantidade = 0 + ), + aliquota = 0, + valor = 0 + ), + cofins=CofinsNfe( + cst="07", # Implementar + baseCalculo=BaseCalculoNfe( + modalidadeDeterminacao = 0, + valor = 0, + quantidade = 0 + ), + aliquota = 0, + valor = 0 + ) + ) + ) + itens_nfe.append(item_nfe) + + pagamentos_nfe = list[PagamentosNfe]() + + for pagamento in pedido_tray.Order.Payment: + pagamento_nfe = PagamentosNfe( + meio = self.localiza_meio_pagamento(pagamento.Payment.method), + valor = pagamento.Payment.value + ) + pagamentos_nfe.append(pagamento_nfe) + + dados_nfe = DadosNfe( + idIntegracao = f"PE_{pedido_tray.Order.id}", + emitente = emitente_nfe, + destinatario = destinatario_nfe, + itens = itens_nfe, + pagamentos = pagamentos_nfe + ) + + fila_retorno = FilaRetorno( + service = self.config.service, + serviceid = self.config.serviceid, + servicekey = self.config.servicekey, + system = self.config.system + ) + + + nfe = EmitirNfe( + fila_retorno = fila_retorno, + fila = "nfe", + metodo = "post", + cliente = self.config.client, + dados = dados_nfe + ) + return nfe + except Exception as e: + print(e ,flush=True) + raise Exception(f"Erro ao mapear pedido: {e}") + \ No newline at end of file diff --git a/src/services/titulo_receber.py b/src/services/titulo_receber.py new file mode 100644 index 0000000..88a3244 --- /dev/null +++ b/src/services/titulo_receber.py @@ -0,0 +1,51 @@ +from models.servico_titulos_receber.contracts_Tittles import ReceivableBillPayload, SiengeCredentials +from models.servico_titulos_receber.tittle_model import PostAccountsReceivableReceivableBills, FinancialAppropriations +from models.sg_tray_models.services_model import TrayOrderRootResponse +from appIC2Builder import SystemConfig +from db.banco import ConfigTenant, ApropriacaoFinanceira, Pedido + +class TituloReceberService: + def __init__(self, config: SystemConfig): + self.config = config + + def map_tray_order_to_bill_data(self, tray_order : TrayOrderRootResponse, config_tenant: ConfigTenant, apropriacao: list[ApropriacaoFinanceira], pedido: Pedido, chave_nfe: str, numero_nfe: str) -> ReceivableBillPayload: + + credenciais = SiengeCredentials( + subdomain = "s8psasistemas", #config_tenant.tenant, + username = config_tenant.usuario_sienge, + password = config_tenant.token_sienge + ) + + apropriacoes = [FinancialAppropriations( + costCenterId = item.centro_custo, + paymentCategoryId = item.plano_financeiro, + percentage = item.percentual_apropriacao, + amount = tray_order.Order.total + ) for item in apropriacao] + + + bill_data = PostAccountsReceivableReceivableBills( + companyId = config_tenant.empresa_integracao, + customerId = pedido.codigo_cliente, + document = "NF", + documentNumber = numero_nfe, + issueDate = tray_order.Order.date, + competenceDate = tray_order.Order.date, + totalInvoiceAmount = tray_order.Order.total, + installmentsNumber = 1,#tray_order.Order.installment, + accountingDate = tray_order.Order.date, + indexerId = config_tenant.indexador_integracao, + baseDate = tray_order.Order.date, + dueDate = tray_order.Order.date, + notes = f"Titulo referente ao pedido {tray_order.Order.id} da plataforma Tray. Chave de acesso da NF-e: {chave_nfe}", + financialAppropriations = apropriacoes, + installments = None + ) + + integrador = ReceivableBillPayload( + tenant=self.config.client, + bill_data=bill_data, + credentials=credenciais + ) + + return integrador \ No newline at end of file diff --git a/src/teste.py b/src/teste.py new file mode 100644 index 0000000..76ecfc1 --- /dev/null +++ b/src/teste.py @@ -0,0 +1,201 @@ +from models.sienge.services_models import Config +from models.plugnotas.services_model import * +# from api.tray import ApiTray +import json, redis +from models.sienge.services_models import * +from models.sg_tray_models.services_model import TrayOrderRootResponse +from send_rpc import SendRPCService + + +from pydantic import ValidationError + +# from api.publicas import busca_cep + +# rd = redis.Redis(host='localhost', port=6388, decode_responses=True) + +# with open("modelo_envio_plugnotas.json", "r") as f: +# payload = json.load(f) + +# emitir_nfe = EmitirNfe(**payload) + +# print(emitir_nfe.model_dump_json(indent=4)) + +# api_tray = ApiTray(url='https://1225878.commercesuite.com.br', +# consumer_key='62598c0d55e7febda429d69a134dad3ac1adadc895e697e62df0ae214df62790', +# consumer_secret='7502e1994fd19108c2fa73a08559a8c76abc6e96a067057848152c78ec38ec66', +# code='beee2e6c9af8820622a72830862ca0f2c04ebc3859eed19ea6cffa799b938da4', +# redis_client=rd, +# service='controller', +# system='integrador', +# refresh_token=None) +# print(api_tray.get_pedido_completo(id_pedido=16101).model_dump()) + +# ceps = [ +# "01001000", "01310100", "02020000", "03150000", "04567000", +# "05010000", "06050000", "07210000", "08021000", "09530000", +# "11035000", "12070000", "13083000", "14025000", "15015000", +# "16050000", "17012000", "18095000", "19061000", "19990000", +# "20040000", "20550000", "21040000", "22070002", "23050000", +# "24030000", "25010000", "26020000", "27035000", "28013000", +# "25555000", "26530000", "27280000", "27510000", "28155000", +# "28470000", "28625000", "28735000", "28890000", "28950000", +# "30110000", "30550000", "31030000", "32010000", "33025000", +# "34010000", "35020000", "36015000", "37010000", "38010000", +# "39010000", "34120000", "35501000", "36570000", "37540000", +# "38500000", "39400000", "39803000", "39950000", "39700000", +# "40015000", "41020000", "42010000", "43050000", "44015000", +# "45025000", "46010000", "47020000", "48015000", "48950000", +# "40170000", "41230000", "42500000", "43570000", "44580000", +# "45530000", "46570000", "47520000", "48550000", "48760000", +# "80010000", "80530000", "81050000", "82020000", "83005000", +# "84010000", "85012000", "86020000", "87013000", "88010000", +# "89010000", "80050100", "81560000", "83203000", "84266000", +# "85200000", "86200000", "87210000", "88220000", "87990000", +# "72800010", "74015000", "74120090", "74450000", "74810100", +# "75020000", "75503000", "75800000", "75901000", "76010000", +# "76300000", "73710000", "74230000", "74553000", "74934000", +# "75110000", "75690000", "76190000", "76400000", "76780000", +# "90010000", "91020000", "92010000", "93020000", "94010000", +# "95020000", "96015000", "97010000", "98020000", "99010000", +# "90550000", "91530000", "92510000", "93520000", "94560000", +# "95570000", "96508000", "97510000", "98560000", "99520000", +# "88010000", "88101000", "88220000", "88301000", "88400000", +# "88501000", "88600000", "88701000", "88805000", "88901000", +# "89010000", "89107000", "89201000", "89301000", "89460000", +# "89520000", "89600000", "89700000", "89801000", "89990000", +# "50030000", "51020000", "52010000", "53020000", "54010000", +# "55020000", "56010000", "56500000", "56750000", "56990000", +# "50710000", "51230000", "52250000", "53210000", "54280000", +# "55295000", "56200000", "56650000", "56890000", "56950000", +# "60020000", "60530000", "61010000", "62010000", "63020000", +# "60175000", "60630000", "61150000", "62110000", "63180000", +# "60250000", "60744000", "61200000", "62200000", "63260000", +# "60330000", "60860000", "61350000", "62320000", "63990000" +# ] + +# for cep in ceps: +# print(f"Buscando CEP: {cep}") +# print(busca_cep(cep)) +# print("-" * 50) + +# meios_pagamento_plugnotas = { +# "01" : "Dinheiro", +# "02" : "Cheque", +# "03" : "Cartão de Crédito", +# "04" : "Cartão de Débito", +# "05" : "Cartão da Loja (Private Label), Crediário Digital, Outros Crediários", +# "10" : "Vale Alimentação", +# "11" : "Vale Refeição", +# "12" : "Vale Presente", +# "13" : "Vale Combustível", +# "14" : "Duplicata Mercantil", +# "15" : "Boleto Bancário", +# "16" : "Depósito Bancário", +# "17" : "Pagamento Instantâneo (PIX) - Dinâmico", +# "18" : "TED (Transferência Eletrônica Disponível)", +# "19" : "Programa de fidelidade, Cashback, Crédito Virtual", +# "20" : "Pagamento Instantâneo (PIX) - Estático", +# "21" : "Crédito em Loja", +# "22" : "Pagamento Eletrônico não Informado - falha de hardware do sistema emissor", +# "23" : "Pagamento Instantâneo (PIX) - Automático", +# "24" : "TEF - (Book Transfer)", +# "90" : "Sem pagamento", +# "91" : "Pagamento Posterior", +# "99" : "Outros" +# } + + + +# def localiza_meio_pagamento(pagamento : str) -> str: +# for key, value in meios_pagamento_plugnotas.items(): +# if pagamento.upper() in value.upper(): +# return key +# return "99" + +# print(localiza_meio_pagamento("PIX")) + + +def _mapper_tray_to_sienge_cliente(pedido : TrayOrderRootResponse) -> PostCustomerSienge: + + enderecos : list[Addresses] = [] + try: + for endereco in pedido.Order.Customer.CustomerAddresses: + enderecos.append(Addresses( + cityId = None, + city = endereco.CustomerAddress.city, + state = endereco.CustomerAddress.state, + neighborhood = endereco.CustomerAddress.neighborhood, + number = endereco.CustomerAddress.number, + complement = endereco.CustomerAddress.complement, + streetName = endereco.CustomerAddress.address, + zipCode = endereco.CustomerAddress.zip_code, + type = 'R' + )) + except ValidationError as e: + print(f"Erro ao mapear endereços: {e}") + raise e + + try: + telefones = [Phones( + type = "CE", + number = pedido.Order.Customer.cellphone, + main = True + )] + except ValidationError as e: + print(f"Erro ao mapear telefones: {e}") + raise e + + try: + if pedido.Order.Customer.cpf is not None and pedido.Order.Customer.cpf != "": + cliente = PostCustomerSienge( + addresses = enderecos, + phones = telefones, + naturalPersonData = NaturalPersonData( + name = pedido.Order.Customer.name, + cpf = pedido.Order.Customer.cpf, + email = pedido.Order.Customer.email, + birthDate = pedido.Order.Customer.birth_date, + sex = 'M' if pedido.Order.Customer.gender == "0" else 'F', + numberIdentityCard=pedido.Order.Customer.rg + ) + ) + elif pedido.Order.Customer.cnpj is not None and pedido.Order.Customer.cnpj != "": + pass + else: + pass + return cliente + except ValidationError as e: + print(f"Erro ao mapear cliente: {e}") + raise e + +with open("pedido_completo.json", "r") as f: + payload = json.load(f) + +pedido = TrayOrderRootResponse(**payload) +cliente = _mapper_tray_to_sienge_cliente(pedido).model_dump() +config = Config( + subdominio = "s8psasistemas", + usuario = "s8psasistemas-midd", + senha = "fq3LdZzvTJq8u8oeASFhVkYV20NIac41", + tenant = None, + cliente = "s8psasistemas" +).model_dump() + +payload = { + "cliente" : cliente, + "config" : config +} + +with open("payload.json", "w") as f: + json.dump(payload, f, indent=4) + +exchange = SendRPCService( + system = "integrador_clientes", + service = "controller", + amqps = "amqps://integracao-vendas:R2E2xV4HHQYSjvlIOPeoA@tall-cyan-dog.rmq4.cloudamqp.com/integracao-vendas", + origin = "pc-balt", + serviceid = "98765432109876543210987654321098", + servicekey = "ZYXWVUTSRQPONMLKJIHGFEDCBA654321" +) + +print(exchange.send_rpc(payload)) \ No newline at end of file diff --git a/src/testeNfe.py b/src/testeNfe.py new file mode 100644 index 0000000..5313303 --- /dev/null +++ b/src/testeNfe.py @@ -0,0 +1,14 @@ +from send_queue import SendQueueService +import json + +with open("modelo_webhook_plugnotas.json", "r") as f: + payload = json.load(f) + +queue_service = SendQueueService(amqps = "amqps://integracao-vendas:R2E2xV4HHQYSjvlIOPeoA@tall-cyan-dog.rmq4.cloudamqp.com/integracao-vendas", + service = "controller", + system = "innovar_amr_tray", + origin = "pc_balt", + serviceid = "G7q7VkP587Kf", + servicekey = "EImD4TXc1PIi4V8jbkRapyhv9veqlg7Q") + +print(queue_service.send_queue(payload)) \ No newline at end of file diff --git a/webhook.json b/webhook.json new file mode 100644 index 0000000..1493f68 --- /dev/null +++ b/webhook.json @@ -0,0 +1,6 @@ +{ + "scope_name": "order", + "scope_id": "16527", + "seller_id": "1225878", + "act": "insert" +} \ No newline at end of file