PRIMEIRO COMMIT

This commit is contained in:
Jaummm777 2026-07-10 16:32:22 -03:00
commit ad749f1009
223 changed files with 34924 additions and 0 deletions

18
Models/config.py Normal file
View file

@ -0,0 +1,18 @@
from pydantic import BaseModel
# Dados de conexão ao banco de dados isolado do cliente e da API PlugNotas
class Config(BaseModel):
api_key: str | None = None
x_api_key: str | None = None
user_postgres: str
pass_postgres: str
host_postgres: str
port_postgres: str
database_postgres: str
# Informações de mensageria para a troca de arquivos (AMQP) via subagent
class SystemConfig(BaseModel):
serviceid: str
servicekey: str
origin: str
amqps: str

97
Models/empresa.py Normal file
View file

@ -0,0 +1,97 @@
from pydantic import BaseModel
###################
## POST /empresas #
###################
class Endereco(BaseModel):
codigoPais: str
descricaoPais: str
tipoLogradouro: str | None = None
logradouro: str
numero: str
complemento: str | None = None
tipoBairro: str | None = None
bairro: str
codigoCidade: str
descricaoCidade: str
estado: str
cep: str
class Rps(BaseModel):
lote: int
numeracao: list
class Email(BaseModel):
envio: bool
class Config(BaseModel):
rps: Rps | None = None
email: Email | None = None
producao: bool | None = None
envioLimitado: bool | None = None
integracoes: list | None = None
prefeitura: dict | str | int | None = None
class Nfse(BaseModel):
config: Config | None = None
ativo: bool = False
tipoContrato: int | None = 0
class Dfe(BaseModel):
primeiroNsu: bool
class ConfigNfe(BaseModel):
dfe: Dfe | None = None
email: Email | None = None
integracoes: dict | None = None
producao: bool | None = None
impressaoFcp: bool | None = None
impressaoPartilha: bool | None = None
versaoManual: str | None = None
versaoEsquema: str | None = None
numeracao: list | None = None
calculoAutomaticoIbpt: bool | None = True
class Nfe(BaseModel):
config: ConfigNfe | None = None
ativo: bool = False
tipoContrato: int | None = 0
class ConfigNfce(BaseModel):
email: Email | None = None
integracoes: dict | None = None
producao: bool | None = None
versaoManual: str | None = None
versaoEsquema: str | None = None
numeracao: list | None = None
class Nfce(BaseModel):
config: ConfigNfce | None = None
ativo: bool = False
tipoContrato: int | None = 0
# Entidade Empresa que cadastra a credencial emissora no sistema da PlugNotas
class Empresa(BaseModel):
endereco: Endereco
nfse: Nfse | None = None
nfe: Nfe | None = None
nfce: Nfce | None = None
incentivoFiscal: bool
incentivadorCultural: bool
cpfCnpj: str
inscricaoMunicipal: str
inscricaoEstadual: str | None = None
certificado: str
razaoSocial: str
nomeFantasia: str | None = None
simplesNacional: bool
regimeTributario: int
regimeTributarioEspecial: int
id: str | None = None
banco: str | None = None
email: str | None = None
id_empresa: int | None = None

115
Models/nfe.py Normal file
View file

@ -0,0 +1,115 @@
from pydantic import BaseModel
from typing import List, Optional, Literal
EstadoEnum = Literal[
"AC", "AL", "AM", "AP", "BA", "CE", "DF", "ES", "GO", "MA", "MG", "MS", "MT",
"PA", "PB", "PE", "PI", "PR", "RJ", "RN", "RO", "RR", "RS", "SC", "SE", "SP", "TO"
]
TipoLogradouroEnum = Literal[
"Alameda", "Avenida", "Chácara", "Colônia", "Condomínio", "Estância", "Estrada",
"Fazenda", "Praça", "Prolongamento", "Rodovia", "Rua", "Sítio", "Travessa", "Vicinal", "Eqnp"
]
class Emitente(BaseModel):
cpfCnpj: str
inscricaoEstadual: Optional[str] = None
class Endereco(BaseModel):
logradouro: str
numero: str
bairro: str
codigoCidade: str
estado: EstadoEnum
cep: str
tipoLogradouro: TipoLogradouroEnum
codigoPais: Optional[str] = "1058"
complemento: Optional[str] = None
descricaoCidade: Optional[str] = None
descricaoPais: Optional[str] = "Brasil"
class Destinatario(BaseModel):
cpfCnpj: str
razaoSocial: str
nomeFantasia: Optional[str] = None
email: Optional[str] = None
inscricaoEstadual: Optional[str] = None
inscricaoMunicipal: Optional[str] = None
inscricaoSuframa: Optional[str] = None
orgaoPublico: Optional[bool] = False
endereco: Endereco
class ValorTributavel(BaseModel):
comercial: float
tributavel: float
class BaseCalculoIcms(BaseModel):
modalidadeDeterminacao: int
valor: float
class Icms(BaseModel):
origem: str
cst: str
baseCalculo: BaseCalculoIcms
aliquota: float
valor: float
class BaseCalculoPis(BaseModel):
valor: float
quantidade: int
class Pis(BaseModel):
cst: str
baseCalculo: BaseCalculoPis
aliquota: float
valor: float
class BaseCalculoCofins(BaseModel):
valor: float
class Cofins(BaseModel):
cst: str
baseCalculo: BaseCalculoCofins
aliquota: float
valor: float
class Tributos(BaseModel):
icms: Optional[Icms] = None
pis: Optional[Pis] = None
cofins: Optional[Cofins] = None
class ItemNFe(BaseModel):
codigo: str
descricao: str
ncm: str
cest: Optional[str] = None
cfop: str
valorUnitario: ValorTributavel
valor: float
tributos: Tributos
class Pagamento(BaseModel):
aVista: Optional[bool] = True
meio: Optional[str] = "01"
valor: float
class Telefone(BaseModel):
ddd: str
numero: str
class ResponsavelTecnico(BaseModel):
cpfCnpj: str
nome: str
email: str
telefone: Telefone
class NFe(BaseModel):
idIntegracao: str
presencial: bool
consumidorFinal: bool
natureza: str
emitente: Emitente
destinatario: Destinatario
itens: List[ItemNFe]
pagamentos: List[Pagamento]
responsavelTecnico: Optional[ResponsavelTecnico] = None

49
Models/nfse.py Normal file
View file

@ -0,0 +1,49 @@
from pydantic import BaseModel
from typing import Optional
class Prestador(BaseModel):
cpfCnpj: str
class Endereco(BaseModel):
descricaoCidade: str
cep: str
tipoLogradouro: str
logradouro: str
tipoBairro: Optional[str] = None
codigoCidade: str
complemento: Optional[str] = None
estado: str
numero: str
bairro: str
class Tomador(BaseModel):
cpfCnpj: str
razaoSocial: str
inscricaoMunicipal: Optional[str] = None
email: str
endereco: dict
class ISS(BaseModel):
tipoTributacao: int
exigibilidade: int
aliquota: float
class Valor(BaseModel):
servico: float
descontoCondicionado: Optional[float] = 0
descontoIncondicionado: Optional[float] = 0
class Servico(BaseModel):
codigo: str
codigoTributacao: str
discriminacao: str
cnae: str
iss: ISS
valor: Valor
# Estrutura padrão de envio de uma NFSe contendo Prestador, Tomador e Serviço prestado
class NFSe(BaseModel):
idIntegracao: str
prestador: Prestador
tomador: Tomador
servico: Servico

518
Models/payload.py Normal file
View file

@ -0,0 +1,518 @@
from enum import Enum
from pydantic import BaseModel, ConfigDict, Field
from Models.config import Config
from Models.empresa import Empresa
from Models.nfe import NFe
from Models.nfse import NFSe
from Models.servico import Servico
from Models.tomador import Tomador
class Fila(str, Enum):
NFE = "nfe"
NFSE = "nfse"
CERTIFICADO_DIGITAL = "certificado_digital"
EMPRESA = "empresa"
SERVICO = "servico"
TOMADOR = "tomador"
RELATORIOS = "relatorios"
WEBHOOKS = "webhooks"
class NFeMetodo(str, Enum):
POST = "post"
GET = "get"
DOWNLOAD = "download"
DOWNLOAD_XML = "download_xml"
CANCELAR = "cancelar"
STATUS_CANCELAMENTO = "status_cancelamento"
CCE = "cce"
STATUS_CCE = "status_cce"
RESUMO = "resumo"
class NFSeMetodo(str, Enum):
POST = "post"
GET = "get"
DOWNLOAD = "download"
DOWNLOAD_XML = "download_xml"
CANCELAR = "cancelar"
STATUS_CANCELAMENTO = "status_cancelamento"
RESUMO = "resumo"
HISTORICO = "historico"
class CertificadoMetodo(str, Enum):
POST = "post"
GET = "get"
PUT = "put"
class EmpresaMetodo(str, Enum):
POST = "post"
GET = "get"
POST_FILE = "post_file"
PATCH = "patch"
class ServicoMetodo(str, Enum):
POST = "post"
GET = "get"
PUT = "put"
DELETE = "delete"
class TomadorMetodo(str, Enum):
POST = "post"
GET = "get"
PUT = "put"
DELETE = "delete"
class RelatorioMetodo(str, Enum):
NFE_EMITIDAS = "nfe_emitidas"
NFE_POR_CNPJ = "nfe_por_cnpj"
NFSE_EMITIDAS = "nfse_emitidas"
NFSE_POR_CNPJ = "nfse_por_cnpj"
class WebhookMetodo(str, Enum):
GET = "get"
POST = "post"
DELETE = "delete"
PUT = "put"
TEST = "test"
class BaseEnvelope(BaseModel):
cliente: str | None = None
config: Config
url_base: str | None = None
# --- Base Data Models ---
class BaseDados(BaseModel):
model_config = ConfigDict(populate_by_name=True)
id_usuario: int | None = 1
class NFePostDadosWrapped(BaseDados):
nfe: NFe
id_empresa: str | None = None
class NFePostDadosDirect(NFe):
id_usuario: int | None = 1
id_empresa: str | None = None
class NFeIdDados(BaseDados):
id: str
class NFeCancelarDados(NFeIdDados):
justificativa: str | None = None
class NFeCCEDados(NFeIdDados):
correcao: str
class NFeResumoDados(BaseDados):
cnpj: str
id_integracao: str | None = None
id: str | None = None
class NFSePostDados(NFSe):
id_usuario: int | None = 1
id_empresa: str | None = None
class NFSeIdDados(BaseDados):
id: str
class NFSeGetDados(BaseDados):
id: str | None = None
class NFSeResumoDados(NFSeIdDados):
cnpj: str
class NFSeHistoricoDados(NFSeIdDados):
cnpj: str | None = None
class CertificadoPostDados(BaseDados):
arquivo: str
senha: str
id_empresa: str | None = None
email: str | None = None
expiration_at: str | None = Field(None, alias="expirationAt")
data_expiracao: str | None = None
class CertificadoGetDados(BaseDados):
id: str | None = None
class CertificadoPutDados(CertificadoPostDados):
id: str
class EmpresaPostDados(Empresa):
id_usuario: int | None = 1
class EmpresaGetDados(BaseDados):
cnpj: str | None = None
class EmpresaPostFileDados(BaseDados):
file: str
cnpj: str
class EmpresaPatchDados(BaseDados):
cnpj: str
certificado: str
class ServicoPostDados(Servico):
id_usuario: int | None = 1
class ServicoGetDados(BaseDados):
id: str | None = None
class ServicoPutDados(Servico):
id: str
id_usuario: int | None = 1
class ServicoDeleteDados(BaseDados):
id: str
class TomadorPostDados(Tomador):
id_usuario: int | None = 1
class TomadorGetDados(BaseDados):
cpf_cnpj: str | None = None
class TomadorPutDados(Tomador):
cpf_cnpj: str
id_usuario: int | None = 1
class TomadorDeleteDados(BaseDados):
cpf_cnpj: str
class RelatorioVazioDados(BaseDados):
pass
class RelatorioCnpjDados(BaseDados):
cnpj: str
class RelatorioPeriodoDados(BaseDados):
data_inicial: str | None = None
data_final: str | None = None
class RelatorioCnpjPeriodoDados(RelatorioPeriodoDados):
cnpj: str
class WebhookGetDeleteTestDados(BaseDados):
cnpj: str
class WebhookPostPutDados(WebhookGetDeleteTestDados):
url: str
# --- Envelopes ---
class NFePostEnvelope(BaseEnvelope):
fila: Fila = Fila.NFE
metodo: NFeMetodo = NFeMetodo.POST
dados: NFePostDadosDirect | NFePostDadosWrapped
class NFeIdEnvelope(BaseEnvelope):
fila: Fila = Fila.NFE
metodo: NFeMetodo
dados: NFeIdDados
class NFeCancelarEnvelope(BaseEnvelope):
fila: Fila = Fila.NFE
metodo: NFeMetodo = NFeMetodo.CANCELAR
dados: NFeCancelarDados
class NFeCCEEnvelope(BaseEnvelope):
fila: Fila = Fila.NFE
metodo: NFeMetodo = NFeMetodo.CCE
dados: NFeCCEDados
class NFeResumoEnvelope(BaseEnvelope):
fila: Fila = Fila.NFE
metodo: NFeMetodo = NFeMetodo.RESUMO
dados: NFeResumoDados
class NFSePostEnvelope(BaseEnvelope):
fila: Fila = Fila.NFSE
metodo: NFSeMetodo = NFSeMetodo.POST
dados: NFSePostDados
class NFSeGetEnvelope(BaseEnvelope):
fila: Fila = Fila.NFSE
metodo: NFSeMetodo = NFSeMetodo.GET
dados: NFSeGetDados
class NFSeIdEnvelope(BaseEnvelope):
fila: Fila = Fila.NFSE
metodo: NFSeMetodo
dados: NFSeIdDados
class NFSeResumoEnvelope(BaseEnvelope):
fila: Fila = Fila.NFSE
metodo: NFSeMetodo = NFSeMetodo.RESUMO
dados: NFSeResumoDados
class NFSeHistoricoEnvelope(BaseEnvelope):
fila: Fila = Fila.NFSE
metodo: NFSeMetodo = NFSeMetodo.HISTORICO
dados: NFSeHistoricoDados
class CertificadoPostEnvelope(BaseEnvelope):
fila: Fila = Fila.CERTIFICADO_DIGITAL
metodo: CertificadoMetodo = CertificadoMetodo.POST
dados: CertificadoPostDados
class CertificadoGetEnvelope(BaseEnvelope):
fila: Fila = Fila.CERTIFICADO_DIGITAL
metodo: CertificadoMetodo = CertificadoMetodo.GET
dados: CertificadoGetDados
class CertificadoPutEnvelope(BaseEnvelope):
fila: Fila = Fila.CERTIFICADO_DIGITAL
metodo: CertificadoMetodo = CertificadoMetodo.PUT
dados: CertificadoPutDados
class EmpresaPostEnvelope(BaseEnvelope):
fila: Fila = Fila.EMPRESA
metodo: EmpresaMetodo = EmpresaMetodo.POST
dados: EmpresaPostDados
class EmpresaGetEnvelope(BaseEnvelope):
fila: Fila = Fila.EMPRESA
metodo: EmpresaMetodo = EmpresaMetodo.GET
dados: EmpresaGetDados
class EmpresaPostFileEnvelope(BaseEnvelope):
fila: Fila = Fila.EMPRESA
metodo: EmpresaMetodo = EmpresaMetodo.POST_FILE
dados: EmpresaPostFileDados
class EmpresaPatchEnvelope(BaseEnvelope):
fila: Fila = Fila.EMPRESA
metodo: EmpresaMetodo = EmpresaMetodo.PATCH
dados: EmpresaPatchDados
class ServicoPostEnvelope(BaseEnvelope):
fila: Fila = Fila.SERVICO
metodo: ServicoMetodo = ServicoMetodo.POST
dados: ServicoPostDados
class ServicoGetEnvelope(BaseEnvelope):
fila: Fila = Fila.SERVICO
metodo: ServicoMetodo = ServicoMetodo.GET
dados: ServicoGetDados
class ServicoPutEnvelope(BaseEnvelope):
fila: Fila = Fila.SERVICO
metodo: ServicoMetodo = ServicoMetodo.PUT
dados: ServicoPutDados
class ServicoDeleteEnvelope(BaseEnvelope):
fila: Fila = Fila.SERVICO
metodo: ServicoMetodo = ServicoMetodo.DELETE
dados: ServicoDeleteDados
class TomadorPostEnvelope(BaseEnvelope):
fila: Fila = Fila.TOMADOR
metodo: TomadorMetodo = TomadorMetodo.POST
dados: TomadorPostDados
class TomadorGetEnvelope(BaseEnvelope):
fila: Fila = Fila.TOMADOR
metodo: TomadorMetodo = TomadorMetodo.GET
dados: TomadorGetDados
class TomadorPutEnvelope(BaseEnvelope):
fila: Fila = Fila.TOMADOR
metodo: TomadorMetodo = TomadorMetodo.PUT
dados: TomadorPutDados
class TomadorDeleteEnvelope(BaseEnvelope):
fila: Fila = Fila.TOMADOR
metodo: TomadorMetodo = TomadorMetodo.DELETE
dados: TomadorDeleteDados
class RelatorioNFeEmitidasEnvelope(BaseEnvelope):
fila: Fila = Fila.RELATORIOS
metodo: RelatorioMetodo = RelatorioMetodo.NFE_EMITIDAS
dados: RelatorioVazioDados
class RelatorioNFeCnpjEnvelope(BaseEnvelope):
fila: Fila = Fila.RELATORIOS
metodo: RelatorioMetodo = RelatorioMetodo.NFE_POR_CNPJ
dados: RelatorioCnpjDados
class RelatorioNFSeEmitidasEnvelope(BaseEnvelope):
fila: Fila = Fila.RELATORIOS
metodo: RelatorioMetodo = RelatorioMetodo.NFSE_EMITIDAS
dados: RelatorioPeriodoDados
class RelatorioNFSeCnpjEnvelope(BaseEnvelope):
fila: Fila = Fila.RELATORIOS
metodo: RelatorioMetodo = RelatorioMetodo.NFSE_POR_CNPJ
dados: RelatorioCnpjPeriodoDados
class WebhookGetDeleteTestEnvelope(BaseEnvelope):
fila: Fila = Fila.WEBHOOKS
metodo: WebhookMetodo
dados: WebhookGetDeleteTestDados
class WebhookPostPutEnvelope(BaseEnvelope):
fila: Fila = Fila.WEBHOOKS
metodo: WebhookMetodo
dados: WebhookPostPutDados
# --- Generic Envelope for basic structural validation ---
class GenericEnvelope(BaseEnvelope):
"""Validates that minimum required fields exist before routing."""
fila: Fila
metodo: str
dados: dict | None = None
# --- Payload Validation Router ---
# Maps (fila, metodo) -> Envelope class
_ENVELOPE_MAP: dict[tuple[str, str], type[BaseEnvelope]] = {
# NFe
("nfe", "post"): NFePostEnvelope,
("nfe", "get"): NFeIdEnvelope,
("nfe", "download"): NFeIdEnvelope,
("nfe", "download_xml"): NFeIdEnvelope,
("nfe", "cancelar"): NFeCancelarEnvelope,
("nfe", "status_cancelamento"): NFeIdEnvelope,
("nfe", "cce"): NFeCCEEnvelope,
("nfe", "status_cce"): NFeIdEnvelope,
("nfe", "resumo"): NFeResumoEnvelope,
# NFSe
("nfse", "post"): NFSePostEnvelope,
("nfse", "get"): NFSeGetEnvelope,
("nfse", "download"): NFSeIdEnvelope,
("nfse", "download_xml"): NFSeIdEnvelope,
("nfse", "cancelar"): NFSeIdEnvelope,
("nfse", "status_cancelamento"): NFSeIdEnvelope,
("nfse", "resumo"): NFSeResumoEnvelope,
("nfse", "historico"): NFSeHistoricoEnvelope,
# Certificado Digital
("certificado_digital", "post"): CertificadoPostEnvelope,
("certificado_digital", "get"): CertificadoGetEnvelope,
("certificado_digital", "put"): CertificadoPutEnvelope,
# Empresa
("empresa", "post"): EmpresaPostEnvelope,
("empresa", "get"): EmpresaGetEnvelope,
("empresa", "post_file"): EmpresaPostFileEnvelope,
("empresa", "patch"): EmpresaPatchEnvelope,
# Serviço
("servico", "post"): ServicoPostEnvelope,
("servico", "get"): ServicoGetEnvelope,
("servico", "put"): ServicoPutEnvelope,
("servico", "delete"): ServicoDeleteEnvelope,
# Tomador
("tomador", "post"): TomadorPostEnvelope,
("tomador", "get"): TomadorGetEnvelope,
("tomador", "put"): TomadorPutEnvelope,
("tomador", "delete"): TomadorDeleteEnvelope,
# Relatórios
("relatorios", "nfe_emitidas"): RelatorioNFeEmitidasEnvelope,
("relatorios", "nfe_por_cnpj"): RelatorioNFeCnpjEnvelope,
("relatorios", "nfse_emitidas"): RelatorioNFSeEmitidasEnvelope,
("relatorios", "nfse_por_cnpj"): RelatorioNFSeCnpjEnvelope,
# Webhooks
("webhooks", "get"): WebhookGetDeleteTestEnvelope,
("webhooks", "post"): WebhookPostPutEnvelope,
("webhooks", "delete"): WebhookGetDeleteTestEnvelope,
("webhooks", "put"): WebhookPostPutEnvelope,
("webhooks", "test"): WebhookGetDeleteTestEnvelope,
}
def validate_payload(payload_dict: dict) -> BaseEnvelope:
# Step 1: Basic structural validation
generic = GenericEnvelope(**payload_dict)
fila = generic.fila.value
metodo = generic.metodo.lower()
# Step 2: Find specific envelope
key = (fila, metodo)
envelope_cls = _ENVELOPE_MAP.get(key)
if envelope_cls is None:
raise ValueError(f"Combinação fila/metodo desconhecida: {fila}/{metodo}")
# Step 3: Normalize metodo in the dict to match enum lowercase values
payload_dict['metodo'] = metodo
# Step 4: Full typed validation
return envelope_cls(**payload_dict)

150
Models/servico.py Normal file
View file

@ -0,0 +1,150 @@
from pydantic import BaseModel
from enum import IntEnum
class TipoTributacao(IntEnum):
NAO_DEFINIDO = 0
ISENTO_ISS = 1
IMUNE = 2
NAO_INCIDENCIA = 3
NAO_TRIBUTAVEL = 4
RETIDO = 5
TRIBUTAVEL_MUNICIPIO = 6
TRIBUTAVEL_FORA_MUNICIPIO = 7
TRIBUTAVEL_MUNICIPIO_TOMADOR = 8
class TipoExigibilidade(IntEnum):
EXIGIVEL = 1
NAO_INCIDENCIA = 2
ISENCAO = 3
EXPORTACAO = 4
IMUNIDADE = 5
SUSPENSO_ACAO_JUDICIAL = 6
SUSPENSO_ACAO_ADMINISTRATIVA = 7
class ISS(BaseModel):
tipoTributacao: int #TipoTributacao
exigibilidade: int #TipoExigibilidade
retido: bool | None = None # Se o ISS foi retido na fonte
aliquota: float # Aliquota do ISS
valor: float # Valor do ISS
valorRetido: float # Valor do ISS retido
processoSuspensao: str # Numero processo de suspensão do ISS
##############################################################################
# tipoTributacao integer
# Default: 6
# Enum: 0 1 2 3 4 5 6 7 8
# Tipo de Tributação do Serviço, valores:
# 0 - Não definido
# 1 - Isento de ISS
# 2 - Imune
# 3 - Não Incidência no Município
# 4 - Não Tributável
# 5 - Retido
# 6 - Tributável Dentro do Município
# 7 - Tributável Fora do Município
# 8 - Tributável Dentro do Município pelo tomador
##############################################################################
# exigibilidade integer
# Default: 1
# Enum: 1 2 3 4 5 6 7
# Exigibilidade do ISS, valores:
# 1 - Exigível
# 2 - Não Incidência
# 3 - Isenção
# 4 - Exportação
# 5 - Imunidade
# 6 - Supenso por Ação Judicial
# 7 - Suspenso por Ação Administrativa
##############################################################################
# responsavelRetencao string
# Valores aceitos:
# 1 - Tomador
# 2 - Intermediário
##############################################################################
class Obra(BaseModel):
art: str # Anotação de Responsabilidade Técnica (Codigo ART)
codigo: str # Código da obra
class Valor(BaseModel):
servico: float # Valor bruto do serviço prestado.
baseCalculo: float # Valor da base de cálculo dos impostos.
deducoes: float # Valor deduções.
descontoCondicionado: float # Valor do desconto condicionado.
descontoIncondicionado: float # Valor do desconto incondicionado.
liquido: float # Valor liquido do serviço, caso você não informe esse campo, vamos calcular automáticamente.
unitario: float # Valor unitário do serviço.
valorAproximadoTributos: float # Valor aproximado dos tributos.
class Deducao(BaseModel):
tipo: int
descricao: str # Descrição da dedução
##############################################################################
# tipo integer
# Tipo de dedução:
# 0 - Sem Deduções
# 1 - Materiais
# 2 - Subempreitada de Mão de Obra
# 3 - Serviços
# 4 - Produção Externa
# 5 - Alimentação e Bebidas
# 6 - Reembolso de Despesas
# 7 - Repasse Consorciado
# 8 - Repasse Plano de Saúde
# 99 - Outras Deduções
##############################################################################
class RetencaoInterno(BaseModel):
aliquota: float # Aliquota da retenção
valor: float # Valor da retenção
class Retencao(BaseModel):
pis: RetencaoInterno
cofins: RetencaoInterno
csll: RetencaoInterno
inss: RetencaoInterno
irrf: RetencaoInterno
outrasRetencoes: float # Valor de outras retenções
cpp: RetencaoInterno
class Aliquota(BaseModel):
municipal: float # Aliquota municipal
estadual: float # Aliquota estadual
federal: float # Aliquota federal
class Detalhado(BaseModel):
aliquota: Aliquota
class Simplificado(BaseModel):
aliquota: float # Aliquota geral de impostos
class IBPT(BaseModel):
simplificado: Simplificado
detalhado: Detalhado
# Representa o detalhamento do serviço prestado na NFSe, impostos incidentes e deduções
class Servico(BaseModel):
codigo: str # Código de serviço, também conhecido como Código Item Lista Serviço, geralmente segue a LC116, podendo váriar de acordo com a prefeitura.Você pode encontrar esse dado no portal da prefeitura, em uma nota emitida, ou junto ao contador.
idIntegracao: str # Código EXTERNO de integração do serviço. Utilizado SOMENTE quando se cadastra previamente um serviço.
discriminacao: str # Descrição do serviço prestado
codigoTributacao: str # Código tributação no Município
cnae: str # O código CNAE ou, por extenso, Classificação Nacional de Atividades Econômicas, é uma forma de padronizar, em todo o território nacional, os códigos de atividades econômicas e os critérios de enquadramento usados pelos mais diversos órgãos da administração tributária do Brasil.
codigoCidadeIncidencia: str # Código IBGE da cidade de incidência do ISS.
descricaoCidadeIncidencia: str # Descrição da cidade de incidência do ISS.
unidade: str # Unidade do serviço prestado.
quantidade: int # Quantidade dos serviços prestados.
iss: ISS
obra: Obra
valor: Valor
deducao: Deducao
retencao: Retencao
tributavel: bool # Indica se o serviço está sujeito a tributação.
ibpt: IBPT
responsavelRetencao: str
##############################################################################
# responsavelRetencao Valores aceitos:
# 1 - Tomador
# 2 - Intermediário
# Caso o responsável pela retenção seja o próprio prestador, não gerar o campo.
##############################################################################

34
Models/tomador.py Normal file
View file

@ -0,0 +1,34 @@
from pydantic import BaseModel
class Endereco(BaseModel):
tipoLogradouro: str
logradouro: str
numero: str
complemento: str
tipoBairro: str
bairro: str
codigoPais: str
descricaoPais: str
codigoCidade: str
descricaoCidade: str
estado: str
cep: str
class Telefone(BaseModel):
ddd: str
numero: str
# Representa o cliente (Tomador do Serviço) vinculado a uma nota fiscal de serviço
class Tomador(BaseModel):
cpfCnpj: str
inscricaoMunicipal: str
inscricaoEstadual: str
inscricaoSuframa: str
indicadorInscricaoEstadual: int
razaoSocial: str
nomeFantasia: str
endereco: Endereco
telefone: Telefone
email: str
orgaoPublico: bool

204
README.md Normal file
View file

@ -0,0 +1,204 @@
# Serviço de Integração PlugNotas da Tecnospeed
Serviço especializado e containerizado para gerenciar a emissão, controle e recebimento de informações sobre **Notas Fiscais (NFe e NFSe)** via API da PlugNotas (Tecnospeed). Com uma arquitetura orientada a eventos e filas (RabbitMQ), o sistema é altamente desacoplado, assíncrono e integrado a um banco de dados PostgreSQL para registro de todas as operações e relatórios de auditoria.
## 🚀 Principais Tecnologias e Arquitetura
- **Python & FastAPI**: Base de processamento, recebimento de Webhooks (via `main_listener.py`) e tipagem rigorosa utilizando Pydantic.
- **RabbitMQ (AMQP)**: Comunicação entre os módulos utilizando as ferramentas especializadas implementadas (como `QueueExchange2` e `RPCExchange2`), promovendo uma arquitetura event-driven livre de bloqueios síncronos.
- **PostgreSQL**: Utilizado com SQLAlchemy para logs, auditorias detalhadas das operações, cache das bases de Serviços, Tomadores, Empresas e relatórios operacionais.
- **Redis**: Sistema de gerenciamento e armazenamento de persistência de curadoria (como configurações dinâmicas de acesso e credenciais de forma ágil).
- **IC2 Logger**: Envio centralizado de todos os logs, fluxos de erros, métricas operacionais para um sistema `InfoConnector` usando RabbitMQ Topic.
## 📦 Estrutura de Módulos (Serviços)
O serviço central funciona consumindo a fila `tecnospeed` e encaminhando as requisições para os seus respectivos módulos especializados.
### 1. NFe (Nota Fiscal Eletrônica de Produto)
Módulo que envia e orquestra operações envolvendo notas fiscais de venda (produtos):
- Envio de emissões de NFe em unitário ou em lote.
- Consulta de resumos processados.
- Cancelamentos suportados por justificativas.
- Envio de Cartas de Correção Eletrônica (CCe).
- Download de relatórios, XMLs originais e PDFs de Danfe.
### 2. NFSe (Nota Fiscal de Serviços Eletrônica)
Voltado a comunicações com prefeituras para emissão de serviços, contém os fluxos de controle essenciais:
- Realiza emissão dos documentos pelo CNPJ do prestador.
- Controle e solicitação formal de cancelamento nas prefeituras.
- Verificação de status e download de histórico de RPS.
- **Tomadores**: Gerenciamento integrado dos clientes finais (Tomadores da NFSe).
- **Serviços**: Cadastro de descritivos padrão, itens e alíquotas com base na lei complementar municipal local (LC116).
### 3. Empresas e Certificados Digitais
Serviço de onboarding das novas contas emissoras junto à PlugNotas:
- **Empresas**: Vinculação do CNPJ emissor (incluindo endereços, configurações fiscais iniciais e logotipos de impressão).
- **Certificados Digitais**: Responsável pelo cadastro do certificado físico formato `.pfx` com envios do tipo `multipart/form-data`, renovação de senhas e update recorrente dos certificados A1.
### 4. Webhooks (Listeners Internos e Externos)
Gerenciador automatizado orientando o lado da PlugNotas para onde devolver as informações assincronamente:
- Criação, leitura, deleção e envio de testes para registro das URLs via API do Plugnotas.
- Um **Listener próprio do lado da integração** (`main_listener.py` + `listenerEndpoint.py`) absorve o payload recebido da PlugNotas e encaminha de volta ao fluxo RabbitMQ principal usando RPC — mantendo assim uma auditoria segura com log para todas as frentes (rejeições, aceites contábeis etc).
### 5. Relatórios
Serviço voltado a buscas gerenciais dentro do portal da Tecnospeed para contabilidades e analistas de sistemas:
- Extratos iterados limitados de NFe e NFSe emitidas ou registradas num dado range de período.
- Recortes centralizados e consolidações por um CNPJ específico.
## ⚙️ Como Funciona o Ciclo de Eventos (Workflows)
1. **Ação Disparada**: O sistema que integra de fora (ERP etc) publica uma mensagem JSON na fila RabbitMQ com os comandos e a carga de dados de Pydantic Models associada, incluindo as configurações do banco.
2. **Workers de Fila (`mainQUEUE.py`)**: As mensagens são absorvidas, descriptografadas, instanciam as credenciais adequadas e ativam a Controller/Service (ex: EmpresaController, RegistroNFeService).
3. **Persistência no Postgre (`utils/banco.py`)**: A operação feita junto as rotas do portal salva um rastro de log com o `status` ("SUCESSO", "ERRO"). Instalações da NFe e NFSe e Certificados refletem diretamente nesse PostgreSQL.
4. **Listener Asincrono de Retorno (`listenerEndpoint.py`)**: A rota em FastAPI escuta atualizações atrasadas (ex: webservices estaduais demorados) e sinaliza ativamente ao logger e à base da empresa o desfecho da nota.
## 🗂️ Estrutura de Diretórios
```
servico_plugnotas/
├── src/
│ ├── main.py # Worker principal (RPCExchange2)
│ ├── mainQUEUE.py # Worker alternativo (QueueExchange2 + IC2 Logger)
│ ├── main_listener.py # Entrypoint do container de Webhook Listener
│ ├── listenerEndpoint.py # Rotas FastAPI que recebem webhooks da PlugNotas
│ ├── test.py # Script de testes manuais
│ │
│ ├── NFe/
│ │ └── registroNFe.py # Service de emissão, cancelamento e download de NFe
│ ├── NFSe/
│ │ ├── nfse.py # Service de emissão e controle de NFSe
│ │ ├── Servico/
│ │ │ └── servico.py # Controller de cadastro de Serviços (LC116)
│ │ └── Tomador/
│ │ └── tomador.py # Controller de cadastro de Tomadores
│ ├── empresas/
│ │ └── empresa.py # Controller de cadastro de Empresas emissoras
│ ├── certificado_digital/
│ │ └── certificado_digital.py # Service de upload e gestão de certificados .pfx
│ ├── webhooks/
│ │ └── webhooks.py # CRUD de URLs de webhook por CNPJ
│ ├── Relatorios/
│ │ └── relatorios.py # Consultas de relatórios em lote
│ │
│ ├── Models/ # Modelos Pydantic que validam os payloads
│ │ ├── config.py # Config (banco) e SystemConfig (AMQP/Redis)
│ │ ├── empresa.py # Modelo de Empresa emissora
│ │ ├── nfe.py # Modelo de NFe (Emitente, Destinatário, Itens, Tributos)
│ │ ├── nfse.py # Modelo de NFSe (Prestador, Tomador, Serviço)
│ │ ├── servico.py # Modelo detalhado de Serviço (ISS, Retenções, IBPT)
│ │ └── tomador.py # Modelo de Tomador (cliente fiscal)
│ │
│ └── utils/ # Classes auxiliares compartilhadas
│ ├── api.py # Wrapper HTTP (requests) para a API PlugNotas
│ ├── banco.py # ORM SQLAlchemy (tabelas + CRUD + logs)
│ ├── redis.py # Cliente Redis para cache de configurações
│ ├── Exchange2.py # QueueExchange2, RPCExchange2, LoggerExchange2
│ ├── exchangeModels.py # Modelos Pydantic do InfoConnector (IC2)
│ └── Logger.py # LogService para logs via mensageria
├── Dockerfile # Imagem base Python 3.12-slim
├── docker-compose.yml # Orquestração dos containers
├── buildImages.sh # Script de build com clone de dependências
├── requirements.txt # Dependências Python
├── modelo_system_config.json # Exemplo de configuração do Redis
└── modelo_json_rpc.json # Exemplo de payload RPC
```
## 🐳 Infraestrutura Docker
O projeto é composto por **4 containers** orquestrados via `docker-compose.yml`:
| Container | Imagem | Porta | Função |
|----------------------|--------------------|---------|------------------------------------------------------------|
| `redis` | `redis` | interna | Armazena `system_config` (credenciais AMQP, chaves) |
| `postgres` | `postgres:15` | interna | Banco de dados de auditoria e registros de notas |
| `tecnospeed` | build local | — | Worker que consome fila RabbitMQ e processa operações |
| `webhook-listener` | build local | `8000` | FastAPI que recebe webhooks da PlugNotas via HTTP |
### Subindo o ambiente
```bash
# Build e execução completa
docker compose up -d --build
# Ou utilizando o script de build (clona dependências externas automaticamente)
bash buildImages.sh clone
```
### Variáveis de ambiente
| Variável | Container | Descrição | Default |
|-----------------|--------------------|-----------------------------------------|-------------|
| `TZ` | todos | Timezone da aplicação | `America/Sao_Paulo` |
| `REDIS_HOST` | tecnospeed, webhook-listener | Host do Redis | `localhost` |
| `WEBHOOK_PORT` | webhook-listener | Porta do servidor FastAPI | `8000` |
## ⚡ Configuração Inicial (Redis)
Antes de subir os workers, é necessário inserir a chave `system_config` no Redis com as credenciais de acesso ao RabbitMQ:
```json
{
"serviceid": "01234567890123456789012345678901",
"servicekey": "ABCDEFGHIJKLMNOPQRSTUVWXYZ123456",
"origin": "nome-da-maquina",
"amqps": "amqps://usuario:senha@host.rmq.cloudamqp.com/vhost"
}
```
Esse JSON é lido automaticamente na inicialização dos workers (`main.py`, `mainQUEUE.py`, `listenerEndpoint.py`).
## 📨 Exemplo de Payload RPC
As mensagens que chegam pela fila RabbitMQ seguem o seguinte formato (já descriptografado):
```json
{
"fila": "nfse",
"cliente": "NomeCliente",
"metodo": "POST",
"dados": { ... },
"config": {
"api_key": "sua-api-key-plugnotas",
"user_postgres": "postgres",
"pass_postgres": "postgres",
"host_postgres": "localhost",
"port_postgres": "5432",
"database_postgres": "tecnospeed"
}
}
```
- **`fila`**: Seleciona o módulo de destino (`nfe`, `nfse`, `empresa`, `certificado_digital`, `servico`, `tomador`, `webhooks`, `relatorios`, `webhook_callback`).
- **`metodo`**: Ação a executar (`post`, `get`, `put`, `delete`, `cancelar`, `download`, etc.).
- **`dados`**: Corpo da requisição conforme o Pydantic Model do módulo.
- **`config`**: Credenciais isoladas por cliente para a API e o banco PostgreSQL.
## 📦 Dependências
```
sqlalchemy # ORM para PostgreSQL
pydantic # Validação de modelos de dados
requests # Requisições HTTP à API PlugNotas
redis # Cliente Redis
pytz # Timezone São Paulo
pika # Cliente AMQP (RabbitMQ)
cryptography # Criptografia AES-256 dos payloads
psycopg2-binary # Driver PostgreSQL
fastapi # Framework HTTP para o Webhook Listener
uvicorn[standard] # Servidor ASGI
```
## 🔖 Versionamento
| Versão | Descrição |
|---------|--------------------------------------------------|
| `1.0.0` | Versão inicial do sistema |
| `1.0.1` | Implementação do ORM (SQLAlchemy) |
| `1.0.2` | Implementação de RPC via RabbitMQ |
| `1.0.3` | Configuração dinâmica via Redis |
| `2.0.0` | Suporte a NFe e refatoração da Main |
| `2.0.1` | Refatoração para RPCExchange2 / QueueExchange2 |
---
*Documento gerado com base no código operacional contido em `src/` do serviço (v2.0.1).*

View file

@ -0,0 +1,649 @@
{
"info": {
"name": "Serviço TecnoSpeed - Payloads do Worker",
"description": "Exemplos completos de todos os payloads esperados pelo `main.py` do Worker no RabbitMQ. ATENÇÃO: Essas requisições são modelos do JSON interno. Seu back-end deve encapsula-las com `hmac` e enviá-las ao RabbitMQ.",
"schema": "https://schema.getpostman.com/json/collection/v2.1.0/collection.json"
},
"item": [
{
"name": "NFe (Nota Fiscal Eletrônica)",
"item": [
{
"name": "1. Cadastrar NFe",
"request": {
"method": "POST",
"header": [
{
"key": "Content-Type",
"value": "application/json"
}
],
"body": {
"mode": "raw",
"raw": "{\n \"fila\": \"nfe\",\n \"cliente\": \"IdentificadorDoCliente\",\n \"metodo\": \"post\",\n \"url_base\": \"https://api.sandbox.plugnotas.com.br\",\n \"config\": {\n \"api_key\": \"SUA_API_KEY\",\n \"user_postgres\": \"postgres\",\n \"pass_postgres\": \"postgres\",\n \"host_postgres\": \"localhost\",\n \"port_postgres\": \"5432\",\n \"database_postgres\": \"seu_banco\"\n },\n \"dados\": {\n \"nfe\": {\n \"emitente\": {\n \"cpfCnpj\": \"12345678000123\"\n },\n \"destinatario\": {\n \"cpfCnpj\": \"98765432000198\",\n \"razaoSocial\": \"Cliente Destino LTDA\"\n },\n \"itens\": [\n {\n \"codigo\": \"123\",\n \"descricao\": \"Produto Teste\",\n \"valorUnitario\": 100.0,\n \"quantidade\": 1\n }\n ]\n }\n }\n}"
},
"description": "Payload interno para a fila **nfe** usando o método **post**.\n\n",
"url": {
"raw": "amqp://seu-rabbitmq/fila/nfe/post",
"protocol": "amqp",
"host": [
"seu-rabbitmq"
],
"path": [
"fila",
"nfe",
"post"
]
}
},
"response": []
},
{
"name": "2. Consultar Resumo NFe",
"request": {
"method": "POST",
"header": [
{
"key": "Content-Type",
"value": "application/json"
}
],
"body": {
"mode": "raw",
"raw": "{\n \"fila\": \"nfe\",\n \"cliente\": \"IdentificadorDoCliente\",\n \"metodo\": \"get\",\n \"url_base\": \"https://api.sandbox.plugnotas.com.br\",\n \"config\": {\n \"api_key\": \"SUA_API_KEY\",\n \"user_postgres\": \"postgres\",\n \"pass_postgres\": \"postgres\",\n \"host_postgres\": \"localhost\",\n \"port_postgres\": \"5432\",\n \"database_postgres\": \"seu_banco\"\n },\n \"dados\": {\n \"id\": \"ID_DA_NFE_AQUI\"\n }\n}"
},
"description": "Payload interno para a fila **nfe** usando o método **get**.\n\n",
"url": {
"raw": "amqp://seu-rabbitmq/fila/nfe/get",
"protocol": "amqp",
"host": [
"seu-rabbitmq"
],
"path": [
"fila",
"nfe",
"get"
]
}
},
"response": []
},
{
"name": "3. Baixar PDF NFe",
"request": {
"method": "POST",
"header": [
{
"key": "Content-Type",
"value": "application/json"
}
],
"body": {
"mode": "raw",
"raw": "{\n \"fila\": \"nfe\",\n \"cliente\": \"IdentificadorDoCliente\",\n \"metodo\": \"download\",\n \"url_base\": \"https://api.sandbox.plugnotas.com.br\",\n \"config\": {\n \"api_key\": \"SUA_API_KEY\",\n \"user_postgres\": \"postgres\",\n \"pass_postgres\": \"postgres\",\n \"host_postgres\": \"localhost\",\n \"port_postgres\": \"5432\",\n \"database_postgres\": \"seu_banco\"\n },\n \"dados\": {\n \"id\": \"ID_DA_NFE_AQUI\"\n }\n}"
},
"description": "Payload interno para a fila **nfe** usando o método **download**.\n\n",
"url": {
"raw": "amqp://seu-rabbitmq/fila/nfe/download",
"protocol": "amqp",
"host": [
"seu-rabbitmq"
],
"path": [
"fila",
"nfe",
"download"
]
}
},
"response": []
},
{
"name": "4. Baixar XML NFe",
"request": {
"method": "POST",
"header": [
{
"key": "Content-Type",
"value": "application/json"
}
],
"body": {
"mode": "raw",
"raw": "{\n \"fila\": \"nfe\",\n \"cliente\": \"IdentificadorDoCliente\",\n \"metodo\": \"download_xml\",\n \"url_base\": \"https://api.sandbox.plugnotas.com.br\",\n \"config\": {\n \"api_key\": \"SUA_API_KEY\",\n \"user_postgres\": \"postgres\",\n \"pass_postgres\": \"postgres\",\n \"host_postgres\": \"localhost\",\n \"port_postgres\": \"5432\",\n \"database_postgres\": \"seu_banco\"\n },\n \"dados\": {\n \"id\": \"ID_DA_NFE_AQUI\"\n }\n}"
},
"description": "Payload interno para a fila **nfe** usando o método **download_xml**.\n\n",
"url": {
"raw": "amqp://seu-rabbitmq/fila/nfe/download_xml",
"protocol": "amqp",
"host": [
"seu-rabbitmq"
],
"path": [
"fila",
"nfe",
"download_xml"
]
}
},
"response": []
},
{
"name": "5. Cancelar NFe",
"request": {
"method": "POST",
"header": [
{
"key": "Content-Type",
"value": "application/json"
}
],
"body": {
"mode": "raw",
"raw": "{\n \"fila\": \"nfe\",\n \"cliente\": \"IdentificadorDoCliente\",\n \"metodo\": \"cancelar\",\n \"url_base\": \"https://api.sandbox.plugnotas.com.br\",\n \"config\": {\n \"api_key\": \"SUA_API_KEY\",\n \"user_postgres\": \"postgres\",\n \"pass_postgres\": \"postgres\",\n \"host_postgres\": \"localhost\",\n \"port_postgres\": \"5432\",\n \"database_postgres\": \"seu_banco\"\n },\n \"dados\": {\n \"id\": \"ID_DA_NFE_AQUI\",\n \"justificativa\": \"Cancelamento a pedido do integrador\"\n }\n}"
},
"description": "Payload interno para a fila **nfe** usando o método **cancelar**.\n\n",
"url": {
"raw": "amqp://seu-rabbitmq/fila/nfe/cancelar",
"protocol": "amqp",
"host": [
"seu-rabbitmq"
],
"path": [
"fila",
"nfe",
"cancelar"
]
}
},
"response": []
}
]
},
{
"name": "NFSe (Nota Fiscal de Serviço)",
"item": [
{
"name": "1. Cadastrar NFSe",
"request": {
"method": "POST",
"header": [
{
"key": "Content-Type",
"value": "application/json"
}
],
"body": {
"mode": "raw",
"raw": "{\n \"fila\": \"nfse\",\n \"cliente\": \"IdentificadorDoCliente\",\n \"metodo\": \"post\",\n \"url_base\": \"https://api.sandbox.plugnotas.com.br\",\n \"config\": {\n \"api_key\": \"SUA_API_KEY\",\n \"user_postgres\": \"postgres\",\n \"pass_postgres\": \"postgres\",\n \"host_postgres\": \"localhost\",\n \"port_postgres\": \"5432\",\n \"database_postgres\": \"seu_banco\"\n },\n \"dados\": {\n \"idIntegracao\": \"INTEGRACAO-NFSE-101\",\n \"prestador\": {\n \"cpfCnpj\": \"12345678000123\"\n },\n \"tomador\": {\n \"cpfCnpj\": \"98765432000198\",\n \"razaoSocial\": \"Jo\\u00e3o Silva\",\n \"endereco\": {\n \"logradouro\": \"Rua Teste\",\n \"numero\": \"123\",\n \"codigoCidade\": \"4106902\"\n }\n },\n \"servico\": {\n \"codigo\": \"1.01\",\n \"valor\": 1500.5,\n \"discriminacao\": \"Servi\\u00e7os prestados de TI\"\n }\n }\n}"
},
"description": "Payload interno para a fila **nfse** usando o método **post**.\n\n",
"url": {
"raw": "amqp://seu-rabbitmq/fila/nfse/post",
"protocol": "amqp",
"host": [
"seu-rabbitmq"
],
"path": [
"fila",
"nfse",
"post"
]
}
},
"response": []
},
{
"name": "2. Consultar/Listar NFSe",
"request": {
"method": "POST",
"header": [
{
"key": "Content-Type",
"value": "application/json"
}
],
"body": {
"mode": "raw",
"raw": "{\n \"fila\": \"nfse\",\n \"cliente\": \"IdentificadorDoCliente\",\n \"metodo\": \"get\",\n \"url_base\": \"https://api.sandbox.plugnotas.com.br\",\n \"config\": {\n \"api_key\": \"SUA_API_KEY\",\n \"user_postgres\": \"postgres\",\n \"pass_postgres\": \"postgres\",\n \"host_postgres\": \"localhost\",\n \"port_postgres\": \"5432\",\n \"database_postgres\": \"seu_banco\"\n },\n \"dados\": {\n \"id\": \"ID_DA_NFSE_AQUI\"\n }\n}"
},
"description": "Payload interno para a fila **nfse** usando o método **get**.\n\n",
"url": {
"raw": "amqp://seu-rabbitmq/fila/nfse/get",
"protocol": "amqp",
"host": [
"seu-rabbitmq"
],
"path": [
"fila",
"nfse",
"get"
]
}
},
"response": []
},
{
"name": "3. Cancelar NFSe",
"request": {
"method": "POST",
"header": [
{
"key": "Content-Type",
"value": "application/json"
}
],
"body": {
"mode": "raw",
"raw": "{\n \"fila\": \"nfse\",\n \"cliente\": \"IdentificadorDoCliente\",\n \"metodo\": \"cancelar\",\n \"url_base\": \"https://api.sandbox.plugnotas.com.br\",\n \"config\": {\n \"api_key\": \"SUA_API_KEY\",\n \"user_postgres\": \"postgres\",\n \"pass_postgres\": \"postgres\",\n \"host_postgres\": \"localhost\",\n \"port_postgres\": \"5432\",\n \"database_postgres\": \"seu_banco\"\n },\n \"dados\": {\n \"id\": \"ID_DA_NFSE_AQUI\"\n }\n}"
},
"description": "Payload interno para a fila **nfse** usando o método **cancelar**.\n\n",
"url": {
"raw": "amqp://seu-rabbitmq/fila/nfse/cancelar",
"protocol": "amqp",
"host": [
"seu-rabbitmq"
],
"path": [
"fila",
"nfse",
"cancelar"
]
}
},
"response": []
}
]
},
{
"name": "Empresa",
"item": [
{
"name": "1. Cadastrar Empresa",
"request": {
"method": "POST",
"header": [
{
"key": "Content-Type",
"value": "application/json"
}
],
"body": {
"mode": "raw",
"raw": "{\n \"fila\": \"empresa\",\n \"cliente\": \"IdentificadorDoCliente\",\n \"metodo\": \"post\",\n \"url_base\": \"https://api.sandbox.plugnotas.com.br\",\n \"config\": {\n \"api_key\": \"SUA_API_KEY\",\n \"user_postgres\": \"postgres\",\n \"pass_postgres\": \"postgres\",\n \"host_postgres\": \"localhost\",\n \"port_postgres\": \"5432\",\n \"database_postgres\": \"seu_banco\"\n },\n \"dados\": {\n \"json\": {\n \"cpfCnpj\": \"12345678000123\",\n \"razaoSocial\": \"Nome da Sua Empresa LTDA\",\n \"nomeFantasia\": \"Nome Fantasia\",\n \"inscricaoMunicipal\": \"123456\",\n \"endereco\": {\n \"logradouro\": \"Rua Exemplo\",\n \"numero\": \"100\",\n \"codigoCidade\": \"4106902\",\n \"uf\": \"PR\",\n \"cep\": \"80000000\"\n }\n },\n \"info\": {\n \"ativo\": \"true\"\n }\n }\n}"
},
"description": "Payload interno para a fila **empresa** usando o método **post**.\n\n",
"url": {
"raw": "amqp://seu-rabbitmq/fila/empresa/post",
"protocol": "amqp",
"host": [
"seu-rabbitmq"
],
"path": [
"fila",
"empresa",
"post"
]
}
},
"response": []
},
{
"name": "2. Consultar Empresa",
"request": {
"method": "POST",
"header": [
{
"key": "Content-Type",
"value": "application/json"
}
],
"body": {
"mode": "raw",
"raw": "{\n \"fila\": \"empresa\",\n \"cliente\": \"IdentificadorDoCliente\",\n \"metodo\": \"get\",\n \"url_base\": \"https://api.sandbox.plugnotas.com.br\",\n \"config\": {\n \"api_key\": \"SUA_API_KEY\",\n \"user_postgres\": \"postgres\",\n \"pass_postgres\": \"postgres\",\n \"host_postgres\": \"localhost\",\n \"port_postgres\": \"5432\",\n \"database_postgres\": \"seu_banco\"\n },\n \"dados\": {\n \"cnpj\": \"12345678000123\"\n }\n}"
},
"description": "Payload interno para a fila **empresa** usando o método **get**.\n\n",
"url": {
"raw": "amqp://seu-rabbitmq/fila/empresa/get",
"protocol": "amqp",
"host": [
"seu-rabbitmq"
],
"path": [
"fila",
"empresa",
"get"
]
}
},
"response": []
}
]
},
{
"name": "Certificado Digital",
"item": [
{
"name": "1. Cadastrar Certificado",
"request": {
"method": "POST",
"header": [
{
"key": "Content-Type",
"value": "application/json"
}
],
"body": {
"mode": "raw",
"raw": "{\n \"fila\": \"certificado_digital\",\n \"cliente\": \"IdentificadorDoCliente\",\n \"metodo\": \"post\",\n \"url_base\": \"https://api.sandbox.plugnotas.com.br\",\n \"config\": {\n \"api_key\": \"SUA_API_KEY\",\n \"user_postgres\": \"postgres\",\n \"pass_postgres\": \"postgres\",\n \"host_postgres\": \"localhost\",\n \"port_postgres\": \"5432\",\n \"database_postgres\": \"seu_banco\"\n },\n \"dados\": {\n \"arquivo\": {\n \"filename\": \"certificado.pfx\",\n \"content\": \"BASE64_CONTEUDO_DO_ARQUIVO\"\n },\n \"senha\": \"SenhaDoCertificadoAqui\",\n \"data_expiracao\": \"2025-12-31\",\n \"id_empresa\": 105,\n \"id_usuario\": 10\n }\n}"
},
"description": "Payload interno para a fila **certificado_digital** usando o método **post**.\n\n",
"url": {
"raw": "amqp://seu-rabbitmq/fila/certificado_digital/post",
"protocol": "amqp",
"host": [
"seu-rabbitmq"
],
"path": [
"fila",
"certificado_digital",
"post"
]
}
},
"response": []
},
{
"name": "2. Consultar Certificado",
"request": {
"method": "POST",
"header": [
{
"key": "Content-Type",
"value": "application/json"
}
],
"body": {
"mode": "raw",
"raw": "{\n \"fila\": \"certificado_digital\",\n \"cliente\": \"IdentificadorDoCliente\",\n \"metodo\": \"get\",\n \"url_base\": \"https://api.sandbox.plugnotas.com.br\",\n \"config\": {\n \"api_key\": \"SUA_API_KEY\",\n \"user_postgres\": \"postgres\",\n \"pass_postgres\": \"postgres\",\n \"host_postgres\": \"localhost\",\n \"port_postgres\": \"5432\",\n \"database_postgres\": \"seu_banco\"\n },\n \"dados\": {\n \"id\": \"ID_DO_CERTIFICADO\"\n }\n}"
},
"description": "Payload interno para a fila **certificado_digital** usando o método **get**.\n\n",
"url": {
"raw": "amqp://seu-rabbitmq/fila/certificado_digital/get",
"protocol": "amqp",
"host": [
"seu-rabbitmq"
],
"path": [
"fila",
"certificado_digital",
"get"
]
}
},
"response": []
},
{
"name": "3. Atualizar Certificado",
"request": {
"method": "POST",
"header": [
{
"key": "Content-Type",
"value": "application/json"
}
],
"body": {
"mode": "raw",
"raw": "{\n \"fila\": \"certificado_digital\",\n \"cliente\": \"IdentificadorDoCliente\",\n \"metodo\": \"put\",\n \"url_base\": \"https://api.sandbox.plugnotas.com.br\",\n \"config\": {\n \"api_key\": \"SUA_API_KEY\",\n \"user_postgres\": \"postgres\",\n \"pass_postgres\": \"postgres\",\n \"host_postgres\": \"localhost\",\n \"port_postgres\": \"5432\",\n \"database_postgres\": \"seu_banco\"\n },\n \"dados\": {\n \"id\": \"ID_DO_CERTIFICADO\",\n \"arquivo\": {\n \"filename\": \"novo_certificado.pfx\",\n \"content\": \"NEW_BASE64_CONTEUDO_DO_ARQUIVO\"\n },\n \"senha\": \"NovaSenhaDoCertificadoAqui\",\n \"data_expiracao\": \"2026-12-31\",\n \"id_empresa\": 105,\n \"id_usuario\": 10\n }\n}"
},
"description": "Payload interno para a fila **certificado_digital** usando o método **put**.\n\n",
"url": {
"raw": "amqp://seu-rabbitmq/fila/certificado_digital/put",
"protocol": "amqp",
"host": [
"seu-rabbitmq"
],
"path": [
"fila",
"certificado_digital",
"put"
]
}
},
"response": []
}
]
},
{
"name": "Serviços",
"item": [
{
"name": "1. Cadastrar Serviço",
"request": {
"method": "POST",
"header": [
{
"key": "Content-Type",
"value": "application/json"
}
],
"body": {
"mode": "raw",
"raw": "{\n \"fila\": \"servico\",\n \"cliente\": \"IdentificadorDoCliente\",\n \"metodo\": \"post\",\n \"url_base\": \"https://api.sandbox.plugnotas.com.br\",\n \"config\": {\n \"api_key\": \"SUA_API_KEY\",\n \"user_postgres\": \"postgres\",\n \"pass_postgres\": \"postgres\",\n \"host_postgres\": \"localhost\",\n \"port_postgres\": \"5432\",\n \"database_postgres\": \"seu_banco\"\n },\n \"dados\": {\n \"json\": {\n \"codigo\": \"1.01\",\n \"idIntegracao\": \"SERV-01\",\n \"descricao\": \"An\\u00e1lise e desenvolvimento de sistemas\",\n \"aliquota\": 2.5\n },\n \"info\": {\n \"parametros_adicionais\": \"teste\"\n }\n }\n}"
},
"description": "Payload interno para a fila **servico** usando o método **post**.\n\n",
"url": {
"raw": "amqp://seu-rabbitmq/fila/servico/post",
"protocol": "amqp",
"host": [
"seu-rabbitmq"
],
"path": [
"fila",
"servico",
"post"
]
}
},
"response": []
},
{
"name": "2. Buscar Serviço",
"request": {
"method": "POST",
"header": [
{
"key": "Content-Type",
"value": "application/json"
}
],
"body": {
"mode": "raw",
"raw": "{\n \"fila\": \"servico\",\n \"cliente\": \"IdentificadorDoCliente\",\n \"metodo\": \"get\",\n \"url_base\": \"https://api.sandbox.plugnotas.com.br\",\n \"config\": {\n \"api_key\": \"SUA_API_KEY\",\n \"user_postgres\": \"postgres\",\n \"pass_postgres\": \"postgres\",\n \"host_postgres\": \"localhost\",\n \"port_postgres\": \"5432\",\n \"database_postgres\": \"seu_banco\"\n },\n \"dados\": {\n \"id\": \"ID_DO_SERVICO\"\n }\n}"
},
"description": "Payload interno para a fila **servico** usando o método **get**.\n\n",
"url": {
"raw": "amqp://seu-rabbitmq/fila/servico/get",
"protocol": "amqp",
"host": [
"seu-rabbitmq"
],
"path": [
"fila",
"servico",
"get"
]
}
},
"response": []
}
]
},
{
"name": "Tomador",
"item": [
{
"name": "1. Cadastrar Tomador",
"request": {
"method": "POST",
"header": [
{
"key": "Content-Type",
"value": "application/json"
}
],
"body": {
"mode": "raw",
"raw": "{\n \"fila\": \"tomador\",\n \"cliente\": \"IdentificadorDoCliente\",\n \"metodo\": \"post\",\n \"url_base\": \"https://api.sandbox.plugnotas.com.br\",\n \"config\": {\n \"api_key\": \"SUA_API_KEY\",\n \"user_postgres\": \"postgres\",\n \"pass_postgres\": \"postgres\",\n \"host_postgres\": \"localhost\",\n \"port_postgres\": \"5432\",\n \"database_postgres\": \"seu_banco\"\n },\n \"dados\": {\n \"cpf_cnpj\": \"00000000000\",\n \"razaoSocial\": \"Tomador Teste SA\",\n \"email\": \"contato@teste.com\",\n \"endereco\": {\n \"logradouro\": \"Rua teste\",\n \"codigoCidade\": \"1234567\"\n }\n }\n}"
},
"description": "Payload interno para a fila **tomador** usando o método **post**.\n\n",
"url": {
"raw": "amqp://seu-rabbitmq/fila/tomador/post",
"protocol": "amqp",
"host": [
"seu-rabbitmq"
],
"path": [
"fila",
"tomador",
"post"
]
}
},
"response": []
},
{
"name": "2. Buscar Tomador",
"request": {
"method": "POST",
"header": [
{
"key": "Content-Type",
"value": "application/json"
}
],
"body": {
"mode": "raw",
"raw": "{\n \"fila\": \"tomador\",\n \"cliente\": \"IdentificadorDoCliente\",\n \"metodo\": \"get\",\n \"url_base\": \"https://api.sandbox.plugnotas.com.br\",\n \"config\": {\n \"api_key\": \"SUA_API_KEY\",\n \"user_postgres\": \"postgres\",\n \"pass_postgres\": \"postgres\",\n \"host_postgres\": \"localhost\",\n \"port_postgres\": \"5432\",\n \"database_postgres\": \"seu_banco\"\n },\n \"dados\": {\n \"cpf_cnpj\": \"00000000000\"\n }\n}"
},
"description": "Payload interno para a fila **tomador** usando o método **get**.\n\n",
"url": {
"raw": "amqp://seu-rabbitmq/fila/tomador/get",
"protocol": "amqp",
"host": [
"seu-rabbitmq"
],
"path": [
"fila",
"tomador",
"get"
]
}
},
"response": []
}
]
},
{
"name": "Relatórios",
"item": [
{
"name": "1. NFSe Emitidas",
"request": {
"method": "POST",
"header": [
{
"key": "Content-Type",
"value": "application/json"
}
],
"body": {
"mode": "raw",
"raw": "{\n \"fila\": \"relatorios\",\n \"cliente\": \"IdentificadorDoCliente\",\n \"metodo\": \"nfse_emitidas\",\n \"url_base\": \"https://api.sandbox.plugnotas.com.br\",\n \"config\": {\n \"api_key\": \"SUA_API_KEY\",\n \"user_postgres\": \"postgres\",\n \"pass_postgres\": \"postgres\",\n \"host_postgres\": \"localhost\",\n \"port_postgres\": \"5432\",\n \"database_postgres\": \"seu_banco\"\n },\n \"dados\": {\n \"data_inicial\": \"01/01/2026\",\n \"data_final\": \"31/01/2026\"\n }\n}"
},
"description": "Payload interno para a fila **relatorios** usando o método **nfse_emitidas**.\n\n",
"url": {
"raw": "amqp://seu-rabbitmq/fila/relatorios/nfse_emitidas",
"protocol": "amqp",
"host": [
"seu-rabbitmq"
],
"path": [
"fila",
"relatorios",
"nfse_emitidas"
]
}
},
"response": []
},
{
"name": "2. NFSe por CNPJ",
"request": {
"method": "POST",
"header": [
{
"key": "Content-Type",
"value": "application/json"
}
],
"body": {
"mode": "raw",
"raw": "{\n \"fila\": \"relatorios\",\n \"cliente\": \"IdentificadorDoCliente\",\n \"metodo\": \"nfse_por_cnpj\",\n \"url_base\": \"https://api.sandbox.plugnotas.com.br\",\n \"config\": {\n \"api_key\": \"SUA_API_KEY\",\n \"user_postgres\": \"postgres\",\n \"pass_postgres\": \"postgres\",\n \"host_postgres\": \"localhost\",\n \"port_postgres\": \"5432\",\n \"database_postgres\": \"seu_banco\"\n },\n \"dados\": {\n \"cnpj\": \"12345678000123\",\n \"data_inicial\": \"01/01/2026\",\n \"data_final\": \"31/01/2026\"\n }\n}"
},
"description": "Payload interno para a fila **relatorios** usando o método **nfse_por_cnpj**.\n\n",
"url": {
"raw": "amqp://seu-rabbitmq/fila/relatorios/nfse_por_cnpj",
"protocol": "amqp",
"host": [
"seu-rabbitmq"
],
"path": [
"fila",
"relatorios",
"nfse_por_cnpj"
]
}
},
"response": []
}
]
},
{
"name": "Webhook Callbacks (Uso Interno do FastAPI)",
"item": [
{
"name": "1. Notificação Concluído",
"request": {
"method": "POST",
"header": [
{
"key": "Content-Type",
"value": "application/json"
}
],
"body": {
"mode": "raw",
"raw": "{\n \"fila\": \"webhook_callback\",\n \"cliente\": \"IdentificadorDoCliente\",\n \"metodo\": \"concluido\",\n \"url_base\": \"https://api.sandbox.plugnotas.com.br\",\n \"config\": {\n \"api_key\": \"SUA_API_KEY\",\n \"user_postgres\": \"postgres\",\n \"pass_postgres\": \"postgres\",\n \"host_postgres\": \"localhost\",\n \"port_postgres\": \"5432\",\n \"database_postgres\": \"seu_banco\"\n },\n \"dados\": {\n \"documento\": \"nfe\",\n \"status\": \"CONCLUIDO\",\n \"id_nota\": \"ID_DA_NFE\",\n \"id_integracao\": \"SEU_SISTEMA_123\",\n \"cnpj\": \"12345678000123\",\n \"payload_plugnotas\": {\n \"id\": \"ID_DA_NFE\",\n \"status\": \"CONCLUIDO\",\n \"mensagem\": \"Autorizado o uso da NFe\"\n }\n }\n}"
},
"description": "Payload interno para a fila **webhook_callback** usando o método **concluido**.\n\n",
"url": {
"raw": "amqp://seu-rabbitmq/fila/webhook_callback/concluido",
"protocol": "amqp",
"host": [
"seu-rabbitmq"
],
"path": [
"fila",
"webhook_callback",
"concluido"
]
}
},
"response": []
}
]
}
]
}

File diff suppressed because it is too large Load diff

8
backup/auxiliar.json Normal file
View file

@ -0,0 +1,8 @@
{
"fila": "empresa",
"cliente": "Verta",
"dados": {
"cnpj": "45738057000120",
"file": "iVBORw0KGgoAAAANSUhEUgAAA7IAAABlCAYAAABwU9/+AAAAAXNSR0IArs4c6QAAAARnQU1BAACxjwv8YQUAAAAJcEhZcwAADsMAAA7DAcdvqGQAAApDSURBVHhe7d3BS5xnHgfwn5IK7aV/gUUwiMEihtxaCtvjEEgq5GC2XRcvsj1KIEMg22WzhTCB4HXJRZLWxEPAJiBz3IXF3CQSGpRQQdb+Ae0lgbFk9jAzzsw746gTTebpfj4wkHnfJ44Bf/m93+d53te+crlcDgAAAEhEf/YAAAAA9DJBFgAAgKQIsgAAACRFkAUAACApgiwAAABJEWQBAABIiiALAABAUgRZAAAAkiLIAgAAkBRBFgAAgKQIsgAAACRFkAUAACApgiwAAABJEWQBAABIiiALAABAUgRZAAAAkiLIAgAAkBRBFgAAgKQIsgAAACRFkAUAACApgiwAAABJEWQBAABIiiALAABAUgRZAAAAkiLIAgAAkBRBFgAAgKQIsgAAACRFkAUAACApgiwAAABJEWQBAABIiiALAABAUgRZAAAAkiLIAgAAkBRBFgAAgKQIsgAAACRFkAUAACApgiwAAABJEWQBAABIiiALAABAUgRZAAAAkiLIAgAAkBRBFgAAgKQIsgAAACRFkAUAACApgiwAAABJEWQBAABIiiALAABAUgRZAAAAkiLIAgAAkBRBFgAAgKQIsgAAACRFkAUAACApgiwAAABJEWQBAABIiiALAABAUgRZAAAAkiLIAgAAkBRBFgAAgKQIsgAAACRFkAUAACApgiwAAABJEWQBAABIiiALAABAUgRZAAAAktL3888/l7MHAQAAoFf1lctlQRYAAIBk2FoMAABAUgRZAAAAkiLIAgAAkBRBFgAAgKQIsgAAACRFkAUAACApgiwAAABJEWQBAABIiiALAABAUgRZAAAAkiLIAgAAkBRB9i1ZW1vLHgJ6gNqE3qQ2AehEkAUAACApgiwAAABJEWQBAABISl+5XC6/evUqdnd3o1wuZ8/TRl9fX7z33nvx/vvvZ0/ta21tLc6dO5c9DB2pzaNRm7wtavNo1CYAx63v5cuX5devX8epU6eiv98C7WG8fv06fvvtt+jv7z90U9aQOapXr16F2jwatcnboDaPTm0CcNz6d3d3NeMj6u/vj1OnTsXu7m72FBwbtXl0apO3QW0endoE4Lj1l8tlzbgL/f39tpRxotRmd9QmJ01tdkdtAnCcdGIAAACS8rsJssWZgRiYKVberEzHwMB0VN8BAEe0fXs8Bs7Ox3b2ROizALx7PRJkt2P+7EAMDDS/pley44Dfk+JMtu7HY34rO6pVxwts6AnFmG7Tx97lz+67/GwAOG49EmQrRm++iFKpVHltFGJ9ciDGb3fRcs/fi1LpXuSyx4EeUbnIvxiP6jVfKkVpYyYWLrnQBgCgs54Ksk2G5+LZRiHi2oWmFZrt2+P11Zv9ZpYbtjy1m4Fu2oacWQ1uDM6VcfN757sK1UCL7dv5WBorxIuFzHTT8Fw8ezoXQ7X3K9MNq7XVbYxb83Hh2mbE83yMNB6H1GzNx/jAeMzfbvg53+tN9f41X9u5UD23bx+sfb29nlnpb9MrHepmaz7GG3dFNHx+xdP4pqFHZleYG+37fQHACejdIBsRMTwZM2ObsfBDpR1u3x6PkWsT8ai6evNoIh8XDgiXQ1cKMfV8IZb3GnsxHiyORuF6rtrkRyI/UVsVehQTmeAciwsRDyuf9+zK3uU10LXtWP5+M0a/mqwH1nZWpmNgcj0KG5X6e3FzPS7OFCOG5+LxzdGIsUK8KJXsviBxm5H/8fJeD5pavNgcFp/n4+ml6o6FhVzEynRLHxw5TGjcp262f3gaM9UaK20UYrTl89fjbLUHlpanYmmy/cRRN/0ZAN5EbwfZGIrRidqfqxe/N2/sXbTmLk3F5vfLBzTwXFz+sh6GY+VBLI3NxORwRGwtx8LzWqhtMzYi4stCzA3X3wLHY2K0HmOb7pWtXpQXHy411d/QFzMxuvig7UU0pKuxB52Js2OZ02OFuHG+/rZSF5frffB6IUabJmuPZujKvXqPG56MmbGI9c2GHljrlxER5y/HVCzFg5ZV2W77MwB0r8eD7HZsrjcf2bw2Ur/gnVxqPrmPxoZafLiUWQnajPyZ+kX0xcWmvwqckMaL5dxCbdV1tGlMLF6s1/uZfGw2n4U0TYx23o1wgNGPz9TfDI/G3nxvVxpvrxmJ/PPs+UZtgnaDbvozAHSrt4NsdcV05ot6y59abngwTKkUpcb76fZz/nJ1e3ExHixORaFpi/DU3lao2ssW4u6sFnJx60n2KGQNxeRXo4darWl6AJxtxCSlfejb+PHNp2M2f9yov9najMx87xFkb695EYU233PdRjztEHS76s//5/RNgO71bpDdmo/xM/mIm4+r254qF7/N9+dsx/zMIe4NilzcuBmxcCkfSw1bsirbqJYq993VbM3H9Du+r+fC0oVDvTrZuT8buVyu8vp6KXayAzI6j9+Jpa+r53K5mL3f7qutxuq/P49PP8keh1aVe9fzMdLyYJm63KWp2Mzcs16cqdT/0OhExPOn0XA5Dz2mTc/amo/8YsTUpe6nY3KXpiIW83t1Ufw2H5u17b/DozERm/G0lpVXvmlaYd2vbuorvJ2Dau0hbY1bnSva/FsP3Z+Px1/+Pn+oVyed+2CrzuP1TYCT1lNBtmlb0pl8TCw3r44OXXlWeeBLbczASCx8fMADY6qGvpiJeL6ZuYAYirmnL6Kw3rh9cSHONqwAJ+nJrZj97nRcLxajWCzG9aG7MVtYzY6qO2D8amE27g5dj2KxGMXi9Tj93WzrDPKT1fjXHz6NTzOHob1c3CuV4lE01N7AQIx8PxMvaqs45+9FaXmieet/VCeizt+IwthS9f+C9g+fgXdt6MqzePRl7ee03tfutQTBIzh/L17cjL26uLheqNdM5OLe8lQsTVY/7+HZ5hXWlroZirlvpxp6bz7Wsyuye085Hqg8zGmfVdY36c894YA+2OKA8fomwMnr++WXX8offPBB9jiH8PLly/jwww+zh9taW1uLc+fOZQ+3ddBqa83jqcfZQxHVrUp3B+/EnT8OVg48uRW5f0RcL15t2zA7j1+NW7m78dGdOzFVPb1ayMW3cT2K+fpXWy3kYvWzYlytzizv3J+N2e/qM9Cf/7V+jsP59ddfQ21256RqE0JtvpGTqs2DVltr/vm3ueyhiAP7YKvO4/VNgLehp1ZkOQ47sZPdyzX4UQzGT7HTbmfTQeN3duKnzOmPBgcjtncatlFltkftLMU/Gmaqi3f+HLHd9sMB4B07oA+2OGC8vgnwVrxZkF2p/BL3Tr8gvae1/PL47LmBGH/H98t26/RQdRr4kDqPPx2DHU7v3L/bvD1q57+x03gBMDgVV2uz1vSo6pNLD/P7KHtFp/qF35sUelIK32MHnftgq87j9U2Ak/YGQbYY05PrUdh4w/t9Iipfq9cC8fBcPNsoRFz7Jsn773464kxu5/H7zUpHROzE6n924vPPGjZffXI17vwp4u5s7UEXt6LDnUb0gO3bFypPLt3n/jfS5smoqduO+Uutz404krcx8aNvNtA3AU7a/wAJTX+BdYrxlwAAAABJRU5ErkJggg=="
}
}

View file

@ -0,0 +1,86 @@
{
"endereco": {
"codigoPais": "1058",
"descricaoPais": "Brasil",
"tipoLogradouro": "Rua",
"logradouro": "T55",
"numero": "882",
"complemento": "Quadra99 Lote 11/14 Sala 1507",
"tipoBairro": "Zona",
"bairro": "Setor Bueno",
"codigoCidade": "5208707",
"descricaoCidade": "Goiania",
"estado": "GO",
"cep": "74215170"
},
"nfse": {
"config": {
"rps": {
"lote": 12,
"numeracao": [
{
"numero": 2,
"serie": "CTBZ",
"numeracaoAtual": 2
}
]
},
"email": {
"envio": false
},
"producao": true,
"envioLimitado": false,
"integracoes": []
},
"ativo": true,
"tipoContrato": 0
},
"nfe": {
"config": {
"dfe": {
"primeiroNsu": true
},
"email": {
"envio": false
},
"integracoes": {
"plugStorage": {
"ativo": false
}
},
"producao": false,
"impressaoFcp": false,
"impressaoPartilha": true,
"versaoManual": "6.0",
"versaoEsquema": "pl_009",
"numeracao": []
},
"ativo": false
},
"nfce": {
"config": {
"email": {
"envio": false
},
"integracoes": {
"plugStorage": {
"ativo": false
}
},
"producao": false,
"versaoManual": "6.0",
"versaoEsquema": "pl_009",
"numeracao": []
},
"ativo": false
},
"incentivoFiscal": false,
"incentivadorCultural": false,
"cpfCnpj": "80972076000118",
"inscricaoMunicipal": "108532836",
"certificado": "668698592698d7831c758d52",
"razaoSocial": "Teste",
"simplesNacional": true,
"regimeTributario": 1,
"regimeTributarioEspecial": 6
}

View file

@ -0,0 +1,125 @@
{
"info": {
"name": "APIs Frontend - Documentação de Payloads",
"description": "Estes são os JSONs puros extremamente completos com todas as tags mapeadas nos modelos Pydantic.",
"schema": "https://schema.getpostman.com/json/collection/v2.1.0/collection.json"
},
"item": [
{
"name": "1. Cadastrar NFe",
"request": {
"method": "POST",
"header": [
{
"key": "Content-Type",
"value": "application/json"
}
],
"body": {
"mode": "raw",
"raw": "{\n \"nfe\": {\n \"idIntegracao\": \"XXXYY99999_TESTE1\",\n \"presencial\": true,\n \"consumidorFinal\": true,\n \"natureza\": \"OPERAÇÃO INTERNA\",\n \"emitente\": {\n \"cpfCnpj\": \"08187168000160\"\n },\n \"destinatario\": {\n \"cpfCnpj\": \"08114280956\",\n \"razaoSocial\": \"NF-E EMITIDA EM HOMOLOGACAO\",\n \"email\": \"contato@tecnospeed.com.br\",\n \"endereco\": {\n \"logradouro\": \"AVENIDA DUQUE DE CAXIAS\",\n \"numero\": \"882\",\n \"bairro\": \"CENTRO\",\n \"codigoCidade\": \"4115200\",\n \"descricaoCidade\": \"MARINGA\",\n \"estado\": \"PR\",\n \"cep\": \"87020025\"\n }\n },\n \"itens\": [\n {\n \"codigo\": \"1\",\n \"descricao\": \"PRODUTO DE TESTE\",\n \"ncm\": \"06029090\",\n \"cest\": \"0123456\",\n \"cfop\": \"5101\",\n \"valorUnitario\": {\n \"comercial\": 4.67,\n \"tributavel\": 4.67\n },\n \"valor\": 4.67,\n \"tributos\": {\n \"icms\": {\n \"origem\": \"0\",\n \"cst\": \"00\",\n \"baseCalculo\": {\n \"modalidadeDeterminacao\": 0,\n \"valor\": 0\n },\n \"aliquota\": 0,\n \"valor\": 0\n },\n \"pis\": {\n \"cst\": \"99\",\n \"baseCalculo\": {\n \"valor\": 0,\n \"quantidade\": 0\n },\n \"aliquota\": 0,\n \"valor\": 0\n },\n \"cofins\": {\n \"cst\": \"07\",\n \"baseCalculo\": {\n \"valor\": 0\n },\n \"aliquota\": 0,\n \"valor\": 0\n }\n }\n }\n ],\n \"pagamentos\": [\n {\n \"aVista\": true,\n \"meio\": \"01\",\n \"valor\": 4.6\n }\n ],\n \"responsavelTecnico\": {\n \"cpfCnpj\": \"08187168000160\",\n \"nome\": \"Tecnospeed\",\n \"email\": \"contato@tecnospeed.com.br\",\n \"telefone\": {\n \"ddd\": \"44\",\n \"numero\": \"30379500\"\n }\n }\n }\n}"
},
"url": {
"raw": "https://api.sandbox.plugnotas.com.br/nfe",
"protocol": "https",
"host": [
"api.sandbox.plugnotas.com.br/nfe"
]
}
}
},
{
"name": "2. Cadastrar NFSe",
"request": {
"method": "POST",
"header": [
{
"key": "Content-Type",
"value": "application/json"
}
],
"body": {
"mode": "raw",
"raw": "{\n \"idIntegracao\": \"INTEGRACAO-NFSE-001\",\n \"prestador\": {\n \"cpfCnpj\": \"12345678000123\"\n },\n \"tomador\": {\n \"cpfCnpj\": \"98765432000198\",\n \"razaoSocial\": \"João Tomador da Silva\",\n \"inscricaoMunicipal\": \"isento\",\n \"email\": \"tomador@email.com\",\n \"endereco\": {\n \"descricaoCidade\": \"São Paulo\",\n \"cep\": \"00000000\",\n \"tipoLogradouro\": \"Rua\",\n \"logradouro\": \"Dois de Ouro\",\n \"tipoBairro\": \"Bairro\",\n \"codigoCidade\": \"3550308\",\n \"complemento\": \"Apto 13\",\n \"estado\": \"SP\",\n \"numero\": \"44\",\n \"bairro\": \"Centro\"\n }\n },\n \"servico\": {\n \"codigo\": \"1.01\",\n \"codigoTributacao\": \"1.01\",\n \"discriminacao\": \"Serviços prestados de tecnologia\",\n \"cnae\": \"6204000\",\n \"iss\": {\n \"tipoTributacao\": 6,\n \"exigibilidade\": 1,\n \"aliquota\": 2.5\n },\n \"valor\": {\n \"servico\": 1500.50,\n \"descontoCondicionado\": 0.0,\n \"descontoIncondicionado\": 0.0\n }\n }\n}"
},
"url": {
"raw": "https://api.sandbox.plugnotas.com.br/nfse",
"protocol": "https",
"host": [
"api.sandbox.plugnotas.com.br/nfse"
]
}
}
},
{
"name": "3. Cadastrar Empresa",
"request": {
"method": "POST",
"header": [
{
"key": "Content-Type",
"value": "application/json"
}
],
"body": {
"mode": "raw",
"raw": "{\n \"endereco\": {\n \"codigoPais\": \"1058\",\n \"descricaoPais\": \"BRASIL\",\n \"tipoLogradouro\": \"Rua\",\n \"logradouro\": \"Teste\",\n \"numero\": \"123\",\n \"complemento\": \"Terreo\",\n \"tipoBairro\": \"Bairro\",\n \"bairro\": \"Centro\",\n \"codigoCidade\": \"4106902\",\n \"descricaoCidade\": \"CURITIBA\",\n \"estado\": \"PR\",\n \"cep\": \"80000000\"\n },\n \"nfse\": {\n \"config\": {\n \"rps\": {\n \"lote\": 1,\n \"numeracao\": [\n 1\n ]\n },\n \"email\": {\n \"envio\": true\n },\n \"producao\": false,\n \"envioLimitado\": false,\n \"integracoes\": []\n },\n \"ativo\": true,\n \"tipoContrato\": 0\n },\n \"nfe\": {\n \"config\": {\n \"dfe\": {\n \"primeiroNsu\": false\n },\n \"email\": {\n \"envio\": false\n },\n \"integracoes\": {},\n \"producao\": false,\n \"impressaoFcp\": false,\n \"impressaoPartilha\": false,\n \"versaoManual\": \"6.0\",\n \"versaoEsquema\": \"PL_009_V4\",\n \"numeracao\": [\n 1\n ]\n },\n \"ativo\": false\n },\n \"nfce\": {\n \"config\": {\n \"email\": {\n \"envio\": false\n },\n \"integracoes\": {},\n \"producao\": false,\n \"versaoManual\": \"6.0\",\n \"versaoEsquema\": \"PL_009_V4\",\n \"numeracao\": [\n 1\n ]\n },\n \"ativo\": false\n },\n \"incentivoFiscal\": false,\n \"incentivadorCultural\": false,\n \"cpfCnpj\": \"12345678000123\",\n \"inscricaoMunicipal\": \"12345\",\n \"certificado\": \"60b8d5a1-UUID-DO-CERT\",\n \"razaoSocial\": \"Empresa Teste Completa SA\",\n \"simplesNacional\": true,\n \"regimeTributario\": 1,\n \"regimeTributarioEspecial\": 0\n}"
},
"url": {
"raw": "https://api.sandbox.plugnotas.com.br/empresa",
"protocol": "https",
"host": [
"api.sandbox.plugnotas.com.br/empresa"
]
}
}
},
{
"name": "4. Cadastrar Certificado Digital",
"request": {
"method": "POST",
"header": [
{
"key": "Content-Type",
"value": "application/json"
}
],
"body": {
"mode": "raw",
"raw": "{\n \"arquivo\": \"MIIJ/AIBAzCCC... (BASE64 STRING)\",\n \"senha\": \"minhasenha123\",\n \"id_empresa\": \"Id da Empresa (Ex: 105)\"\n}"
},
"url": {
"raw": "https://api.sandbox.plugnotas.com.br/certificado",
"protocol": "https",
"host": [
"api.sandbox.plugnotas.com.br/certificado"
]
},
"description": "Payload enxuto como refatorado."
}
},
{
"name": "5. Cadastrar Serviço de NFSe",
"request": {
"method": "POST",
"header": [
{
"key": "Content-Type",
"value": "application/json"
}
],
"body": {
"mode": "raw",
"raw": "{\n \"codigo\": \"1.01\",\n \"idIntegracao\": \"SRV-101\",\n \"discriminacao\": \"An\\u00e1lise e desenvolvimento de sistemas\",\n \"codigoTributacao\": \"1.01\",\n \"cnae\": \"6204000\",\n \"codigoCidadeIncidencia\": \"3550308\",\n \"descricaoCidadeIncidencia\": \"S\\u00c3O PAULO\",\n \"unidade\": \"UN\",\n \"quantidade\": 1,\n \"iss\": {\n \"tipoTributacao\": 6,\n \"exigibilidade\": 1,\n \"retido\": false,\n \"aliquota\": 2.5,\n \"valor\": 37.5,\n \"valorRetido\": 0.0,\n \"processoSuspensao\": \"\"\n },\n \"obra\": {\n \"art\": \"\",\n \"codigo\": \"\"\n },\n \"valor\": {\n \"servico\": 1500.0,\n \"baseCalculo\": 1500.0,\n \"deducoes\": 0.0,\n \"descontoCondicionado\": 0.0,\n \"descontoIncondicionado\": 0.0,\n \"liquido\": 1500.0,\n \"unitario\": 1500.0,\n \"valorAproximadoTributos\": 0.0\n },\n \"deducao\": {\n \"tipo\": 0,\n \"descricao\": \"\"\n },\n \"retencao\": {\n \"pis\": {\n \"aliquota\": 0,\n \"valor\": 0\n },\n \"cofins\": {\n \"aliquota\": 0,\n \"valor\": 0\n },\n \"csll\": {\n \"aliquota\": 0,\n \"valor\": 0\n },\n \"inss\": {\n \"aliquota\": 0,\n \"valor\": 0\n },\n \"irrf\": {\n \"aliquota\": 0,\n \"valor\": 0\n },\n \"outrasRetencoes\": 0,\n \"cpp\": {\n \"aliquota\": 0,\n \"valor\": 0\n }\n },\n \"tributavel\": true,\n \"ibpt\": {\n \"simplificado\": {\n \"aliquota\": 0\n },\n \"detalhado\": {\n \"aliquota\": {\n \"municipal\": 0.0,\n \"estadual\": 0.0,\n \"federal\": 0.0\n }\n }\n },\n \"responsavelRetencao\": \"1\"\n}"
},
"url": {
"raw": "https://api.sandbox.plugnotas.com.br/nfse/servico",
"protocol": "https",
"host": [
"api.sandbox.plugnotas.com.br/nfse/servico"
]
}
}
}
]
}

View file

@ -0,0 +1,86 @@
{
"endereco": {
"codigoPais": "1058",
"descricaoPais": "Brasil",
"tipoLogradouro": "Rua",
"logradouro": "T55",
"numero": "882",
"complemento": "Quadra99 Lote 11/14 Sala 1507",
"tipoBairro": "Zona",
"bairro": "Setor Bueno",
"codigoCidade": "5208707",
"descricaoCidade": "Goiania",
"estado": "GO",
"cep": "74215170"
},
"nfse": {
"config": {
"rps": {
"lote": 12,
"numeracao": [
{
"numero": 2,
"serie": "CTBZ",
"numeracaoAtual": 2
}
]
},
"email": {
"envio": false
},
"producao": true,
"envioLimitado": false,
"integracoes": []
},
"ativo": true,
"tipoContrato": 0
},
"nfe": {
"config": {
"dfe": {
"primeiroNsu": true
},
"email": {
"envio": false
},
"integracoes": {
"plugStorage": {
"ativo": false
}
},
"producao": false,
"impressaoFcp": false,
"impressaoPartilha": true,
"versaoManual": "6.0",
"versaoEsquema": "pl_009",
"numeracao": []
},
"ativo": false
},
"nfce": {
"config": {
"email": {
"envio": false
},
"integracoes": {
"plugStorage": {
"ativo": false
}
},
"producao": false,
"versaoManual": "6.0",
"versaoEsquema": "pl_009",
"numeracao": []
},
"ativo": false
},
"incentivoFiscal": false,
"incentivadorCultural": false,
"cpfCnpj": "45732212000100",
"inscricaoMunicipal": "108532836",
"certificado": "668698592698d7831c758d52",
"razaoSocial": "Teste",
"simplesNacional": true,
"regimeTributario": 1,
"regimeTributarioEspecial": 6
}

View file

@ -0,0 +1,29 @@
{
"system": "integracao",
"target": "rpcclt",
"service": "tecnospeed",
"serviceId": "01234567890123456789012345678901",
"payload": {
"fila": "nfse",
"cliente": "Verta",
"metodo": "POST",
"dados": null,
"config":{
"api_key": "2da392a6-79d2-4304-a8b7-959572c7e44d",
"user_postgres": "postgres",
"pass_postgres": "postgres",
"host_postgres": "localhost",
"port_postgres": "5432",
"database_postgres": "tecnospeed"
}
},
"hmac": "38051e9be76496a05a237fdabc19898ef7ab3f469c5873b96a4fdbb5be30b10c",
"type": "rpc",
"correlationId": "884ea465-e2b6-4eab-bfdf-a08c9fb493d3",
"version": "1.0",
"origin": "kuririn",
"client": "verta",
"caller": "callRPC",
"logDate": "2025-02-24T16:41:40.882Z",
"msg": {"cliente":"Verta","servico":"webhooks"}
}

View file

@ -0,0 +1,6 @@
{
"serviceid":"01234567890123456789012345678901",
"servicekey":"ABCDEFGHIJKLMNOPQRSTUVWXYZ123456",
"origin":"kuririn",
"amqps":"amqps://qyqordbr:5tDIJIX6z5k3qaMmpJGC4HIieqq4n29z@gull.rmq.cloudamqp.com/qyqordbr"
}

View file

@ -0,0 +1,129 @@
{
"info": {
"_postman_id": "55e2d8ab-7890-4813-89bd-a287c7e44d1a",
"name": "Serviço PlugNotas — Messaging (RPC & Queue)",
"description": "Collection de mensageria para o Serviço PlugNotas.",
"schema": "https://schema.getpostman.com/json/collection/v2.1.0/collection.json"
},
"item": [
{
"name": "NFe",
"description": "Operações de Nota Fiscal Eletrônica (NFe) — emissão, consulta, download, cancelamento e resumo.",
"item": [
{
"name": "POST - Emitir NFe",
"request": {
"method": "POST",
"header": [],
"body": {
"mode": "raw",
"raw": "{\n \"fila\": \"nfe\",\n \"metodo\": \"post\",\n \"cliente\": \"{{cliente}}\",\n \"url_base\": \"https://api.sandbox.plugnotas.com.br\",\n \"config\": {\n \"api_key\": \"{{api_key}}\",\n \"user_postgres\": \"{{user_postgres}}\",\n \"pass_postgres\": \"{{pass_postgres}}\",\n \"host_postgres\": \"{{host_postgres}}\",\n \"port_postgres\": \"{{port_postgres}}\",\n \"database_postgres\": \"{{database_postgres}}\"\n },\n \"dados\": {\n \"idIntegracao\": \"NFe_TESTE_001\",\n \"presencial\": true,\n \"consumidorFinal\": true,\n \"natureza\": \"OPERAÇÃO INTERNA\",\n \"emitente\": {\n \"cpfCnpj\": \"00000000000000\",\n \"inscricaoEstadual\": \"ISENTO\"\n },\n \"destinatario\": {\n \"cpfCnpj\": \"08114280956\",\n \"razaoSocial\": \"NF-E EMITIDA EM HOMOLOGACAO\",\n \"endereco\": {\n \"logradouro\": \"AVENIDA DUQUE DE CAXIAS\",\n \"numero\": \"882\",\n \"bairro\": \"CENTRO\",\n \"codigoCidade\": \"4115200\",\n \"descricaoCidade\": \"MARINGA\",\n \"estado\": \"PR\",\n \"cep\": \"87020025\"\n }\n },\n \"itens\": [\n {\n \"codigo\": \"1\",\n \"descricao\": \"PRODUTO DE TESTE\",\n \"ncm\": \"06029090\",\n \"cfop\": \"5101\",\n \"valorUnitario\": {\n \"comercial\": 4.67,\n \"tributavel\": 4.67\n },\n \"valor\": 4.67,\n \"tributos\": {\n \"icms\": {\n \"origem\": \"0\",\n \"cst\": \"00\",\n \"baseCalculo\": {\n \"modalidadeDeterminacao\": 0,\n \"valor\": 0\n },\n \"aliquota\": 0,\n \"valor\": 0\n }\n }\n }\n ],\n \"pagamentos\": [\n {\n \"aVista\": true,\n \"meio\": \"01\",\n \"valor\": 4.67\n }\n ]\n }\n}",
"options": {
"raw": {
"language": "json"
}
}
},
"url": {},
"description": "Emite uma NFe de acordo com as exigências da PlugNotas.\n\n**Campos obrigatórios em `dados`:** idIntegracao, presencial, consumidorFinal, natureza, emitente (cpfCnpj), destinatario (cpfCnpj, razaoSocial, endereco), itens, pagamentos."
},
"response": []
},
{
"name": "GET - Consultar NFe",
"request": {
"method": "POST",
"header": [],
"body": {
"mode": "raw",
"raw": "{\n \"fila\": \"nfe\",\n \"metodo\": \"get\",\n \"cliente\": \"{{cliente}}\",\n \"config\": {\n \"api_key\": \"{{api_key}}\",\n \"user_postgres\": \"{{user_postgres}}\",\n \"pass_postgres\": \"{{pass_postgres}}\",\n \"host_postgres\": \"{{host_postgres}}\",\n \"port_postgres\": \"{{port_postgres}}\",\n \"database_postgres\": \"{{database_postgres}}\"\n },\n \"dados\": {\n \"id\": \"ID_DA_NOTA\"\n }\n}",
"options": {
"raw": {
"language": "json"
}
}
},
"url": {},
"description": "Consulta os dados de uma NFe pelo ID."
},
"response": []
}
]
},
{
"name": "NFSe",
"description": "Operações de NFSe — emissão, consulta, download, etc.",
"item": [
{
"name": "POST - Emitir NFSe",
"request": {
"method": "POST",
"header": [],
"body": {
"mode": "raw",
"raw": "{\n \"fila\": \"nfse\",\n \"metodo\": \"post\",\n \"cliente\": \"{{cliente}}\",\n \"config\": {\n \"api_key\": \"{{api_key}}\",\n \"user_postgres\": \"{{user_postgres}}\",\n \"pass_postgres\": \"{{pass_postgres}}\",\n \"host_postgres\": \"{{host_postgres}}\",\n \"port_postgres\": \"{{port_postgres}}\",\n \"database_postgres\": \"{{database_postgres}}\"\n },\n \"dados\": {\n \"idIntegracao\": \"NFSe_001\",\n \"prestador\": {\n \"cpfCnpj\": \"00000000000000\"\n },\n \"tomador\": {\n \"cpfCnpj\": \"11111111111\",\n \"razaoSocial\": \"Razao Social do Tomador\",\n \"inscricaoMunicipal\": \"ISENTO\",\n \"email\": \"tomador@exemplo.com\",\n \"endereco\": {\n \"descricaoCidade\": \"Sao Paulo\",\n \"cep\": \"00000000\",\n \"tipoLogradouro\": \"Rua\",\n \"logradouro\": \"Rua de Teste\",\n \"tipoBairro\": \"Bairro\",\n \"codigoCidade\": \"3550308\",\n \"complemento\": \"\",\n \"estado\": \"SP\",\n \"numero\": \"123\",\n \"bairro\": \"Centro\"\n }\n },\n \"servico\": {\n \"codigo\": \"1.01\",\n \"codigoTributacao\": \"1.01.01\",\n \"discriminacao\": \"Descricao do servico\",\n \"cnae\": \"6201100\",\n \"iss\": {\n \"tipoTributacao\": 6,\n \"exigibilidade\": 1,\n \"aliquota\": 2.0\n },\n \"valor\": {\n \"servico\": 150.0,\n \"descontoCondicionado\": 0.0,\n \"descontoIncondicionado\": 0.0\n }\n }\n }\n}",
"options": {
"raw": {
"language": "json"
}
}
},
"url": {},
"description": "Emite uma NFSe."
},
"response": []
}
]
},
{
"name": "Empresa",
"description": "Cadastro e gerenciamento de empresas.",
"item": [
{
"name": "POST - Cadastro Completo",
"request": {
"method": "POST",
"header": [],
"body": {
"mode": "raw",
"raw": "{\n \"fila\": \"empresa\",\n \"metodo\": \"post\",\n \"cliente\": \"{{cliente}}\",\n \"config\": {\n \"api_key\": \"{{api_key}}\",\n \"user_postgres\": \"{{user_postgres}}\",\n \"pass_postgres\": \"{{pass_postgres}}\",\n \"host_postgres\": \"{{host_postgres}}\",\n \"port_postgres\": \"{{port_postgres}}\",\n \"database_postgres\": \"{{database_postgres}}\"\n },\n \"dados\": {\n \"cpfCnpj\": \"00000000000000\",\n \"razaoSocial\": \"Empresa Teste S/A\",\n \"inscricaoMunicipal\": \"1234\",\n \"inscricaoEstadual\": \"ISENTO\",\n \"simplesNacional\": true,\n \"regimeTributario\": 1,\n \"regimeTributarioEspecial\": 0,\n \"incentivoFiscal\": false,\n \"incentivadorCultural\": false,\n \"certificado\": \"A1\",\n \"endereco\": {\n \"codigoPais\": \"1058\",\n \"descricaoPais\": \"Brasil\",\n \"tipoLogradouro\": \"Rua\",\n \"logradouro\": \"Rua das Industrias\",\n \"numero\": \"1050\",\n \"complemento\": \"Hangar 2\",\n \"tipoBairro\": \"Bairro\",\n \"bairro\": \"Parque Industrial\",\n \"codigoCidade\": \"3550308\",\n \"descricaoCidade\": \"Sao Paulo\",\n \"estado\": \"SP\",\n \"cep\": \"00000000\"\n },\n \"nfse\": {\n \"ativo\": true,\n \"tipoContrato\": 1,\n \"config\": {\n \"rps\": { \"lote\": 1, \"numeracao\": [] },\n \"email\": { \"envio\": true },\n \"producao\": false,\n \"envioLimitado\": false,\n \"integracoes\": [],\n \"prefeitura\": null\n }\n },\n \"nfe\": {\n \"ativo\": true,\n \"config\": {\n \"dfe\": { \"primeiroNsu\": true },\n \"email\": { \"envio\": true },\n \"integracoes\": {},\n \"producao\": false,\n \"impressaoFcp\": false,\n \"impressaoPartilha\": false,\n \"versaoManual\": \"4.0\",\n \"versaoEsquema\": \"4.00\",\n \"numeracao\": []\n }\n }\n }\n}",
"options": {
"raw": {
"language": "json"
}
}
},
"url": {},
"description": "Cadastro de empresa com NFe e NFSe."
},
"response": []
}
]
},
{
"name": "Tomador",
"description": "Cadastro de tomadores/clientes.",
"item": [
{
"name": "POST - Criar Tomador",
"request": {
"method": "POST",
"header": [],
"body": {
"mode": "raw",
"raw": "{\n \"fila\": \"tomador\",\n \"metodo\": \"post\",\n \"cliente\": \"{{cliente}}\",\n \"config\": {\n \"api_key\": \"{{api_key}}\",\n \"user_postgres\": \"{{user_postgres}}\",\n \"pass_postgres\": \"{{pass_postgres}}\",\n \"host_postgres\": \"{{host_postgres}}\",\n \"port_postgres\": \"{{port_postgres}}\",\n \"database_postgres\": \"{{database_postgres}}\"\n },\n \"dados\": {\n \"cpfCnpj\": \"12345678901\",\n \"inscricaoMunicipal\": \"ISENTO\",\n \"inscricaoEstadual\": \"ISENTO\",\n \"inscricaoSuframa\": \"\",\n \"indicadorInscricaoEstadual\": 9,\n \"razaoSocial\": \"Cliete Teste\",\n \"nomeFantasia\": \"Empresa Teste\",\n \"email\": \"tomador@exemplo.com\",\n \"orgaoPublico\": false,\n \"endereco\": {\n \"tipoLogradouro\": \"Rua\",\n \"logradouro\": \"Rua de Teste\",\n \"numero\": \"100\",\n \"complemento\": \"\",\n \"tipoBairro\": \"Bairro\",\n \"bairro\": \"Centro\",\n \"codigoPais\": \"1058\",\n \"descricaoPais\": \"Brasil\",\n \"codigoCidade\": \"3550308\",\n \"descricaoCidade\": \"Sao Paulo\",\n \"estado\": \"SP\",\n \"cep\": \"00000000\"\n },\n \"telefone\": {\n \"ddd\": \"11\",\n \"numero\": \"999999999\"\n }\n }\n}",
"options": {
"raw": {
"language": "json"
}
}
},
"url": {},
"description": "Cadastro de tomador."
},
"response": []
}
]
}
]
}

View file

@ -0,0 +1,18 @@
from pydantic import BaseModel
# Dados de conexão ao banco de dados isolado do cliente e da API PlugNotas
class Config(BaseModel):
api_key: str | None = None
x_api_key: str | None = None
user_postgres: str
pass_postgres: str
host_postgres: str
port_postgres: str
database_postgres: str
# Informações de mensageria para a troca de arquivos (AMQP) via subagent
class SystemConfig(BaseModel):
serviceid: str
servicekey: str
origin: str
amqps: str

View file

@ -0,0 +1,96 @@
from pydantic import BaseModel
###################
## POST /empresas #
###################
class Endereco(BaseModel):
codigoPais: str
descricaoPais: str
tipoLogradouro: str
logradouro: str
numero: str
complemento: str
tipoBairro: str
bairro: str
codigoCidade: str
descricaoCidade: str
estado: str
cep: str
class Rps(BaseModel):
lote: int
numeracao: list
class Email(BaseModel):
envio: bool
class Config(BaseModel):
rps: Rps | None = None
email: Email | None = None
producao: bool | None = None
envioLimitado: bool | None = None
integracoes: list | None = None
prefeitura: dict | str | int | None = None
class Nfse(BaseModel):
config: Config | None = None
ativo: bool = False
tipoContrato: int | None = 0
class Dfe(BaseModel):
primeiroNsu: bool
class ConfigNfe(BaseModel):
dfe: Dfe | None = None
email: Email | None = None
integracoes: dict | None = None
producao: bool | None = None
impressaoFcp: bool | None = None
impressaoPartilha: bool | None = None
versaoManual: str | None = None
versaoEsquema: str | None = None
numeracao: list | None = None
class Nfe(BaseModel):
config: ConfigNfe | None = None
ativo: bool = False
tipoContrato: int | None = 0
class ConfigNfce(BaseModel):
email: Email | None = None
integracoes: dict | None = None
producao: bool | None = None
versaoManual: str | None = None
versaoEsquema: str | None = None
numeracao: list | None = None
class Nfce(BaseModel):
config: ConfigNfce | None = None
ativo: bool = False
tipoContrato: int | None = 0
# Entidade Empresa que cadastra a credencial emissora no sistema da PlugNotas
class Empresa(BaseModel):
endereco: Endereco
nfse: Nfse | None = None
nfe: Nfe | None = None
nfce: Nfce | None = None
incentivoFiscal: bool
incentivadorCultural: bool
cpfCnpj: str
inscricaoMunicipal: str
inscricaoEstadual: str | None = None
certificado: str
razaoSocial: str
nomeFantasia: str | None = None
simplesNacional: bool
regimeTributario: int
regimeTributarioEspecial: int
id: str | None = None
banco: str | None = None
email: str | None = None
id_empresa: int | None = None

99
backup/src/Models/nfe.py Normal file
View file

@ -0,0 +1,99 @@
from pydantic import BaseModel
from typing import List, Optional
# Grupo de dados correspondente ao prestador/emitente da nota
class Emitente(BaseModel):
cpfCnpj: str
inscricaoEstadual: str
class Endereco(BaseModel):
logradouro: str
numero: str
bairro: str
codigoCidade: str
descricaoCidade: str
estado: str
cep: str
class Destinatario(BaseModel):
cpfCnpj: str
razaoSocial: str
email: Optional[str] = None
endereco: Endereco
class ValorTributavel(BaseModel):
comercial: float
tributavel: float
class BaseCalculoIcms(BaseModel):
modalidadeDeterminacao: int
valor: float
class Icms(BaseModel):
origem: str
cst: str
baseCalculo: BaseCalculoIcms
aliquota: float
valor: float
class BaseCalculoPis(BaseModel):
valor: float
quantidade: int
class Pis(BaseModel):
cst: str
baseCalculo: BaseCalculoPis
aliquota: float
valor: float
class BaseCalculoCofins(BaseModel):
valor: float
class Cofins(BaseModel):
cst: str
baseCalculo: BaseCalculoCofins
aliquota: float
valor: float
class Tributos(BaseModel):
icms: Optional[Icms] = None
pis: Optional[Pis] = None
cofins: Optional[Cofins] = None
# Representa o produto ou serviço vendido, incluindo valores, quantidades e tributação
class ItemNFe(BaseModel):
codigo: str
descricao: str
ncm: str
cest: Optional[str] = None
cfop: str
valorUnitario: ValorTributavel
valor: float
tributos: Tributos
class Pagamento(BaseModel):
aVista: bool
meio: str
valor: float
class Telefone(BaseModel):
ddd: str
numero: str
class ResponsavelTecnico(BaseModel):
cpfCnpj: str
nome: str
email: str
telefone: Telefone
# Root do JSON para envio da NFe; agrupa recebedor, compras, responsável, e pagamentos
class NFe(BaseModel):
idIntegracao: str
presencial: bool
consumidorFinal: bool
natureza: str
emitente: Emitente
destinatario: Destinatario
itens: List[ItemNFe]
pagamentos: List[Pagamento]
responsavelTecnico: Optional[ResponsavelTecnico] = None

49
backup/src/Models/nfse.py Normal file
View file

@ -0,0 +1,49 @@
from pydantic import BaseModel
from typing import Optional
class Prestador(BaseModel):
cpfCnpj: str
class Endereco(BaseModel):
descricaoCidade: str
cep: str
tipoLogradouro: str
logradouro: str
tipoBairro: Optional[str] = None
codigoCidade: str
complemento: Optional[str] = None
estado: str
numero: str
bairro: str
class Tomador(BaseModel):
cpfCnpj: str
razaoSocial: str
inscricaoMunicipal: Optional[str] = None
email: str
endereco: dict
class ISS(BaseModel):
tipoTributacao: int
exigibilidade: int
aliquota: float
class Valor(BaseModel):
servico: float
descontoCondicionado: Optional[float] = 0
descontoIncondicionado: Optional[float] = 0
class Servico(BaseModel):
codigo: str
codigoTributacao: str
discriminacao: str
cnae: str
iss: ISS
valor: Valor
# Estrutura padrão de envio de uma NFSe contendo Prestador, Tomador e Serviço prestado
class NFSe(BaseModel):
idIntegracao: str
prestador: Prestador
tomador: Tomador
servico: Servico

View file

@ -0,0 +1,518 @@
from enum import Enum
from pydantic import BaseModel, ConfigDict, Field
from Models.config import Config
from Models.empresa import Empresa
from Models.nfe import NFe
from Models.nfse import NFSe
from Models.servico import Servico
from Models.tomador import Tomador
class Fila(str, Enum):
NFE = "nfe"
NFSE = "nfse"
CERTIFICADO_DIGITAL = "certificado_digital"
EMPRESA = "empresa"
SERVICO = "servico"
TOMADOR = "tomador"
RELATORIOS = "relatorios"
WEBHOOKS = "webhooks"
class NFeMetodo(str, Enum):
POST = "post"
GET = "get"
DOWNLOAD = "download"
DOWNLOAD_XML = "download_xml"
CANCELAR = "cancelar"
STATUS_CANCELAMENTO = "status_cancelamento"
CCE = "cce"
STATUS_CCE = "status_cce"
RESUMO = "resumo"
class NFSeMetodo(str, Enum):
POST = "post"
GET = "get"
DOWNLOAD = "download"
DOWNLOAD_XML = "download_xml"
CANCELAR = "cancelar"
STATUS_CANCELAMENTO = "status_cancelamento"
RESUMO = "resumo"
HISTORICO = "historico"
class CertificadoMetodo(str, Enum):
POST = "post"
GET = "get"
PUT = "put"
class EmpresaMetodo(str, Enum):
POST = "post"
GET = "get"
POST_FILE = "post_file"
PATCH = "patch"
class ServicoMetodo(str, Enum):
POST = "post"
GET = "get"
PUT = "put"
DELETE = "delete"
class TomadorMetodo(str, Enum):
POST = "post"
GET = "get"
PUT = "put"
DELETE = "delete"
class RelatorioMetodo(str, Enum):
NFE_EMITIDAS = "nfe_emitidas"
NFE_POR_CNPJ = "nfe_por_cnpj"
NFSE_EMITIDAS = "nfse_emitidas"
NFSE_POR_CNPJ = "nfse_por_cnpj"
class WebhookMetodo(str, Enum):
GET = "get"
POST = "post"
DELETE = "delete"
PUT = "put"
TEST = "test"
class BaseEnvelope(BaseModel):
cliente: str | None = None
config: Config
url_base: str | None = None
# --- Base Data Models ---
class BaseDados(BaseModel):
model_config = ConfigDict(populate_by_name=True)
id_usuario: int | None = 1
class NFePostDadosWrapped(BaseDados):
nfe: NFe
id_empresa: str | None = None
class NFePostDadosDirect(NFe):
id_usuario: int | None = 1
id_empresa: str | None = None
class NFeIdDados(BaseDados):
id: str
class NFeCancelarDados(NFeIdDados):
justificativa: str | None = None
class NFeCCEDados(NFeIdDados):
correcao: str
class NFeResumoDados(BaseDados):
cnpj: str
id_integracao: str | None = None
id: str | None = None
class NFSePostDados(NFSe):
id_usuario: int | None = 1
id_empresa: str | None = None
class NFSeIdDados(BaseDados):
id: str
class NFSeGetDados(BaseDados):
id: str | None = None
class NFSeResumoDados(NFSeIdDados):
cnpj: str
class NFSeHistoricoDados(NFSeIdDados):
cnpj: str | None = None
class CertificadoPostDados(BaseDados):
arquivo: str
senha: str
id_empresa: str | None = None
email: str | None = None
expiration_at: str | None = Field(None, alias="expirationAt")
data_expiracao: str | None = None
class CertificadoGetDados(BaseDados):
id: str | None = None
class CertificadoPutDados(CertificadoPostDados):
id: str
class EmpresaPostDados(Empresa):
id_usuario: int | None = 1
class EmpresaGetDados(BaseDados):
cnpj: str | None = None
class EmpresaPostFileDados(BaseDados):
file: str
cnpj: str
class EmpresaPatchDados(BaseDados):
cnpj: str
certificado: str
class ServicoPostDados(Servico):
id_usuario: int | None = 1
class ServicoGetDados(BaseDados):
id: str | None = None
class ServicoPutDados(Servico):
id: str
id_usuario: int | None = 1
class ServicoDeleteDados(BaseDados):
id: str
class TomadorPostDados(Tomador):
id_usuario: int | None = 1
class TomadorGetDados(BaseDados):
cpf_cnpj: str | None = None
class TomadorPutDados(Tomador):
cpf_cnpj: str
id_usuario: int | None = 1
class TomadorDeleteDados(BaseDados):
cpf_cnpj: str
class RelatorioVazioDados(BaseDados):
pass
class RelatorioCnpjDados(BaseDados):
cnpj: str
class RelatorioPeriodoDados(BaseDados):
data_inicial: str | None = None
data_final: str | None = None
class RelatorioCnpjPeriodoDados(RelatorioPeriodoDados):
cnpj: str
class WebhookGetDeleteTestDados(BaseDados):
cnpj: str
class WebhookPostPutDados(WebhookGetDeleteTestDados):
url: str
# --- Envelopes ---
class NFePostEnvelope(BaseEnvelope):
fila: Fila = Fila.NFE
metodo: NFeMetodo = NFeMetodo.POST
dados: NFePostDadosDirect | NFePostDadosWrapped
class NFeIdEnvelope(BaseEnvelope):
fila: Fila = Fila.NFE
metodo: NFeMetodo
dados: NFeIdDados
class NFeCancelarEnvelope(BaseEnvelope):
fila: Fila = Fila.NFE
metodo: NFeMetodo = NFeMetodo.CANCELAR
dados: NFeCancelarDados
class NFeCCEEnvelope(BaseEnvelope):
fila: Fila = Fila.NFE
metodo: NFeMetodo = NFeMetodo.CCE
dados: NFeCCEDados
class NFeResumoEnvelope(BaseEnvelope):
fila: Fila = Fila.NFE
metodo: NFeMetodo = NFeMetodo.RESUMO
dados: NFeResumoDados
class NFSePostEnvelope(BaseEnvelope):
fila: Fila = Fila.NFSE
metodo: NFSeMetodo = NFSeMetodo.POST
dados: NFSePostDados
class NFSeGetEnvelope(BaseEnvelope):
fila: Fila = Fila.NFSE
metodo: NFSeMetodo = NFSeMetodo.GET
dados: NFSeGetDados
class NFSeIdEnvelope(BaseEnvelope):
fila: Fila = Fila.NFSE
metodo: NFSeMetodo
dados: NFSeIdDados
class NFSeResumoEnvelope(BaseEnvelope):
fila: Fila = Fila.NFSE
metodo: NFSeMetodo = NFSeMetodo.RESUMO
dados: NFSeResumoDados
class NFSeHistoricoEnvelope(BaseEnvelope):
fila: Fila = Fila.NFSE
metodo: NFSeMetodo = NFSeMetodo.HISTORICO
dados: NFSeHistoricoDados
class CertificadoPostEnvelope(BaseEnvelope):
fila: Fila = Fila.CERTIFICADO_DIGITAL
metodo: CertificadoMetodo = CertificadoMetodo.POST
dados: CertificadoPostDados
class CertificadoGetEnvelope(BaseEnvelope):
fila: Fila = Fila.CERTIFICADO_DIGITAL
metodo: CertificadoMetodo = CertificadoMetodo.GET
dados: CertificadoGetDados
class CertificadoPutEnvelope(BaseEnvelope):
fila: Fila = Fila.CERTIFICADO_DIGITAL
metodo: CertificadoMetodo = CertificadoMetodo.PUT
dados: CertificadoPutDados
class EmpresaPostEnvelope(BaseEnvelope):
fila: Fila = Fila.EMPRESA
metodo: EmpresaMetodo = EmpresaMetodo.POST
dados: EmpresaPostDados
class EmpresaGetEnvelope(BaseEnvelope):
fila: Fila = Fila.EMPRESA
metodo: EmpresaMetodo = EmpresaMetodo.GET
dados: EmpresaGetDados
class EmpresaPostFileEnvelope(BaseEnvelope):
fila: Fila = Fila.EMPRESA
metodo: EmpresaMetodo = EmpresaMetodo.POST_FILE
dados: EmpresaPostFileDados
class EmpresaPatchEnvelope(BaseEnvelope):
fila: Fila = Fila.EMPRESA
metodo: EmpresaMetodo = EmpresaMetodo.PATCH
dados: EmpresaPatchDados
class ServicoPostEnvelope(BaseEnvelope):
fila: Fila = Fila.SERVICO
metodo: ServicoMetodo = ServicoMetodo.POST
dados: ServicoPostDados
class ServicoGetEnvelope(BaseEnvelope):
fila: Fila = Fila.SERVICO
metodo: ServicoMetodo = ServicoMetodo.GET
dados: ServicoGetDados
class ServicoPutEnvelope(BaseEnvelope):
fila: Fila = Fila.SERVICO
metodo: ServicoMetodo = ServicoMetodo.PUT
dados: ServicoPutDados
class ServicoDeleteEnvelope(BaseEnvelope):
fila: Fila = Fila.SERVICO
metodo: ServicoMetodo = ServicoMetodo.DELETE
dados: ServicoDeleteDados
class TomadorPostEnvelope(BaseEnvelope):
fila: Fila = Fila.TOMADOR
metodo: TomadorMetodo = TomadorMetodo.POST
dados: TomadorPostDados
class TomadorGetEnvelope(BaseEnvelope):
fila: Fila = Fila.TOMADOR
metodo: TomadorMetodo = TomadorMetodo.GET
dados: TomadorGetDados
class TomadorPutEnvelope(BaseEnvelope):
fila: Fila = Fila.TOMADOR
metodo: TomadorMetodo = TomadorMetodo.PUT
dados: TomadorPutDados
class TomadorDeleteEnvelope(BaseEnvelope):
fila: Fila = Fila.TOMADOR
metodo: TomadorMetodo = TomadorMetodo.DELETE
dados: TomadorDeleteDados
class RelatorioNFeEmitidasEnvelope(BaseEnvelope):
fila: Fila = Fila.RELATORIOS
metodo: RelatorioMetodo = RelatorioMetodo.NFE_EMITIDAS
dados: RelatorioVazioDados
class RelatorioNFeCnpjEnvelope(BaseEnvelope):
fila: Fila = Fila.RELATORIOS
metodo: RelatorioMetodo = RelatorioMetodo.NFE_POR_CNPJ
dados: RelatorioCnpjDados
class RelatorioNFSeEmitidasEnvelope(BaseEnvelope):
fila: Fila = Fila.RELATORIOS
metodo: RelatorioMetodo = RelatorioMetodo.NFSE_EMITIDAS
dados: RelatorioPeriodoDados
class RelatorioNFSeCnpjEnvelope(BaseEnvelope):
fila: Fila = Fila.RELATORIOS
metodo: RelatorioMetodo = RelatorioMetodo.NFSE_POR_CNPJ
dados: RelatorioCnpjPeriodoDados
class WebhookGetDeleteTestEnvelope(BaseEnvelope):
fila: Fila = Fila.WEBHOOKS
metodo: WebhookMetodo
dados: WebhookGetDeleteTestDados
class WebhookPostPutEnvelope(BaseEnvelope):
fila: Fila = Fila.WEBHOOKS
metodo: WebhookMetodo
dados: WebhookPostPutDados
# --- Generic Envelope for basic structural validation ---
class GenericEnvelope(BaseEnvelope):
"""Validates that minimum required fields exist before routing."""
fila: Fila
metodo: str
dados: dict | None = None
# --- Payload Validation Router ---
# Maps (fila, metodo) -> Envelope class
_ENVELOPE_MAP: dict[tuple[str, str], type[BaseEnvelope]] = {
# NFe
("nfe", "post"): NFePostEnvelope,
("nfe", "get"): NFeIdEnvelope,
("nfe", "download"): NFeIdEnvelope,
("nfe", "download_xml"): NFeIdEnvelope,
("nfe", "cancelar"): NFeCancelarEnvelope,
("nfe", "status_cancelamento"): NFeIdEnvelope,
("nfe", "cce"): NFeCCEEnvelope,
("nfe", "status_cce"): NFeIdEnvelope,
("nfe", "resumo"): NFeResumoEnvelope,
# NFSe
("nfse", "post"): NFSePostEnvelope,
("nfse", "get"): NFSeGetEnvelope,
("nfse", "download"): NFSeIdEnvelope,
("nfse", "download_xml"): NFSeIdEnvelope,
("nfse", "cancelar"): NFSeIdEnvelope,
("nfse", "status_cancelamento"): NFSeIdEnvelope,
("nfse", "resumo"): NFSeResumoEnvelope,
("nfse", "historico"): NFSeHistoricoEnvelope,
# Certificado Digital
("certificado_digital", "post"): CertificadoPostEnvelope,
("certificado_digital", "get"): CertificadoGetEnvelope,
("certificado_digital", "put"): CertificadoPutEnvelope,
# Empresa
("empresa", "post"): EmpresaPostEnvelope,
("empresa", "get"): EmpresaGetEnvelope,
("empresa", "post_file"): EmpresaPostFileEnvelope,
("empresa", "patch"): EmpresaPatchEnvelope,
# Serviço
("servico", "post"): ServicoPostEnvelope,
("servico", "get"): ServicoGetEnvelope,
("servico", "put"): ServicoPutEnvelope,
("servico", "delete"): ServicoDeleteEnvelope,
# Tomador
("tomador", "post"): TomadorPostEnvelope,
("tomador", "get"): TomadorGetEnvelope,
("tomador", "put"): TomadorPutEnvelope,
("tomador", "delete"): TomadorDeleteEnvelope,
# Relatórios
("relatorios", "nfe_emitidas"): RelatorioNFeEmitidasEnvelope,
("relatorios", "nfe_por_cnpj"): RelatorioNFeCnpjEnvelope,
("relatorios", "nfse_emitidas"): RelatorioNFSeEmitidasEnvelope,
("relatorios", "nfse_por_cnpj"): RelatorioNFSeCnpjEnvelope,
# Webhooks
("webhooks", "get"): WebhookGetDeleteTestEnvelope,
("webhooks", "post"): WebhookPostPutEnvelope,
("webhooks", "delete"): WebhookGetDeleteTestEnvelope,
("webhooks", "put"): WebhookPostPutEnvelope,
("webhooks", "test"): WebhookGetDeleteTestEnvelope,
}
def validate_payload(payload_dict: dict) -> BaseEnvelope:
# Step 1: Basic structural validation
generic = GenericEnvelope(**payload_dict)
fila = generic.fila.value
metodo = generic.metodo.lower()
# Step 2: Find specific envelope
key = (fila, metodo)
envelope_cls = _ENVELOPE_MAP.get(key)
if envelope_cls is None:
raise ValueError(f"Combinação fila/metodo desconhecida: {fila}/{metodo}")
# Step 3: Normalize metodo in the dict to match enum lowercase values
payload_dict['metodo'] = metodo
# Step 4: Full typed validation
return envelope_cls(**payload_dict)

View file

@ -0,0 +1,150 @@
from pydantic import BaseModel
from enum import IntEnum
class TipoTributacao(IntEnum):
NAO_DEFINIDO = 0
ISENTO_ISS = 1
IMUNE = 2
NAO_INCIDENCIA = 3
NAO_TRIBUTAVEL = 4
RETIDO = 5
TRIBUTAVEL_MUNICIPIO = 6
TRIBUTAVEL_FORA_MUNICIPIO = 7
TRIBUTAVEL_MUNICIPIO_TOMADOR = 8
class TipoExigibilidade(IntEnum):
EXIGIVEL = 1
NAO_INCIDENCIA = 2
ISENCAO = 3
EXPORTACAO = 4
IMUNIDADE = 5
SUSPENSO_ACAO_JUDICIAL = 6
SUSPENSO_ACAO_ADMINISTRATIVA = 7
class ISS(BaseModel):
tipoTributacao: int #TipoTributacao
exigibilidade: int #TipoExigibilidade
retido: bool | None = None # Se o ISS foi retido na fonte
aliquota: float # Aliquota do ISS
valor: float # Valor do ISS
valorRetido: float # Valor do ISS retido
processoSuspensao: str # Numero processo de suspensão do ISS
##############################################################################
# tipoTributacao integer
# Default: 6
# Enum: 0 1 2 3 4 5 6 7 8
# Tipo de Tributação do Serviço, valores:
# 0 - Não definido
# 1 - Isento de ISS
# 2 - Imune
# 3 - Não Incidência no Município
# 4 - Não Tributável
# 5 - Retido
# 6 - Tributável Dentro do Município
# 7 - Tributável Fora do Município
# 8 - Tributável Dentro do Município pelo tomador
##############################################################################
# exigibilidade integer
# Default: 1
# Enum: 1 2 3 4 5 6 7
# Exigibilidade do ISS, valores:
# 1 - Exigível
# 2 - Não Incidência
# 3 - Isenção
# 4 - Exportação
# 5 - Imunidade
# 6 - Supenso por Ação Judicial
# 7 - Suspenso por Ação Administrativa
##############################################################################
# responsavelRetencao string
# Valores aceitos:
# 1 - Tomador
# 2 - Intermediário
##############################################################################
class Obra(BaseModel):
art: str # Anotação de Responsabilidade Técnica (Codigo ART)
codigo: str # Código da obra
class Valor(BaseModel):
servico: float # Valor bruto do serviço prestado.
baseCalculo: float # Valor da base de cálculo dos impostos.
deducoes: float # Valor deduções.
descontoCondicionado: float # Valor do desconto condicionado.
descontoIncondicionado: float # Valor do desconto incondicionado.
liquido: float # Valor liquido do serviço, caso você não informe esse campo, vamos calcular automáticamente.
unitario: float # Valor unitário do serviço.
valorAproximadoTributos: float # Valor aproximado dos tributos.
class Deducao(BaseModel):
tipo: int
descricao: str # Descrição da dedução
##############################################################################
# tipo integer
# Tipo de dedução:
# 0 - Sem Deduções
# 1 - Materiais
# 2 - Subempreitada de Mão de Obra
# 3 - Serviços
# 4 - Produção Externa
# 5 - Alimentação e Bebidas
# 6 - Reembolso de Despesas
# 7 - Repasse Consorciado
# 8 - Repasse Plano de Saúde
# 99 - Outras Deduções
##############################################################################
class RetencaoInterno(BaseModel):
aliquota: float # Aliquota da retenção
valor: float # Valor da retenção
class Retencao(BaseModel):
pis: RetencaoInterno
cofins: RetencaoInterno
csll: RetencaoInterno
inss: RetencaoInterno
irrf: RetencaoInterno
outrasRetencoes: float # Valor de outras retenções
cpp: RetencaoInterno
class Aliquota(BaseModel):
municipal: float # Aliquota municipal
estadual: float # Aliquota estadual
federal: float # Aliquota federal
class Detalhado(BaseModel):
aliquota: Aliquota
class Simplificado(BaseModel):
aliquota: float # Aliquota geral de impostos
class IBPT(BaseModel):
simplificado: Simplificado
detalhado: Detalhado
# Representa o detalhamento do serviço prestado na NFSe, impostos incidentes e deduções
class Servico(BaseModel):
codigo: str # Código de serviço, também conhecido como Código Item Lista Serviço, geralmente segue a LC116, podendo váriar de acordo com a prefeitura.Você pode encontrar esse dado no portal da prefeitura, em uma nota emitida, ou junto ao contador.
idIntegracao: str # Código EXTERNO de integração do serviço. Utilizado SOMENTE quando se cadastra previamente um serviço.
discriminacao: str # Descrição do serviço prestado
codigoTributacao: str # Código tributação no Município
cnae: str # O código CNAE ou, por extenso, Classificação Nacional de Atividades Econômicas, é uma forma de padronizar, em todo o território nacional, os códigos de atividades econômicas e os critérios de enquadramento usados pelos mais diversos órgãos da administração tributária do Brasil.
codigoCidadeIncidencia: str # Código IBGE da cidade de incidência do ISS.
descricaoCidadeIncidencia: str # Descrição da cidade de incidência do ISS.
unidade: str # Unidade do serviço prestado.
quantidade: int # Quantidade dos serviços prestados.
iss: ISS
obra: Obra
valor: Valor
deducao: Deducao
retencao: Retencao
tributavel: bool # Indica se o serviço está sujeito a tributação.
ibpt: IBPT
responsavelRetencao: str
##############################################################################
# responsavelRetencao Valores aceitos:
# 1 - Tomador
# 2 - Intermediário
# Caso o responsável pela retenção seja o próprio prestador, não gerar o campo.
##############################################################################

View file

@ -0,0 +1,34 @@
from pydantic import BaseModel
class Endereco(BaseModel):
tipoLogradouro: str
logradouro: str
numero: str
complemento: str
tipoBairro: str
bairro: str
codigoPais: str
descricaoPais: str
codigoCidade: str
descricaoCidade: str
estado: str
cep: str
class Telefone(BaseModel):
ddd: str
numero: str
# Representa o cliente (Tomador do Serviço) vinculado a uma nota fiscal de serviço
class Tomador(BaseModel):
cpfCnpj: str
inscricaoMunicipal: str
inscricaoEstadual: str
inscricaoSuframa: str
indicadorInscricaoEstadual: int
razaoSocial: str
nomeFantasia: str
endereco: Endereco
telefone: Telefone
email: str
orgaoPublico: bool

View file

@ -0,0 +1,105 @@
from Models.servico import Servico
from utils.api import Api
from utils.banco import Banco, ServicoTable
from utils.Exchange2 import LoggerExchange2
from utils.exchangeModels import RealmType
from sqlalchemy.exc import SQLAlchemyError
import json
# Controller responsável pelos cadastros e busca de Serviços a serem vinculados em NFSe
class ServicoController:
def __init__(self, url_base, api_key, banco: Banco, logger: LoggerExchange2 = None):
self.api = Api(api_key)
self.url = f"{url_base}/nfse/servico"
self.banco = banco
self.logger = logger
def _log(self, realm: RealmType, msg: str):
if self.logger:
self.logger.logIC2(realm, (msg,))
def cadastrar_servico(self, servico: Servico, data: dict):
try:
resposta = self.api.post(self.url, servico.model_dump())
if 'error' in resposta:
self._log(RealmType.ERRORS, f"Erro na API ao cadastrar serviço")
return resposta
servico_novo = ServicoTable(
descricao=servico.discriminacao,
id_tecnospeed=resposta['data']['id'],
id_usuario=data.get('id_usuario', 1),
status=True
)
self.banco.add(servico_novo)
self._log(RealmType.OPERATIONAL, f"Serviço cadastrado com sucesso - ID: {resposta['data']['id']}")
return resposta
except SQLAlchemyError as e:
self.banco.rollback()
self._log(RealmType.ERRORS, f"Erro no banco ao cadastrar serviço: {e}")
return {"error": f"Erro ao cadastrar serviço: {str(e)}"}
except Exception as e:
return {"error": f"Erro inesperado: {str(e)}"}
def buscar_servico(self, id_servico, data: dict = None):
try:
resposta = self.api.get(f"{self.url}/{id_servico}")
self._log(RealmType.OPERATIONAL, f"Serviço consultado - ID: {id_servico}")
return resposta
except Exception as e:
self._log(RealmType.ERRORS, f"Erro ao buscar serviço: {e}")
return {"error": f"Erro ao buscar serviço: {str(e)}"}
def listar_servicos(self, data: dict = None):
try:
servicos = self.banco.session.query(ServicoTable.id_tecnospeed, ServicoTable.descricao, ServicoTable.status).all()
resultado = [{"id_tecnospeed": servico.id_tecnospeed, "descricao": servico.descricao, "status": servico.status} for servico in servicos]
self._log(RealmType.OPERATIONAL, f"Listagem de serviços - {len(resultado)} registros")
return resultado
except SQLAlchemyError as e:
self._log(RealmType.ERRORS, f"Erro ao listar serviços: {e}")
return {"error": f"Erro ao listar serviços: {str(e)}"}
except Exception as e:
return {"error": f"Erro inesperado: {str(e)}"}
def atualizar_servico(self, id_servico, servico: Servico):
try:
resposta = self.api.patch(f"{self.url}/{id_servico}", servico.model_dump())
servico_existente = self.banco.session.query(ServicoTable).filter_by(id=id_servico).first()
if not servico_existente:
self._log(RealmType.ERRORS, f"Serviço não encontrado - ID: {id_servico}")
return {"error": "Serviço não encontrado"}
servico_existente.descricao = servico.discriminacao
self.banco.session.commit()
self._log(RealmType.OPERATIONAL, f"Serviço atualizado com sucesso - ID: {id_servico}")
return resposta
except SQLAlchemyError as e:
self.banco.rollback()
self._log(RealmType.ERRORS, f"Erro ao atualizar serviço: {e}")
return {"error": f"Erro ao atualizar serviço: {str(e)}"}
except Exception as e:
return {"error": f"Erro inesperado: {str(e)}"}
def deletar_servico(self, id_servico):
try:
resposta = self.api.delete(f"{self.url}/{id_servico}")
servico_existente = self.banco.session.query(ServicoTable).filter_by(id=id_servico).first()
if not servico_existente:
self._log(RealmType.ERRORS, f"Serviço não encontrado para deletar - ID: {id_servico}")
return {"error": "Serviço não encontrado"}
self.banco.session.delete(servico_existente)
self.banco.session.commit()
self._log(RealmType.OPERATIONAL, f"Serviço deletado com sucesso - ID: {id_servico}")
return resposta
except SQLAlchemyError as e:
self.banco.rollback()
self._log(RealmType.ERRORS, f"Erro ao deletar serviço: {e}")
return {"error": f"Erro ao deletar serviço: {str(e)}"}
except Exception as e:
return {"error": f"Erro inesperado: {str(e)}"}

View file

@ -0,0 +1,91 @@
from utils.api import Api
from utils.banco import Banco, TomadorTable
from utils.Exchange2 import LoggerExchange2
from utils.exchangeModels import RealmType
from Models.tomador import Tomador
import json
# Controller para gerenciamento no banco e na API dos Tomadores (clientes finais) das notas
class TomadorController:
def __init__(self, url_base, api_key, banco: Banco, logger: LoggerExchange2 = None):
self.api = Api(api_key)
self.url = f"{url_base}/nfse/tomador"
self.banco = banco
self.logger = logger
def _log(self, realm: RealmType, msg: str):
if self.logger:
self.logger.logIC2(realm, (msg,))
def cadastrar_tomador(self, tomador: Tomador, data: dict):
try:
retorno = self.api.post(self.url, tomador.model_dump())
if 'error' in retorno:
self._log(RealmType.ERRORS, f"Erro na API ao cadastrar tomador")
return retorno
tomador_novo = TomadorTable(
id_tecnospeed=retorno['id'],
nome=tomador.razaoSocial,
cpf_cnpj=tomador.cpfCnpj,
email=tomador.email,
telefone=f"({tomador.telefone.ddd}) {tomador.telefone.numero}",
id_usuario=data.get('id_usuario', 1),
status=True
)
self.banco.add(tomador_novo)
self._log(RealmType.OPERATIONAL, f"Tomador cadastrado com sucesso - CPF/CNPJ: {tomador.cpfCnpj}")
return retorno
except Exception as e:
self._log(RealmType.ERRORS, f"Erro ao cadastrar tomador: {e}")
return {"error": str(e)}
def buscar_tomador(self, cpf_cnpj):
try:
tomador = self.api.get(f"{self.url}/{cpf_cnpj}")
self._log(RealmType.OPERATIONAL, f"Tomador consultado - CPF/CNPJ: {cpf_cnpj}")
return tomador
except Exception as e:
self._log(RealmType.ERRORS, f"Erro ao buscar tomador: {e}")
return {"error": str(e)}
def atualizar_tomador(self, cpf_cnpj, tomador: Tomador):
try:
tomador_existente = self.banco.session.query(TomadorTable).filter_by(cpf_cnpj=cpf_cnpj).first()
if not tomador_existente:
self._log(RealmType.ERRORS, f"Tomador não encontrado - CPF/CNPJ: {cpf_cnpj}")
return {"error": "Tomador não encontrado"}
resposta = self.api.patch(f"{self.url}/{cpf_cnpj}", tomador.model_dump())
tomador_existente.nome = tomador.razaoSocial
tomador_existente.email = tomador.email
tomador_existente.telefone = f"({tomador.telefone.ddd}) {tomador.telefone.numero}"
self.banco.session.commit()
self._log(RealmType.OPERATIONAL, f"Tomador atualizado com sucesso - CPF/CNPJ: {cpf_cnpj}")
return resposta
except Exception as e:
self.banco.session.rollback()
self._log(RealmType.ERRORS, f"Erro ao atualizar tomador: {e}")
return {"error": str(e)}
def deletar_tomador(self, cpf_cnpj):
try:
tomador_existente = self.banco.session.query(TomadorTable).filter_by(cpf_cnpj=cpf_cnpj).first()
if not tomador_existente:
self._log(RealmType.ERRORS, f"Tomador não encontrado para deletar - CPF/CNPJ: {cpf_cnpj}")
return {"error": "Tomador não encontrado"}
resposta = self.api.delete(f"{self.url}/{cpf_cnpj}")
self.banco.session.delete(tomador_existente)
self.banco.session.commit()
self._log(RealmType.OPERATIONAL, f"Tomador deletado com sucesso - CPF/CNPJ: {cpf_cnpj}")
return resposta
except Exception as e:
self.banco.session.rollback()
self._log(RealmType.ERRORS, f"Erro ao deletar tomador: {e}")
return {"error": str(e)}

167
backup/src/NFSe/nfse.py Normal file
View file

@ -0,0 +1,167 @@
from Models.nfse import NFSe
from utils.api import Api
from utils.banco import Banco, NFSeTable
from utils.Exchange2 import LoggerExchange2
from utils.exchangeModels import RealmType
from utils.notificador import NotificadorInterno
import json
# Serviço responsável pelo controle, emissão e cancelamento das Notas Fiscais de Serviço Eletrônicas (NFSe)
class NFSeService:
def __init__(self, api_key, url_base, banco: Banco, logger: LoggerExchange2 = None):
self.api = Api(api_key)
self.url = f"{url_base}/nfse"
self.banco = banco
self.logger = logger
self.notificador = NotificadorInterno(logger=logger) if logger else None
def _log(self, realm: RealmType, msg: str):
if self.logger:
self.logger.logIC2(realm, (msg,))
def cadastrar_nfse(self, nfse: NFSe, data: dict):
try:
id_empresa = data.get('id_empresa', "0")
resposta = self.api.post(self.url, [nfse.model_dump()])
if 'error' in resposta:
self._log(RealmType.ERRORS, f"Erro na API ao cadastrar NFSe: {resposta.get('error')}")
return resposta
# A API da Plugnotas retorna 'documents' contendo uma lista com os sucessos
documentos = resposta.get('documents', [])
if not documentos:
self._log(RealmType.ERRORS, f"Nenhum documento retornado pela API. Resposta: {resposta}")
return resposta
id_recebido = documentos[0].get('id')
if not id_recebido:
raise Exception("ID do documento não encontrado na resposta.")
nfse_nova = NFSeTable(
id_tecnospeed=id_recebido,
id_empresa=id_empresa,
id_usuario=data.get('id_usuario', 1),
status=True
)
self.banco.add(nfse_nova)
self._log(RealmType.OPERATIONAL, f"NFSe cadastrada com sucesso - ID: {id_recebido}")
# Notifica o listener interno para sincronização imediata
if self.notificador:
self.notificador.notificar_emissao(id_tecnospeed=id_recebido, documento="nfse", cnpj=id_empresa)
return resposta
except Exception as e:
self._log(RealmType.ERRORS, f"Erro ao cadastrar NFSe: {e}")
raise Exception(f"Erro ao cadastrar NFSe: {e}")
def buscar_nfse(self, id_nfse):
try:
resposta = self.api.get(f"{self.url}/{id_nfse}")
self._log(RealmType.OPERATIONAL, f"NFSe consultada com sucesso - ID: {id_nfse}")
return resposta
except Exception as e:
self._log(RealmType.ERRORS, f"Erro ao buscar NFSe: {e}")
raise Exception(f"Erro ao buscar NFSe: {e}")
def listar_nfse(self):
try:
nfse = self.banco.session.query(NFSeTable.id, NFSeTable.id_tecnospeed).all()
resultado = [{"id": n[0], "descricao": n[1]} for n in nfse]
self._log(RealmType.OPERATIONAL, f"Listagem de NFSe - {len(resultado)} registros")
return resultado
except Exception as e:
self._log(RealmType.ERRORS, f"Erro ao listar NFSe: {e}")
raise Exception(f"Erro ao listar NFSe: {e}")
def baixar_nfse(self, id_nfse):
try:
resposta = self.api.get(f"{self.url}/pdf/{id_nfse}")
self._log(RealmType.OPERATIONAL, f"PDF NFSe baixado com sucesso - ID: {id_nfse}")
return resposta
except Exception as e:
self._log(RealmType.ERRORS, f"Erro ao baixar NFSe: {e}")
raise Exception(f"Erro ao baixar NFSe: {e}")
def baixar_xml_nfse(self, id_nfse):
try:
resposta = self.api.get(f"{self.url}/xml/{id_nfse}")
self._log(RealmType.OPERATIONAL, f"XML NFSe baixado com sucesso - ID: {id_nfse}")
return resposta
except Exception as e:
self._log(RealmType.ERRORS, f"Erro ao baixar XML NFSe: {e}")
raise Exception(f"Erro ao baixar XML NFSe: {e}")
def solicitar_cancelamento_nfse(self, id_nfse):
try:
resposta = self.api.post(f"{self.url}/cancelar/{id_nfse}", {})
self._log(RealmType.OPERATIONAL, f"Cancelamento NFSe solicitado - ID: {id_nfse}")
return resposta
except Exception as e:
self._log(RealmType.ERRORS, f"Erro ao solicitar cancelamento NFSe: {e}")
raise Exception(f"Erro ao solicitar cancelamento NFSe: {e}")
def consultar_status_cancelamento_nfse(self, id_nfse):
try:
resposta = self.api.get(f"{self.url}/cancelar/status/{id_nfse}")
self._log(RealmType.OPERATIONAL, f"Status cancelamento NFSe consultado - ID: {id_nfse}")
return resposta
except Exception as e:
self._log(RealmType.ERRORS, f"Erro ao consultar status cancelamento NFSe: {e}")
raise Exception(f"Erro ao consultar status cancelamento NFSe: {e}")
def consultar_resumo_nfse(self, id_nfse, cnpj):
try:
resposta = self.api.get(f"{self.url}/consultar/{id_nfse}/{cnpj}")
self._log(RealmType.OPERATIONAL, f"Resumo NFSe consultado - ID: {id_nfse}")
return resposta
except Exception as e:
self._log(RealmType.ERRORS, f"Erro ao consultar resumo NFSe: {e}")
raise Exception(f"Erro ao consultar resumo NFSe: {e}")
def baixar_histotico_nfse(self, id_nfse, cnpj = None):
try:
resposta = self.api.get(f"{self.url}/historico/{id_nfse}") if cnpj is None else self.api.get(f"{self.url}/historico/{id_nfse}/{cnpj}")
self._log(RealmType.OPERATIONAL, f"Histórico NFSe baixado - ID: {id_nfse}")
return resposta
except Exception as e:
self._log(RealmType.ERRORS, f"Erro ao baixar histórico NFSe: {e}")
raise Exception(f"Erro ao baixar histórico NFSe: {e}")
def enviar_email_nfse(self, id_nfse, email:list[str], reenvio:bool = False):
try:
resposta = self.api.post(f"{self.url}/email/{id_nfse}", {"reenvio": reenvio,"destinatarios": email})
self._log(RealmType.OPERATIONAL, f"Email NFSe enviado para {email} - ID: {id_nfse}")
return resposta
except Exception as e:
self._log(RealmType.ERRORS, f"Erro ao enviar email NFSe: {e}")
raise Exception(f"Erro ao enviar email NFSe: {e}")
def resolver_nfse(self, id_nfse, identificacaoNota:str):
try:
resposta = self.api.post(f"{self.url}/resolve/{id_nfse}", {"identificacaoNota": identificacaoNota})
self._log(RealmType.OPERATIONAL, f"NFSe resolvida com sucesso - ID: {id_nfse}")
return resposta
except Exception as e:
self._log(RealmType.ERRORS, f"Erro ao resolver NFSe: {e}")
raise Exception(f"Erro ao resolver NFSe: {e}")
def consultar_nfse_periodo(self, data_inicio, data_fim):
try:
resposta = self.api.get_paginated(f"{self.url}/periodo/{data_inicio}/{data_fim}")
self._log(RealmType.OPERATIONAL, f"NFSe consultada por período: {data_inicio} a {data_fim}")
return resposta
except Exception as e:
self._log(RealmType.ERRORS, f"Erro ao consultar NFSe por período: {e}")
raise Exception(f"Erro ao consultar NFSe por período: {e}")
def baixar_recibo_provisorio_servico(self, id_nfse, cnpj = None):
try:
resposta = self.api.get(f"{self.url}/rps/pdf/{id_nfse}") if cnpj is None else self.api.get(f"{self.url}/rps/pdf/{id_nfse}/{cnpj}")
self._log(RealmType.OPERATIONAL, f"RPS baixado com sucesso - ID: {id_nfse}")
return resposta
except Exception as e:
self._log(RealmType.ERRORS, f"Erro ao baixar recibo provisório de serviço: {e}")
raise Exception(f"Erro ao baixar recibo provisório de serviço: {e}")

View file

@ -0,0 +1,126 @@
import json as json_lib
from utils.api import Api
from utils.banco import Banco, NFeTable
from Models.nfe import NFe
from typing import List
from utils.Exchange2 import LoggerExchange2
from utils.exchangeModels import RealmType
from utils.notificador import NotificadorInterno
class RegistroNFeService:
# Classe responsável por fornecer as operações de comunicação com a PlugNotas para o documento NFe
def __init__(self, api_key: str, url_base: str, banco: Banco, logger: LoggerExchange2 = None):
self.api = Api(api_key)
self.url = f"{url_base}/nfe"
self.banco = banco
self.logger = logger
self.notificador = NotificadorInterno(logger=logger) if logger else None
def cadastrar_nfe(self, nfe_list: List[NFe], data: dict):
# Envia uma lista de NFes em lote ou unitária para emissão e registra no banco de dados
try:
id_empresa = data.get('id_empresa', "0")
payload = [nfe.model_dump() for nfe in nfe_list]
resposta = self.api.post(self.url, payload)
if 'error' in resposta:
id_usr = data.get('id_usuario', 1)
if self.logger: self.logger.logIC2(RealmType.OPERATIONAL, ('INSERT', 'nfe', id_usr, '0', 'ERRO', f"Erro na API: {resposta.get('error')}", json_lib.dumps(resposta)))
return resposta
if 'documents' in resposta:
for doc in resposta['documents']:
id_tecnospeed = doc.get('id')
if id_tecnospeed:
nfe_nova = NFeTable(
id_tecnospeed=id_tecnospeed,
id_empresa=id_empresa,
id_usuario=data.get('id_usuario', 1),
status=True
)
self.banco.add(nfe_nova)
id_usr = data.get('id_usuario', 1)
if self.logger: self.logger.logIC2(RealmType.OPERATIONAL, ('INSERT', 'nfe', id_usr, str(nfe_nova.id), 'SUCESSO', 'NFe cadastrada com sucesso', json_lib.dumps(doc)))
# Notifica o listener interno para sincronização imediata
if self.notificador:
self.notificador.notificar_emissao(id_tecnospeed=id_tecnospeed, documento="nfe", cnpj=id_empresa)
return resposta
except Exception as e:
if self.logger: self.logger.logIC2(RealmType.OPERATIONAL, ('INSERT', 'nfe', data.get('id_usuario', 1), '0', 'ERRO', f"Erro ao cadastrar NFe: {e}", "{}"))
raise Exception(f"Erro ao cadastrar NFe: {e}")
def cancelar_nfe(self, id_nota: str, justificativa: str):
# Solicita o cancelamento de uma NFe existente mediante uma justificativa
try:
resposta = self.api.post(f"{self.url}/{id_nota}/cancelamento", {"justificativa": justificativa})
if self.logger: self.logger.logIC2(RealmType.OPERATIONAL, ('DELETE', 'nfe', 1, id_nota, 'SUCESSO', 'NFe cancelada com sucesso', json_lib.dumps(resposta)))
return resposta
except Exception as e:
if self.logger: self.logger.logIC2(RealmType.OPERATIONAL, ('DELETE', 'nfe', 1, id_nota, 'ERRO', f"Erro ao cancelar NFe: {e}", "{}"))
raise Exception(f"Erro ao cancelar NFe: {e}")
def enviar_cce(self, id_nota: str, correcao: str):
# Envia uma Carta de Correção Eletrônica (CCe) para retificar uma NFe já emitida
try:
resposta = self.api.post(f"{self.url}/{id_nota}/cce", {"correcao": correcao})
if self.logger: self.logger.logIC2(RealmType.OPERATIONAL, ('UPDATE', 'nfe', 1, id_nota, 'SUCESSO', 'CCe enviada com sucesso', json_lib.dumps(resposta)))
return resposta
except Exception as e:
if self.logger: self.logger.logIC2(RealmType.OPERATIONAL, ('UPDATE', 'nfe', 1, id_nota, 'ERRO', f"Erro ao enviar CCe NFe: {e}", "{}"))
raise Exception(f"Erro ao enviar CCe NFe: {e}")
def consultar_resumo(self, id_nota: str):
# Consulta o resumo e o status atual da NFe na PlugNotas pelo seu ID
try:
resposta = self.api.get(f"{self.url}/{id_nota}/resumo")
if self.logger: self.logger.logIC2(RealmType.OPERATIONAL, ('SELECT', 'nfe', 1, id_nota, 'SUCESSO', 'Resumo de NFe consultado', json_lib.dumps(resposta)))
return resposta
except Exception as e:
if self.logger: self.logger.logIC2(RealmType.OPERATIONAL, ('SELECT', 'nfe', 1, id_nota, 'ERRO', f"Erro ao consultar resumo NFe: {e}", "{}"))
raise Exception(f"Erro ao consultar resumo NFe: {e}")
def consultar_resumo_por_cnpj_integracao(self, cnpj: str, id_integracao: str):
try:
resposta = self.api.get(f"{self.url}/{cnpj}/{id_integracao}/resumo")
if self.logger: self.logger.logIC2(RealmType.OPERATIONAL, ('SELECT', 'nfe', id_integracao, id_integracao, 'SUCESSO', 'Resumo por CNPJ/Integração consultado', json_lib.dumps(resposta)))
return resposta
except Exception as e:
if self.logger: self.logger.logIC2(RealmType.OPERATIONAL, ('SELECT', 'nfe', id_integracao, id_integracao, 'ERRO', f"Erro ao consultar resumo NFe por CNPJ/Integração: {e}", "{}"))
raise Exception(f"Erro ao consultar resumo NFe por CNPJ/Integração: {e}")
def consultar_status_cancelamento(self, id_nota: str):
try:
resposta = self.api.get(f"{self.url}/{id_nota}/cancelamento/status")
if self.logger: self.logger.logIC2(RealmType.OPERATIONAL, ('SELECT', 'nfe', id_nota, id_nota, 'SUCESSO', 'Status de cancelamento consultado', json_lib.dumps(resposta)))
return resposta
except Exception as e:
if self.logger: self.logger.logIC2(RealmType.OPERATIONAL, ('SELECT', 'nfe', id_nota, id_nota, 'ERRO', f"Erro ao consultar status de cancelamento NFe: {e}", "{}"))
raise Exception(f"Erro ao consultar status de cancelamento NFe: {e}")
def consultar_status_cce(self, id_nota: str):
try:
resposta = self.api.get(f"{self.url}/{id_nota}/cce/status")
if self.logger: self.logger.logIC2(RealmType.OPERATIONAL, ('SELECT', 'nfe', id_nota, id_nota, 'SUCESSO', 'Status CCe consultado', json_lib.dumps(resposta)))
return resposta
except Exception as e:
if self.logger: self.logger.logIC2(RealmType.OPERATIONAL, ('SELECT', 'nfe', id_nota, id_nota, 'ERRO', f"Erro ao consultar status CCe NFe: {e}", "{}"))
raise Exception(f"Erro ao consultar status CCe NFe: {e}")
def baixar_pdf(self, id_nota: str):
# Obtém o link para download ou o stream do PDF da Nota Fiscal processada
try:
resposta = self.api.get(f"{self.url}/{id_nota}/pdf")
if self.logger: self.logger.logIC2(RealmType.OPERATIONAL, ('DOWNLOAD (PDF)', 'nfe', id_nota, id_nota, 'SUCESSO', 'PDF baixado com sucesso', json_lib.dumps(resposta) if isinstance(resposta, dict) else '{}'))
return resposta
except Exception as e:
if self.logger: self.logger.logIC2(RealmType.OPERATIONAL, ('DOWNLOAD (PDF)', 'nfe', id_nota, id_nota, 'ERRO', f"Erro ao baixar PDF NFe: {e}", "{}"))
raise Exception(f"Erro ao baixar PDF NFe: {e}")
def baixar_xml(self, id_nota: str):
try:
resposta = self.api.get(f"{self.url}/{id_nota}/xml")
if self.logger: self.logger.logIC2(RealmType.OPERATIONAL, ('DOWNLOAD (XML)', 'nfe', id_nota, id_nota, 'SUCESSO', 'XML baixado com sucesso', json_lib.dumps(resposta) if isinstance(resposta, dict) else '{}'))
return resposta
except Exception as e:
if self.logger: self.logger.logIC2(RealmType.OPERATIONAL, ('DOWNLOAD (XML)', 'nfe', id_nota, id_nota, 'ERRO', f"Erro ao baixar XML NFe: {e}", "{}"))
raise Exception(f"Erro ao baixar XML NFe: {e}")

View file

@ -0,0 +1,70 @@
from utils.api import Api
from utils.banco import Banco
from utils.Exchange2 import LoggerExchange2
from utils.exchangeModels import RealmType
from datetime import datetime, timedelta
import json
# Extração e consumo de relatórios massivos de emissões diretamente da API e consolidações em lotes
class Relatorios:
def __init__(self, url_base, api_key, banco: Banco, logger: LoggerExchange2 = None):
self.api = Api(api_key)
self.url = f"{url_base}/"
self.banco = banco
self.logger = logger
def _log(self, realm: RealmType, msg: str):
if self.logger:
self.logger.logIC2(realm, (msg,))
def nfe_emitidas(self, data: dict = None):
try:
resposta = self.api.get_paginated(f"{self.url}nfe/relatorio")
self._log(RealmType.OPERATIONAL, "Relatório de NFe emitidas consultado")
return resposta
except Exception as e:
msg_erro = f"Erro ao consultar relatório NFe emitidas: {e}"
self._log(RealmType.ERRORS, msg_erro)
return {"error": msg_erro}
def nfe_por_cnpj(self, cnpj, data: dict = None):
try:
resposta = self.api.get_paginated(f"{self.url}nfe/{cnpj}/relatorio")
self._log(RealmType.OPERATIONAL, f"Relatório de NFe por CNPJ: {cnpj}")
return resposta
except Exception as e:
msg_erro = f"Erro ao consultar relatório NFe por CNPJ: {e}"
self._log(RealmType.ERRORS, msg_erro)
return {"error": msg_erro}
def nfse_emitidas(self, data_inicial=None, data_final=None, data: dict = None):
try:
if not data_inicial:
data_inicial = (datetime.now() - timedelta(days=30)).strftime('%Y-%m-%d')
if not data_final:
data_final = datetime.now().strftime('%Y-%m-%d')
resposta = self.api.get_paginated(f"{self.url}nfse/relatorio?from={data_inicial}&to={data_final}")
self._log(RealmType.OPERATIONAL, f"Relatório NFSe emitidas: {data_inicial} a {data_final}")
return resposta
except Exception as e:
msg_erro = f"Erro ao consultar relatório NFSe emitidas: {e}"
self._log(RealmType.ERRORS, msg_erro)
return {"error": msg_erro}
def nfse_por_cnpj(self, cnpj, data_inicial=None, data_final=None, data: dict = None):
try:
if not data_inicial:
data_inicial = (datetime.now() - timedelta(days=30)).strftime('%Y-%m-%d')
if not data_final:
data_final = datetime.now().strftime('%Y-%m-%d')
resposta = self.api.get_paginated(f"{self.url}nfse/{cnpj}/relatorio?from={data_inicial}&to={data_final}")
self._log(RealmType.OPERATIONAL, f"Relatório NFSe por CNPJ: {cnpj}")
return resposta
except Exception as e:
msg_erro = f"Erro ao consultar relatório NFSe por CNPJ: {e}"
self._log(RealmType.ERRORS, msg_erro)
return {"error": msg_erro}

View file

@ -0,0 +1,165 @@
from utils.api import Api
from utils.banco import Banco, CertificadoDigitalTable
from datetime import date
from utils.Exchange2 import LoggerExchange2
from utils.exchangeModels import RealmType
import json
import base64
class CertificadoDigital:
# Serviço que gerencia o upload e atualização de certificados digitais A1/A3 na plataforma da PlugNotas
def __init__(self, url_base, banco: Banco, api_key: str, logger: LoggerExchange2 = None):
self.api = Api(api_key=api_key)
self.banco = banco
self.url = f"{url_base}/certificado"
self.logger = logger
def post_form_data(self, data):
# Realiza o envio multipart/form-data do arquivo físico do certificado .pfx para a API e o salva no banco
try:
files = {
'arquivo': (
'certificado.pfx',
base64.b64decode(data['arquivo']) if isinstance(data['arquivo'], str) else data['arquivo'],
'application/x-pkcs12'
),
'senha': (None, data['senha'])
}
if 'email' in data:
files['email'] = (None, data['email'])
response = self.api.post_form_data(self.url, files)
id_tecnospeed = response.get('data', {}).get('id')
id_usr = data.get('id_usuario', 0)
id_emp = data.get('id_empresa', "0")
if not id_tecnospeed:
if self.logger: self.logger.logIC2(RealmType.OPERATIONAL, ('INSERT', 'certificado_digital', id_usr, '0', 'ERRO', 'ID do certificado não encontrado na resposta da API', json.dumps(response)))
raise ValueError("Erro ao obter ID do certificado na resposta da API.")
expiration_value = data.get('expirationAt') or data.get('data_expiracao')
certificado = CertificadoDigitalTable(
id_tecnospeed=id_tecnospeed,
certificado=data['arquivo'],
expirationAt=date.fromisoformat(expiration_value) if expiration_value else date.today(),
senha=data['senha'],
status=True,
id_empresa=id_emp,
id_usuario=id_usr,
email=data.get('email')
)
self.banco.add(certificado)
if self.logger: self.logger.logIC2(RealmType.OPERATIONAL, ('INSERT', 'certificado_digital', id_usr, id_tecnospeed, 'SUCESSO', 'Certificado digital cadastrado com sucesso', json.dumps(response)))
return id_tecnospeed
except Exception as e:
if self.logger: self.logger.logIC2(RealmType.OPERATIONAL, ('INSERT', 'certificado_digital', data.get('id_usuario', 0), '0', 'ERRO', f"Erro ao adicionar certificado: {e}", "{}"))
raise
def contar_vencidos(self, lista_certificados):
# Auxiliar para contar certificados ativos e vencidos na listagem
if isinstance(lista_certificados, dict) and "data" in lista_certificados:
lista = lista_certificados["data"]
elif isinstance(lista_certificados, list):
lista = lista_certificados
else:
return {"certificados_ativos": 0, "certificados_vencidos": 0}
ativos = 0
vencidos = 0
hoje = date.today()
for cert in lista:
if not isinstance(cert, dict): continue
# Tenta encontrar o campo de expiração (vencimento ou outros nomes conhecidos)
exp_str = cert.get("vencimento") or cert.get("expirationAt") or cert.get("dataVencimento") or cert.get("data_expiracao")
if exp_str:
try:
# Tenta converter a string de data:
# 1. Formato DD/MM/YYYY (do snipped do usuário)
# 2. Formato ISO YYYY-MM-DD
if "/" in exp_str:
partes = exp_str.split(" ")[0].split("/")
if len(partes) == 3:
data_exp = date(int(partes[2]), int(partes[1]), int(partes[0]))
else:
data_exp = date.fromisoformat(exp_str[:10])
if data_exp >= hoje:
ativos += 1
else:
vencidos += 1
except:
continue
return {
"certificados_ativos": ativos,
"certificados_vencidos": vencidos
}
def get(self, id=None):
# Consulta a situação de um certificado digital específico pelo ID ou lista todos vinculados
try:
if id:
resultado = self.api.get(f"{self.url}/{id}")
if self.logger: self.logger.logIC2(RealmType.OPERATIONAL, ('SELECT', 'certificado_digital', id, id, 'SUCESSO', f"Certificado consultado - ID: {id}", json.dumps(resultado)))
return resultado
else:
resposta = self.api.get(self.url)
# Calcula totais
totais = self.contar_vencidos(resposta)
# Organiza a resposta unificada
resultado = {
"certificados": resposta if isinstance(resposta, list) else resposta.get("data", []),
"totais": totais
}
if self.logger: self.logger.logIC2(RealmType.OPERATIONAL, ('SELECT', 'certificado_digital', 'GERAL', '0', 'SUCESSO', 'Listagem de certificados digitais', json.dumps(resultado)))
return resultado
except Exception as e:
if self.logger: self.logger.logIC2(RealmType.OPERATIONAL, ('SELECT', 'certificado_digital', id or 'GERAL', '0', 'ERRO', f"Erro ao buscar certificado: {e}", "{}"))
raise
def put_certificado(self, id, data):
# Atualiza o arquivo físico ou a senha de um certificado já existente
try:
certificado = self.banco.session.query(CertificadoDigitalTable).filter_by(id_tecnospeed=id).first()
id_usr = data.get('id_usuario', 0)
if not certificado:
if self.logger: self.logger.logIC2(RealmType.OPERATIONAL, ('UPDATE', 'certificado_digital', id_usr, id, 'ERRO', f"Certificado com ID {id} não encontrado", "{}"))
raise ValueError(f"Certificado com ID {id} não encontrado no banco.")
files = {
'arquivo': (
'certificado.pfx',
base64.b64decode(data['arquivo']) if isinstance(data['arquivo'], str) else data['arquivo'],
'application/x-pkcs12'
),
'senha': (None, data['senha'])
}
if 'email' in data:
files['email'] = (None, data['email'])
self.api.put_form_data(f"{self.url}/{id}", files)
certificado.certificado = data['arquivo']
certificado.senha = data['senha']
expiration_value = data.get('expirationAt') or data.get('data_expiracao')
if expiration_value:
certificado.expirationAt = date.fromisoformat(expiration_value)
certificado.updatedAt = date.today()
self.banco.update(certificado)
if self.logger: self.logger.logIC2(RealmType.OPERATIONAL, ('UPDATE', 'certificado_digital', id_usr, id, 'SUCESSO', 'Certificado digital atualizado com sucesso', json.dumps({"id": id})))
return id
except Exception as e:
if self.logger: self.logger.logIC2(RealmType.OPERATIONAL, ('UPDATE', 'certificado_digital', data.get('id_usuario', 0), id, 'ERRO', f"Erro ao atualizar certificado: {e}", "{}"))
raise

View file

@ -0,0 +1,177 @@
from Models.empresa import Empresa
from utils.api import Api
from utils.banco import Banco, EnterpriseTable, LogoTable
from utils.Exchange2 import LoggerExchange2
from utils.exchangeModels import RealmType
from sqlalchemy.exc import SQLAlchemyError
import json
import string
import random
import time
def generate_cuid():
"""Gera um ID no formato aproximado de CUID (c + timestamp + random)."""
ts = hex(int(time.time() * 1000))[2:]
rand = ''.join(random.choices(string.ascii_lowercase + string.digits, k=12))
return f"c{ts}{rand}"[:25]
# Gerenciador de contas de Empresas emissoras de notas e seus respectivos certificados e logos
class EmpresaController:
def __init__(self, url_base, api_key: str, banco: Banco, logger: LoggerExchange2 = None):
self.api = Api(api_key=api_key)
self.banco = banco
self.url = f"{url_base}/empresa"
self.logger = logger
def _log(self, realm: RealmType, msg: str):
if self.logger:
self.logger.logIC2(realm, (msg,))
def post(self, empresa: Empresa, data: dict):
try:
dict_api = empresa.model_dump(exclude={'id', 'id_empresa', 'banco', 'email'}, exclude_none=True)
# Injeção das configurações fixas e obrigatórias para a PlugNotas
if dict_api.get("nfe") and dict_api["nfe"].get("ativo"):
nfe_config_front = dict_api["nfe"].get("config") or {}
dict_api["nfe"]["config"] = {
"producao": False,
"email": {"envio": False},
"dfe": {"primeiroNsu": False},
"impressaoFcp": False,
"impressaoPartilha": False,
**nfe_config_front
}
dict_api["nfe"]["tipoContrato"] = 0
if dict_api.get("nfse") and dict_api["nfse"].get("ativo"):
nfse_config_front = dict_api["nfse"].get("config") or {}
dict_api["nfse"]["config"] = {
"producao": False,
"email": {"envio": False},
"rps": {"lote": 1, "numeracao": [{"numero": 1, "serie": "1"}]},
"prefeitura": {"login": "12345", "senha": "123"},
**nfse_config_front
}
dict_api["nfse"]["tipoContrato"] = 0
print("===========================")
print("ENVIANDO PARA A API:", json.dumps(dict_api, indent=2))
print("===========================")
retorno = self.api.post(data=dict_api, url=self.url)
if 'error' in retorno:
err_data = retorno.get('response', {}).get('error', {})
msg = err_data.get('message', 'Erro desconhecido')
detalhes = err_data.get('data', [])
if detalhes:
msg += f" Detalhes: {detalhes}"
print(f"\n[ERRO API PLUGNOTAS] {msg}\n", flush=True)
self._log(RealmType.ERRORS, f"Erro na API ao cadastrar empresa: {msg}")
raise Exception(msg)
# Gerar ID CUID se não vier no objeto ou se for nulo
empresa_id = empresa.id if empresa.id else generate_cuid()
empresa_nova = EnterpriseTable(
id=empresa_id,
cnpj=empresa.cpfCnpj,
name=empresa.razaoSocial,
banco=empresa.banco,
email=empresa.email,
status=True
)
self.banco.add(empresa_nova)
self._log(RealmType.OPERATIONAL, f"Empresa cadastrada com sucesso - CNPJ: {empresa.cpfCnpj} (ID: {empresa_id})")
if isinstance(retorno, dict) and 'data' in retorno:
retorno['data']['id'] = empresa_nova.id
retorno['data']['id_plugnotas'] = retorno['data'].get('_id')
retorno['data']['name'] = empresa_nova.name
retorno['data']['status'] = empresa_nova.status
retorno['data'].pop('razaoSocial', None)
retorno['data'].pop('id_empresa', None)
return retorno
except SQLAlchemyError as e:
self.banco.rollback()
self._log(RealmType.ERRORS, f"Erro no banco ao cadastrar empresa: {e}")
return {"error": f"Erro ao cadastrar empresa: {str(e)}"}
except Exception as e:
return {"error": f"Erro inesperado: {str(e)}"}
def get(self, cnpj: str = None, data: dict = None):
try:
if cnpj:
resposta = self.api.get(f"{self.url}/{cnpj}")
if isinstance(resposta, dict) and "error" not in resposta:
empresa_local = self.banco.session.query(EnterpriseTable).filter_by(cnpj=cnpj).first()
if empresa_local:
resposta["id"] = empresa_local.id
resposta["name"] = empresa_local.name
resposta["status"] = empresa_local.status
resposta.pop("razaoSocial", None)
if "_id" in resposta:
resposta["id_plugnotas"] = resposta["_id"]
else:
resposta = self.api.get_paginated(self.url)
# Injetar o ID do banco local em cada empresa listada
if isinstance(resposta, dict) and "data" in resposta:
for emp in resposta["data"]:
empresa_local = self.banco.session.query(EnterpriseTable).filter_by(cnpj=emp.get("cpfCnpj")).first()
if empresa_local:
emp["id"] = empresa_local.id
emp["name"] = empresa_local.name
emp["status"] = empresa_local.status
emp.pop("razaoSocial", None)
if "_id" in emp:
emp["id_plugnotas"] = emp["_id"]
self._log(RealmType.OPERATIONAL, f"Empresa consultada - CNPJ: {cnpj or 'TODAS'}")
return resposta
except Exception as e:
self._log(RealmType.ERRORS, f"Erro ao buscar empresa: {e}")
return {"error": f"Erro ao buscar empresa: {str(e)}"}
def post_file(self, file, cnpj, data=None):
try:
empresa = self.banco.session.query(EnterpriseTable).filter_by(cnpj=cnpj).first()
if not empresa:
self._log(RealmType.ERRORS, f"Empresa não encontrada para upload de logo - CNPJ: {cnpj}")
return {"error": "Empresa não encontrada"}
logo = LogoTable(
id_empresa=empresa.id,
logo=file,
status=True
)
self.banco.add(logo)
files = {'arquivo': ('logo.png', file, 'image/png')}
resposta = self.api.post_file(file=files, url=f"{self.url}/{cnpj}/logotipo")
self._log(RealmType.OPERATIONAL, f"Logo enviada com sucesso - CNPJ: {cnpj}")
return resposta
except SQLAlchemyError as e:
self.banco.rollback()
self._log(RealmType.ERRORS, f"Erro no banco ao salvar logo: {e}")
return {"error": f"Erro ao salvar logo: {str(e)}"}
except Exception as e:
self._log(RealmType.ERRORS, f"Erro inesperado ao enviar logo: {e}")
return {"error": f"Erro inesperado: {str(e)}"}
def patch_certificado(self, cnpj: str, certificado: str, data: dict):
try:
empresa = self.banco.session.query(EnterpriseTable).filter_by(cnpj=cnpj).first()
if not empresa:
self._log(RealmType.ERRORS, f"Empresa não encontrada para atualizar certificado - CNPJ: {cnpj}")
return {"error": "Empresa não encontrada"}
resposta = self.api.patch(url=f"{self.url}/{cnpj}/certificado", data={"certificado": certificado})
self._log(RealmType.OPERATIONAL, f"Certificado da empresa atualizado - CNPJ: {cnpj}")
return resposta
except Exception as e:
self._log(RealmType.ERRORS, f"Erro ao atualizar certificado: {e}")
return {"error": f"Erro ao atualizar certificado: {str(e)}"}

View file

@ -0,0 +1,173 @@
import sys
import os
import json
from contextlib import asynccontextmanager
from fastapi import FastAPI, Request
from fastapi.responses import JSONResponse
import uvicorn
sys.path.append(os.path.dirname(os.path.abspath(__file__)))
from Models.config import SystemConfig
from utils.redis import RedisClient
from utils.Exchange2 import RPCExchange2, LoggerExchange2, AppIC2Builder
from utils.exchangeModels import RealmType
# Campos obrigatórios que o PlugNotas envia no Endpoint
CAMPOS_OBRIGATORIOS = ["id", "status", "documento"]
# Status válidos que o PlugNotas envia
STATUS_VALIDOS = ["CONCLUIDO", "REJEITADO", "CANCELADO"]
# Tipos de documento/entidades aceitos
DOCUMENTOS_VALIDOS = ["nfe", "nfse", "certificado_digital"]
# Variáveis globais
redis_client = None
system_config = None
exchange = None
logger = None
@asynccontextmanager
async def ciclo_app(app: FastAPI):
# Gerencia o ciclo de vida da aplicação, inicializando conexões com Redis e RabbitMQ (Exchange) na inicialização
global redis_client, system_config, exchange
try:
redis_client = RedisClient(host=os.getenv('REDIS_HOST', 'localhost'), port=6379)
system_config_data = redis_client.get("system_config")
if not system_config_data:
print("Erro: system_config não encontrado no Redis.")
else:
system_config = SystemConfig(**json.loads(system_config_data))
builder = AppIC2Builder(service_name="tecnospeed", versionstr="2.0.1", origin=system_config.origin)
global logger
logger = LoggerExchange2(ic2_builder=builder, amqps=system_config.amqps, system="integracao", service="tecnospeed", version="2.0.1", origin=system_config.origin)
logger.logIC2(RealmType.LOGS, (f"[{system_config.origin}] Configuração lida com sucesso pelo Listener Endpoint.",))
exchange = RPCExchange2(
amqps=system_config.amqps,
origin=system_config.origin,
system="integracao",
service="controller",
version="2.0.1",
client=""
)
servicekey = system_config.servicekey.ljust(32, '0')[:32]
exchange.setCrypto(serviceid=system_config.serviceid, servicekey=servicekey)
logger.logIC2(RealmType.LOGS, ("Exchange inicializado com sucesso para o Listener Endpoint.",))
except Exception as e:
if logger:
logger.logIC2(RealmType.ERRORS, (f"Erro ao inicializar o Listener Endpoint: {e}",))
else:
print(f"Erro ao inicializar o Listener Endpoint: {e}")
yield # Aplicação rodando
if logger:
logger.logIC2(RealmType.LOGS, ("Listener Endpoint encerrando...",))
logger.close()
app = FastAPI(
title="Listener Endpoint PlugNotas",
description="Endpoint para receber notificações Endpoint do PlugNotas",
version="2.0.1",
lifespan=ciclo_app
)
@app.get("/health")
async def healthy():
# Verifica a saúde da aplicação, validando se as dependências (Redis e Exchange) estão conectadas
status = {
"status": "ok",
"redis": redis_client is not None,
"exchange": exchange is not None,
"system_config": system_config is not None
}
if not exchange:
return JSONResponse(status_code=503, content={**status, "status": "degraded", "message": "Exchange não inicializado"})
return status
@app.post("/listenerPlugnotas")
@app.post("/listenerPlugnotas/{cnpj}")
async def receber_Endpoint_plugnotas(request: Request, cnpj: str = None):
# Rota principal que recebe a notificação da PlugNotas e repassa para processamento no RabbitMQ
try:
try:
payload = await request.json()
except Exception as e:
logger.logIC2(RealmType.ERRORS, (f"Payload inválido recebido (não é JSON válido): {e}",))
return JSONResponse(
status_code=400,
content={"status": "error", "message": "Payload não é um JSON válido"}
)
documento = payload.get("documento", "desconhecido")
status_nota = payload.get("status", "desconhecido")
id_nota = payload.get("id", "")
id_integracao = payload.get("idIntegracao", "")
emitente = payload.get("emitente", cnpj or "")
logger.logIC2(RealmType.LOGS, (
f"Endpoint recebido - Documento: {documento} | Status: {status_nota} | "
f"ID: {id_nota} | IDIntegracao: {id_integracao} | Emitente: {emitente}",
))
campos_faltantes = [campo for campo in CAMPOS_OBRIGATORIOS if campo not in payload]
if campos_faltantes:
logger.logIC2(RealmType.ERRORS, (
f"Endpoint com campos obrigatórios faltantes: {campos_faltantes} | "
f"Payload recebido: {json.dumps(payload)}",
))
return {"status": "warning", "message": f"Campos faltantes: {campos_faltantes}"}
if documento not in DOCUMENTOS_VALIDOS:
logger.logIC2(RealmType.ERRORS, (
f"Tipo de documento desconhecido: {documento} | Payload: {json.dumps(payload)}",
))
if status_nota not in STATUS_VALIDOS:
logger.logIC2(RealmType.ERRORS, (
f"Status desconhecido: {status_nota} | Documento: {documento} | ID: {id_nota}",
))
if not exchange:
logger.logIC2(RealmType.ERRORS, ("Exchange não inicializado no momento da chegada do Endpoint.",))
return JSONResponse(
status_code=500,
content={"status": "error", "message": "Serviço temporariamente indisponível"}
)
# Envia o EXATO objeto dict recebido do PlugNotas
exchange.setPayload(json.dumps(payload))
# Chama a rota RPC aguardando retorno
try:
resposta_rpc = exchange.callRPC("Endpoint PlugNotas Recebido", timeout=15)
logger.logIC2(RealmType.LOGS, (
f"Endpoint enviado ao RPC Controller. "
f"Documento: {documento} | Status: {status_nota} | ID: {id_nota} | Resposta: {resposta_rpc}",
))
return {"status": "ok", "message": "Enviado ao Controller com sucesso", "controller_response": resposta_rpc}
except Exception as e:
logger.logIC2(RealmType.ERRORS, (f"Erro ou Timeout ao conversar com o Controller (RPC): {e}",))
return JSONResponse(
status_code=502,
content={"status": "error", "message": "Falha na comunicação RPC com o Controller"}
)
except Exception as e:
logger.logIC2(RealmType.ERRORS, (f"Erro inesperado no Listener Endpoint: {e}",))
return JSONResponse(
status_code=500,
content={"status": "error", "message": "Erro interno no processamento do Endpoint"}
)

View file

@ -0,0 +1,24 @@
2026-04-01 09:09:33,082 - integracao - INFO - { 'caller': 'ciclo_app',
'client': '',
'logDate': '2026-04-01 09:09:33.082579-03:00',
'msg': '[local] Configuração lida com sucesso pelo Listener Webhook.',
'origin': '',
'service': 'tecnospeed',
'system': 'integracao',
'version': '2.0.1'}
2026-04-01 09:09:33,700 - integracao - INFO - { 'caller': 'ciclo_app',
'client': '',
'logDate': '2026-04-01 09:09:33.700242-03:00',
'msg': 'Exchange inicializado com sucesso para o Listener Webhook.',
'origin': '',
'service': 'tecnospeed',
'system': 'integracao',
'version': '2.0.1'}
2026-04-01 09:14:24,402 - integracao - INFO - { 'caller': 'ciclo_app',
'client': '',
'logDate': '2026-04-01 09:14:24.402193-03:00',
'msg': 'Listener Webhook encerrando...',
'origin': '',
'service': 'tecnospeed',
'system': 'integracao',
'version': '2.0.1'}

View file

@ -0,0 +1,194 @@
2026-04-02 09:14:16,362 - integracao - INFO - { 'caller': 'ciclo_app',
'client': '',
'logDate': '2026-04-02 09:14:16.362304-03:00',
'msg': '[local] Configuração lida com sucesso pelo Listener Webhook.',
'origin': '',
'service': 'tecnospeed',
'system': 'integracao',
'version': '2.0.1'}
2026-04-02 09:14:16,932 - integracao - INFO - { 'caller': 'ciclo_app',
'client': '',
'logDate': '2026-04-02 09:14:16.932459-03:00',
'msg': 'Exchange inicializado com sucesso para o Listener Webhook.',
'origin': '',
'service': 'tecnospeed',
'system': 'integracao',
'version': '2.0.1'}
2026-04-02 14:14:30,014 - integracao - INFO - { 'caller': 'ciclo_app',
'client': '',
'logDate': '2026-04-02 14:14:30.013914-03:00',
'msg': '[local] Configuração lida com sucesso pelo Listener Webhook.',
'origin': '',
'service': 'tecnospeed',
'system': 'integracao',
'version': '2.0.1'}
2026-04-02 14:14:31,040 - integracao - INFO - { 'caller': 'ciclo_app',
'client': '',
'logDate': '2026-04-02 14:14:31.039753-03:00',
'msg': 'Exchange inicializado com sucesso para o Listener Webhook.',
'origin': '',
'service': 'tecnospeed',
'system': 'integracao',
'version': '2.0.1'}
2026-04-02 14:22:20,024 - integracao - INFO - { 'caller': 'receber_webhook_plugnotas',
'client': '',
'logDate': '2026-04-02 14:22:20.024167-03:00',
'msg': 'Webhook recebido - Documento: nfe | Status: CONCLUIDO | ID: '
'69cab453... | IDIntegracao: | Emitente:',
'origin': '',
'service': 'tecnospeed',
'system': 'integracao',
'version': '2.0.1'}
2026-04-02 14:22:20,263 - integracao - INFO - { 'caller': 'receber_webhook_plugnotas',
'client': '',
'logDate': '2026-04-02 14:22:20.263204-03:00',
'msg': 'Webhook repassado ao worker com sucesso - Documento: nfe | Status: '
'CONCLUIDO | ID: 69cab453...',
'origin': '',
'service': 'tecnospeed',
'system': 'integracao',
'version': '2.0.1'}
2026-04-02 14:23:45,608 - integracao - INFO - { 'caller': 'receber_webhook_plugnotas',
'client': '',
'logDate': '2026-04-02 14:23:45.608300-03:00',
'msg': 'Webhook recebido - Documento: nfe | Status: CONCLUIDO | ID: '
'69cab45329f21a7de7691961 | IDIntegracao: XXXYY99999_TESTE445 | '
'Emitente: 08187168000160',
'origin': '',
'service': 'tecnospeed',
'system': 'integracao',
'version': '2.0.1'}
2026-04-02 14:23:46,107 - integracao - INFO - { 'caller': 'receber_webhook_plugnotas',
'client': '',
'logDate': '2026-04-02 14:23:46.107170-03:00',
'msg': 'Webhook repassado ao worker com sucesso - Documento: nfe | Status: '
'CONCLUIDO | ID: 69cab45329f21a7de7691961',
'origin': '',
'service': 'tecnospeed',
'system': 'integracao',
'version': '2.0.1'}
2026-04-02 14:26:07,402 - integracao - INFO - { 'caller': 'ciclo_app',
'client': '',
'logDate': '2026-04-02 14:26:07.401683-03:00',
'msg': 'Listener Webhook encerrando...',
'origin': '',
'service': 'tecnospeed',
'system': 'integracao',
'version': '2.0.1'}
2026-04-02 14:37:36,191 - integracao - INFO - { 'caller': 'ciclo_app',
'client': '',
'logDate': '2026-04-02 14:37:36.190896-03:00',
'msg': '[local] Configuração lida com sucesso pelo Listener Webhook.',
'origin': '',
'service': 'tecnospeed',
'system': 'integracao',
'version': '2.0.1'}
2026-04-02 14:37:36,969 - integracao - INFO - { 'caller': 'ciclo_app',
'client': '',
'logDate': '2026-04-02 14:37:36.969622-03:00',
'msg': 'Exchange inicializado com sucesso para o Listener Webhook.',
'origin': '',
'service': 'tecnospeed',
'system': 'integracao',
'version': '2.0.1'}
2026-04-02 14:37:37,459 - integracao - INFO - { 'caller': 'ciclo_app',
'client': '',
'logDate': '2026-04-02 14:37:37.459047-03:00',
'msg': 'Listener Webhook encerrando...',
'origin': '',
'service': 'tecnospeed',
'system': 'integracao',
'version': '2.0.1'}
2026-04-02 14:37:42,582 - integracao - INFO - { 'caller': 'ciclo_app',
'client': '',
'logDate': '2026-04-02 14:37:42.582792-03:00',
'msg': '[local] Configuração lida com sucesso pelo Listener Webhook.',
'origin': '',
'service': 'tecnospeed',
'system': 'integracao',
'version': '2.0.1'}
2026-04-02 14:37:43,391 - integracao - INFO - { 'caller': 'ciclo_app',
'client': '',
'logDate': '2026-04-02 14:37:43.391071-03:00',
'msg': 'Exchange inicializado com sucesso para o Listener Webhook.',
'origin': '',
'service': 'tecnospeed',
'system': 'integracao',
'version': '2.0.1'}
2026-04-02 14:37:43,782 - integracao - INFO - { 'caller': 'ciclo_app',
'client': '',
'logDate': '2026-04-02 14:37:43.781995-03:00',
'msg': 'Listener Webhook encerrando...',
'origin': '',
'service': 'tecnospeed',
'system': 'integracao',
'version': '2.0.1'}
2026-04-02 16:03:48,010 - integracao - INFO - { 'caller': 'run',
'client': '',
'logDate': '2026-04-02 16:03:48.009993-03:00',
'msg': 'Conectando ao RabbitMQ',
'origin': '',
'service': 'tecnospeed',
'system': 'integracao',
'version': '2.0.1'}
2026-04-02 16:03:48,217 - integracao - INFO - { 'caller': 'run',
'client': '',
'logDate': '2026-04-02 16:03:48.216535-03:00',
'msg': "Configuração do sistema carregada - serviceid='local' "
"servicekey='local' origin='local' "
"amqps='amqp://guest:guest@localhost:5672/'",
'origin': '',
'service': 'tecnospeed',
'system': 'integracao',
'version': '2.0.1'}
2026-04-02 16:12:17,812 - integracao - INFO - { 'caller': 'run',
'client': '',
'logDate': '2026-04-02 16:12:17.812369-03:00',
'msg': 'Conectando ao RabbitMQ',
'origin': '',
'service': 'tecnospeed',
'system': 'integracao',
'version': '2.0.1'}
2026-04-02 16:12:18,488 - integracao - INFO - { 'caller': 'run',
'client': '',
'logDate': '2026-04-02 16:12:18.487913-03:00',
'msg': "Configuração do sistema carregada - serviceid='local' "
"servicekey='local' origin='local' "
"amqps='amqp://guest:guest@localhost:5672/'",
'origin': '',
'service': 'tecnospeed',
'system': 'integracao',
'version': '2.0.1'}
2026-04-02 16:12:18,864 - integracao - INFO - { 'caller': 'run',
'client': '',
'logDate': '2026-04-02 16:12:18.863889-03:00',
'msg': 'Erro fatal ao conectar ao RabbitMQ - VERIFY PARAMETERS: '
'is_amqp_url: True\n'
' amqps[amqp://guest:guest@localhost:5672/]\n'
' origin[local]\n'
' system[integracao]\n'
' service[tecnospeed]\n'
' version[2.0.1\n'
' client[]',
'origin': '',
'service': 'tecnospeed',
'system': 'integracao',
'version': '2.0.1'}
2026-04-02 16:12:30,426 - integracao - INFO - { 'caller': 'run',
'client': '',
'logDate': '2026-04-02 16:12:30.426681-03:00',
'msg': 'Conectando ao RabbitMQ',
'origin': '',
'service': 'tecnospeed',
'system': 'integracao',
'version': '2.0.1'}
2026-04-02 16:12:30,947 - integracao - INFO - { 'caller': 'run',
'client': '',
'logDate': '2026-04-02 16:12:30.947199-03:00',
'msg': "Configuração do sistema carregada - serviceid='local' "
"servicekey='local' origin='local' "
"amqps='amqp://guest:guest@localhost:5672/'",
'origin': '',
'service': 'tecnospeed',
'system': 'integracao',
'version': '2.0.1'}

View file

@ -0,0 +1,56 @@
2026-04-06 08:25:17,847 - teste_wrapper.integracao - INFO - params: <URLParameters host=jackal.rmq.cloudamqp.com port=5671 virtual_host=iawkzmhh ssl=True> amqps: amqps://iawkzmhh:4iIVXCgLDsLy2oVyVWeV-o6shg-BZ89z@jackal.rmq.cloudamqp.com/iawkzmhh
2026-04-06 08:25:18,327 - teste_wrapper.integracao - INFO - Logger Messages topics: logs.teste_wrapper.integracao.
2026-04-06 08:25:18,361 - teste_wrapper.integracao - INFO - {'caller': 'IC2',
'client': '',
'hmac': '862c786853960277c4d518d1eb01143b2fe7782092dbf5403fb61b52127c7829',
'logDate': '2026-04-06 08:25:18.361486-03:00',
'msg': "LOGIC2 LOGS [('Testando o novo fo ...]",
'origin': '',
'payload': '44102feb05c76aac7b2b',
'service': 'teste_wrapper',
'serviceid': 'AF1699F719F1B6C81DE2BD38AB811994E3089B35C8E9314813A770AC7AE25D8D',
'system': 'integracao',
'target': '',
'type': 'logger',
'version': ''}
2026-04-06 08:25:18,368 - teste_wrapper.integracao - INFO - {'caller': 'logMsg',
'client': '',
'hmac': '',
'logDate': '2026-04-06 08:25:18.367868-03:00',
'msg': 'Testando o novo formato IC2',
'origin': '',
'payload': '',
'service': 'teste_wrapper',
'serviceid': 'AF1699F719F1B6C81DE2BD38AB811994E3089B35C8E9314813A770AC7AE25D8D',
'system': 'integracao',
'target': '',
'type': 'logger',
'version': ''}
2026-04-06 08:28:37,324 - teste_wrapper.integracao - INFO - params: <URLParameters host=jackal.rmq.cloudamqp.com port=5671 virtual_host=iawkzmhh ssl=True> amqps: amqps://iawkzmhh:4iIVXCgLDsLy2oVyVWeV-o6shg-BZ89z@jackal.rmq.cloudamqp.com/iawkzmhh
2026-04-06 08:28:37,834 - teste_wrapper.integracao - INFO - Logger Messages topics: logs.teste_wrapper.integracao.
2026-04-06 08:28:37,855 - teste_wrapper.integracao - INFO - {'caller': 'IC2',
'client': '',
'hmac': '841681411efce4ae99ff2602ae79da6284e6c3ca247a27da3a87dedf3771244e',
'logDate': '2026-04-06 08:28:37.855637-03:00',
'msg': "LOGIC2 LOGS [('Testando o formato ...]",
'origin': '',
'payload': '353fd214129406d78301',
'service': 'teste_wrapper',
'serviceid': 'AF1699F719F1B6C81DE2BD38AB811994E3089B35C8E9314813A770AC7AE25D8D',
'system': 'integracao',
'target': '',
'type': 'logger',
'version': ''}
2026-04-06 08:28:37,865 - teste_wrapper.integracao - INFO - {'caller': 'logMsg',
'client': '',
'hmac': '',
'logDate': '2026-04-06 08:28:37.865799-03:00',
'msg': 'Testando o formato IC2',
'origin': '',
'payload': '',
'service': 'teste_wrapper',
'serviceid': 'AF1699F719F1B6C81DE2BD38AB811994E3089B35C8E9314813A770AC7AE25D8D',
'system': 'integracao',
'target': '',
'type': 'logger',
'version': ''}

View file

View file

View file

@ -0,0 +1,140 @@
2026-03-27 14:57:57,807 - integracao - INFO - { 'caller': 'run',
'client': '',
'logDate': '2026-03-27 14:57:57.807604-03:00',
'msg': 'Conectando ao RabbitMQ',
'origin': '',
'service': 'tecnospeed',
'system': 'integracao',
'version': '2.0.1'}
2026-03-27 14:57:58,282 - integracao - INFO - { 'caller': 'run',
'client': '',
'logDate': '2026-03-27 14:57:58.282491-03:00',
'msg': "Configuração do sistema carregada - serviceid='local' "
"servicekey='local' origin='local' "
"amqps='amqp://guest:guest@localhost:5672/'",
'origin': '',
'service': 'tecnospeed',
'system': 'integracao',
'version': '2.0.1'}
2026-03-27 14:59:07,273 - integracao - INFO - { 'caller': 'callback',
'client': 'Test',
'logDate': '2026-03-27 14:59:07.273291-03:00',
'msg': "Configuração recebida - {'api_key': None, 'x_api_key': "
"'2da392a6-79d2-4304-a8b7-959572c7e44d', 'user_postgres': "
"'postgres', 'pass_postgres': 'postgres', 'host_postgres': "
"'localhost', 'port_postgres': '5432', 'database_postgres': "
"'tecnospeed'}",
'origin': '',
'service': 'tecnospeed',
'system': 'integracao',
'version': '2.0.1'}
2026-03-27 14:59:08,015 - integracao - INFO - { 'caller': 'callback',
'client': 'Test',
'logDate': '2026-03-27 14:59:08.014993-03:00',
'msg': 'Configuração incompleta',
'origin': '',
'service': 'tecnospeed',
'system': 'integracao',
'version': '2.0.1'}
2026-03-27 15:00:56,891 - integracao - INFO - { 'caller': 'callback',
'client': 'Test',
'logDate': '2026-03-27 15:00:56.890566-03:00',
'msg': "Configuração recebida - {'api_key': None, 'x_api_key': "
"'2da392a6-79d2-4304-a8b7-959572c7e44d', 'user_postgres': "
"'postgres', 'pass_postgres': 'postgres', 'host_postgres': "
"'localhost', 'port_postgres': '5432', 'database_postgres': "
"'sg_servico_tecnospeed'}",
'origin': '',
'service': 'tecnospeed',
'system': 'integracao',
'version': '2.0.1'}
2026-03-27 15:00:57,787 - integracao - INFO - { 'caller': 'callback',
'client': 'Test',
'logDate': '2026-03-27 15:00:57.787472-03:00',
'msg': 'Configuração incompleta',
'origin': '',
'service': 'tecnospeed',
'system': 'integracao',
'version': '2.0.1'}
2026-03-27 15:02:14,102 - integracao - INFO - { 'caller': 'callback',
'client': 'Test',
'logDate': '2026-03-27 15:02:14.101859-03:00',
'msg': "Configuração recebida - {'api_key': None, 'x_api_key': "
"'2da392a6-79d2-4304-a8b7-959572c7e44d', 'user_postgres': "
"'postgres', 'pass_postgres': 'postgres', 'host_postgres': "
"'localhost', 'port_postgres': '5432', 'database_postgres': "
"'tecnospeed'}",
'origin': '',
'service': 'tecnospeed',
'system': 'integracao',
'version': '2.0.1'}
2026-03-27 15:02:14,792 - integracao - INFO - { 'caller': 'callback',
'client': 'Test',
'logDate': '2026-03-27 15:02:14.791942-03:00',
'msg': 'Configuração incompleta',
'origin': '',
'service': 'tecnospeed',
'system': 'integracao',
'version': '2.0.1'}
2026-03-27 15:09:33,329 - integracao - INFO - { 'caller': 'run',
'client': '',
'logDate': '2026-03-27 15:09:33.329870-03:00',
'msg': 'Conectando ao RabbitMQ',
'origin': '',
'service': 'tecnospeed',
'system': 'integracao',
'version': '2.0.1'}
2026-03-27 15:09:33,698 - integracao - INFO - { 'caller': 'run',
'client': '',
'logDate': '2026-03-27 15:09:33.698183-03:00',
'msg': "Configuração do sistema carregada - serviceid='local' "
"servicekey='local' origin='local' "
"amqps='amqp://guest:guest@localhost:5672/'",
'origin': '',
'service': 'tecnospeed',
'system': 'integracao',
'version': '2.0.1'}
2026-03-27 15:09:39,159 - integracao - INFO - { 'caller': 'callback',
'client': 'Test',
'logDate': '2026-03-27 15:09:39.158573-03:00',
'msg': "Configuração recebida - {'api_key': None, 'x_api_key': "
"'2da392a6-79d2-4304-a8b7-959572c7e44d', 'user_postgres': "
"'postgres', 'pass_postgres': 'postgres', 'host_postgres': "
"'localhost', 'port_postgres': '5432', 'database_postgres': "
"'tecnospeed'}",
'origin': '',
'service': 'tecnospeed',
'system': 'integracao',
'version': '2.0.1'}
2026-03-27 15:09:44,975 - integracao - INFO - { 'caller': 'callback',
'client': 'Test',
'logDate': '2026-03-27 15:09:44.975168-03:00',
'msg': 'Configuração completa para o cliente Test',
'origin': '',
'service': 'tecnospeed',
'system': 'integracao',
'version': '2.0.1'}
2026-03-27 15:09:45,763 - integracao - INFO - { 'caller': 'nfe',
'client': 'Test',
'logDate': '2026-03-27 15:09:45.763614-03:00',
'msg': 'Iniciando serviço de NFe - Método: GET',
'origin': '',
'service': 'tecnospeed',
'system': 'integracao',
'version': '2.0.1'}
2026-03-27 15:09:46,610 - integracao - INFO - { 'caller': 'nfe',
'client': 'Test',
'logDate': '2026-03-27 15:09:46.610671-03:00',
'msg': 'Consultando resumo de NFe',
'origin': '',
'service': 'tecnospeed',
'system': 'integracao',
'version': '2.0.1'}
2026-03-27 15:09:47,761 - integracao - INFO - { 'caller': 'nfe',
'client': 'Test',
'logDate': '2026-03-27 15:09:47.761417-03:00',
'msg': 'Operação realizada com sucesso',
'origin': '',
'service': 'tecnospeed',
'system': 'integracao',
'version': '2.0.1'}

686
backup/src/main.py Normal file
View file

@ -0,0 +1,686 @@
# IMPORT DAS CLASSES DE TRABALHO
from certificado_digital.certificado_digital import CertificadoDigital
from empresas.empresa import EmpresaController
from NFSe.nfse import NFSeService
from NFSe.Servico.servico import ServicoController
from NFSe.Tomador.tomador import TomadorController
from Relatorios.relatorios import Relatorios
from webhooks.webhooks import Webhooks
from NFe.registroNFe import RegistroNFeService
# IMPORTS DE MODELS
from Models.config import Config, SystemConfig
from Models.empresa import Empresa
from Models.nfse import NFSe
from Models.servico import Servico
from Models.nfe import NFe
from Models.tomador import Tomador
from Models.payload import validate_payload
# IMPORTS DE BIBLIOTECAS
from pydantic import ValidationError
import json, sys, time, random, os, uuid
# IMPORTS DE CLASSES AUXILIARES
from utils.Exchange2 import RPCExchange2, LoggerExchange2, AppIC2Builder
from utils.exchangeModels import RealmType
from utils.banco import Banco
from utils.redis import RedisClient
# VERSÃO DO SISTEMA
version = "1.0.0" # Versão inicial do sistema
version = "1.0.1" # Implementando ORM
version = "1.0.2" # Implementando RPC
version = "1.0.3" # Implementando configuração via Redis
version = "2.0.0" # NFe e refatoração da Main
version = "2.0.1" # Refatoração para RPCExchange
class Main:
def __init__(self):
self.client = "#"
self.api_key = ""
self.exchange = None
self.banco = None
self.system_config: SystemConfig = None
self.redis = RedisClient(host=os.getenv('REDIS_HOST', 'localhost'), port=6379)
self.service = "tecnospeed"
self.system = "integracao"
self.version = version
self.logger = None
self.url_base = os.getenv('PLUGNOTAS_URL', 'https://api.sandbox.plugnotas.com.br')
self.funcoes = {
"nfe": self.nfe,
"nfse": self.nfse,
"certificado_digital": self.certificado_digital,
"empresa": self.empresa,
"servico": self.servico,
"tomador": self.tomador,
"relatorios": self.relatorios,
"webhooks": self.webhooks
}
# Métodos de serviços orientado a eventos.
def nfe(self, metodo, dados=None, correlation_id=None):
# Roteador de emissões, baixas e cancelamentos via NFe
id_usr = dados.get('id_usuario', 1) if dados else 1
try:
self.logger.logIC2(RealmType.LOGS, (f"Iniciando serviço de NFe - Método: {metodo.upper()}",))
nfe_service = RegistroNFeService(api_key=self.api_key,
url_base=self.url_base,
banco=self.banco,
logger=self.logger)
metodo = metodo.lower()
resultado = None
match metodo:
case "post":
self.logger.logIC2(RealmType.LOGS, ("Cadastrando NFe",))
nfe = NFe(**dados.get('nfe', dados))
resultado = nfe_service.cadastrar_nfe(nfe_list=[nfe], data=dados)
case "get":
self.logger.logIC2(RealmType.LOGS, ("Consultando resumo de NFe",))
resultado = nfe_service.consultar_resumo(id_nota=dados.get('id'))
case "download":
self.logger.logIC2(RealmType.LOGS, ("Baixando PDF NFe",))
resultado = nfe_service.baixar_pdf(id_nota=dados.get('id'))
case "download_xml":
self.logger.logIC2(RealmType.LOGS, ("Baixando XML NFe",))
resultado = nfe_service.baixar_xml(id_nota=dados.get('id'))
case "cancelar":
self.logger.logIC2(RealmType.LOGS, ("Cancelando NFe",))
resultado = nfe_service.cancelar_nfe(id_nota=dados.get('id'), justificativa=dados.get('justificativa', 'Cancelamento padrão'))
case "status_cancelamento":
self.logger.logIC2(RealmType.LOGS, ("Consultando status de cancelamento",))
resultado = nfe_service.consultar_status_cancelamento(id_nota=dados.get('id'))
case "cce":
self.logger.logIC2(RealmType.LOGS, ("Enviando Carta de Correção NFe",))
resultado = nfe_service.enviar_cce(id_nota=dados.get('id'), correcao=dados.get('correcao'))
case "status_cce":
self.logger.logIC2(RealmType.LOGS, ("Consultando status CCe NFe",))
resultado = nfe_service.consultar_status_cce(id_nota=dados.get('id'))
case "resumo":
self.logger.logIC2(RealmType.LOGS, ("Consultando resumo de NFe por CNPJ de integracao",))
resultado = nfe_service.consultar_resumo_por_cnpj_integracao(cnpj=dados.get('cnpj'), id_integracao=dados.get('id_integracao', dados.get('id')))
case _:
self.logger.logIC2(RealmType.ERRORS, ("Método não encontrado",))
self.banco.logar_sistema(id_usuario=id_usr, acao=metodo.upper(), tabela='nfe', status='ERRO', mensagem='Método não encontrado', cliente=self.client, correlation_id=correlation_id)
return json.dumps({"status": "error", "message": "Método não encontrado"})
if isinstance(resultado, dict) and "error" in resultado:
msg = resultado["error"]
if "response" in resultado:
msg = f"{msg} | Detalhes: {resultado['response']}"
raise Exception(msg)
self.logger.logIC2(RealmType.LOGS, ("Operação realizada com sucesso",))
self.banco.logar_sistema(id_usuario=id_usr, acao=metodo.upper(), tabela='nfe', status='SUCESSO', mensagem='Operação concluída', json=json.dumps(resultado) if resultado else '{}', cliente=self.client, correlation_id=correlation_id)
return self._responder("ok", resultado)
except Exception as e:
self.logger.logIC2(RealmType.ERRORS, (f"Erro ao executar método {metodo} - {e}",))
self.banco.logar_sistema(id_usuario=id_usr, acao=metodo.upper() if isinstance(metodo, str) else 'NFE', tabela='nfe', status='ERRO', mensagem=str(e), cliente=self.client, correlation_id=correlation_id)
raise e
def nfse(self, metodo, dados=None, correlation_id=None):
# Roteador de Notas Fiscais de Roteiro/Serviço (NFSe)
id_usr = dados.get('id_usuario', 1) if dados else 1
try:
self.logger.logIC2(RealmType.LOGS, (f"Iniciando serviço de NFSe - Método: {metodo.upper()}",))
nfse_service = NFSeService(api_key=self.api_key,
banco=self.banco,
url_base=self.url_base,
logger=self.logger)
metodo = metodo.lower()
resultado = None
match metodo:
case "post":
self.logger.logIC2(RealmType.LOGS, ("Cadastrando NFSe",))
nfse = NFSe(**dados)
resultado = nfse_service.cadastrar_nfse(nfse=nfse, data=dados)
case "get":
self.logger.logIC2(RealmType.LOGS, ("Listando NFSe",))
resultado = nfse_service.buscar_nfse(id_nfse=dados.get('id')) if dados and 'id' in dados else nfse_service.listar_nfse()
case "download":
self.logger.logIC2(RealmType.LOGS, ("Baixando NFSe",))
resultado = nfse_service.baixar_nfse(id_nfse=dados.get('id'))
case "download_xml":
self.logger.logIC2(RealmType.LOGS, ("Baixando XML NFSe",))
resultado = nfse_service.baixar_xml_nfse(id_nfse=dados.get('id'))
case "cancelar":
self.logger.logIC2(RealmType.LOGS, ("Cancelando NFSe",))
resultado = nfse_service.solicitar_cancelamento_nfse(id_nfse=dados.get('id'))
case "status_cancelamento":
self.logger.logIC2(RealmType.LOGS, ("Consultando status de cancelamento",))
resultado = nfse_service.consultar_status_cancelamento_nfse(id_nfse=dados.get('id'))
case "resumo":
self.logger.logIC2(RealmType.LOGS, ("Consultando resumo de NFSe",))
resultado = nfse_service.consultar_resumo_nfse(id_nfse=dados.get('id'), cnpj=dados.get('cnpj'))
case "historico":
self.logger.logIC2(RealmType.LOGS, ("Baixando histórico de NFSe",))
resultado = nfse_service.baixar_histotico_nfse(id_nfse=dados.get('id'), cnpj=dados.get('cnpj'))
case _:
self.logger.logIC2(RealmType.ERRORS, ("Método não encontrado",))
self.banco.logar_sistema(id_usuario=id_usr, acao=metodo.upper(), tabela='nfse', status='ERRO', mensagem='Método não encontrado', cliente=self.client, correlation_id=correlation_id)
return json.dumps({"status": "error", "message": "Método não encontrado"})
if isinstance(resultado, dict) and "error" in resultado:
raise Exception(json.dumps(resultado))
self.logger.logIC2(RealmType.LOGS, ("Operação realizada com sucesso",))
self.banco.logar_sistema(id_usuario=id_usr, acao=metodo.upper(), tabela='nfse', status='SUCESSO', mensagem='Operação concluída', json=json.dumps(resultado) if resultado else '{}', cliente=self.client, correlation_id=correlation_id)
return self._responder("ok", resultado)
except Exception as e:
self.logger.logIC2(RealmType.ERRORS, (f"Erro ao executar método {metodo} - {e}",))
self.banco.logar_sistema(id_usuario=id_usr, acao=metodo.upper() if isinstance(metodo, str) else 'NFSE', tabela='nfse', status='ERRO', mensagem=str(e), cliente=self.client, correlation_id=correlation_id)
raise e
def certificado_digital(self, metodo, dados=None, correlation_id=None):
# Gerenciamento de envio dos certificados A3/A1
id_usr = dados.get('id_usuario', 1) if dados else 1
try:
self.logger.logIC2(RealmType.LOGS, (f"Iniciando serviço de Certificado Digital - Método: {metodo.upper()}",))
certificado_digital = CertificadoDigital(api_key=self.api_key, banco=self.banco, url_base=self.url_base, logger=self.logger)
metodo = metodo.lower()
resultado = None
match metodo:
case "post":
self.logger.logIC2(RealmType.LOGS, ("Cadastrando certificado digital",))
resultado = certificado_digital.post_form_data(data=dados)
case "get":
self.logger.logIC2(RealmType.LOGS, ("Listando certificados digitais",))
resultado = certificado_digital.get(id=dados.get('id')) if dados and 'id' in dados else certificado_digital.get()
case "put":
self.logger.logIC2(RealmType.LOGS, ("Atualizando certificado digital",))
resultado = certificado_digital.put_certificado(id=dados.get('id'), data=dados)
case _:
self.logger.logIC2(RealmType.ERRORS, ("Método não encontrado",))
self.banco.logar_sistema(id_usuario=id_usr, acao=metodo.upper(), tabela='certificado_digital', status='ERRO', mensagem='Método não encontrado', cliente=self.client, correlation_id=correlation_id)
return json.dumps({"status": "error", "message": "Método não encontrado"})
if isinstance(resultado, dict) and "error" in resultado:
msg = resultado["error"]
if "response" in resultado:
msg = f"{msg} | Detalhes: {resultado['response']}"
raise Exception(msg)
self.logger.logIC2(RealmType.LOGS, ("Operação realizada com sucesso",))
self.banco.logar_sistema(id_usuario=id_usr, acao=metodo.upper(), tabela='certificado_digital', status='SUCESSO', mensagem='Operação concluída', cliente=self.client, correlation_id=correlation_id)
return self._responder("ok", resultado)
except Exception as e:
self.logger.logIC2(RealmType.ERRORS, (f"Erro ao executar método {metodo} - {e}",))
self.banco.logar_sistema(id_usuario=id_usr, acao=metodo.upper() if isinstance(metodo, str) else 'CERT', tabela='certificado_digital', status='ERRO', mensagem=str(e), cliente=self.client, correlation_id=correlation_id)
raise e
def empresa(self, metodo, dados=None, correlation_id=None):
# Intermediador do cadastro e listagem das Empresas emissoras
id_usr = dados.get('id_usuario', 1) if dados else 1
try:
self.logger.logIC2(RealmType.LOGS, (f"Iniciando serviço de Empresa - Método: {metodo.upper()}",))
empresa_controller = EmpresaController(
api_key=self.api_key,
banco=self.banco,
url_base=self.url_base,
logger=self.logger
)
metodo = metodo.lower()
resultado = None
match metodo:
case "post":
self.logger.logIC2(RealmType.LOGS, ("Cadastrando empresa",))
if dados and 'incentivoCultural' in dados:
dados['incentivadorCultural'] = dados.pop('incentivoCultural')
empresa = Empresa(**dados)
resultado = empresa_controller.post(empresa=empresa, data=dados)
case "get":
self.logger.logIC2(RealmType.LOGS, ("Listando empresas",))
resultado = empresa_controller.get(cnpj=dados.get('cnpj'), data=dados) if dados and 'cnpj' in dados else empresa_controller.get(data=dados)
case "post_file":
self.logger.logIC2(RealmType.LOGS, ("Cadastrando empresa via arquivo",))
if not dados or 'file' not in dados or 'cnpj' not in dados:
raise ValueError("Parâmetros 'file' e 'cnpj' são obrigatórios para 'post_file'")
resultado = empresa_controller.post_file(file=dados['file'], cnpj=dados['cnpj'])
case "patch":
self.logger.logIC2(RealmType.LOGS, ("Atualizando certificado da empresa",))
if not dados or 'cnpj' not in dados or 'certificado' not in dados:
raise ValueError("Parâmetros 'cnpj' e 'certificado' (ID) são obrigatórios para 'patch'")
resultado = empresa_controller.patch_certificado(cnpj=dados['cnpj'], certificado=dados['certificado'], data=dados)
case _:
self.logger.logIC2(RealmType.ERRORS, ("Método não encontrado",))
self.banco.logar_sistema(id_usuario=id_usr, acao=metodo.upper(), tabela='empresa', status='ERRO', mensagem='Método não encontrado', cliente=self.client, correlation_id=correlation_id)
return json.dumps({"status": "error", "message": "Método não encontrado"})
if isinstance(resultado, dict) and "error" in resultado:
msg = resultado["error"]
if "response" in resultado:
msg = f"{msg} | Detalhes: {resultado['response']}"
raise Exception(msg)
self.logger.logIC2(RealmType.LOGS, ("Operação realizada com sucesso",))
self.banco.logar_sistema(id_usuario=id_usr, acao=metodo.upper(), tabela='empresa', status='SUCESSO', mensagem='Operação concluída', json=json.dumps(resultado) if resultado else '{}', cliente=self.client, correlation_id=correlation_id)
return self._responder("ok", resultado)
except Exception as e:
self.logger.logIC2(RealmType.ERRORS, (f"Erro ao executar método {metodo.upper()} - {str(e)}",))
self.banco.logar_sistema(id_usuario=id_usr, acao=metodo.upper() if isinstance(metodo, str) else 'EMPRESA', tabela='empresa', status='ERRO', mensagem=str(e), cliente=self.client, correlation_id=correlation_id)
return json.dumps({"status": "error", "message": str(e)})
def servico(self, metodo, dados=None, correlation_id=None):
# Intermediador do CRUD de Serviços com configurações de impostos
id_usr = dados.get('id_usuario', 1) if dados else 1
try:
self.logger.logIC2(RealmType.LOGS, (f"Iniciando serviço de Serviço - Método: {metodo.upper()}",))
servico_controller = ServicoController(
api_key=self.api_key,
banco=self.banco,
url_base=self.url_base,
logger=self.logger
)
metodo = metodo.lower()
resultado = None
match metodo:
case "post":
self.logger.logIC2(RealmType.LOGS, ("Cadastrando serviço",))
try:
servico = Servico(**dados)
except ValidationError as e:
self.logger.logIC2(RealmType.ERRORS, (f"Erro ao validar dados do serviço: {e}",))
self.banco.logar_sistema(id_usuario=id_usr, acao='POST', tabela='servico', status='ERRO', mensagem=f'Validação: {e}', cliente=self.client, correlation_id=correlation_id)
return json.dumps({"status": "error", "message": str(e)})
resultado = servico_controller.cadastrar_servico(servico=servico, data=dados)
case "get":
self.logger.logIC2(RealmType.LOGS, ("Listando serviços",))
if dados and "id" in dados:
resultado = servico_controller.buscar_servico(id_servico=dados.get('id'), data=dados)
else:
resultado = servico_controller.listar_servicos(data=dados)
case "put":
self.logger.logIC2(RealmType.LOGS, ("Atualizando serviço",))
resultado = servico_controller.atualizar_servico(id_servico=dados.get('id'), servico=dados)
case "delete":
self.logger.logIC2(RealmType.LOGS, ("Deletando serviço",))
resultado = servico_controller.deletar_servico(id_servico=dados.get('id'))
case _:
self.logger.logIC2(RealmType.ERRORS, ("Método não encontrado",))
self.banco.logar_sistema(id_usuario=id_usr, acao=metodo.upper(), tabela='servico', status='ERRO', mensagem='Método não encontrado', cliente=self.client, correlation_id=correlation_id)
return json.dumps({"status": "error", "message": "Método não encontrado"})
if isinstance(resultado, dict) and "error" in resultado:
msg = resultado["error"]
if "response" in resultado:
msg = f"{msg} | Detalhes: {resultado['response']}"
raise Exception(msg)
self.logger.logIC2(RealmType.LOGS, ("Operação realizada com sucesso",))
self.banco.logar_sistema(id_usuario=id_usr, acao=metodo.upper(), tabela='servico', status='SUCESSO', mensagem='Operação concluída', cliente=self.client, correlation_id=correlation_id)
return self._responder("ok", resultado)
except Exception as e:
self.logger.logIC2(RealmType.ERRORS, (f"Erro ao executar método {metodo} - {e}",))
self.banco.logar_sistema(id_usuario=id_usr, acao=metodo.upper() if isinstance(metodo, str) else 'SERVICO', tabela='servico', status='ERRO', mensagem=str(e), cliente=self.client, correlation_id=correlation_id)
raise e
def tomador(self, metodo, dados=None, correlation_id=None):
# Gerencia clientes/clientes fiscais envolvidos nas transações (Tomadores)
id_usr = dados.get('id_usuario', 1) if dados else 1
try:
self.logger.logIC2(RealmType.LOGS, (f"Iniciando serviço de Tomador - Método: {metodo.upper()}",))
tomador_controller = TomadorController(
api_key=self.api_key,
banco=self.banco,
url_base=self.url_base,
logger=self.logger
)
metodo = metodo.lower()
resultado = None
match metodo:
case "post":
self.logger.logIC2(RealmType.LOGS, ("Cadastrando tomador",))
tomador_model = Tomador(**dados)
resultado = tomador_controller.cadastrar_tomador(tomador=tomador_model, data=dados)
case "get":
self.logger.logIC2(RealmType.LOGS, ("Listando tomadores",))
resultado = tomador_controller.buscar_tomador(cpf_cnpj=dados.get('cpf_cnpj')) if dados and 'cpf_cnpj' in dados else tomador_controller.buscar_tomador()
case "put":
self.logger.logIC2(RealmType.LOGS, ("Atualizando tomador",))
tomador_model = Tomador(**dados)
resultado = tomador_controller.atualizar_tomador(cpf_cnpj=dados.get('cpf_cnpj'), tomador=tomador_model)
case "delete":
self.logger.logIC2(RealmType.LOGS, ("Deletando tomador",))
resultado = tomador_controller.deletar_tomador(cpf_cnpj=dados.get('cpf_cnpj'))
case _:
self.logger.logIC2(RealmType.ERRORS, ("Método não encontrado",))
self.banco.logar_sistema(id_usuario=id_usr, acao=metodo.upper(), tabela='tomador', status='ERRO', mensagem='Método não encontrado', cliente=self.client, correlation_id=correlation_id)
return json.dumps({"status": "error", "message": "Método não encontrado"})
if isinstance(resultado, dict) and "error" in resultado:
msg = resultado["error"]
if "response" in resultado:
msg = f"{msg} | Detalhes: {resultado['response']}"
raise Exception(msg)
self.logger.logIC2(RealmType.LOGS, ("Operação realizada com sucesso",))
self.banco.logar_sistema(id_usuario=id_usr, acao=metodo.upper(), tabela='tomador', status='SUCESSO', mensagem='Operação concluída', cliente=self.client, correlation_id=correlation_id)
return self._responder("ok", resultado)
except Exception as e:
self.logger.logIC2(RealmType.ERRORS, (f"Erro ao executar método {metodo} - {e}",))
self.banco.logar_sistema(id_usuario=id_usr, acao=metodo.upper() if isinstance(metodo, str) else 'TOMADOR', tabela='tomador', status='ERRO', mensagem=str(e), cliente=self.client, correlation_id=correlation_id)
raise e
def relatorios(self, metodo, dados=None, correlation_id=None):
# Extrai planilhas mensais/períodicas e relatórios aglomerados
id_usr = dados.get('id_usuario', 1) if dados else 1
try:
self.logger.logIC2(RealmType.LOGS, (f"Iniciando serviço de Relatórios - Método: {metodo.upper()}",))
relatorios = Relatorios(
url_base=self.url_base,
api_key=self.api_key,
banco=self.banco,
logger=self.logger
)
metodo = metodo.lower()
resultado = None
match metodo:
case "nfe_emitidas":
self.logger.logIC2(RealmType.LOGS, ("Relatório de NF-e emitidas",))
resultado = relatorios.nfe_emitidas()
case "nfe_por_cnpj":
self.logger.logIC2(RealmType.LOGS, ("Relatório de NF-e por CNPJ",))
resultado = relatorios.nfe_por_cnpj(cnpj=dados.get('cnpj'))
case "nfse_emitidas":
self.logger.logIC2(RealmType.LOGS, ("Relatório de NFS-e emitidas",))
resultado = relatorios.nfse_emitidas(
data_inicial=dados.get('data_inicial'),
data_final=dados.get('data_final')
)
case "nfse_por_cnpj":
self.logger.logIC2(RealmType.LOGS, ("Relatório de NFS-e por CNPJ",))
resultado = relatorios.nfse_por_cnpj(
cnpj=dados.get('cnpj'),
data_inicial=dados.get('data_inicial'),
data_final=dados.get('data_final')
)
case _:
self.logger.logIC2(RealmType.ERRORS, ("Método não encontrado",))
self.banco.logar_sistema(id_usuario=id_usr, acao=metodo.upper(), tabela='relatorios', status='ERRO', mensagem='Método não encontrado', cliente=self.client, correlation_id=correlation_id)
return json.dumps({"status": "error", "message": "Método não encontrado"})
if isinstance(resultado, dict) and "error" in resultado:
msg = resultado["error"]
if "response" in resultado:
msg = f"{msg} | Detalhes: {resultado['response']}"
raise Exception(msg)
self.logger.logIC2(RealmType.LOGS, ("Operação realizada com sucesso",))
self.banco.logar_sistema(id_usuario=id_usr, acao=metodo.upper(), tabela='relatorios', status='SUCESSO', mensagem='Operação concluída', cliente=self.client, correlation_id=correlation_id)
return self._responder("ok", resultado)
except Exception as e:
self.logger.logIC2(RealmType.ERRORS, (f"Erro ao executar método {metodo} - {e}",))
self.banco.logar_sistema(id_usuario=id_usr, acao=metodo.upper() if isinstance(metodo, str) else 'RELATORIOS', tabela='relatorios', status='ERRO', mensagem=str(e), cliente=self.client, correlation_id=correlation_id)
raise e
def webhooks(self, metodo, dados=None, correlation_id=None):
# Gerencia as rotas externas para qual a PlugNotas envia atualização
id_usr = dados.get('id_usuario', 1) if dados else 1
try:
self.logger.logIC2(RealmType.LOGS, (f"Iniciando serviço de Webhooks - Método: {metodo.upper()}",))
webhooks = Webhooks(
url_base=self.url_base,
api_key=self.api_key,
banco=self.banco,
logger=self.logger
)
metodo = metodo.lower()
resultado = None
match metodo:
case "get":
self.logger.logIC2(RealmType.LOGS, ("Consultando webhooks",))
resultado = webhooks.consultar_webhooks(cnpj=dados.get('cnpj'))
case "post":
self.logger.logIC2(RealmType.LOGS, ("Criando webhook",))
resultado = webhooks.criar_webhook(cnpj=dados.get('cnpj'), url=dados.get('url'))
case "delete":
self.logger.logIC2(RealmType.LOGS, ("Deletando webhook",))
resultado = webhooks.deletar_webhook(cnpj=dados.get('cnpj'))
case "put":
self.logger.logIC2(RealmType.LOGS, ("Atualizando webhook",))
resultado = webhooks.atualizar_webhook(cnpj=dados.get('cnpj'), url=dados.get('url'))
case "test":
self.logger.logIC2(RealmType.LOGS, ("Testando webhook",))
resultado = webhooks.testar_webhook(cnpj=dados.get('cnpj'))
case _:
self.logger.logIC2(RealmType.ERRORS, ("Método não encontrado",))
self.banco.logar_sistema(id_usuario=id_usr, acao=metodo.upper(), tabela='webhooks', status='ERRO', mensagem='Método não encontrado', cliente=self.client, correlation_id=correlation_id)
return json.dumps({"status": "error", "message": "Método não encontrado"})
if isinstance(resultado, dict) and "error" in resultado:
msg = resultado["error"]
if "response" in resultado:
msg = f"{msg} | Detalhes: {resultado['response']}"
raise Exception(msg)
self.logger.logIC2(RealmType.LOGS, ("Operação realizada com sucesso",))
self.banco.logar_sistema(id_usuario=id_usr, acao=metodo.upper(), tabela='webhooks', status='SUCESSO', mensagem='Operação concluída', cliente=self.client, correlation_id=correlation_id)
return self._responder("ok", resultado)
except Exception as e:
self.logger.logIC2(RealmType.ERRORS, (f"Erro ao executar método {metodo} - {e}",))
self.banco.logar_sistema(id_usuario=id_usr, acao=metodo.upper() if isinstance(metodo, str) else 'WEBHOOKS', tabela='webhooks', status='ERRO', mensagem=str(e), cliente=self.client, correlation_id=correlation_id)
raise e
def callback(self, message):
#Descriptografa a msg, e repassa pra função correta (ex: nfe)
try:
dado = json.loads(message)
hmac = dado.get('hmac')
payload = dado.get('payload')
if hmac and payload:
servicekey = self.system_config.servicekey.ljust(32, '0')[:32]
self.exchange.setCrypto(serviceid=self.system_config.serviceid,
servicekey=servicekey)
if self.exchange.getPayload(hmac, payload):
payload_dict = json.loads(self.exchange.payload)
# Validação do payload contra os modelos tipados
try:
if isinstance(payload_dict.get('dados'), dict) and 'incentivoCultural' in payload_dict['dados']:
payload_dict['dados']['incentivadorCultural'] = payload_dict['dados'].pop('incentivoCultural')
validated_envelope = validate_payload(payload_dict)
self.logger.logIC2(RealmType.LOGS, (f"Payload validado: {payload_dict.get('fila')}/{payload_dict.get('metodo')}",))
except (ValidationError, ValueError) as ve:
self.logger.logIC2(RealmType.ERRORS, (f"Payload inválido: {str(ve)}",))
return json.dumps({"status": "error", "message": f"Payload inválido: {str(ve)}"})
config = Config(**payload_dict['config'])
self.client = payload_dict.get('cliente')
self.logger.client = self.client
self.logger.logIC2(RealmType.LOGS, (f"Configuração recebida para cliente {self.client}",))
api_key_extraida = config.api_key or config.x_api_key
expected_pg_user = os.getenv("POSTGRES_USER", "tecnospeed")
if config.user_postgres != expected_pg_user:
self.logger.logIC2(
RealmType.ERRORS,
(f"Usuário PostgreSQL inválido: {config.user_postgres}. Permitido: {expected_pg_user}",)
)
return json.dumps({"status": "error", "message": f"Apenas o usuário PostgreSQL '{expected_pg_user}' é permitido"})
if api_key_extraida and config.user_postgres and config.pass_postgres and config.host_postgres and config.port_postgres and config.database_postgres:
self.api_key = api_key_extraida
self.banco = Banco(user=config.user_postgres,
password=config.pass_postgres,
host=config.host_postgres,
port=config.port_postgres,
database=config.database_postgres)
self.banco.create_tables()
self.logger.logIC2(RealmType.LOGS, (f"Configuração completa para o cliente {self.client}",))
if 'url_base' in payload_dict:
self.url_base = payload_dict['url_base']
if payload_dict['fila'] not in self.funcoes:
self.logger.logIC2(RealmType.ERRORS, ("Fila não encontrada",))
return json.dumps({"status":"error","message":"Fila não encontrada"})
corr_id = self.banco.gerar_correlation_id()
self.logger.logIC2(RealmType.LOGS, (f"[{corr_id}] Processando {payload_dict['fila']}/{payload_dict['metodo']} para cliente {self.client}",))
retorno = self.funcoes[payload_dict['fila']](payload_dict['metodo'], payload_dict['dados'], correlation_id=corr_id)
return retorno
else:
self.logger.logIC2(RealmType.ERRORS, ("Configuração incompleta",))
return json.dumps({"status":"error","message":"Configuração incompleta"})
else:
self.logger.logIC2(RealmType.ERRORS, (f"Erro ao decodificar payload - {payload}",))
return json.dumps({"status":"error","message":"Erro ao decodificar payload"})
else:
if self.logger:
self.logger.logIC2(RealmType.ERRORS, (f"Erro ao receber dado (sem hmac/payload) - {dado}",))
return json.dumps({"status":"error","message":"Erro ao receber dado"})
except Exception as e:
if self.logger:
self.logger.logIC2(RealmType.ERRORS, (f"Erro ao executar callback - {e}",))
return json.dumps({"status":"error","message":str(e)})
finally:
if hasattr(self, 'banco') and self.banco:
self.banco.close()
def run(self):
# Ponto de Partida. Conecta no Redis, constrói os parâmetros AMQP (URL) e inicia o loop RabbitMQ
try:
print("[STATUS] Conectando ao RabbitMQ")
try:
system_config = self.redis.get("system_config")
if system_config:
system_config = json.loads(system_config)
self.system_config = SystemConfig(**system_config)
print(f"[INFO] Configuração do sistema carregada - {self.system_config}")
else:
msg = "Configuração do sistema não encontrada no Redis"
print(f"[ERRO] {msg}")
raise Exception(msg)
except Exception as e:
msg = f"Erro ao conectar ao Redis - {e}"
print(f"[ERRO] {msg}")
raise
# Inicializa LoggerExchange2 após obter a URL AMQP do Redis
builder = AppIC2Builder(service_name=self.service, versionstr=self.version, origin=self.system_config.origin)
self.logger = LoggerExchange2(
ic2_builder=builder,
amqps=self.system_config.amqps,
system=self.system,
service=self.service,
version=self.version,
origin=self.system_config.origin
)
self.logger.logIC2(RealmType.LOGS, ("LoggerExchange2 inicializado com sucesso",))
self.exchange = RPCExchange2(
amqps=self.system_config.amqps,
origin=self.system_config.origin,
system=self.system,
service=self.service,
version=self.version,
client=self.client,
workfunction=self.callback,
delivery_mode = 2
)
self.logger.logIC2(RealmType.LOGS, (f"Consumindo da fila {self.exchange.queue}",))
self.exchange.start_consuming()
except Exception as e:
if self.logger:
self.logger.logIC2(RealmType.ERRORS, (f"Erro fatal ao conectar ao RabbitMQ - {e}",))
else:
print(f"[ERRO] Erro ao conectar ao RabbitMQ - {e}")
time.sleep(5)
sys.exit(1)
def _responder(self, status, resultado):
# Auxiliar para formatar a resposta RPC criptografada conforme esperado pelo Exchange2
print(f"[RPC] Preparando resposta criptografada para o cliente (status: {status})", flush=True)
servicekey = self.system_config.servicekey.ljust(32, '0')[:32]
return json.dumps({
"status": status,
"payload": resultado, # A classe RPCExchange2 vai criptografar este campo
"serviceid": self.system_config.serviceid,
"servicekey": servicekey
})
if __name__ == "__main__":
main = Main()
main.run()

660
backup/src/mainQUEUE.py Normal file
View file

@ -0,0 +1,660 @@
# IMPORT DAS CLASSES DE TRABALHO
from certificado_digital.certificado_digital import CertificadoDigital
from empresas.empresa import EmpresaController
from NFSe.nfse import NFSeService
from NFSe.Servico.servico import ServicoController
from NFSe.Tomador.tomador import TomadorController
from Relatorios.relatorios import Relatorios
from webhooks.webhooks import Webhooks
from NFe.registroNFe import RegistroNFeService
# IMPORTS DE MODELS
from Models.config import Config, SystemConfig
from Models.empresa import Empresa
from Models.nfse import NFSe
from Models.servico import Servico
from Models.nfe import NFe
from Models.tomador import Tomador
from Models.payload import validate_payload
# IMPORTS DE BIBLIOTECAS
from pydantic import ValidationError
import json, sys, time, random, os, uuid
# IMPORTS DE CLASSES AUXILIARES
from utils.Exchange2 import QueueExchange2, LoggerExchange2, AppIC2Builder
from utils.exchangeModels import RealmType
from utils.banco import Banco
from utils.redis import RedisClient
# VERSÃO DO SISTEMA
version = "1.0.0" # Versão inicial do sistema
version = "1.0.1" # Implementando ORM
version = "1.0.2" # Implementando RPC
version = "1.0.3" # Implementando configuração via Redis
version = "2.0.0" # NFe e refatoração da Main
version = "2.0.1" # Refatoração para QueueExchange
class Main:
def __init__(self):
self.client = "#"
self.api_key = ""
self.exchange = None
self.banco = None
self.system_config: SystemConfig = None
self.redis = RedisClient(host=os.getenv('REDIS_HOST', 'localhost'), port=6379)
self.service = "tecnospeed"
self.system = "integracao"
self.version = version
self.logger = None
self.url_base = os.getenv('PLUGNOTAS_URL', 'https://api.sandbox.plugnotas.com.br')
self.funcoes = {
"nfe": self.nfe,
"nfse": self.nfse,
"certificado_digital": self.certificado_digital,
"empresa": self.empresa,
"servico": self.servico,
"tomador": self.tomador,
"relatorios": self.relatorios,
"webhooks": self.webhooks
}
# Métodos de serviços orientado a eventos.
def nfe(self, metodo, dados=None, correlation_id=None):
# Roteador de emissões, baixas e cancelamentos via NFe
id_usr = dados.get('id_usuario', 1) if dados else 1
try:
self.logger.logIC2(RealmType.LOGS, (f"Iniciando serviço de NFe - Método: {metodo.upper()}",))
nfe_service = RegistroNFeService(api_key=self.api_key,
url_base=self.url_base,
banco=self.banco,
logger=self.logger)
metodo = metodo.lower()
resultado = None
match metodo:
case "post":
self.logger.logIC2(RealmType.LOGS, ("Cadastrando NFe",))
nfe = NFe(**dados.get('nfe', dados))
resultado = nfe_service.cadastrar_nfe(nfe_list=[nfe], data=dados)
case "get":
self.logger.logIC2(RealmType.LOGS, ("Consultando resumo de NFe",))
resultado = nfe_service.consultar_resumo(id_nota=dados.get('id'))
case "download":
self.logger.logIC2(RealmType.LOGS, ("Baixando PDF NFe",))
resultado = nfe_service.baixar_pdf(id_nota=dados.get('id'))
case "download_xml":
self.logger.logIC2(RealmType.LOGS, ("Baixando XML NFe",))
resultado = nfe_service.baixar_xml(id_nota=dados.get('id'))
case "cancelar":
self.logger.logIC2(RealmType.LOGS, ("Cancelando NFe",))
resultado = nfe_service.cancelar_nfe(id_nota=dados.get('id'), justificativa=dados.get('justificativa', 'Cancelamento padrão'))
case "status_cancelamento":
self.logger.logIC2(RealmType.LOGS, ("Consultando status de cancelamento",))
resultado = nfe_service.consultar_status_cancelamento(id_nota=dados.get('id'))
case "cce":
self.logger.logIC2(RealmType.LOGS, ("Enviando Carta de Correção NFe",))
resultado = nfe_service.enviar_cce(id_nota=dados.get('id'), correcao=dados.get('correcao'))
case "status_cce":
self.logger.logIC2(RealmType.LOGS, ("Consultando status CCe NFe",))
resultado = nfe_service.consultar_status_cce(id_nota=dados.get('id'))
case "resumo":
self.logger.logIC2(RealmType.LOGS, ("Consultando resumo de NFe por CNPJ de integracao",))
resultado = nfe_service.consultar_resumo_por_cnpj_integracao(cnpj=dados.get('cnpj'), id_integracao=dados.get('id_integracao', dados.get('id')))
case _:
self.logger.logIC2(RealmType.ERRORS, ("Método não encontrado",))
self.banco.logar_sistema(id_usuario=id_usr, acao=metodo.upper(), tabela='nfe', status='ERRO', mensagem='Método não encontrado', cliente=self.client, correlation_id=correlation_id)
return json.dumps({"status": "error", "message": "Método não encontrado"})
if isinstance(resultado, dict) and "error" in resultado:
msg_erro = resultado["error"]
if "response" in resultado and resultado["response"]:
msg_erro += f" - Detalhes: {json.dumps(resultado['response'])}"
raise Exception(msg_erro)
self.exchange.setPayload(json.dumps(resultado))
self.logger.logIC2(RealmType.LOGS, ("Operação realizada com sucesso",))
self.banco.logar_sistema(id_usuario=id_usr, acao=metodo.upper(), tabela='nfe', status='SUCESSO', mensagem='Operação concluída', json=json.dumps(resultado) if resultado else '{}', cliente=self.client, correlation_id=correlation_id)
return json.dumps({"status": "ok", "data": resultado})
except Exception as e:
msg_completa = str(e)
self.logger.logIC2(RealmType.ERRORS, (f"Erro ao executar método {metodo} - {msg_completa}",))
self.banco.logar_sistema(id_usuario=id_usr, acao=metodo.upper() if isinstance(metodo, str) else 'NFE', tabela='nfe', status='ERRO', mensagem=msg_completa, cliente=self.client, correlation_id=correlation_id)
raise e
def nfse(self, metodo, dados=None, correlation_id=None):
# Roteador de Notas Fiscais de Roteiro/Serviço (NFSe)
id_usr = dados.get('id_usuario', 1) if dados else 1
try:
self.logger.logIC2(RealmType.LOGS, (f"Iniciando serviço de NFSe - Método: {metodo.upper()}",))
nfse_service = NFSeService(api_key=self.api_key,
banco=self.banco,
url_base=self.url_base,
logger=self.logger)
metodo = metodo.lower()
resultado = None
match metodo:
case "post":
self.logger.logIC2(RealmType.LOGS, ("Cadastrando NFSe",))
nfse = NFSe(**dados.get('nfse', dados))
resultado = nfse_service.cadastrar_nfse(nfse=nfse, data=dados)
case "get":
self.logger.logIC2(RealmType.LOGS, ("Listando NFSe",))
resultado = nfse_service.buscar_nfse(id_nfse=dados.get('id')) if dados and 'id' in dados else nfse_service.listar_nfse()
case "download":
self.logger.logIC2(RealmType.LOGS, ("Baixando NFSe",))
resultado = nfse_service.baixar_nfse(id_nfse=dados.get('id'))
case "download_xml":
self.logger.logIC2(RealmType.LOGS, ("Baixando XML NFSe",))
resultado = nfse_service.baixar_xml_nfse(id_nfse=dados.get('id'))
case "cancelar":
self.logger.logIC2(RealmType.LOGS, ("Cancelando NFSe",))
resultado = nfse_service.solicitar_cancelamento_nfse(id_nfse=dados.get('id'))
case "status_cancelamento":
self.logger.logIC2(RealmType.LOGS, ("Consultando status de cancelamento",))
resultado = nfse_service.consultar_status_cancelamento_nfse(id_nfse=dados.get('id'))
case "resumo":
self.logger.logIC2(RealmType.LOGS, ("Consultando resumo de NFSe",))
resultado = nfse_service.consultar_resumo_nfse(id_nfse=dados.get('id'), cnpj=dados.get('cnpj'))
case "historico":
self.logger.logIC2(RealmType.LOGS, ("Baixando histórico de NFSe",))
resultado = nfse_service.baixar_histotico_nfse(id_nfse=dados.get('id'), cnpj=dados.get('cnpj'))
case _:
self.logger.logIC2(RealmType.ERRORS, ("Método não encontrado",))
self.banco.logar_sistema(id_usuario=id_usr, acao=metodo.upper(), tabela='nfse', status='ERRO', mensagem='Método não encontrado', cliente=self.client, correlation_id=correlation_id)
return json.dumps({"status": "error", "message": "Método não encontrado"})
if isinstance(resultado, dict) and "error" in resultado:
raise Exception(json.dumps(resultado))
self.exchange.setPayload(json.dumps(resultado))
self.logger.logIC2(RealmType.LOGS, ("Operação realizada com sucesso",))
self.banco.logar_sistema(id_usuario=id_usr, acao=metodo.upper(), tabela='nfse', status='SUCESSO', mensagem='Operação concluída', json=json.dumps(resultado) if resultado else '{}', cliente=self.client, correlation_id=correlation_id)
return json.dumps({"status": "ok", "data": resultado})
except Exception as e:
self.logger.logIC2(RealmType.ERRORS, (f"Erro ao executar método {metodo} - {e}",))
self.banco.logar_sistema(id_usuario=id_usr, acao=metodo.upper() if isinstance(metodo, str) else 'NFSE', tabela='nfse', status='ERRO', mensagem=str(e), cliente=self.client, correlation_id=correlation_id)
raise e
def certificado_digital(self, metodo, dados=None, correlation_id=None):
# Gerenciamento de envio dos certificados A3/A1
id_usr = dados.get('id_usuario', 1) if dados else 1
try:
self.logger.logIC2(RealmType.LOGS, (f"Iniciando serviço de Certificado Digital - Método: {metodo.upper()}",))
certificado_digital = CertificadoDigital(api_key=self.api_key, banco=self.banco, url_base=self.url_base, logger=self.logger)
metodo = metodo.lower()
resultado = None
match metodo:
case "post":
self.logger.logIC2(RealmType.LOGS, ("Cadastrando certificado digital",))
resultado = certificado_digital.post_form_data(data=dados)
case "get":
self.logger.logIC2(RealmType.LOGS, ("Listando certificados digitais",))
resultado = certificado_digital.get(id=dados.get('id')) if dados and 'id' in dados else certificado_digital.get()
case "put":
self.logger.logIC2(RealmType.LOGS, ("Atualizando certificado digital",))
resultado = certificado_digital.put_certificado(id=dados.get('id'), data=dados)
case _:
self.logger.logIC2(RealmType.ERRORS, ("Método não encontrado",))
self.banco.logar_sistema(id_usuario=id_usr, acao=metodo.upper(), tabela='certificado_digital', status='ERRO', mensagem='Método não encontrado', cliente=self.client, correlation_id=correlation_id)
return json.dumps({"status": "error", "message": "Método não encontrado"})
if isinstance(resultado, dict) and "error" in resultado:
raise Exception(resultado["error"])
self.exchange.setPayload(json.dumps(resultado))
self.logger.logIC2(RealmType.LOGS, ("Operação realizada com sucesso",))
self.banco.logar_sistema(id_usuario=id_usr, acao=metodo.upper(), tabela='certificado_digital', status='SUCESSO', mensagem='Operação concluída', cliente=self.client, correlation_id=correlation_id)
return json.dumps({"status": "ok", "data": resultado})
except Exception as e:
self.logger.logIC2(RealmType.ERRORS, (f"Erro ao executar método {metodo} - {e}",))
self.banco.logar_sistema(id_usuario=id_usr, acao=metodo.upper() if isinstance(metodo, str) else 'CERT', tabela='certificado_digital', status='ERRO', mensagem=str(e), cliente=self.client, correlation_id=correlation_id)
raise e
def empresa(self, metodo, dados=None, correlation_id=None):
# Intermediador do cadastro e listagem das Empresas emissoras
id_usr = dados.get('id_usuario', 1) if dados else 1
try:
self.logger.logIC2(RealmType.LOGS, (f"Iniciando serviço de Enterprise - Método: {metodo.upper()}",))
empresa_controller = EmpresaController(
api_key=self.api_key,
banco=self.banco,
url_base=self.url_base,
logger=self.logger
)
metodo = metodo.lower()
resultado = None
match metodo:
case "post":
self.logger.logIC2(RealmType.LOGS, ("Cadastrando empresa",))
empresa = Empresa(**dados)
resultado = empresa_controller.post(empresa=empresa, data=dados)
case "get":
self.logger.logIC2(RealmType.LOGS, ("Listando empresas",))
resultado = empresa_controller.get(cnpj=dados.get('cnpj'), data=dados) if dados and 'cnpj' in dados else empresa_controller.get(data=dados)
case "post_file":
self.logger.logIC2(RealmType.LOGS, ("Cadastrando empresa via arquivo",))
if not dados or 'file' not in dados or 'cnpj' not in dados:
raise ValueError("Parâmetros 'file' e 'cnpj' são obrigatórios para 'post_file'")
resultado = empresa_controller.post_file(file=dados['file'], cnpj=dados['cnpj'])
case _:
self.logger.logIC2(RealmType.ERRORS, ("Método não encontrado",))
self.banco.logar_sistema(id_usuario=id_usr, acao=metodo.upper(), tabela='Enterprise', status='ERRO', mensagem='Método não encontrado', cliente=self.client, correlation_id=correlation_id)
return json.dumps({"status": "error", "message": "Método não encontrado"})
if isinstance(resultado, dict) and "error" in resultado:
raise Exception(resultado["error"])
self.exchange.setPayload(json.dumps(resultado))
self.logger.logIC2(RealmType.LOGS, ("Operação realizada com sucesso",))
self.banco.logar_sistema(id_usuario=id_usr, acao=metodo.upper(), tabela='Enterprise', status='SUCESSO', mensagem='Operação concluída', json=json.dumps(resultado) if resultado else '{}', cliente=self.client, correlation_id=correlation_id)
return json.dumps({"status": "ok", "data": resultado})
except Exception as e:
print(f"\n[ERRO CRITICAL] {metodo.upper()} -> {str(e)}\n", flush=True); self.logger.logIC2(RealmType.ERRORS, (f"Erro ao executar método {metodo.upper()} - {str(e)}",))
self.banco.logar_sistema(id_usuario=id_usr, acao=metodo.upper() if isinstance(metodo, str) else 'ENTERPRISE', tabela='Enterprise', status='ERRO', mensagem=str(e), cliente=self.client, correlation_id=correlation_id)
return json.dumps({"status": "error", "message": str(e)})
def servico(self, metodo, dados=None, correlation_id=None):
# Intermediador do CRUD de Serviços com configurações de impostos
id_usr = dados.get('id_usuario', 1) if dados else 1
try:
self.logger.logIC2(RealmType.LOGS, (f"Iniciando serviço de Serviço - Método: {metodo.upper()}",))
servico_controller = ServicoController(
api_key=self.api_key,
banco=self.banco,
url_base=self.url_base,
logger=self.logger
)
metodo = metodo.lower()
resultado = None
match metodo:
case "post":
self.logger.logIC2(RealmType.LOGS, ("Cadastrando serviço",))
try:
servico = Servico(**dados)
except ValidationError as e:
self.logger.logIC2(RealmType.ERRORS, (f"Erro ao validar dados do serviço: {e}",))
self.banco.logar_sistema(id_usuario=id_usr, acao='POST', tabela='servico', status='ERRO', mensagem=f'Validação: {e}', cliente=self.client, correlation_id=correlation_id)
return json.dumps({"status": "error", "message": str(e)})
resultado = servico_controller.cadastrar_servico(servico=servico, data=dados)
case "get":
self.logger.logIC2(RealmType.LOGS, ("Listando serviços",))
if dados and "id" in dados:
resultado = servico_controller.buscar_servico(id_servico=dados.get('id'), data=dados)
else:
resultado = servico_controller.listar_servicos(data=dados)
case "put":
self.logger.logIC2(RealmType.LOGS, ("Atualizando serviço",))
resultado = servico_controller.atualizar_servico(id_servico=dados.get('id'), servico=dados)
case "delete":
self.logger.logIC2(RealmType.LOGS, ("Deletando serviço",))
resultado = servico_controller.deletar_servico(id_servico=dados.get('id'))
case _:
self.logger.logIC2(RealmType.ERRORS, ("Método não encontrado",))
self.banco.logar_sistema(id_usuario=id_usr, acao=metodo.upper(), tabela='servico', status='ERRO', mensagem='Método não encontrado', cliente=self.client, correlation_id=correlation_id)
return json.dumps({"status": "error", "message": "Método não encontrado"})
if isinstance(resultado, dict) and "error" in resultado:
raise Exception(resultado["error"])
self.exchange.setPayload(json.dumps(resultado))
self.logger.logIC2(RealmType.LOGS, ("Operação realizada com sucesso",))
self.banco.logar_sistema(id_usuario=id_usr, acao=metodo.upper(), tabela='servico', status='SUCESSO', mensagem='Operação concluída', cliente=self.client, correlation_id=correlation_id)
return json.dumps({"status": "ok", "data": resultado})
except Exception as e:
self.logger.logIC2(RealmType.ERRORS, (f"Erro ao executar método {metodo} - {e}",))
self.banco.logar_sistema(id_usuario=id_usr, acao=metodo.upper() if isinstance(metodo, str) else 'SERVICO', tabela='servico', status='ERRO', mensagem=str(e), cliente=self.client, correlation_id=correlation_id)
raise e
def tomador(self, metodo, dados=None, correlation_id=None):
# Gerencia clientes/clientes fiscais envolvidos nas transações (Tomadores)
id_usr = dados.get('id_usuario', 1) if dados else 1
try:
self.logger.logIC2(RealmType.LOGS, (f"Iniciando serviço de Tomador - Método: {metodo.upper()}",))
tomador_controller = TomadorController(
api_key=self.api_key,
banco=self.banco,
url_base=self.url_base,
logger=self.logger
)
metodo = metodo.lower()
resultado = None
match metodo:
case "post":
self.logger.logIC2(RealmType.LOGS, ("Cadastrando tomador",))
tomador_model = Tomador(**dados)
resultado = tomador_controller.cadastrar_tomador(tomador=tomador_model, data=dados)
case "get":
self.logger.logIC2(RealmType.LOGS, ("Listando tomadores",))
resultado = tomador_controller.buscar_tomador(cpf_cnpj=dados.get('cpf_cnpj')) if dados and 'cpf_cnpj' in dados else tomador_controller.buscar_tomador()
case "put":
self.logger.logIC2(RealmType.LOGS, ("Atualizando tomador",))
tomador_model = Tomador(**dados)
resultado = tomador_controller.atualizar_tomador(cpf_cnpj=dados.get('cpf_cnpj'), tomador=tomador_model)
case "delete":
self.logger.logIC2(RealmType.LOGS, ("Deletando tomador",))
resultado = tomador_controller.deletar_tomador(cpf_cnpj=dados.get('cpf_cnpj'))
case _:
self.logger.logIC2(RealmType.ERRORS, ("Método não encontrado",))
self.banco.logar_sistema(id_usuario=id_usr, acao=metodo.upper(), tabela='tomador', status='ERRO', mensagem='Método não encontrado', cliente=self.client, correlation_id=correlation_id)
return json.dumps({"status": "error", "message": "Método não encontrado"})
if isinstance(resultado, dict) and "error" in resultado:
raise Exception(resultado["error"])
self.exchange.setPayload(json.dumps(resultado))
self.logger.logIC2(RealmType.LOGS, ("Operação realizada com sucesso",))
self.banco.logar_sistema(id_usuario=id_usr, acao=metodo.upper(), tabela='tomador', status='SUCESSO', mensagem='Operação concluída', cliente=self.client, correlation_id=correlation_id)
return json.dumps({"status": "ok", "data": resultado})
except Exception as e:
self.logger.logIC2(RealmType.ERRORS, (f"Erro ao executar método {metodo} - {e}",))
self.banco.logar_sistema(id_usuario=id_usr, acao=metodo.upper() if isinstance(metodo, str) else 'TOMADOR', tabela='tomador', status='ERRO', mensagem=str(e), cliente=self.client, correlation_id=correlation_id)
raise e
def relatorios(self, metodo, dados=None, correlation_id=None):
# Extrai planilhas mensais/períodicas e relatórios aglomerados
id_usr = dados.get('id_usuario', 1) if dados else 1
try:
self.logger.logIC2(RealmType.LOGS, (f"Iniciando serviço de Relatórios - Método: {metodo.upper()}",))
relatorios = Relatorios(
url_base=self.url_base,
api_key=self.api_key,
banco=self.banco,
logger=self.logger
)
metodo = metodo.lower()
resultado = None
match metodo:
case "nfe_emitidas":
self.logger.logIC2(RealmType.LOGS, ("Relatório de NF-e emitidas",))
resultado = relatorios.nfe_emitidas()
case "nfe_por_cnpj":
self.logger.logIC2(RealmType.LOGS, ("Relatório de NF-e por CNPJ",))
resultado = relatorios.nfe_por_cnpj(cnpj=dados.get('cnpj'))
case "nfse_emitidas":
self.logger.logIC2(RealmType.LOGS, ("Relatório de NFS-e emitidas",))
resultado = relatorios.nfse_emitidas(data_inicial=dados.get('data_inicial'), data_final=dados.get('data_final'))
case "nfse_por_cnpj":
self.logger.logIC2(RealmType.LOGS, ("Relatório de NFS-e por CNPJ",))
resultado = relatorios.nfse_por_cnpj(cnpj=dados.get('cnpj'), data_inicial=dados.get('data_inicial'), data_final=dados.get('data_final'))
case _:
self.logger.logIC2(RealmType.ERRORS, ("Método não encontrado",))
self.banco.logar_sistema(id_usuario=id_usr, acao=metodo.upper(), tabela='relatorios', status='ERRO', mensagem='Método não encontrado', cliente=self.client, correlation_id=correlation_id)
return json.dumps({"status": "error", "message": "Método não encontrado"})
if isinstance(resultado, dict) and "error" in resultado:
raise Exception(resultado["error"])
self.exchange.setPayload(json.dumps(resultado))
self.logger.logIC2(RealmType.LOGS, ("Operação realizada com sucesso",))
self.banco.logar_sistema(id_usuario=id_usr, acao=metodo.upper(), tabela='relatorios', status='SUCESSO', mensagem='Operação concluída', cliente=self.client, correlation_id=correlation_id)
return json.dumps({"status": "ok", "data": resultado})
except Exception as e:
self.logger.logIC2(RealmType.ERRORS, (f"Erro ao executar método {metodo} - {e}",))
self.banco.logar_sistema(id_usuario=id_usr, acao=metodo.upper() if isinstance(metodo, str) else 'RELATORIOS', tabela='relatorios', status='ERRO', mensagem=str(e), cliente=self.client, correlation_id=correlation_id)
raise e
def webhooks(self, metodo, dados=None, correlation_id=None):
# Gerencia as rotas externas para qual a PlugNotas envia atualização
id_usr = dados.get('id_usuario', 1) if dados else 1
try:
self.logger.logIC2(RealmType.LOGS, (f"Iniciando serviço de Webhooks - Método: {metodo.upper()}",))
webhooks = Webhooks(
url_base=self.url_base,
api_key=self.api_key,
banco=self.banco,
logger=self.logger
)
metodo = metodo.lower()
resultado = None
match metodo:
case "get":
self.logger.logIC2(RealmType.LOGS, ("Consultando webhooks",))
resultado = webhooks.consultar_webhooks(cnpj=dados.get('cnpj'))
case "post":
self.logger.logIC2(RealmType.LOGS, ("Criando webhook",))
resultado = webhooks.criar_webhook(cnpj=dados.get('cnpj'), url=dados.get('url'))
case "delete":
self.logger.logIC2(RealmType.LOGS, ("Deletando webhook",))
resultado = webhooks.deletar_webhook(cnpj=dados.get('cnpj'))
case "put":
self.logger.logIC2(RealmType.LOGS, ("Atualizando webhook",))
resultado = webhooks.atualizar_webhook(cnpj=dados.get('cnpj'), url=dados.get('url'))
case "test":
self.logger.logIC2(RealmType.LOGS, ("Testando webhook",))
resultado = webhooks.testar_webhook(cnpj=dados.get('cnpj'))
case _:
self.logger.logIC2(RealmType.ERRORS, ("Método não encontrado",))
self.banco.logar_sistema(id_usuario=id_usr, acao=metodo.upper(), tabela='webhooks', status='ERRO', mensagem='Método não encontrado', cliente=self.client, correlation_id=correlation_id)
return json.dumps({"status": "error", "message": "Método não encontrado"})
if isinstance(resultado, dict) and "error" in resultado:
raise Exception(resultado["error"])
self.exchange.setPayload(json.dumps(resultado))
self.logger.logIC2(RealmType.LOGS, ("Operação realizada com sucesso",))
self.banco.logar_sistema(id_usuario=id_usr, acao=metodo.upper(), tabela='webhooks', status='SUCESSO', mensagem='Operação concluída', cliente=self.client, correlation_id=correlation_id)
except Exception as e:
self.logger.logIC2(RealmType.ERRORS, (f"Erro ao executar método {metodo} - {e}",))
self.banco.logar_sistema(id_usuario=id_usr, acao=metodo.upper() if isinstance(metodo, str) else 'WEBHOOKS', tabela='webhooks', status='ERRO', mensagem=str(e), cliente=self.client, correlation_id=correlation_id)
raise e
def callback(self, ch, method, properties, body):
#Descriptografa a msg, e repassa pra função correta (ex: nfe)
try:
message = body.decode("utf-8")
dado = json.loads(message)
hmac = dado.get('hmac')
payload = dado.get('payload')
if hmac and payload:
time.sleep(random.uniform(1,5))
servicekey = self.system_config.servicekey.ljust(32, '0')[:32]
self.exchange.setCrypto(serviceid=self.system_config.serviceid,
servicekey=servicekey)
if self.exchange.getPayload(hmac, payload):
payload_dict = json.loads(self.exchange.payload)
# Validação do payload contra os modelos tipados
try:
if isinstance(payload_dict.get('dados'), dict) and 'incentivoCultural' in payload_dict['dados']:
payload_dict['dados']['incentivadorCultural'] = payload_dict['dados'].pop('incentivoCultural')
validated_envelope = validate_payload(payload_dict)
self.logger.logIC2(RealmType.LOGS, (f"Payload validado: {payload_dict.get('fila')}/{payload_dict.get('metodo')}",))
except (ValidationError, ValueError) as ve:
self.logger.logIC2(RealmType.ERRORS, (f"Payload inválido: {str(ve)}",))
ch.basic_nack(delivery_tag=method.delivery_tag, requeue=False)
return json.dumps({"status": "error", "message": f"Payload inválido: {str(ve)}"})
config = Config(**payload_dict['config'])
self.client = payload_dict.get('cliente')
self.logger.client = self.client
self.logger.logIC2(RealmType.LOGS, (f"Configuração recebida para cliente {self.client}",))
api_key_extraida = config.api_key or config.x_api_key
expected_pg_user = os.getenv("POSTGRES_USER", "tecnospeed")
if config.user_postgres != expected_pg_user:
self.logger.logIC2(
RealmType.ERRORS,
(f"Usuário PostgreSQL inválido: {config.user_postgres}. Permitido: {expected_pg_user}",)
)
ch.basic_nack(delivery_tag=method.delivery_tag, requeue=False)
return json.dumps({"status": "error", "message": f"Apenas o usuário PostgreSQL '{expected_pg_user}' é permitido"})
if api_key_extraida and config.user_postgres and config.pass_postgres and config.host_postgres and config.port_postgres and config.database_postgres:
self.api_key = api_key_extraida
self.banco = Banco(user=config.user_postgres,
password=config.pass_postgres,
host=config.host_postgres,
port=config.port_postgres,
database=config.database_postgres)
self.banco.create_tables()
self.logger.logIC2(RealmType.LOGS, (f"Configuração completa para o cliente {self.client}",))
if 'url_base' in payload_dict:
self.url_base = payload_dict['url_base']
if payload_dict['fila'] not in self.funcoes:
self.logger.logIC2(RealmType.ERRORS, ("Fila não encontrada",))
ch.basic_nack(delivery_tag=method.delivery_tag, requeue=False)
return json.dumps({"status":"error","message":"Fila não encontrada"})
corr_id = self.banco.gerar_correlation_id()
self.logger.logIC2(RealmType.LOGS, (f"[{corr_id}] Processando {payload_dict['fila']}/{payload_dict['metodo']} para cliente {self.client}",))
retorno = self.funcoes[payload_dict['fila']](payload_dict['metodo'], payload_dict['dados'], correlation_id=corr_id)
ch.basic_ack(delivery_tag=method.delivery_tag)
return retorno
else:
self.logger.logIC2(RealmType.ERRORS, ("Configuração incompleta",))
ch.basic_nack(delivery_tag=method.delivery_tag, requeue=False)
return json.dumps({"status":"error","message":"Configuração incompleta"})
else:
self.logger.logIC2(RealmType.ERRORS, (f"Erro ao decodificar payload - {payload}",))
ch.basic_nack(delivery_tag=method.delivery_tag, requeue=False)
return json.dumps({"status":"error","message":"Erro ao decodificar payload"})
else:
if self.logger:
self.logger.logIC2(RealmType.ERRORS, (f"Erro ao receber dado (sem hmac/payload) - {dado}",))
ch.basic_nack(delivery_tag=method.delivery_tag, requeue=False)
return json.dumps({"status":"error","message":"Erro ao receber dado"})
except Exception as e:
if self.logger:
self.logger.logIC2(RealmType.ERRORS, (f"Erro ao executar callback - {e}",))
ch.basic_nack(delivery_tag=method.delivery_tag, requeue=False)
return json.dumps({"status":"error","message":str(e)})
finally:
if hasattr(self, 'banco') and self.banco:
self.banco.close()
def run(self):
# Ponto de Partida. Conecta no Redis, constrói os parâmetros AMQP (URL) e inicia o loop RabbitMQ
try:
print("[STATUS] Conectando ao RabbitMQ")
try:
system_config = self.redis.get("system_config")
if system_config:
system_config = json.loads(system_config)
self.system_config = SystemConfig(**system_config)
print(f"[INFO] Configuração do sistema carregada - {self.system_config}")
else:
msg = "Configuração do sistema não encontrada no Redis"
print(f"[ERRO] {msg}")
raise Exception(msg)
except Exception as e:
msg = f"Erro ao conectar ao Redis - {e}"
print(f"[ERRO] {msg}")
raise
# Inicializa LoggerExchange2 após obter a URL AMQP do Redis
builder = AppIC2Builder(service_name=self.service, versionstr=self.version, origin=self.system_config.origin)
self.logger = LoggerExchange2(
ic2_builder=builder,
amqps=self.system_config.amqps,
system=self.system,
service=self.service,
version=self.version,
origin=self.system_config.origin
)
self.logger.logIC2(RealmType.LOGS, ("LoggerExchange2 inicializado com sucesso",))
self.exchange = QueueExchange2(
amqps=self.system_config.amqps,
origin=self.system_config.origin,
system=self.system,
client=self.client,
service=self.service,
version=self.version,
callback=self.callback,
delivery_mode = 1)
self.logger.logIC2(RealmType.LOGS, (f"Consumindo da fila {self.exchange.queue}",))
self.exchange.start_consuming()
except Exception as e:
print(f"[ERRO FATAL (CONSOLE)] Erro ao conectar ao RabbitMQ - {e}")
if self.logger:
self.logger.logIC2(RealmType.ERRORS, (f"Erro fatal ao conectar ao RabbitMQ - {e}",))
else:
print(f"[ERRO] Erro ao conectar ao RabbitMQ - {e}")
time.sleep(5)
sys.exit(1)
if __name__ == "__main__":
main = Main()
main.run()

View file

@ -0,0 +1,21 @@
import sys
import os
import uvicorn
sys.path.append(os.path.dirname(os.path.abspath(__file__)))
from listenerEndpoint import app
# O EndpointListener tem a função de manter a aplicação servindo de webhooks ativa recebendo HTTP e convertendo para RabbitMQ
class EndpointListener:
def __init__(self):
self.host = "0.0.0.0"
self.port = int(os.getenv("WEBHOOK_PORT", 8000))
def run(self):
print(f"[STATUS] Endpoint Listener rodando na porta {self.port}...")
uvicorn.run(app, host=self.host, port=self.port)
if __name__ == "__main__":
main = EndpointListener()
main.run()

View file

@ -0,0 +1 @@
# tests package

View file

@ -0,0 +1,119 @@
"""
Fixtures compartilhados para os testes E2E do servico_plugnotas.
Cria um banco SQLite in-memory, mocks do Logger e da Api,
e uma instância pré-configurada da classe Main para cada teste.
"""
import pytest
import json
import sys
from unittest.mock import MagicMock, patch
mock_pika = MagicMock()
sys.modules["pika"] = mock_pika
mock_redis_module = MagicMock()
sys.modules["redis"] = mock_redis_module
from utils.banco import Base, Banco, LogTable
from utils.api import Api
@pytest.fixture
def banco_teste():
"""Cria um Banco real com SQLite in-memory para cada teste (isolado)."""
from sqlalchemy import create_engine
from sqlalchemy.orm import sessionmaker
engine = create_engine("sqlite:///:memory:")
Base.metadata.create_all(engine)
Session = sessionmaker(bind=engine)
banco = Banco.__new__(Banco)
banco.engine = engine
banco.session = Session()
yield banco
banco.session.close()
engine.dispose()
@pytest.fixture
def mock_logger():
"""Logger que grava todas as chamadas de log em uma lista para inspeção."""
logger = MagicMock()
logger.logs = []
logger.client = "TestClient"
def capture_log(realm, msg_tuple):
logger.logs.append({"realm": str(realm), "message": msg_tuple[0] if msg_tuple else ""})
logger.logIC2.side_effect = capture_log
return logger
@pytest.fixture
def mock_api():
"""Api que retorna respostas configuráveis sem fazer chamadas HTTP reais."""
api = MagicMock(spec=Api)
api.get.return_value = {"status": "ok"}
api.post.return_value = {"documents": [{"id": "PLUG-001"}]}
api.put.return_value = {"status": "ok"}
api.patch.return_value = {"status": "ok"}
api.delete.return_value = {"status": "ok"}
api.post_file.return_value = {"status": "ok"}
api.post_form_data.return_value = {"data": {"id": "CERT-001"}}
api.get_paginated.return_value = {"data": [], "total": 0}
return api
@pytest.fixture
def mock_exchange():
"""Exchange que simula setPayload sem enviar nada."""
exchange = MagicMock()
exchange.payload = ""
exchange.payloads_set = []
def capture_payload(p):
exchange.payloads_set.append(p)
exchange.payload = p
exchange.setPayload.side_effect = capture_payload
return exchange
@pytest.fixture
def main_instance(banco_teste, mock_logger, mock_exchange):
with patch("utils.Exchange2.RPCExchange2"), \
patch("utils.Exchange2.QueueExchange2"), \
patch("utils.Exchange2.LoggerExchange2"), \
patch("utils.redis.RedisClient"):
from main import Main
with patch.object(Main, '__init__', lambda self: None):
main = Main()
main.client = "TestClient"
main.api_key = "test-api-key-fake"
main.exchange = mock_exchange
main.banco = banco_teste
main.system_config = MagicMock()
main.system_config.serviceid = "test-id"
main.system_config.servicekey = "test-key-32-chars-long-12345678"
main.redis = MagicMock()
main.service = "tecnospeed"
main.system = "integracao"
main.version = "2.0.1-test"
main.logger = mock_logger
main.url_base = "https://api.sandbox.plugnotas.com.br"
main.funcoes = {
"nfe": main.nfe,
"nfse": main.nfse,
"certificado_digital": main.certificado_digital,
"empresa": main.empresa,
"servico": main.servico,
"tomador": main.tomador,
"relatorios": main.relatorios,
"webhooks": main.webhooks,
}
return main

View file

@ -0,0 +1,67 @@
import json
import os
import sys
# Garante que o Python encontre os módulos do projeto
sys.path.append(os.path.dirname(os.path.dirname(os.path.abspath(__file__))))
from Models.payload import validate_payload
sample_payload = {
"fila": "nfse",
"cliente": "MassiveNFSeTest",
"metodo": "POST",
"dados": {
"id_empresa": "some-uuid",
"id_usuario": 1,
"idIntegracao": "TEST_123",
"prestador": {
"cpfCnpj": "00000000000000"
},
"tomador": {
"cpfCnpj": "08114280956",
"razaoSocial": "NFSE EMITIDA EM HOMOLOGACAO",
"email": "contato@tecnospeed.com.br",
"endereco": {
"logradouro": "AVENIDA DUQUE DE CAXIAS",
"numero": "882",
"bairro": "CENTRO",
"codigoCidade": "4115200",
"descricaoCidade": "MARINGA",
"estado": "PR",
"cep": "87020025",
"tipoLogradouro": "Avenida"
}
},
"servico": {
"codigo": "1.01",
"codigoTributacao": "1.01",
"discriminacao": "SERVICO DE TESTE DE CARGA MASSIVA",
"cnae": "6201501",
"iss": {
"tipoTributacao": 1,
"exigibilidade": 1,
"aliquota": 2.0
},
"valor": {
"servico": 100.0
}
}
},
"config": {
"x_api_key": "2da392a6-79d2-4304-a8b7-959572c7e44d",
"user_postgres": "tecnospeed",
"pass_postgres": "u7QimG7E!SF//JC2c8",
"host_postgres": "banco",
"port_postgres": "5432",
"database_postgres": "tecnospeed"
},
"url_base": "https://api.sandbox.plugnotas.com.br"
}
try:
print("Tentando validar payload...")
validated = validate_payload(sample_payload)
print("Sucesso! Payload válido.")
except Exception as e:
print(f"Erro de validação: {e}")

View file

@ -0,0 +1,33 @@
import requests
import os
import json
def debug_report():
api_key = "2da392a6-79d2-4304-a8b7-959572c7e44d" # From src/utils/api.py comment
url = "https://api.sandbox.plugnotas.com.br/nfse/relatorio?from=2026-04-01&to=2026-04-30"
headers = {
"x-api-key": api_key
}
print(f"Testing URL: {url}")
response = requests.get(url, headers=headers)
print(f"Status Code: {response.status_code}")
try:
print(f"Response Body: {json.dumps(response.json(), indent=2)}")
except:
print(f"Response Body: {response.text}")
# Test with ISO format
url_iso = "https://api.sandbox.plugnotas.com.br/nfse/relatorio?from=2026-04-01T00:00:00&to=2026-04-30T23:59:59"
print(f"\nTesting URL (ISO): {url_iso}")
response = requests.get(url_iso, headers=headers)
print(f"Status Code: {response.status_code}")
try:
print(f"Response Body: {json.dumps(response.json(), indent=2)}")
except:
print(f"Response Body: {response.text}")
if __name__ == "__main__":
debug_report()

View file

@ -0,0 +1,46 @@
import pika
import os
import sys
import json
# Garante que o Python encontre os módulos do projeto
sys.path.append(os.path.dirname(os.path.dirname(os.path.abspath(__file__))))
from utils.redis import RedisClient
def purge_queue():
# 1. Obter URL do RabbitMQ do Redis
redis_host = os.getenv('REDIS_HOST', 'localhost')
redis_client = RedisClient(host=redis_host, port=6379)
try:
system_config_data = redis_client.get("system_config")
if not system_config_data:
print("Erro: system_config não encontrado no Redis.")
return
system_config = json.loads(system_config_data)
amqps_url = system_config.get("amqps")
except Exception as e:
print(f"Erro ao ler configs do Redis: {e}")
return
if not amqps_url:
print("Erro: AMQPS URL não encontrada na configuração.")
return
# 2. Conectar e limpar a fila
queue_name = "integracao_tecnospeed"
try:
params = pika.URLParameters(amqps_url)
connection = pika.BlockingConnection(params)
channel = connection.channel()
# Tenta realizar o purge
result = channel.queue_purge(queue=queue_name)
print(f"Sucesso! Fila [{queue_name}] limpa. Mensagens removidas: {result.method.message_count}")
connection.close()
except Exception as e:
print(f"Erro ao limpar a fila: {e}")
if __name__ == "__main__":
purge_queue()

134
backup/src/tests/test.py Normal file
View file

@ -0,0 +1,134 @@
import sys
import os
import json
sys.path.append(os.path.dirname(os.path.abspath(__file__)))
from Models.config import SystemConfig
from utils.redis import RedisClient
from utils.Exchange2 import RPCExchange2
def main():
print("Iniciando script de testes da mensageria...")
# Carregando configuração do Redis
redis_client = RedisClient(host=os.getenv('REDIS_HOST', 'localhost'), port=6379)
try:
system_config_data = redis_client.get("system_config")
if not system_config_data:
print("Erro: system_config não encontrado no Redis. Certifique-se de que a variável existe.")
return
system_config = SystemConfig(**json.loads(system_config_data))
print(f"[{system_config.origin}] Configuração do sistema lida com sucesso.")
except Exception as e:
print(f"Erro ao ler configs do Redis: {e}")
return
# Inicializando mensageria RPC
try:
amqps_local = system_config.amqps.replace("host.docker.internal", "localhost")
exchange = RPCExchange2(
amqps=amqps_local,
origin=system_config.origin,
system="integracao",
service="tecnospeed",
version="2.0.1",
client="#"
)
except Exception as e:
print(f"Falha ao inicializar mensageria: {e}")
return
# Configurando criptografia e payload
servicekey = system_config.servicekey.ljust(32, '0')[:32]
exchange.setCrypto(serviceid=system_config.serviceid, servicekey=servicekey)
# Payload da mensageria
payload_interno = {
"fila": "nfse",
"cliente": "Test",
"metodo": "POST",
"dados": {
"idIntegracao": "INTEGRACAO-NFSE-002",
"prestador": {
"cpfCnpj": "08187168000160"
},
"tomador": {
"cpfCnpj": "08114280956",
"razaoSocial": "João Tomador da Silva",
"inscricaoMunicipal": "isento",
"email": "tomador@email.com",
"endereco": {
"descricaoCidade": "São Paulo",
"cep": "00000000",
"tipoLogradouro": "Rua",
"logradouro": "Dois de Ouro",
"tipoBairro": "Bairro",
"codigoCidade": "3550308",
"complemento": "Apto 13",
"estado": "SP",
"numero": "44",
"bairro": "Centro"
}
},
"servico": {
"codigo": "1.01",
"codigoTributacao": "1.01",
"discriminacao": "Serviços prestados de tecnologia",
"cnae": "6204000",
"iss": {
"tipoTributacao": 6,
"exigibilidade": 1,
"aliquota": 2.5
},
"valor": {
"servico": 1500.50,
"descontoCondicionado": 0.0,
"descontoIncondicionado": 0.0
}
}
},
"config": {
"x_api_key": os.getenv('PLUGNOTAS_API_KEY', ''),
"user_postgres": os.getenv('POSTGRES_USER', 'tecnospeed'),
"pass_postgres": os.getenv('POSTGRES_PASSWORD', 'tecnospeed'),
"host_postgres": os.getenv('POSTGRES_HOST', 'localhost'),
"port_postgres": os.getenv('POSTGRES_PORT', '5432'),
"database_postgres": os.getenv('POSTGRES_DB', 'tecnospeed')
},
"url_base": "https://api.sandbox.plugnotas.com.br"
}
# Criptografando e enviando tarefa
exchange.setPayload(json.dumps(payload_interno))
print("Disparando chamada RPC para o RabbitMQ...")
# Executando chamada RPC
response = exchange.callRPC("Disparo de testes executado pelo test.py")
print("\n[RAW RPC RESPONSE]:")
print(response)
# Descriptografando a resposta
try:
resp_obj = json.loads(response)
hmac_resp = resp_obj.get('hmac')
payload_resp = resp_obj.get('payload')
if hmac_resp and payload_resp:
if exchange.getPayload(hmac_resp, payload_resp):
print("\n[DECRYPTED RPC DATA]:")
print(json.dumps(json.loads(exchange.payload), indent=2))
else:
print("\n[ERROR]: Failed to decrypt response.")
else:
print("\n[INFO]: Response does not contain an encrypted payload.")
except Exception as e:
print(f"\n[ERROR]: Error processing response: {e}")
if __name__ == "__main__":
main()

View file

@ -0,0 +1,58 @@
import pytest
from unittest.mock import patch, MagicMock
from fastapi.testclient import TestClient
from src.listenerEndpoint import app
client = TestClient(app)
PAYLOAD_VALIDO = {
"id": "60a7c4f1c9d2a00015...",
"status": "CONCLUIDO",
"documento": "nfe"
}
HEADERS_VALIDOS = {"x-plugnotas-signature": "assinatura_hmac_valida"}
class TestPlugNotasWebhookEndpoint:
@patch("src.listenerEndpoint.exchange")
def test_deve_receber_payload_e_enfileirar_com_sucesso(self, mock_exchange):
"""Critério: Caminho feliz. Valida, enfileira e responde com sucesso (200)."""
mock_exchange.sendTask.return_value = True
response = client.post("/listenerPlugnotas", json=PAYLOAD_VALIDO, headers=HEADERS_VALIDOS)
assert response.status_code == 200
assert response.json() == {"status": "ok", "message": "Webhook recebido e encaminhado para processamento"}
mock_exchange.setPayload.assert_called_once()
mock_exchange.sendTask.assert_called_once_with("Webhook PlugNotas Recebido")
@patch("src.listenerEndpoint.exchange")
def test_deve_rejeitar_payload_invalido_antes_de_processar(self, mock_exchange):
"""Critério: Integridade de Dados. Valida campos obrigatórios recebidos do Hook."""
payload_invalido = {"status": "CONCLUIDO"} # Faltando ID e documento
response = client.post("/listenerPlugnotas", json=payload_invalido, headers=HEADERS_VALIDOS)
assert response.status_code == 200
assert response.json()["status"] == "warning"
assert "Campos faltantes" in response.json()["message"]
@patch("src.listenerEndpoint.exchange")
def test_deve_retornar_erro_se_internal_error_no_rabbitmq(self, mock_exchange):
"""Critério: Resiliência. Retorna 500 em caso de erro ao enviar para fila."""
# Simulando uma queda ao enviar task
mock_exchange.sendTask.side_effect = Exception("RabbitMQ is down")
response = client.post("/listenerPlugnotas", json=PAYLOAD_VALIDO, headers=HEADERS_VALIDOS)
assert response.status_code == 500
assert response.json()["message"] == "Erro interno no processamento do webhook"
def test_deve_retornar_erro_se_exchange_nao_inicializado(self):
"""Critério: Resiliência. Retorna 500 se o exchange global for nulo."""
# Sem patch de exchange, então pegará o exchange default (None) em client sem lifespan completo
response = client.post("/listenerPlugnotas", json=PAYLOAD_VALIDO)
assert response.status_code == 500
assert response.json()["message"] == "Serviço temporariamente indisponível"

View file

@ -0,0 +1,144 @@
import os
import sys
import json
import time
# Garante que o Python encontre os módulos do projeto
sys.path.append(os.path.dirname(os.path.dirname(os.path.abspath(__file__))))
from Models.config import SystemConfig
from utils.redis import RedisClient
from utils.Exchange2 import RPCExchange2, QueueExchange2
def get_empresa_payload(i):
"""Gera um payload de empresa com CNPJ e Razão Social baseados no índice i"""
# Gerando um CNPJ fictício baseado no loop para evitar duplicidade no banco
# Ex: 94074986000001, 94074986000002...
cnpj = f"94074986{str(i).zfill(6)}"
return {
"id_usuario": 1,
"cpfCnpj": cnpj,
"razaoSocial": f"Varejo de Teste {i} LTDA",
"nomeFantasia": f"Varejo {i}",
"inscricaoMunicipal": "0001",
"inscricaoEstadual": "123456789",
"simplesNacional": True,
"regimeTributario": 1,
"regimeTributarioEspecial": 1,
"incentivadorCultural": False,
"incentivoFiscal": True,
"certificado": "5af59d271f6e8f409178fbf3",
"endereco": {
"codigoPais": "1058",
"descricaoPais": "BRASIL",
"tipoLogradouro": "Avenida",
"logradouro": "Avenida de Teste",
"numero": str(i),
"complemento": "Casa",
"tipoBairro": "Bairro",
"bairro": "Centro",
"codigoCidade": "4115200",
"descricaoCidade": "Maringá",
"estado": "PR",
"cep": "87020025"
},
"nfe": {"ativo": True, "tipoContrato": 0},
"nfse": {"ativo": False}
}
def run_rpc_test(exchange, config_rpc):
"""Envia uma única empresa via RPC e aguarda a resposta (bom para debug)"""
print("\n--- INICIANDO TESTE RPC INDIVIDUAL ---")
payload_dados = get_empresa_payload(1)
full_payload = {
"fila": "empresa",
"metodo": "post",
"cliente": "tray",
"url_base": "https://api.sandbox.plugnotas.com.br",
"config": config_rpc,
"dados": payload_dados
}
print(f"Enviando empresa {payload_dados['cpfCnpj']} via RPC...")
exchange.setPayload(json.dumps(full_payload))
resposta = exchange.callRPC(json.dumps(full_payload), timeout=30)
print("\nRESPOSTA DO SERVIDOR:")
print(resposta)
print("--------------------------------------\n")
def run_mass_test(exchange, config_rpc, total=1000):
"""Envia N empresas via Fila (sem esperar resposta), ideal para teste de carga"""
print(f"\n--- INICIANDO CARGA MASSIVA: {total} Mensagens ---")
start_time = time.time()
for i in range(1, total + 1):
payload_dados = get_empresa_payload(i)
full_payload = {
"fila": "empresa",
"metodo": "post",
"cliente": "MassTest",
"url_base": "https://api.sandbox.plugnotas.com.br",
"config": config_rpc,
"dados": payload_dados
}
exchange.setPayload(json.dumps(full_payload))
exchange.sendMsg(json.dumps(full_payload), service="tecnospeed")
if i % 100 == 0:
print(f"Enviadas: {i}/{total}...")
end_time = time.time()
print(f"Carga concluída em {end_time - start_time:.2f} segundos.")
print("--------------------------------------\n")
def main():
# 1. Carrega configurações do Redis
redis_host = os.getenv('REDIS_HOST', 'localhost')
redis_client = RedisClient(host=redis_host, port=6379)
try:
config_data = redis_client.get("system_config")
if not config_data:
print("Erro: system_config não encontrado no Redis.")
return
sc = SystemConfig(**json.loads(config_data))
except Exception as e:
print(f"Erro ao carregar configs: {e}")
return
# Configurações de Banco e API para o Worker
config_rpc = {
"x_api_key": "2da392a6-79d2-4304-a8b7-959572c7e44d",
"user_postgres": "tecnospeed",
"pass_postgres": "u7QimG7E%21SF%2F%2FJC2c8",
"host_postgres": "banco",
"port_postgres": "5432",
"database_postgres": "tecnospeed"
}
print(f"Conectando ao RabbitMQ de {sc.origin}...")
# 2. Escolha do Modo de Operação
# Mude para True para testar o retorno (RPC Server) ou False para carga assíncrona (Queue Worker)
MODO_DEBUG_RPC = True
if MODO_DEBUG_RPC:
# Usa RPCExchange2 para modo Síncrono (Request-Response)
exchange = RPCExchange2(sc.amqps, sc.origin, "integracao", "tecnospeed", "2.1.0", client="")
servicekey = sc.servicekey.ljust(32, '0')[:32]
exchange.setCrypto(sc.serviceid, servicekey)
run_rpc_test(exchange, config_rpc)
else:
# Usa QueueExchange2 para modo Assíncrono (Fire and Forget)
exchange = QueueExchange2(sc.amqps, sc.origin, "integracao", "tecnospeed", "2.1.0", client="#")
servicekey = sc.servicekey.ljust(32, '0')[:32]
exchange.setCrypto(sc.serviceid, servicekey)
run_mass_test(exchange, config_rpc, total=1000)
if __name__ == "__main__":
main()

View file

@ -0,0 +1,219 @@
import sys
import os
import json
import time
# Adiciona o diretório atual ao path para importar as classes locais
sys.path.append(os.path.dirname(os.path.abspath(__file__)))
from Models.config import SystemConfig
from utils.redis import RedisClient
from utils.Exchange2 import QueueExchange2
from utils.banco import Banco, EnterpriseTable
import random
def main():
print("Iniciando script de teste de carga: 1000 NFes...", flush=True)
# 1. Carregando configuração do Redis para obter dados do RabbitMQ
redis_host = os.getenv('REDIS_HOST', 'localhost')
redis_client = RedisClient(host=redis_host, port=6379)
try:
system_config_data = redis_client.get("system_config")
if not system_config_data:
print("Erro: system_config não encontrado no Redis.")
return
system_config = SystemConfig(**json.loads(system_config_data))
print(f"Configuração [{system_config.origin}] carregada.", flush=True)
except Exception as e:
print(f"Erro ao ler configs do Redis: {e}")
return
# 2. Inicializando a conexão com a FILA (RabbitMQ)
try:
print("Tentando conectar ao RabbitMQ...", flush=True)
exchange = QueueExchange2(
amqps=system_config.amqps,
origin=system_config.origin,
system="integracao",
service="tecnospeed",
version="2.1.0",
client="#"
)
except Exception as e:
print(f"Falha ao iniciar mensageria: {e}")
return
# 3. Configurando criptografia (necessário para o worker aceitar a mensagem)
servicekey = system_config.servicekey.ljust(32, '0')[:32]
exchange.setCrypto(serviceid=system_config.serviceid, servicekey=servicekey)
# 4. Buscando empresas válidas no banco para o teste
try:
# Configuração do banco - Força conexão interna se estiver no Docker
# O .env às vezes aponta para o IP externo que não resolve dentro do container
db_host = os.getenv('POSTGRES_HOST', 'postgres')
db_port = int(os.getenv('POSTGRES_PORT', 5432))
# Se estivermos DENTRO do docker, o host 'postgres' é o correto.
if db_host != 'postgres':
print(f"Aviso: POSTGRES_HOST no ambiente é {db_host}. Tentando 'postgres' primeiro por estar no Docker...")
db_host = 'postgres'
db_port = 5432
print(f"Conectando ao banco em {db_host}:{db_port}...")
banco = Banco(
host=db_host,
port=db_port,
user=os.getenv('POSTGRES_USER', 'tecnospeed'),
password=os.getenv('POSTGRES_PASSWORD', 'u7QimG7E!SF//JC2c8'),
database=os.getenv('POSTGRES_DB', 'tecnospeed')
)
print("Executando query na tabela Enterprise...", flush=True)
empresas = banco.session.query(EnterpriseTable.id, EnterpriseTable.cnpj).filter(EnterpriseTable.cnpj != None, EnterpriseTable.cnpj != "").limit(100).all()
lista_empresas = [{"id": emp.id, "cnpj": emp.cnpj} for emp in empresas]
if not lista_empresas:
print("Erro: Nenhuma empresa com CNPJ válido encontrada na tabela 'Enterprise'.", flush=True)
return
print(f"Sucesso: {len(lista_empresas)} empresas válidas encontradas para o teste.", flush=True)
except Exception as e:
print(f"Erro ao conectar ao banco de dados: {e}")
return
# Base do payload fornecido pelo usuário (omitindo emitente fixo para preencher no loop)
payload_base = {
"nfe": {
"idIntegracao": "", # Será preenchido no loop
"presencial": True,
"consumidorFinal": True,
"natureza": "OPERAÇÃO INTERNA",
"destinatario": {
"cpfCnpj": "08114280956",
"razaoSocial": "NF-E EMITIDA EM HOMOLOGACAO",
"email": "contato@tecnospeed.com.br",
"endereco": {
"logradouro": "AVENIDA DUQUE DE CAXIAS",
"numero": "882",
"bairro": "CENTRO",
"codigoCidade": "4115200",
"descricaoCidade": "MARINGA",
"estado": "PR",
"cep": "87020025"
}
},
"itens": [
{
"codigo": "1",
"descricao": "PRODUTO DE TESTE",
"ncm": "06029090",
"cest": "0123456",
"cfop": "5101",
"valorUnitario": {
"comercial": 4.67,
"tributavel": 4.67
},
"valor": 4.67,
"tributos": {
"icms": {
"origem": "0",
"cst": "00",
"baseCalculo": {
"modalidadeDeterminacao": 0,
"valor": 0
},
"aliquota": 0,
"valor": 0
},
"pis": {
"cst": "99",
"baseCalculo": {
"valor": 0,
"quantidade": 0
},
"aliquota": 0,
"valor": 0
},
"cofins": {
"cst": "07",
"baseCalculo": {
"valor": 0
},
"aliquota": 0,
"valor": 0
}
}
}
],
"pagamentos": [
{
"aVista": True,
"meio": "01",
"valor": 4.6
}
],
"responsavelTecnico": {
"cpfCnpj": "08187168000160",
"nome": "Tecnospeed",
"email": "contato@tecnospeed.com.br",
"telefone": {
"ddd": "44",
"numero": "30379500"
}
}
}
}
# Estrutura completa exigida pelo mainQUEUE.py
def get_full_payload(id_num, id_empresa, cnpj_empresa):
# Gerando um timestamp para garantir idIntegracao único
ts = int(time.time())
payload = json.loads(json.dumps(payload_base)) # Deep copy simples
payload["nfe"]["idIntegracao"] = f"TESTE_{ts}_{id_num}"
payload["nfe"]["emitente"] = {
"cpfCnpj": cnpj_empresa,
"inscricaoEstadual": "ISENTO"
}
return {
"fila": "nfe",
"cliente": "LoadTest1000",
"metodo": "POST",
"dados": {
"id_empresa": id_empresa,
**payload
},
"config": {
"x_api_key": "2da392a6-79d2-4304-a8b7-959572c7e44d",
"user_postgres": "tecnospeed",
"pass_postgres": "u7QimG7E!SF//JC2c8",
"host_postgres": "postgres",
"port_postgres": "5432",
"database_postgres": "tecnospeed"
},
"url_base": "https://api.sandbox.plugnotas.com.br"
}
print(f"Enviando 1000 mensagens para a fila...")
start_time = time.time()
for i in range(1, 1001):
empresa_sorteada = random.choice(lista_empresas)
full_payload = get_full_payload(i, id_empresa=empresa_sorteada["id"], cnpj_empresa=empresa_sorteada["cnpj"])
# É necessário criptografar o payload antes de cada envio
exchange.setPayload(json.dumps(full_payload))
exchange.sendMsg(json.dumps(full_payload), service="tecnospeed")
if i % 100 == 0:
print(f"Enviadas: {i}/1000...", flush=True)
end_time = time.time()
print("\nConcluído!")
print(f"Tempo total de envio: {end_time - start_time:.2f} segundos.")
print("Agora você pode acompanhar o processamento nos logs do worker (tecnospeed-queue).")
if __name__ == "__main__":
main()

View file

@ -0,0 +1,173 @@
import sys
import os
import json
import time
import random
# Adiciona o diretório atual ao path para importar as classes locais
sys.path.append(os.path.dirname(os.path.abspath(__file__)))
# Adiciona o diretório src ao path para encontrar os Models e Utils
sys.path.append(os.path.dirname(os.path.dirname(os.path.abspath(__file__))))
from Models.config import SystemConfig
from utils.redis import RedisClient
from utils.Exchange2 import QueueExchange2
from utils.banco import Banco, EnterpriseTable
def main():
print("Iniciando script de teste de carga: 1000 NFSe...", flush=True)
# 1. Carregando configuração do Redis para obter dados do RabbitMQ
redis_host = os.getenv('REDIS_HOST', 'localhost')
redis_client = RedisClient(host=redis_host, port=6379)
try:
system_config_data = redis_client.get("system_config")
if not system_config_data:
print("Erro: system_config não encontrado no Redis.")
return
system_config = SystemConfig(**json.loads(system_config_data))
print(f"Configuração [{system_config.origin}] carregada.", flush=True)
except Exception as e:
print(f"Erro ao ler configs do Redis: {e}")
return
# 2. Inicializando a conexão com a FILA (RabbitMQ)
try:
print("Tentando conectar ao RabbitMQ...", flush=True)
exchange = QueueExchange2(
amqps=system_config.amqps,
origin=system_config.origin,
system="integracao",
service="tecnospeed",
version="2.1.0",
client="#"
)
except Exception as e:
print(f"Falha ao iniciar mensageria: {e}")
return
# 3. Configurando criptografia (necessário para o worker aceitar a mensagem)
servicekey = system_config.servicekey.ljust(32, '0')[:32]
exchange.setCrypto(serviceid=system_config.serviceid, servicekey=servicekey)
# 4. Buscando empresas válidas no banco para o teste
try:
db_host = os.getenv('POSTGRES_HOST', 'banco')
db_port = int(os.getenv('POSTGRES_PORT', 5432))
# Se estivermos DENTRO do docker e o host configurado for o IP externo ou porta mapeada, forçamos 'banco' e '5432'
if os.path.exists('/.dockerenv') or os.path.exists('/proc/1/cgroup'):
print(f"Aviso: Ambiente Docker detectado. Forçando host 'banco' e porta 5432.")
db_host = 'banco'
db_port = 5432
print(f"Conectando ao banco em {db_host}:{db_port}...")
banco = Banco(
host=db_host,
port=db_port,
user=os.getenv('POSTGRES_USER', 'tecnospeed'),
password=os.getenv('POSTGRES_PASSWORD', 'u7QimG7E!SF//JC2c8'),
database=os.getenv('POSTGRES_DB', 'tecnospeed')
)
print("Executando query na tabela Enterprise...", flush=True)
empresas = banco.session.query(EnterpriseTable.id, EnterpriseTable.cnpj).filter(EnterpriseTable.cnpj != None, EnterpriseTable.cnpj != "").limit(100).all()
lista_empresas = [{"id": emp.id, "cnpj": emp.cnpj} for emp in empresas]
if not lista_empresas:
print("Erro: Nenhuma empresa com CNPJ válido encontrada na tabela 'Enterprise'.", flush=True)
return
print(f"Sucesso: {len(lista_empresas)} empresas válidas encontradas para o teste.", flush=True)
except Exception as e:
print(f"Erro ao conectar ao banco de dados: {e}")
return
# Base do payload de NFSe
payload_base = {
"nfse": {
"idIntegracao": "", # Será preenchido no loop
"prestador": {
"cpfCnpj": "" # Será preenchido no loop
},
"tomador": {
"cpfCnpj": "08114280956",
"razaoSocial": "NFSE EMITIDA EM HOMOLOGACAO",
"email": "contato@tecnospeed.com.br",
"endereco": {
"logradouro": "AVENIDA DUQUE DE CAXIAS",
"numero": "882",
"bairro": "CENTRO",
"codigoCidade": "4115200",
"descricaoCidade": "MARINGA",
"estado": "PR",
"cep": "87020025",
"tipoLogradouro": "Avenida"
}
},
"servico": {
"codigo": "1.01",
"codigoTributacao": "1.01",
"discriminacao": "SERVICO DE TESTE DE CARGA MASSIVA",
"cnae": "6201501",
"iss": {
"tipoTributacao": 1,
"exigibilidade": 1,
"aliquota": 2.0
},
"valor": {
"servico": 100.0
}
}
}
}
# Estrutura completa exigida pelo mainQUEUE.py
def get_full_payload(id_num, id_empresa, cnpj_empresa):
ts = int(time.time() * 1000)
payload = json.loads(json.dumps(payload_base)) # Deep copy
payload["nfse"]["idIntegracao"] = f"LOAD_NFSE_{ts}_{id_num}"
payload["nfse"]["prestador"]["cpfCnpj"] = cnpj_empresa
return {
"fila": "nfse",
"cliente": "MassiveNFSeTest",
"metodo": "POST",
"dados": {
"id_empresa": id_empresa,
"id_usuario": 1,
**payload["nfse"]
},
"config": {
"x_api_key": "2da392a6-79d2-4304-a8b7-959572c7e44d",
"user_postgres": "tecnospeed",
"pass_postgres": "u7QimG7E!SF//JC2c8",
"host_postgres": "banco",
"port_postgres": "5432",
"database_postgres": "tecnospeed"
},
"url_base": "https://api.sandbox.plugnotas.com.br"
}
print(f"Enviando 1000 NFSe para a fila...")
start_time = time.time()
for i in range(1, 1001):
empresa_sorteada = random.choice(lista_empresas)
full_payload = get_full_payload(i, id_empresa=empresa_sorteada["id"], cnpj_empresa=empresa_sorteada["cnpj"])
exchange.setPayload(json.dumps(full_payload))
exchange.sendMsg(json.dumps(full_payload), service="tecnospeed")
if i % 100 == 0:
print(f"Enviadas: {i}/1000...", flush=True)
end_time = time.time()
print("\nConcluído!")
print(f"Tempo total de envio: {end_time - start_time:.2f} segundos.")
print("Acompanhe o processamento nos logs do worker (tecnospeed-queue).")
if __name__ == "__main__":
main()

View file

@ -0,0 +1,29 @@
import json
import sys
import os
sys.path.append(os.path.dirname(os.path.abspath(__file__)))
from utils.api import Api
api = Api("2da392a6-79d2-4304-a8b7-959572c7e44d")
print("\n--- Consultando Certificados ---")
# GET /certificado (Lista todos)
certificados = api.get("https://api.sandbox.plugnotas.com.br/certificado")
print("GET /certificado response:")
print(json.dumps(certificados, indent=2))
# Se houver certificados, faz um GET específico no primeiro
if isinstance(certificados, list) and len(certificados) > 0:
first_cert_id = certificados[0].get('id')
print(f"\nConsultando certificado específico (ID: {first_cert_id}):")
spec_cert = api.get(f"https://api.sandbox.plugnotas.com.br/certificado/{first_cert_id}")
print(json.dumps(spec_cert, indent=2))
elif isinstance(certificados, dict) and "data" in certificados:
# Caso a resposta venha com envelope "data"
data_list = certificados["data"]
if data_list:
first_cert_id = data_list[0].get('id')
print(f"\nConsultando certificado específico (ID: {first_cert_id}):")
spec_cert = api.get(f"https://api.sandbox.plugnotas.com.br/certificado/{first_cert_id}")
print(json.dumps(spec_cert, indent=2))

View file

@ -0,0 +1,96 @@
import json
import pytest
from unittest.mock import patch, MagicMock
class TestRoteamento:
"""Verifica que todas as filas registradas no dict existem e são callable."""
def test_todas_filas_registradas(self, main_instance):
filas_esperadas = ["nfe", "nfse", "certificado_digital", "empresa",
"servico", "tomador", "relatorios", "webhooks"]
for fila in filas_esperadas:
assert fila in main_instance.funcoes, f"Fila '{fila}' não está registrada"
assert callable(main_instance.funcoes[fila]), f"Fila '{fila}' não é callable"
def test_fila_inexistente_nao_registrada(self, main_instance):
assert "webhook_callback" not in main_instance.funcoes
assert "fila_fantasma" not in main_instance.funcoes
class TestCallbackErros:
"""Testa que o callback lida corretamente com payloads inválidos."""
def test_callback_json_invalido(self, main_instance):
"""Payload que não é JSON válido deve retornar erro."""
resultado = json.loads(main_instance.callback("isso não é json"))
assert resultado["status"] == "error"
def test_callback_sem_hmac(self, main_instance):
"""Payload sem HMAC deve retornar erro."""
resultado = json.loads(main_instance.callback(json.dumps({"dados": "algo"})))
assert resultado["status"] == "error"
def test_callback_com_hmac_e_payload(self, main_instance, mock_exchange):
"""Payload com hmac+payload mas sem crypto configurada deve retornar erro."""
mock_exchange.getPayload.return_value = False
main_instance.system_config = MagicMock()
main_instance.system_config.servicekey = "testkey1234567890"
main_instance.system_config.serviceid = "test-service"
resultado = json.loads(main_instance.callback(json.dumps({
"hmac": "fake-hmac",
"payload": "fake-encrypted-payload"
})))
assert resultado["status"] == "error"
class TestBancoIntegracao:
"""Testa que os logs são gravados no banco durante as operações."""
def test_log_gravado_no_banco(self, main_instance, banco_teste, mock_api):
"""Ao executar uma operação, deve gravar log no banco."""
mock_api.get.return_value = {"id": "PLUG-NFE-001", "status": "CONCLUIDO"}
with patch("NFe.registroNFe.Api", return_value=mock_api):
main_instance.nfe("get", {"id": "PLUG-NFE-001", "id_usuario": 42})
from utils.banco import LogTable
logs = banco_teste.session.query(LogTable).all()
# Deve ter pelo menos 2 logs: INICIO + SUCESSO
assert len(logs) >= 2
acoes = [log.acao for log in logs]
assert "GET" in acoes
# Verifica que o id_usuario foi gravado
assert any(log.id_usuario == 42 for log in logs)
def test_log_erro_gravado(self, main_instance, banco_teste, mock_api):
"""Erros também devem ser gravados no banco."""
mock_api.get.side_effect = Exception("Timeout na API")
with patch("NFe.registroNFe.Api", return_value=mock_api):
with pytest.raises(Exception):
main_instance.nfe("get", {"id": "PLUG-NFE-001", "id_usuario": 7})
from utils.banco import LogTable
logs = banco_teste.session.query(LogTable).filter_by(status="ERRO").all()
assert len(logs) >= 1
class TestExchangePayload:
"""Testa que o exchange.setPayload é chamado com o resultado correto."""
def test_payload_set_no_sucesso(self, main_instance, mock_api, mock_exchange):
"""Ao concluir com sucesso, setPayload deve ser chamado com o resultado."""
mock_api.get.return_value = {"id": "PLUG-NFE-001", "status": "CONCLUIDO"}
with patch("NFe.registroNFe.Api", return_value=mock_api):
main_instance.nfe("get", {"id": "PLUG-NFE-001", "id_usuario": 1})
assert mock_exchange.setPayload.called
payload_enviado = json.loads(mock_exchange.payloads_set[-1])
assert payload_enviado["status"] == "CONCLUIDO"

View file

@ -0,0 +1,179 @@
import json
import pytest
from unittest.mock import patch, MagicMock
class TestNFePost:
@pytest.fixture
def dados_nfe(self):
return {
"id_usuario": 1,
"id_empresa": 10,
"nfe": {
"idIntegracao": "INT-NFE-001",
"presencial": True,
"consumidorFinal": True,
"natureza": "Venda de mercadoria",
"emitente": {"cpfCnpj": "08187168000160"},
"destinatario": {
"cpfCnpj": "08114280956",
"razaoSocial": "Destinatário Teste",
"endereco": {
"logradouro": "Rua Teste",
"numero": "100",
"bairro": "Centro",
"codigoCidade": "5201405",
"descricaoCidade": "GOIANIA",
"estado": "GO",
"cep": "74000000"
}
},
"itens": [{
"codigo": "001",
"descricao": "Produto Teste",
"ncm": "61091000",
"cfop": "5102",
"valorUnitario": {"comercial": 100.0, "tributavel": 100.0},
"valor": 100.0,
"tributos": {
"icms": {
"origem": "0",
"cst": "00",
"baseCalculo": {"modalidadeDeterminacao": 0, "valor": 100.0},
"aliquota": 18.0,
"valor": 18.0
},
"pis": {
"cst": "01",
"baseCalculo": {"valor": 100.0, "quantidade": 1},
"aliquota": 1.65,
"valor": 1.65
},
"cofins": {
"cst": "01",
"baseCalculo": {"valor": 100.0},
"aliquota": 7.6,
"valor": 7.6
}
}
}],
"pagamentos": [{"aVista": True, "meio": "01", "valor": 100.0}]
}
}
def test_post_nfe_sucesso(self, main_instance, mock_api, dados_nfe):
mock_api.post.return_value = {
"documents": [{"id": "PLUG-NFE-001"}]
}
with patch("NFe.registroNFe.Api", return_value=mock_api):
resultado = json.loads(main_instance.nfe("post", dados_nfe))
assert resultado["status"] == "ok"
assert resultado["data"]["documents"][0]["id"] == "PLUG-NFE-001"
def test_post_nfe_erro_api(self, main_instance, mock_api, dados_nfe):
mock_api.post.return_value = {"error": "CPF/CNPJ inválido"}
with patch("NFe.registroNFe.Api", return_value=mock_api):
with pytest.raises(Exception, match="CPF/CNPJ inválido"):
main_instance.nfe("post", dados_nfe)
class TestNFeGet:
def test_get_nfe_resumo(self, main_instance, mock_api):
mock_api.get.return_value = {
"id": "PLUG-NFE-001",
"status": "CONCLUIDO",
"xml": "https://..."
}
with patch("NFe.registroNFe.Api", return_value=mock_api):
resultado = json.loads(main_instance.nfe("get", {"id": "PLUG-NFE-001", "id_usuario": 1}))
assert resultado["status"] == "ok"
assert resultado["data"]["status"] == "CONCLUIDO"
class TestNFeDownload:
"""Testa download de PDF e XML de NFe."""
def test_download_pdf(self, main_instance, mock_api):
mock_api.get.return_value = {"url": "https://cdn.plugnotas.com.br/nfe/pdf/123"}
with patch("NFe.registroNFe.Api", return_value=mock_api):
resultado = json.loads(main_instance.nfe("download", {"id": "PLUG-NFE-001", "id_usuario": 1}))
assert resultado["status"] == "ok"
def test_download_xml(self, main_instance, mock_api):
mock_api.get.return_value = {"url": "https://cdn.plugnotas.com.br/nfe/xml/123"}
with patch("NFe.registroNFe.Api", return_value=mock_api):
resultado = json.loads(main_instance.nfe("download_xml", {"id": "PLUG-NFE-001", "id_usuario": 1}))
assert resultado["status"] == "ok"
class TestNFeCancelar:
def test_cancelar_nfe_sucesso(self, main_instance, mock_api):
mock_api.post.return_value = {"id": "PLUG-NFE-001", "status": "CANCELAMENTO_SOLICITADO"}
with patch("NFe.registroNFe.Api", return_value=mock_api):
resultado = json.loads(main_instance.nfe("cancelar", {
"id": "PLUG-NFE-001",
"justificativa": "Erro de emissão",
"id_usuario": 1
}))
assert resultado["status"] == "ok"
def test_status_cancelamento(self, main_instance, mock_api):
mock_api.get.return_value = {"id": "PLUG-NFE-001", "status": "CANCELADO"}
with patch("NFe.registroNFe.Api", return_value=mock_api):
resultado = json.loads(main_instance.nfe("status_cancelamento", {"id": "PLUG-NFE-001", "id_usuario": 1}))
assert resultado["status"] == "ok"
class TestNFeCCe:
def test_enviar_cce(self, main_instance, mock_api):
mock_api.post.return_value = {"id": "PLUG-NFE-001", "status": "CCE_ENVIADA"}
with patch("NFe.registroNFe.Api", return_value=mock_api):
resultado = json.loads(main_instance.nfe("cce", {
"id": "PLUG-NFE-001",
"correcao": "Corrigir endereço do destinatário",
"id_usuario": 1
}))
assert resultado["status"] == "ok"
def test_status_cce(self, main_instance, mock_api):
mock_api.get.return_value = {"id": "PLUG-NFE-001", "status": "CCE_AUTORIZADA"}
with patch("NFe.registroNFe.Api", return_value=mock_api):
resultado = json.loads(main_instance.nfe("status_cce", {"id": "PLUG-NFE-001", "id_usuario": 1}))
assert resultado["status"] == "ok"
class TestNFeResumo:
def test_resumo_por_cnpj(self, main_instance, mock_api):
mock_api.get.return_value = {"id": "PLUG-NFE-001", "chave": "3524..."}
with patch("NFe.registroNFe.Api", return_value=mock_api):
resultado = json.loads(main_instance.nfe("resumo", {
"cnpj": "08187168000160",
"id_integracao": "INT-001",
"id_usuario": 1
}))
assert resultado["status"] == "ok"
class TestNFeInvalido:
def test_metodo_invalido(self, main_instance):
resultado = json.loads(main_instance.nfe("inexistente", {"id_usuario": 1}))
assert resultado["status"] == "error"
assert "não encontrado" in resultado["message"].lower()

View file

@ -0,0 +1,146 @@
"""
Testes E2E para o serviço NFSe.
Testa: POST, GET, DOWNLOAD, DOWNLOAD_XML, CANCELAR,
STATUS_CANCELAMENTO, RESUMO, HISTORICO.
"""
import json
import pytest
from unittest.mock import patch, MagicMock
@pytest.fixture
def dados_nfse():
return {
"id_usuario": 1,
"id_empresa": 10,
"idIntegracao": "INT-NFSE-001",
"prestador": {"cpfCnpj": "08187168000160"},
"tomador": {
"cpfCnpj": "08114280956",
"razaoSocial": "Tomador Teste",
"inscricaoMunicipal": "isento",
"email": "tomador@teste.com",
"endereco": {
"descricaoCidade": "São Paulo",
"cep": "01000000",
"tipoLogradouro": "Rua",
"logradouro": "Teste",
"tipoBairro": "Bairro",
"codigoCidade": "3550308",
"complemento": "",
"estado": "SP",
"numero": "100",
"bairro": "Centro"
}
},
"servico": {
"codigo": "1.01",
"codigoTributacao": "1.01",
"discriminacao": "Serviços de tecnologia",
"cnae": "6204000",
"iss": {"tipoTributacao": 6, "exigibilidade": 1, "aliquota": 2.5},
"valor": {"servico": 1500.0, "descontoCondicionado": 0, "descontoIncondicionado": 0}
}
}
class TestNFSePost:
def test_post_nfse_sucesso(self, main_instance, mock_api, dados_nfse):
mock_api.post.return_value = {"documents": [{"id": "PLUG-NFSE-001"}]}
with patch("NFSe.nfse.Api", return_value=mock_api):
resultado = json.loads(main_instance.nfse("post", dados_nfse))
assert resultado["status"] == "ok"
assert resultado["data"]["documents"][0]["id"] == "PLUG-NFSE-001"
def test_post_nfse_erro_api(self, main_instance, mock_api, dados_nfse):
mock_api.post.return_value = {"error": "CNPJ do prestador não cadastrado"}
with patch("NFSe.nfse.Api", return_value=mock_api):
with pytest.raises(Exception):
main_instance.nfse("post", dados_nfse)
class TestNFSeGet:
def test_get_nfse_por_id(self, main_instance, mock_api):
mock_api.get.return_value = {"id": "PLUG-NFSE-001", "status": "CONCLUIDO"}
with patch("NFSe.nfse.Api", return_value=mock_api):
resultado = json.loads(main_instance.nfse("get", {"id": "PLUG-NFSE-001", "id_usuario": 1}))
assert resultado["status"] == "ok"
def test_listar_nfse(self, main_instance, banco_teste):
"""Listar NFSe sem ID deve buscar do banco."""
resultado = json.loads(main_instance.nfse("get", {"id_usuario": 1}))
assert resultado["status"] == "ok"
class TestNFSeDownload:
def test_download_pdf(self, main_instance, mock_api):
mock_api.get.return_value = {"pdf_url": "https://..."}
with patch("NFSe.nfse.Api", return_value=mock_api):
resultado = json.loads(main_instance.nfse("download", {"id": "PLUG-NFSE-001", "id_usuario": 1}))
assert resultado["status"] == "ok"
def test_download_xml(self, main_instance, mock_api):
mock_api.get.return_value = {"xml_url": "https://..."}
with patch("NFSe.nfse.Api", return_value=mock_api):
resultado = json.loads(main_instance.nfse("download_xml", {"id": "PLUG-NFSE-001", "id_usuario": 1}))
assert resultado["status"] == "ok"
class TestNFSeCancelar:
def test_cancelar_nfse(self, main_instance, mock_api):
mock_api.post.return_value = {"status": "CANCELAMENTO_SOLICITADO"}
with patch("NFSe.nfse.Api", return_value=mock_api):
resultado = json.loads(main_instance.nfse("cancelar", {"id": "PLUG-NFSE-001", "id_usuario": 1}))
assert resultado["status"] == "ok"
def test_status_cancelamento(self, main_instance, mock_api):
mock_api.get.return_value = {"status": "CANCELADO"}
with patch("NFSe.nfse.Api", return_value=mock_api):
resultado = json.loads(main_instance.nfse("status_cancelamento", {"id": "PLUG-NFSE-001", "id_usuario": 1}))
assert resultado["status"] == "ok"
class TestNFSeResumo:
def test_consultar_resumo(self, main_instance, mock_api):
mock_api.get.return_value = {"id": "PLUG-NFSE-001", "numero": "12345"}
with patch("NFSe.nfse.Api", return_value=mock_api):
resultado = json.loads(main_instance.nfse("resumo", {
"id": "PLUG-NFSE-001",
"cnpj": "08187168000160",
"id_usuario": 1
}))
assert resultado["status"] == "ok"
class TestNFSeHistorico:
def test_historico_nfse(self, main_instance, mock_api):
mock_api.get.return_value = [{"data": "2026-01-01", "status": "CONCLUIDO"}]
with patch("NFSe.nfse.Api", return_value=mock_api):
resultado = json.loads(main_instance.nfse("historico", {
"id": "PLUG-NFSE-001",
"id_usuario": 1
}))
assert resultado["status"] == "ok"
class TestNFSeInvalido:
def test_metodo_invalido(self, main_instance):
resultado = json.loads(main_instance.nfse("inexistente", {"id_usuario": 1}))
assert resultado["status"] == "error"

View file

@ -0,0 +1,164 @@
import sys
import os
import json
import time
sys.path.append(os.path.dirname(os.path.abspath(__file__)))
from Models.config import SystemConfig
from utils.redis import RedisClient
from utils.Exchange2 import QueueExchange2
def main():
print("Iniciando script de testes da FILA (Queue Exchange)...")
# Carregando configuração do Redis
redis_host = os.getenv('REDIS_HOST', 'localhost')
redis_client = RedisClient(host=redis_host, port=6379)
try:
system_config_data = redis_client.get("system_config")
if not system_config_data:
print("Erro: system_config não encontrado no Redis.")
return
system_config = SystemConfig(**json.loads(system_config_data))
print(f"[{system_config.origin}] Configuração carregada.")
except Exception as e:
print(f"Erro ao ler configs: {e}")
return
# Inicializando mensageria de FILA
try:
# Nota: O QueueExchange2 envia para 'system_service_client'
exchange = QueueExchange2(
amqps=system_config.amqps,
origin=system_config.origin,
system="integracao",
service="tecnospeed",
version="2.1.0",
client="#" # '#' para bater com a fila padrão do worker (evita erro de validação length)
)
except Exception as e:
print(f"Falha ao iniciar mensageria: {e}")
return
# Configurando criptografia
servicekey = system_config.servicekey.ljust(32, '0')[:32]
exchange.setCrypto(serviceid=system_config.serviceid, servicekey=servicekey)
# Payload (mesmo do teste RPC)
payload_interno = {
"fila": "empresa",
"cliente": "TestQueue",
"metodo": "POST",
"dados": {
"endereco" : {
"codigoPais": "1058",
"descricaoPais": "Brasil",
"tipoLogradouro": "Rua",
"logradouro": "T55",
"numero": "882",
"complemento": "Quadra99 Lote 11/14 Sala 1507",
"tipoBairro": "Zona",
"bairro": "Setor Bueno",
"codigoCidade": "5208707",
"descricaoCidade": "Goiania",
"estado": "GO",
"cep": "74215170"
},
"nfse" : {
"config" : {
"rps" : {
"lote" : 12,
"numeracao" : [
{
"numero" : 2,
"serie" : "CTBZ",
"numeracaoAtual" : 2
}
]
},
"email" : {
"envio" : False
},
"producao" : True,
"envioLimitado" : False,
"integracoes" : []
},
"ativo" : True,
"tipoContrato" : 0
},
"nfe" : {
"config" : {
"dfe" : {
"primeiroNsu" : True
},
"email" : {
"envio" : False
},
"integracoes" : {
"plugStorage" : {
"ativo" : False
}
},
"producao" : False,
"impressaoFcp" : False,
"impressaoPartilha" : True,
"versaoManual" : "6.0",
"versaoEsquema" : "pl_009",
"numeracao" : []
},
"ativo" : False
},
"nfce" : {
"config" : {
"email" : {
"envio" : False
},
"integracoes" : {
"plugStorage" : {
"ativo" : False
}
},
"producao" : False,
"versaoManual" : "6.0",
"versaoEsquema" : "pl_009",
"numeracao" : []
},
"ativo" : False
},
"incentivoFiscal" : False,
"incentivadorCultural" : False,
"cpfCnpj" : "14358405000100",
"inscricaoMunicipal" : "1085328365",
"certificado" : "668698592698d7831c758d52",
"razaoSocial" : "Teste da SIlva do teste",
"simplesNacional" : True,
"regimeTributario" : 1,
"regimeTributarioEspecial" : 6
},
"config": {
"x_api_key": "2da392a6-79d2-4304-a8b7-959572c7e44d",
"user_postgres": "tecnospeed",
"pass_postgres": "u7QimG7E!SF//JC2c8",
"host_postgres": "postgres",
"port_postgres": "5432",
"database_postgres": "tecnospeed"
},
"url_base": "https://api.sandbox.plugnotas.com.br"
}
# Criptografando e enviando
exchange.setPayload(json.dumps(payload_interno))
print("\n--- DEBUG ENVIO ---")
print(f"Alvo (targetQueue): integracao_tecnospeed")
print(f"Config Enviada: {payload_interno['config']['host_postgres']}:{payload_interno['config']['port_postgres']}")
print("-------------------\n")
print("Disparando mensagem para a FILA...")
exchange.sendMsg(json.dumps(payload_interno), service="tecnospeed")
print("Mensagem enviada com sucesso. Verifique os logs do worker mainQUEUE.py")
if __name__ == "__main__":
main()

View file

@ -0,0 +1,328 @@
import json
import pytest
from unittest.mock import patch, MagicMock
from utils.banco import EmpresaTable, ServicoTable, TomadorTable
class TestEmpresaPost:
@pytest.fixture
def dados_empresa(self):
return {
"id_usuario": 1,
"endereco": {
"codigoPais": "1058", "descricaoPais": "BRASIL",
"tipoLogradouro": "Rua", "logradouro": "Teste", "numero": "123",
"complemento": "", "tipoBairro": "Bairro", "bairro": "Centro",
"codigoCidade": "5201405", "descricaoCidade": "GOIANIA",
"estado": "GO", "cep": "74000000"
},
"nfse": {
"config": {"rps": {"lote": 1, "numeracao": [1]}, "email": {"envio": True},
"producao": False, "envioLimitado": False, "integracoes": []},
"ativo": True, "tipoContrato": 0
},
"nfe": {
"config": {"dfe": {"primeiroNsu": False}, "email": {"envio": False},
"integracoes": {}, "producao": False, "impressaoFcp": False,
"impressaoPartilha": False, "versaoManual": "6.0",
"versaoEsquema": "PL_009_V4", "numeracao": [1]},
"ativo": False
},
"nfce": {
"config": {"email": {"envio": False}, "integracoes": {}, "producao": False,
"versaoManual": "6.0", "versaoEsquema": "PL_009_V4", "numeracao": [1]},
"ativo": False
},
"incentivoFiscal": False, "incentivadorCultural": False,
"cpfCnpj": "17956143000174", "inscricaoMunicipal": "12345",
"certificado": "CERT-UUID", "razaoSocial": "Empresa Teste SA",
"simplesNacional": True, "regimeTributario": 1, "regimeTributarioEspecial": 0
}
def test_post_empresa_sucesso(self, main_instance, mock_api, dados_empresa):
mock_api.post.return_value = {"_id": "PLUG-EMP-001", "cpfCnpj": "17956143000174"}
with patch("empresas.empresa.Api", return_value=mock_api):
resultado = json.loads(main_instance.empresa("post", dados_empresa))
assert resultado["status"] == "ok"
def test_post_empresa_erro(self, main_instance, mock_api, dados_empresa):
mock_api.post.return_value = {
"error": "Erro",
"response": {"error": {"message": "CNPJ já cadastrado", "data": []}}
}
with patch("empresas.empresa.Api", return_value=mock_api):
resultado = json.loads(main_instance.empresa("post", dados_empresa))
assert resultado["status"] == "error"
class TestEmpresaGet:
def test_get_empresa_por_cnpj(self, main_instance, mock_api):
mock_api.get.return_value = {"_id": "PLUG-EMP-001", "cpfCnpj": "17956143000174", "razaoSocial": "Teste SA"}
with patch("empresas.empresa.Api", return_value=mock_api):
resultado = json.loads(main_instance.empresa("get", {"cnpj": "17956143000174", "id_usuario": 1}))
assert resultado["status"] == "ok"
def test_listar_empresas(self, main_instance, mock_api):
mock_api.get_paginated.return_value = {"data": [{"cpfCnpj": "111", "razaoSocial": "Emp A"}], "total": 1}
with patch("empresas.empresa.Api", return_value=mock_api):
resultado = json.loads(main_instance.empresa("get", {"id_usuario": 1}))
assert resultado["status"] == "ok"
class TestEmpresaInvalido:
def test_metodo_invalido(self, main_instance):
resultado = json.loads(main_instance.empresa("inexistente", {"id_usuario": 1}))
assert resultado["status"] == "error"
class TestCertificadoPost:
@pytest.fixture
def dados_certificado(self):
return {
"id_usuario": 1,
"id_empresa": 10,
"arquivo": "CONTEUDO_PFX_BASE64",
"senha": "12345678",
"expirationAt": "2027-12-31"
}
def test_post_certificado_sucesso(self, main_instance, mock_api, dados_certificado):
mock_api.post_form_data.return_value = {"data": {"id": "CERT-001"}}
with patch("certificado_digital.certificado_digital.Api", return_value=mock_api):
resultado = json.loads(main_instance.certificado_digital("post", dados_certificado))
assert resultado["status"] == "ok"
assert resultado["data"] == "CERT-001"
class TestCertificadoGet:
def test_get_certificado_por_id(self, main_instance, mock_api):
mock_api.get.return_value = {"id": "CERT-001", "status": "VALIDO"}
with patch("certificado_digital.certificado_digital.Api", return_value=mock_api):
resultado = json.loads(main_instance.certificado_digital("get", {"id": "CERT-001", "id_usuario": 1}))
assert resultado["status"] == "ok"
def test_listar_certificados(self, main_instance, mock_api):
mock_api.get.return_value = [{"id": "CERT-001"}, {"id": "CERT-002"}]
with patch("certificado_digital.certificado_digital.Api", return_value=mock_api):
resultado = json.loads(main_instance.certificado_digital("get", {"id_usuario": 1}))
assert resultado["status"] == "ok"
class TestCertificadoInvalido:
def test_metodo_invalido(self, main_instance):
resultado = json.loads(main_instance.certificado_digital("inexistente", {"id_usuario": 1}))
assert resultado["status"] == "error"
class TestServicoPost:
"""Testa criação de Serviço na API + banco."""
@pytest.fixture
def dados_servico_completo(self):
"""Payload completo do Model Servico com todos campos obrigatórios."""
return {
"id_usuario": 1,
"codigo": "1.01",
"idIntegracao": "SERV-INT-001",
"discriminacao": "Consultoria em TI",
"codigoTributacao": "1.01",
"cnae": "6204000",
"codigoCidadeIncidencia": "3550308",
"descricaoCidadeIncidencia": "São Paulo",
"unidade": "UN",
"quantidade": 1,
"iss": {
"tipoTributacao": 6, "exigibilidade": 1,
"retido": False, "aliquota": 2.5, "valor": 37.5,
"valorRetido": 0, "processoSuspensao": ""
},
"obra": {"art": "", "codigo": ""},
"valor": {
"servico": 1500.0, "baseCalculo": 1500.0, "deducoes": 0,
"descontoCondicionado": 0, "descontoIncondicionado": 0,
"liquido": 1500.0, "unitario": 1500.0, "valorAproximadoTributos": 0
},
"deducao": {"tipo": 0, "descricao": ""},
"retencao": {
"pis": {"aliquota": 0, "valor": 0},
"cofins": {"aliquota": 0, "valor": 0},
"csll": {"aliquota": 0, "valor": 0},
"inss": {"aliquota": 0, "valor": 0},
"irrf": {"aliquota": 0, "valor": 0},
"outrasRetencoes": 0,
"cpp": {"aliquota": 0, "valor": 0}
},
"tributavel": True,
"ibpt": {
"simplificado": {"aliquota": 8.65},
"detalhado": {"aliquota": {"municipal": 2.5, "estadual": 0, "federal": 6.15}}
},
"responsavelRetencao": ""
}
def test_post_servico_sucesso(self, main_instance, mock_api, dados_servico_completo):
mock_api.post.return_value = {"data": {"id": "SERV-001"}}
with patch("NFSe.Servico.servico.Api", return_value=mock_api):
resultado = json.loads(main_instance.servico("post", dados_servico_completo))
assert resultado["status"] == "ok"
class TestServicoGet:
def test_listar_servicos(self, main_instance, banco_teste):
"""Deve retornar lista vazia quando não há serviços."""
resultado = json.loads(main_instance.servico("get", {"id_usuario": 1}))
assert resultado["status"] == "ok"
def test_buscar_servico_por_id(self, main_instance, mock_api):
mock_api.get.return_value = {"id": "SERV-001", "discriminacao": "Consultoria"}
with patch("NFSe.Servico.servico.Api", return_value=mock_api):
resultado = json.loads(main_instance.servico("get", {"id": "SERV-001", "id_usuario": 1}))
assert resultado["status"] == "ok"
class TestServicoInvalido:
def test_metodo_invalido(self, main_instance):
resultado = json.loads(main_instance.servico("inexistente", {"id_usuario": 1}))
assert resultado["status"] == "error"
class TestTomadorPost:
@pytest.fixture
def dados_tomador(self):
return {
"id_usuario": 1,
"cpfCnpj": "08114280956",
"inscricaoMunicipal": "isento",
"inscricaoEstadual": "",
"inscricaoSuframa": "",
"indicadorInscricaoEstadual": 9,
"razaoSocial": "Tomador Teste LTDA",
"nomeFantasia": "Tomador Teste",
"endereco": {
"tipoLogradouro": "Rua", "logradouro": "Teste", "numero": "100",
"complemento": "", "tipoBairro": "Bairro", "bairro": "Centro",
"codigoPais": "1058", "descricaoPais": "BRASIL",
"codigoCidade": "3550308", "descricaoCidade": "São Paulo",
"estado": "SP", "cep": "01000000"
},
"telefone": {"ddd": "11", "numero": "999999999"},
"email": "tomador@teste.com",
"orgaoPublico": False
}
def test_post_tomador_sucesso(self, main_instance, mock_api, dados_tomador):
mock_api.post.return_value = {"id": "TOM-001", "cpfCnpj": "08114280956"}
with patch("NFSe.Tomador.tomador.Api", return_value=mock_api):
resultado = json.loads(main_instance.tomador("post", dados_tomador))
assert resultado["status"] == "ok"
class TestTomadorGet:
def test_buscar_tomador(self, main_instance, mock_api):
mock_api.get.return_value = {"cpfCnpj": "08114280956", "razaoSocial": "Tomador Teste"}
with patch("NFSe.Tomador.tomador.Api", return_value=mock_api):
resultado = json.loads(main_instance.tomador("get", {"cpf_cnpj": "08114280956", "id_usuario": 1}))
assert resultado["status"] == "ok"
class TestTomadorInvalido:
def test_metodo_invalido(self, main_instance):
resultado = json.loads(main_instance.tomador("inexistente", {"id_usuario": 1}))
assert resultado["status"] == "error"
class TestRelatorios:
def test_nfe_emitidas(self, main_instance, mock_api):
mock_api.get_paginated.return_value = {"data": [{"id": "NFE-1"}], "total": 1}
with patch("Relatorios.relatorios.Api", return_value=mock_api):
resultado = json.loads(main_instance.relatorios("nfe_emitidas", {"id_usuario": 1}))
assert resultado["status"] == "ok"
def test_nfe_por_cnpj(self, main_instance, mock_api):
mock_api.get_paginated.return_value = {"data": [], "total": 0}
with patch("Relatorios.relatorios.Api", return_value=mock_api):
resultado = json.loads(main_instance.relatorios("nfe_por_cnpj", {"cnpj": "08187168000160", "id_usuario": 1}))
assert resultado["status"] == "ok"
def test_nfse_emitidas(self, main_instance, mock_api):
mock_api.get_paginated.return_value = {"data": [], "total": 0}
with patch("Relatorios.relatorios.Api", return_value=mock_api):
resultado = json.loads(main_instance.relatorios("nfse_emitidas", {
"data_inicial": "2026-01-01", "data_final": "2026-12-31", "id_usuario": 1
}))
assert resultado["status"] == "ok"
def test_nfse_por_cnpj(self, main_instance, mock_api):
mock_api.get_paginated.return_value = {"data": [], "total": 0}
with patch("Relatorios.relatorios.Api", return_value=mock_api):
resultado = json.loads(main_instance.relatorios("nfse_por_cnpj", {
"cnpj": "08187168000160",
"data_inicial": "2026-01-01", "data_final": "2026-12-31",
"id_usuario": 1
}))
assert resultado["status"] == "ok"
def test_metodo_invalido(self, main_instance):
resultado = json.loads(main_instance.relatorios("inexistente", {"id_usuario": 1}))
assert resultado["status"] == "error"
class TestWebhooks:
def test_consultar_webhooks(self, main_instance, mock_api):
mock_api.get.return_value = {"url": " ", "method": "POST"}
with patch("webhooks.webhooks.Api", return_value=mock_api):
main_instance.webhooks("get", {"cnpj": "08187168000160", "id_usuario": 1})
def test_criar_webhook(self, main_instance, mock_api):
mock_api.post.return_value = {"status": "ok"}
with patch("webhooks.webhooks.Api", return_value=mock_api):
main_instance.webhooks("post", {"cnpj": "08187168000160", "url": " ", "id_usuario": 1})
def test_deletar_webhook(self, main_instance, mock_api):
mock_api.delete.return_value = {"status": "ok"}
with patch("webhooks.webhooks.Api", return_value=mock_api):
main_instance.webhooks("delete", {"cnpj": "08187168000160", "id_usuario": 1})
def test_atualizar_webhook(self, main_instance, mock_api):
mock_api.put.return_value = {"status": "ok"}
with patch("webhooks.webhooks.Api", return_value=mock_api):
main_instance.webhooks("put", {"cnpj": "08187168000160", "url": " ", "id_usuario": 1})
def test_testar_webhook(self, main_instance, mock_api):
mock_api.post.return_value = {"status": "ok", "message": "Webhook testado"}
with patch("webhooks.webhooks.Api", return_value=mock_api):
main_instance.webhooks("test", {"cnpj": "08187168000160", "id_usuario": 1})
def test_metodo_invalido(self, main_instance):
resultado = json.loads(main_instance.webhooks("inexistente", {"id_usuario": 1}))
assert resultado["status"] == "error"

View file

@ -0,0 +1,80 @@
import os
import sys
import json
import time
sys.path.append(os.path.dirname(os.path.dirname(os.path.abspath(__file__))))
from Models.config import SystemConfig
from utils.redis import RedisClient
from utils.Exchange2 import RPCExchange2
def run_rpc_test():
redis_host = os.getenv('REDIS_HOST', 'localhost')
redis_client = RedisClient(host=redis_host, port=6379)
try:
config_data = redis_client.get("system_config")
sc = SystemConfig(**json.loads(config_data))
except Exception as e:
print(f"Erro ao carregar configs: {e}")
return
config_rpc = {
"x_api_key": "2da392a6-79d2-4304-a8b7-959572c7e44d",
"user_postgres": "tecnospeed",
"pass_postgres": "u7QimG7E%21SF%2F%2FJC2c8",
"host_postgres": "banco",
"port_postgres": "5432",
"database_postgres": "tecnospeed"
}
exchange = RPCExchange2(sc.amqps, sc.origin, "integracao", "tecnospeed", "2.1.0", client="#")
servicekey = sc.servicekey.ljust(32, '0')[:32]
exchange.setCrypto(sc.serviceid, servicekey)
# Payload "simplificado" do front-end
full_payload = {
"fila": "empresa",
"metodo": "POST",
"cliente": "tray",
"url_base": "https://api.sandbox.plugnotas.com.br",
"config": config_rpc,
"dados": {
"cpfCnpj": "94074986000198",
"razaoSocial": "Tech nux",
"nomeFantasia": "Tech nux ltda",
"inscricaoMunicipal": "0001",
"simplesNacional": True,
"regimeTributario": 1,
"regimeTributarioEspecial": 1,
"incentivoCultural": False,
"incentivoFiscal": True,
"certificado": "cert-mock-01",
"endereco": {
"codigoPais": "1058",
"descricaoPais": "BRASIL",
"tipoLogradouro": "Avenida",
"logradouro": "Avenida",
"numero": "123",
"complemento": "Casa",
"tipoBairro": "Bairro",
"bairro": "Centro",
"codigoCidade": "5200175",
"descricaoCidade": "ÁGUA FRIA DE GOIÁS",
"estado": "GO",
"cep": "68907-460"
},
"nfe": { "ativo": True },
"nfse": { "ativo": False }
}
}
print("Enviando payload via RPC...")
exchange.setPayload(json.dumps(full_payload))
resposta = exchange.callRPC(json.dumps(full_payload), timeout=30)
print("\nRESPOSTA DO SERVIDOR (RPC):")
print(resposta)
if __name__ == "__main__":
run_rpc_test()

View file

@ -0,0 +1,88 @@
import sys
import os
import json
import time
from dotenv import load_dotenv
# Carrega variáveis de ambiente
load_dotenv()
# Adiciona o diretório atual ao path para importar as classes locais
sys.path.append(os.path.dirname(os.path.abspath(__file__)))
from Models.config import SystemConfig
from utils.redis import RedisClient
from utils.Exchange2 import QueueExchange2
# Dados do Certificado (Preenchidos dinamicamente)
PFX_BASE64 = " "
PFX_PASSWORD = str(sys.argv[1]) if len(sys.argv) > 1 else " "
def main():
print(f"Iniciando teste de upload de certificado para senha: {PFX_PASSWORD}")
# 1. Carregando configuração do Redis
redis_host = os.getenv('REDIS_HOST', 'localhost')
redis_client = RedisClient(host=redis_host, port=6379)
try:
system_config_data = redis_client.get("system_config")
if not system_config_data:
print("Erro: system_config não encontrado no Redis.")
return
system_config = SystemConfig(**json.loads(system_config_data))
except Exception as e:
print(f"Erro ao ler configs do Redis: {e}")
return
# 2. Inicializando QueueExchange2
try:
exchange = QueueExchange2(
amqps=system_config.amqps,
origin=system_config.origin,
system="integracao",
service="tecnospeed",
version="1.0.0",
client="#"
)
servicekey = system_config.servicekey.ljust(32, '0')[:32]
exchange.setCrypto(serviceid=system_config.serviceid, servicekey=servicekey)
except Exception as e:
print(f"Falha ao iniciar mensageria: {e}")
return
# 3. Montando o payload interno
internal_payload = {
"fila": "certificado_digital",
"cliente": "UploadTest",
"metodo": "POST",
"dados": {
"arquivo": PFX_BASE64,
"senha": PFX_PASSWORD,
"id_empresa": "c19d874447b1kxlapg1xv783",
"id_usuario": 1,
"expirationAt": "2027-04-13"
},
"config": {
"x_api_key": os.getenv("PLUGNOTAS_API_KEY"),
"user_postgres": os.getenv("POSTGRES_USER", "tecnospeed"),
"pass_postgres": os.getenv("POSTGRES_PASSWORD", "u7QimG7E!SF//JC2c8"),
"host_postgres": "postgres",
"port_postgres": "5432",
"database_postgres": os.getenv("POSTGRES_DB", "tecnospeed")
},
"url_base": "https://api.sandbox.plugnotas.com.br"
}
# 4. Enviando com criptografia (hmac/payload)
try:
msg_json = json.dumps(internal_payload)
exchange.setPayload(msg_json)
exchange.sendMsg(msg_json, service="tecnospeed")
print("Mensagem enviada com sucesso (criptografada)!")
except Exception as e:
print(f"Erro ao enviar mensagem: {e}")
if __name__ == "__main__":
main()

File diff suppressed because it is too large Load diff

158
backup/src/utils/api.py Normal file
View file

@ -0,0 +1,158 @@
import requests
# Wrapper base para realizar requests (GET/POST/PUT/DELETE) à API da PlugNotas de forma padronizada
class Api:
def __init__(self, api_key):
self.api_key = api_key
def get(self, url):
try:
headers = {
"x-api-key": self.api_key
}
response = requests.get(url, headers=headers, timeout=30)
response.raise_for_status()
return response.json()
except requests.exceptions.RequestException as e:
resp_data = {}
if 'response' in locals() and response is not None:
try:
resp_data = response.json()
except ValueError:
resp_data = response.text
return {"error": str(e), "response": resp_data}
def get_paginated(self, url, max_pages=100):
try:
headers = {
"x-api-key": self.api_key
}
all_data = []
current_url = url
page = 0
while current_url and page < max_pages:
separator = "&" if "?" in current_url else "?"
request_url = current_url if page == 0 else current_url
response = requests.get(request_url, headers=headers, timeout=30)
response.raise_for_status()
result = response.json()
if isinstance(result, list):
all_data.extend(result)
break
elif isinstance(result, dict):
page_data = result.get("data", [])
if isinstance(page_data, list):
all_data.extend(page_data)
elif page_data:
all_data.append(page_data)
hash_proxima = result.get("hashProximaPagina")
if hash_proxima:
separator = "&" if "?" in url else "?"
current_url = f"{url}{separator}hashProximaPagina={hash_proxima}"
page += 1
else:
break
else:
break
return {"data": all_data, "total_paginas": page + 1, "total_registros": len(all_data)}
except requests.exceptions.RequestException as e:
resp_data = {}
if 'response' in locals() and response is not None:
try:
resp_data = response.json()
except ValueError:
resp_data = response.text
return {"error": str(e), "response": resp_data}
def post(self, url, data):
try:
headers = {
"Content-Type": "application/json",
"x-api-key": self.api_key
}
response = requests.post(url, headers=headers, json=data, timeout=30)
response.raise_for_status()
return response.json()
except requests.exceptions.RequestException as e:
resp_data = {}
if 'response' in locals() and response is not None:
try:
resp_data = response.json()
except ValueError:
resp_data = response.text
return {"error": str(e), "response": resp_data}
def put(self, url, data):
try:
headers = {
"x-api-key": self.api_key
}
response = requests.put(url, headers=headers, json=data, timeout=30)
response.raise_for_status()
return response.json()
except requests.exceptions.RequestException as e:
return {"error": str(e)}
def patch(self, url, data):
try:
headers = {
"x-api-key": self.api_key
}
response = requests.patch(url, headers=headers, json=data, timeout=30)
response.raise_for_status()
return response.json()
except requests.exceptions.RequestException as e:
return {"error": str(e)}
def delete(self, url):
try:
headers = {
"x-api-key": self.api_key
}
response = requests.delete(url, headers=headers, timeout=30)
response.raise_for_status()
return response.json()
except requests.exceptions.RequestException as e:
return {"error": str(e)}
def post_file(self, url, file):
try:
headers = {
"x-api-key": self.api_key
}
response = requests.post(url, headers=headers, files=file, timeout=60)
response.raise_for_status()
return response.json()
except requests.exceptions.RequestException as e:
return {"error": str(e)}
def post_form_data(self, url, data):
try:
headers = {
"x-api-key": self.api_key
}
response = requests.post(url, headers=headers, files=data, timeout=60)
response.raise_for_status()
return response.json()
except requests.exceptions.RequestException as e:
return {"error": str(e)}
def put_form_data(self, url, data):
try:
headers = {
"x-api-key": self.api_key
}
response = requests.put(url, headers=headers, files=data, timeout=60)
response.raise_for_status()
return response.json()
except requests.exceptions.RequestException as e:
return {"error": str(e)}
# api = Api("2da392a6-79d2-4304-a8b7-959572c7e44d")
# response = api.get("https://api.sandbox.plugnotas.com.br/certificado")
# print(response)

241
backup/src/utils/banco.py Normal file
View file

@ -0,0 +1,241 @@
from datetime import datetime
import uuid
from sqlalchemy import Column, Integer, String, DateTime, Text, create_engine, Boolean
from sqlalchemy.orm import declarative_base, sessionmaker
from sqlalchemy.exc import SQLAlchemyError
Base = declarative_base()
class CertificadoDigitalTable(Base):
__tablename__ = 'certificado_digital'
id = Column(Integer, primary_key=True, autoincrement=True)
id_tecnospeed = Column(String(50), nullable=False)
certificado = Column(Text, nullable=False)
createdAt = Column(DateTime, default=datetime.now)
updatedAt = Column(DateTime, default=datetime.now, onupdate=datetime.now)
deletedAt = Column(DateTime, nullable=True)
expirationAt = Column(DateTime, nullable=False)
senha = Column(String(50), nullable=False)
status = Column(Boolean, nullable=False)
id_empresa = Column(String(50), nullable=False)
id_usuario = Column(Integer, nullable=False)
email = Column(String(255), nullable=True)
class EnterpriseTable(Base):
__tablename__ = 'Enterprise'
id = Column(String(50), primary_key=True)
name = Column(String(255), nullable=False)
cnpj = Column(String(50), nullable=False)
banco = Column(Text, nullable=True)
email = Column(String(255), nullable=True)
createdAt = Column(DateTime, default=datetime.now)
updatedAt = Column(DateTime, default=datetime.now, onupdate=datetime.now)
deletedAt = Column(DateTime, nullable=True)
status = Column(Boolean, nullable=False, default=True)
class LogoTable(Base):
__tablename__ = 'logo'
id = Column(Integer, primary_key=True, autoincrement=True)
logo = Column(Text, nullable=False)
id_empresa = Column(String(50), nullable=False)
createdAt = Column(DateTime, default=datetime.now)
updatedAt = Column(DateTime, default=datetime.now, onupdate=datetime.now)
deletedAt = Column(DateTime, nullable=True)
status = Column(Boolean, nullable=False)
class LogTable(Base):
__tablename__ = 'log'
id = Column(Integer, primary_key=True, autoincrement=True)
id_usuario = Column(Integer, nullable=False)
createdAt = Column(DateTime, default=datetime.now)
updatedAt = Column(DateTime, default=datetime.now, onupdate=datetime.now)
deletedAt = Column(DateTime, nullable=True)
acao = Column(String(50), nullable=False)
tabela = Column(String(50), nullable=False)
id_registro = Column(String(50), nullable=False)
json = Column(Text, nullable=False)
status = Column(String(20), nullable=False, default='INICIO')
mensagem = Column(Text, nullable=False, default='')
cliente = Column(String(100), nullable=True)
correlation_id = Column(String(50), nullable=True)
class ServicoTable(Base):
__tablename__ = 'servico'
id = Column(Integer, primary_key=True, autoincrement=True)
id_tecnospeed = Column(String(50), nullable=False)
descricao = Column(Text, nullable=False)
id_usuario = Column(Integer, nullable=False)
createdAt = Column(DateTime, default=datetime.now)
updatedAt = Column(DateTime, default=datetime.now, onupdate=datetime.now)
deletedAt = Column(DateTime, nullable=True)
status = Column(Boolean, nullable=False)
class TomadorTable(Base):
__tablename__ = 'tomador'
id = Column(Integer, primary_key=True, autoincrement=True)
id_tecnospeed = Column(String(50), nullable=False)
nome = Column(String(50), nullable=False)
cpf_cnpj = Column(String(50), nullable=False)
email = Column(String(50), nullable=False)
telefone = Column(String(50), nullable=False)
id_usuario = Column(Integer, nullable=False)
createdAt = Column(DateTime, default=datetime.now)
updatedAt = Column(DateTime, default=datetime.now, onupdate=datetime.now)
deletedAt = Column(DateTime, nullable=True)
status = Column(Boolean, nullable=False)
class NFSeTable(Base):
__tablename__ = 'nfse'
id = Column(Integer, primary_key=True, autoincrement=True)
id_tecnospeed = Column(String(50), nullable=False)
id_empresa = Column(String(50), nullable=False)
id_usuario = Column(Integer, nullable=False)
createdAt = Column(DateTime, default=datetime.now)
updatedAt = Column(DateTime, default=datetime.now, onupdate=datetime.now)
deletedAt = Column(DateTime, nullable=True)
status = Column(Boolean, nullable=False)
class NFeTable(Base):
__tablename__ = 'nfe'
id = Column(Integer, primary_key=True, autoincrement=True)
id_tecnospeed = Column(String(50), nullable=False)
id_empresa = Column(String(50), nullable=False)
id_usuario = Column(Integer, nullable=False)
createdAt = Column(DateTime, default=datetime.now)
updatedAt = Column(DateTime, default=datetime.now, onupdate=datetime.now)
deletedAt = Column(DateTime, nullable=True)
status = Column(Boolean, nullable=False)
# Cache de motores criados pelo SQLAlchemy por URL de conexao
_engines = {}
# Abstração de conexão base e de persistência de instâncias de metadados relacionais usando PostgreSQL
class Banco:
def __init__(self, host, port, user, password, database):
conn_str = f'postgresql://{user}:{password}@{host}:{port}/{database}'
if conn_str not in _engines:
_engines[conn_str] = create_engine(
conn_str,
pool_pre_ping=True,
pool_recycle=3600
)
self.engine = _engines[conn_str]
self.Session = sessionmaker(bind=self.engine)
self.session = self.Session()
def close(self):
"""Encerra a sessão SQLAlchemy atual liberando do pooler."""
if hasattr(self, 'session') and self.session is not None:
self.session.close()
def create_tables(self):
"""Cria as tabelas no banco de dados."""
try:
Base.metadata.create_all(self.engine)
except SQLAlchemyError as e:
print(f"Erro ao criar tabelas (SQLAlchemy): {e}")
except Exception as e:
print(f"Erro ao criar tabelas: {e}")
def add(self, obj):
"""Adiciona um novo registro no banco."""
try:
self.session.add(obj)
self.session.commit()
return obj
except SQLAlchemyError as e:
self.session.rollback()
print(f"Erro ao adicionar registro (SQLAlchemy): {e}")
raise e
except Exception as e:
self.session.rollback()
print(f"Erro ao adicionar registro: {e}")
raise e
def get(self, model, obj_id):
"""Busca um registro pelo ID."""
try:
return self.session.query(model).get(obj_id)
except SQLAlchemyError as e:
self.session.rollback()
print(f"Erro ao buscar registro (SQLAlchemy): {e}")
raise e
except Exception as e:
self.session.rollback()
print(f"Erro ao buscar registro: {e}")
raise e
def get_all(self, model):
"""Retorna todos os registros de um modelo."""
try:
return self.session.query(model).all()
except SQLAlchemyError as e:
print(f"Erro ao buscar registros (SQLAlchemy): {e}")
raise e
except Exception as e:
print(f"Erro ao buscar registros: {e}")
raise e
def update(self, obj):
"""Atualiza um registro no banco."""
try:
self.session.commit()
return obj
except SQLAlchemyError as e:
self.session.rollback()
print(f"Erro ao atualizar registro (SQLAlchemy): {e}")
raise e
except Exception as e:
self.session.rollback()
print(f"Erro ao atualizar registro: {e}")
raise e
def delete(self, obj):
"""Deleta um registro do banco."""
try:
self.session.delete(obj)
self.session.commit()
return True
except SQLAlchemyError as e:
self.session.rollback()
print(f"Erro ao deletar registro (SQLAlchemy): {e}")
raise e
except Exception as e:
self.session.rollback()
print(f"Erro ao deletar registro: {e}")
raise e
def rollback(self):
"""Desfaz as alterações pendentes."""
try:
self.session.rollback()
except SQLAlchemyError as e:
print(f"Erro ao desfazer alterações (SQLAlchemy): {e}")
except Exception as e:
print(f"Erro ao desfazer alterações: {e}")
def logar_sistema(self, id_usuario, acao, tabela, id_registro=0, json="{}", status="INICIO", mensagem="", cliente=None, correlation_id=None):
"""Registra uma ação no log do sistema."""
try:
log = LogTable(
id_usuario=id_usuario,
acao=acao,
tabela=tabela,
id_registro=id_registro,
json=json,
status=status,
mensagem=mensagem,
cliente=cliente,
correlation_id=correlation_id
)
self.add(log)
except SQLAlchemyError as e:
print(f"Erro ao logar ação (SQLAlchemy): {e}")
except Exception as e:
print(f"Erro ao logar ação: {e}")
@staticmethod
def gerar_correlation_id():
"""Gera um ID único para rastrear toda a cadeia de uma operação."""
return str(uuid.uuid4())[:8]

View file

@ -0,0 +1,35 @@
import requests
import json
import os
class enviaListerner:
def __init__(self, logger=None):
self.url = os.getenv("LISTENER_INTERNAL_URL", "http://webhook_listener:8000/listenerPlugnotas")
self.logger = logger
def notificar_emissao(self, id_tecnospeed: str, documento: str, cnpj: str, status: str = "PROCESSANDO"):
"""
Envia um POST para o listener local para sincronização imediata.
"""
payload = {
"id": id_tecnospeed,
"status": status,
"documento": documento,
"emitente": cnpj
}
try:
response = requests.post(self.url, json=payload, timeout=5)
if self.logger:
if response.status_code in [200, 201]:
self.logger.log_logs(f"Notificação interna enviada com sucesso - ID: {id_tecnospeed} | Status: {status}")
else:
self.logger.log_errors(f"Falha na notificação interna - Status: {response.status_code} | Resposta: {response.text}")
return response.status_code in [200, 201]
except Exception as e:
if self.logger:
self.logger.log_errors(f"Erro ao tentar notificar listener interno: {e}")
return False

View file

@ -0,0 +1,127 @@
__version__ = "1.0.0 - 18/02/2025" # VERSAO PARA TODOS OS SISTEMAS TRANSMITINDO PARA O SERVIÇO LOGGER_CONTROLLER DO SG_IC2
__version__ = "1.0.1 - 15/01/2026 - S.D.G. - Ajustes"
__version__ = "1.0.2 - 23/01/2026 - S.D.G. - ForwardCompatibleModel & SerializeAsAny"
from pydantic import BaseModel, field_validator, ValidationInfo, Field, ConfigDict, SerializeAsAny
from enum import StrEnum
from typing import Union, Any, Optional
class ForwardCompatibleModel(BaseModel):
"""
Base para todos os modelos que permite campos extras.
O Pydantic V2 gerencia automaticamente o dicionário 'model_extra'.
"""
model_config = ConfigDict(extra='allow')
class RealmType(StrEnum):
LOGS = "LOGS"
METRICS = "METRICS"
STATUS = "STATUS"
ERRORS = "ERRORS"
OPERATIONAL = "OPERATIONAL"
class OpStatus(StrEnum):
ACTIVE = "ACTIVE"
OK = "OK"
ERROR = "ERROR"
class ICLogs(ForwardCompatibleModel):
log_date : str
service_date : str
service : str
versionstr : str
origin : str
enterprise_id : str
enterprise_name : str
message : str
tenant : Optional[str] = Field(default='#', description="Tenant ou '#' para global")
class ICErrors(ForwardCompatibleModel):
log_date : str
service_date : str
service : str
versionstr : str
origin : str
enterprise_id : str
enterprise_name : str
message : str
tenant : Optional[str] = Field(default='#', description="Tenant ou '#' para global")
class ICMetrics(ForwardCompatibleModel):
metric_date : str
metric : str
metric_name : str
value : str
unit : str
tenant : Optional[str] = Field(default='#', description="Tenant ou '#' para global")
class ICOperational(ForwardCompatibleModel):
log_date : str # YYYY-MM-DD HH:MM:S
service_date : str # YYYY-MM-DD HH:MM:S
service_name : str
versionstr : str
origin : str
enterprise_id : str
enterprise_name : str
status : OpStatus
step_status : str
message : str
class ICStatus(ForwardCompatibleModel):
service : str
versionstr : str
system_name : str
startup_date : str
access_date : str
origin : str
sid : Optional[str] = ""
task_count : Optional[int] = 0
cpu : Optional[float] = 0.0
memory : Optional[float] = 0.0
disk : Optional[float] = 0.0
network : Optional[float] = 0.0
# Modelo polimórfico gerador de metadados JSON para o Exchange de Logs do Sistema Inteirador
class InfoConnector(ForwardCompatibleModel):
realm: RealmType
# SerializeAsAny garante que se 'messageIC' for uma subclasse (como 'Operation'),
# os campos extras da subclasse (como 'reserva_id') aparecerão no JSON final.
messageIC: SerializeAsAny[Union[ICLogs, ICErrors, ICMetrics, ICStatus, ICOperational]]
@field_validator('messageIC', mode='before')
@classmethod
def validate_schema(cls, v: Any, info: ValidationInfo) -> Any:
realm = info.data.get("realm")
if realm is None:
# Se realm não existe, o próprio Pydantic lançará erro de validação do campo 'realm'
return v
# Mapeamento de Realm para Classe Esperada
target_map = {
RealmType.LOGS : ICLogs,
RealmType.ERRORS : ICErrors,
RealmType.METRICS : ICMetrics,
RealmType.STATUS : ICStatus,
RealmType.OPERATIONAL: ICOperational,
}
expected_class = target_map.get(realm)
if not expected_class:
raise ValueError(f"Invalid realm: {realm}")
# Se for um dicionário (vindo de um JSON), validamos contra a classe mapeada
if isinstance(v, dict):
return expected_class.model_validate(v)
# Se já for um objeto instanciado (como no seu Builder), verificamos a herança
if not isinstance(v, expected_class):
raise ValueError(f"Expected {expected_class.__name__} for realm {realm}, got {type(v).__name__}")
return v
model_config = ConfigDict(
extra="allow",
frozen=False,
str_strip_whitespace=True
)

107
backup/src/utils/redis.py Normal file
View file

@ -0,0 +1,107 @@
import redis
# Fornece mecanismos de Caching e Persistência Volátil/PubSub na aplicação usando Redis
class RedisClient:
def __init__(self, host='localhost', port=6379, db=0):
"""
Inicializa o cliente Redis com as configurações fornecidas.
"""
self.client = redis.Redis(host=host, port=port, db=db, decode_responses=True)
def set(self, key, value):
"""
Define um valor para uma chave no Redis.
"""
self.client.set(key, value)
def setex(self, key, value, ex):
"""
Define um valor para uma chave no Redis com um tempo de expiração em segundos.
"""
self.client.set(key, value, ex)
def get(self, key):
"""
Obtém o valor de uma chave no Redis.
"""
return self.client.get(key)
def get_keys(self, pattern="*"):
"""
Obtém todas as chaves que correspondem a um padrão.
"""
return self.client.keys(pattern)
def get_all(self):
"""
Obtém todas as chaves e seus valores no Redis.
"""
keys = self.client.keys('*')
return {key: self.client.get(key) for key in keys}
def delete(self, key):
"""
Remove uma chave do Redis.
"""
self.client.delete(key)
def exists(self, key):
"""
Verifica se uma chave existe no Redis.
Retorna True se a chave existir, caso contrário, False.
"""
return self.client.exists(key) > 0
def close(self):
"""
Fecha a conexão com o Redis.
"""
self.client.close()
def set_expiration(self, key, value, seconds):
"""
Define um valor para uma chave e configura um tempo de expiração em segundos.
"""
self.client.set(key, value, ex=seconds)
def increment(self, key, amount=1):
"""
Incrementa o valor de uma chave numérica no Redis.
"""
return self.client.incr(key, amount)
def decrement(self, key, amount=1):
"""
Decrementa o valor de uma chave numérica no Redis.
"""
return self.client.decr(key, amount)
def hset(self, name, key, value):
"""
Define um campo em um hash no Redis.
"""
self.client.hset(name, key, value)
def hget(self, name, key):
"""
Obtém o valor de um campo em um hash no Redis.
"""
return self.client.hget(name, key)
def hgetall(self, name):
"""
Obtém todos os campos e valores de um hash no Redis.
"""
return self.client.hgetall(name)
def expire(self, key, seconds):
"""
Define um tempo de expiração para uma chave existente.
"""
self.client.expire(key, seconds)
def ttl(self, key):
"""
Obtém o tempo restante de vida (TTL) de uma chave.
"""
return self.client.ttl(key)

View file

@ -0,0 +1,64 @@
from utils.api import Api
from utils.banco import Banco
from utils.Exchange2 import LoggerExchange2
from utils.exchangeModels import RealmType
import json
# Serviço responsável por vincular rotas (webhooks) por CNPJ onde a PlugNotas enviará notificações
class Webhooks:
def __init__(self, url_base, api_key, banco:Banco, logger: LoggerExchange2 = None):
self.api = Api(api_key)
self.url = f"{url_base}/empresa/"
self.banco = banco
self.logger = logger
def _log(self, realm: RealmType, msg: str):
if self.logger:
self.logger.logIC2(realm, (msg,))
def consultar_webhooks(self, cnpj):
try:
resposta = self.api.get(f"{self.url}{cnpj}/webhook")
self._log(RealmType.OPERATIONAL, f"Webhook consultado - CNPJ: {cnpj}")
return resposta
except Exception as e:
self._log(RealmType.ERRORS, f"Erro ao consultar webhook: {e}")
return {"error": str(e)}
def criar_webhook(self, cnpj, url):
try:
resposta = self.api.post(f"{self.url}{cnpj}/webhook", {"url": url})
self._log(RealmType.OPERATIONAL, f"Webhook criado - CNPJ: {cnpj}")
return resposta
except Exception as e:
self._log(RealmType.ERRORS, f"Erro ao criar webhook: {e}")
return {"error": str(e)}
def deletar_webhook(self, cnpj):
try:
resposta = self.api.delete(f"{self.url}{cnpj}/webhook")
self._log(RealmType.OPERATIONAL, f"Webhook deletado - CNPJ: {cnpj}")
return resposta
except Exception as e:
self._log(RealmType.ERRORS, f"Erro ao deletar webhook: {e}")
return {"error": str(e)}
def atualizar_webhook(self, cnpj, url):
try:
resposta = self.api.put(f"{self.url}{cnpj}/webhook", {"url": url,"method": "POST"})
self._log(RealmType.OPERATIONAL, f"Webhook atualizado - CNPJ: {cnpj}")
return resposta
except Exception as e:
self._log(RealmType.ERRORS, f"Erro ao atualizar webhook: {e}")
return {"error": str(e)}
def testar_webhook(self, cnpj):
try:
resposta = self.api.post(f"{self.url}{cnpj}/webhook/verify", {})
self._log(RealmType.OPERATIONAL, f"Webhook testado - CNPJ: {cnpj}")
return resposta
except Exception as e:
self._log(RealmType.ERRORS, f"Erro ao testar webhook: {e}")
return {"error": str(e)}

52
backup/test_payload.py Normal file
View file

@ -0,0 +1,52 @@
import json
from src.Models.payload import validate_payload
payload = {
"fila": "empresa",
"metodo": "POST",
"cliente": "tray",
"url_base": "https://api.sandbox.plugnotas.com.br",
"config": {
"x_api_key": "2da392a6-79d2-4304-a8b7-959572c7e44d",
"user_postgres": "tecnospeed",
"pass_postgres": "u7QimG7E%21SF%2F%2FJC2c8",
"host_postgres": "postgres",
"port_postgres": "5432",
"database_postgres": "tecnospeed"
},
"dados": {
"cpfCnpj": "94074986000198",
"razaoSocial": "Tech nux",
"nomeFantasia": "Tech nux ltda",
"inscricaoMunicipal": "0001",
"simplesNacional": True,
"regimeTributario": 1,
"regimeTributarioEspecial": 1,
"incentivadorCultural": False,
"incentivoFiscal": True,
"certificado": "cert-mock-01",
"endereco": {
"codigoPais": "1058",
"descricaoPais": "BRASIL",
"tipoLogradouro": "Avenida",
"logradouro": "Avenida",
"numero": "123",
"complemento": "Casa",
"tipoBairro": "Bairro",
"bairro": "Centro",
"codigoCidade": "5200175",
"descricaoCidade": "ÁGUA FRIA DE GOIÁS",
"estado": "GO",
"cep": "68907-460"
},
"nfe": { "ativo": True },
"nfse": { "ativo": False }
}
}
try:
env = validate_payload(payload)
print("VALIDO")
except Exception as e:
print("ERRO:", type(e))
print(e)

86
buildImages.sh Normal file
View file

@ -0,0 +1,86 @@
# GERA AS IMAGENS QUE SERAO PUBLICADAS NO HUB DOCKER
#
# TODOS OS PROJETOS COMPARTILHAM DA MAJOR VERSION DA IMAGEM
# EMBORA INTERNAMENTE TENHA MINOR E PATCH VERSION DIFERENTES
#
# FORMATO MAJOR.MINOR.PATCH
#
# A IMAGEM LISTA APENAS A MAJOR VERSION COMEÇANDO DA v1
# OPCOES DE EXECUÇÃO - git clone prune
version=':v1' # Versão de Inicial
# BUILD IMAGE 1/7/2025 - Caso não aja o diretorio utils ou Models clone é compulsorio
# BUILD IMAGE 1/7/2025 - Identifica o comando se docker compose ou docker-compose
# BUILD IMAGE 1/7/2025 - Permite mudar a configuração do Dockerfile com argumentos
buildarg='INSTALL_GCC=false'
# Try to find the correct $DOCKER_COMPOSE_CMD command
if command -v docker-compose &> /dev/null; then
DOCKER_COMPOSE_CMD="docker-compose"
elif command -v docker &> /dev/null && docker compose version &> /dev/null; then
DOCKER_COMPOSE_CMD="docker compose"
else
echo "$DOCKER_COMPOSE_CMD is not installed."
exit 1
fi
clone() {
echo "VERSAO ATUAL CLASSES ACESSORIAS Exchange2 SoftGOModel"
$DOCKER_COMPOSE_CMD down
if [ -d "utils" ]; then
rm -rf utils
fi
if [ -d "Model" ]; then
rm -rf Model
fi
git clone git@github.com:desenvolvimentopsa/Exchange2.git utils/Exchange2
git clone git@github.com:desenvolvimentopsa/SoftGOModel.git Model
cd utils
find . -name "*.py" | xargs -I {} cp {} .
rm -rf Exchange2
cd ..
}
for arg in "$@"; do
if [ -n "$arg" ] && [ "$arg" == "git" ]; then
echo "VERSAO ATUAL SERVICO_TECNOSPEED DO GIT ${version}"
git stash
git pull
fi
done
if [ ! -d "utils" ] || [ ! -d "Model" ]; then
echo "DOWNLOAD CLASSES ACESSORIAS"
clone
else
for arg in "$@"; do
if [ -n "$arg" ] && [ "$arg" == "clone" ]; then
echo "DOWNLOAD CLASSES ACESSORIAS"
clone
fi
done
fi
for arg in "$@"; do
if [ -n "$arg" ] && [ "$arg" == "prune" ]; then
echo "REMOVER IMAGENS SERVICO_TECNOSPEED ${version}"
$DOCKER_COMPOSE_CMD down
docker image ls -q -f "reference=servico_tecnospeed-*${version}" | xargs -I {} docker image rm -f {}
docker system prune -f
fi
done
echo "GERAR IMAGENS SERVICO_TECNOSPEED ${version}"
echo "SERVICO_TECNOSPEED SERVICE"
cd src
cp ../utils/Exchange2.py ./utils
cp ../Model/sg_tecnospeed_models/*.py ./Models
echo servico_tecnospeed${version}
cd ..
docker build --build-arg ${buildarg} -t sg_servico_tecnospeed${version} .
rm -rf ./utils
rm -rf ./Model

111
docker-compose.yml Normal file
View file

@ -0,0 +1,111 @@
services:
redis:
image: redis
command: redis-server --save 300 1
restart: always
volumes:
- ./redis-data:/data
banco:
image: postgres:15
command: postgres -c listen_addresses='*'
restart: always
env_file: .env
environment:
POSTGRES_DB: ${POSTGRES_DB:-tecnospeed}
POSTGRES_USER: ${POSTGRES_USER:-tecnospeed}
POSTGRES_PASSWORD: ${POSTGRES_PASSWORD:-tecnospeed}
volumes:
- ./postgres-data:/var/lib/postgresql/data
ports:
- "15432:5432"
tecnospeed_rpc:
build: ./tecnospeed_rpc
image: servico_plugnotas-tecnospeed_rpc:v2
command: python tecnospeed_rpc.py
restart: always
env_file: .env
environment:
- "TZ=America/Sao_Paulo"
- "REDIS_HOST=redis"
- "PLUGNOTAS_URL=https://api.sandbox.plugnotas.com.br"
- "LISTENER_INTERNAL_URL=http://endpoint_listener:8000/listenerPlugnotas"
volumes:
- ./tecnospeed_rpc/src:/app
extra_hosts:
- "host.docker.internal:host-gateway"
logging:
driver: "json-file"
options:
max-size: "100m"
max-file: "3"
shm_size: "500m"
deploy:
resources:
limits:
cpus: "1"
memory: "500m"
depends_on:
- redis
- banco
tecnospeed_queue:
build: ./tecnospeed_queue
image: servico_plugnotas-tecnospeed_queue:v2
command: python tecnospeed_queue.py
restart: always
env_file: .env
environment:
- "TZ=America/Sao_Paulo"
- "REDIS_HOST=redis"
- "PLUGNOTAS_URL=https://api.sandbox.plugnotas.com.br"
- "LISTENER_INTERNAL_URL=http://endpoint_listener:8000/listenerPlugnotas"
volumes:
- ./tecnospeed_queue/src:/app
extra_hosts:
- "host.docker.internal:host-gateway"
logging:
driver: "json-file"
options:
max-size: "100m"
max-file: "3"
shm_size: "500m"
deploy:
resources:
limits:
cpus: "1"
memory: "500m"
networks:
- default
depends_on:
- redis
- banco
endpoint_listener:
build: ./endpoint_listener
image: servico_plugnotas-endpoint_listener:v2
command: python endpoint_listener.py
restart: always
environment:
- "TZ=America/Sao_Paulo"
- "REDIS_HOST=redis"
- "WEBHOOK_PORT=8000"
ports:
- "8000:8000"
volumes:
- ./endpoint_listener/src:/app
extra_hosts:
- "host.docker.internal:host-gateway"
logging:
driver: "json-file"
options:
max-size: "10k"
max-file: "3"
depends_on:
- redis
shm_size: "200m"
networks:
default:
name: servico_plugnotas_default

View file

@ -0,0 +1,28 @@
FROM python:3.12-slim
ARG INSTALL_GCC
ENV LANG=C.UTF-8 \
LC_ALL=C.UTF-8
RUN apt-get update && apt-get install -y --no-install-recommends \
locales \
vim \
iputils-ping \
&& locale-gen en_US.UTF-8 \
&& if [ "$INSTALL_GCC" = "true" ]; then \
apt-get install -y --no-install-recommends build-essential curl; \
fi \
&& apt-get clean && apt-get autoremove -y \
&& rm -rf /var/lib/apt/lists/*
RUN pip install --no-cache-dir unidecode
# PARTE ESPECIFICA DA IMAGEM
WORKDIR /app
RUN mkdir /app/logs
COPY requirements.txt ./
RUN pip install --no-cache-dir -r requirements.txt
COPY src .
CMD [ "python", "endpoint_listener.py"]

View file

@ -0,0 +1,13 @@
sqlalchemy
pydantic
requests
datetime
redis
pytz
pika
cryptography
psycopg2-binary
fastapi
uvicorn[standard]
pytest
pytest-cov

View file

@ -0,0 +1,18 @@
from pydantic import BaseModel
# Dados de conexão ao banco de dados isolado do cliente e da API PlugNotas
class Config(BaseModel):
api_key: str | None = None
x_api_key: str | None = None
user_postgres: str
pass_postgres: str
host_postgres: str
port_postgres: str
database_postgres: str
# Informações de mensageria para a troca de arquivos (AMQP) via subagent
class SystemConfig(BaseModel):
serviceid: str
servicekey: str
origin: str
amqps: str

View file

@ -0,0 +1,97 @@
from pydantic import BaseModel
###################
## POST /empresas #
###################
class Endereco(BaseModel):
codigoPais: str
descricaoPais: str
tipoLogradouro: str | None = None
logradouro: str
numero: str
complemento: str | None = None
tipoBairro: str | None = None
bairro: str
codigoCidade: str
descricaoCidade: str
estado: str
cep: str
class Rps(BaseModel):
lote: int
numeracao: list
class Email(BaseModel):
envio: bool
class Config(BaseModel):
rps: Rps | None = None
email: Email | None = None
producao: bool | None = None
envioLimitado: bool | None = None
integracoes: list | None = None
prefeitura: dict | str | int | None = None
class Nfse(BaseModel):
config: Config | None = None
ativo: bool = False
tipoContrato: int | None = 0
class Dfe(BaseModel):
primeiroNsu: bool
class ConfigNfe(BaseModel):
dfe: Dfe | None = None
email: Email | None = None
integracoes: dict | None = None
producao: bool | None = None
impressaoFcp: bool | None = None
impressaoPartilha: bool | None = None
versaoManual: str | None = None
versaoEsquema: str | None = None
numeracao: list | None = None
calculoAutomaticoIbpt: bool | None = True
class Nfe(BaseModel):
config: ConfigNfe | None = None
ativo: bool = False
tipoContrato: int | None = 0
class ConfigNfce(BaseModel):
email: Email | None = None
integracoes: dict | None = None
producao: bool | None = None
versaoManual: str | None = None
versaoEsquema: str | None = None
numeracao: list | None = None
class Nfce(BaseModel):
config: ConfigNfce | None = None
ativo: bool = False
tipoContrato: int | None = 0
# Entidade Empresa que cadastra a credencial emissora no sistema da PlugNotas
class Empresa(BaseModel):
endereco: Endereco
nfse: Nfse | None = None
nfe: Nfe | None = None
nfce: Nfce | None = None
incentivoFiscal: bool
incentivadorCultural: bool
cpfCnpj: str
inscricaoMunicipal: str
inscricaoEstadual: str | None = None
certificado: str
razaoSocial: str
nomeFantasia: str | None = None
simplesNacional: bool
regimeTributario: int
regimeTributarioEspecial: int
id: str | None = None
banco: str | None = None
email: str | None = None
id_empresa: int | None = None

View file

@ -0,0 +1,115 @@
from pydantic import BaseModel
from typing import List, Optional, Literal
EstadoEnum = Literal[
"AC", "AL", "AM", "AP", "BA", "CE", "DF", "ES", "GO", "MA", "MG", "MS", "MT",
"PA", "PB", "PE", "PI", "PR", "RJ", "RN", "RO", "RR", "RS", "SC", "SE", "SP", "TO"
]
TipoLogradouroEnum = Literal[
"Alameda", "Avenida", "Chácara", "Colônia", "Condomínio", "Estância", "Estrada",
"Fazenda", "Praça", "Prolongamento", "Rodovia", "Rua", "Sítio", "Travessa", "Vicinal", "Eqnp"
]
class Emitente(BaseModel):
cpfCnpj: str
inscricaoEstadual: Optional[str] = None
class Endereco(BaseModel):
logradouro: str
numero: str
bairro: str
codigoCidade: str
estado: EstadoEnum
cep: str
tipoLogradouro: TipoLogradouroEnum
codigoPais: Optional[str] = "1058"
complemento: Optional[str] = None
descricaoCidade: Optional[str] = None
descricaoPais: Optional[str] = "Brasil"
class Destinatario(BaseModel):
cpfCnpj: str
razaoSocial: str
nomeFantasia: Optional[str] = None
email: Optional[str] = None
inscricaoEstadual: Optional[str] = None
inscricaoMunicipal: Optional[str] = None
inscricaoSuframa: Optional[str] = None
orgaoPublico: Optional[bool] = False
endereco: Endereco
class ValorTributavel(BaseModel):
comercial: float
tributavel: float
class BaseCalculoIcms(BaseModel):
modalidadeDeterminacao: int
valor: float
class Icms(BaseModel):
origem: str
cst: str
baseCalculo: BaseCalculoIcms
aliquota: float
valor: float
class BaseCalculoPis(BaseModel):
valor: float
quantidade: int
class Pis(BaseModel):
cst: str
baseCalculo: BaseCalculoPis
aliquota: float
valor: float
class BaseCalculoCofins(BaseModel):
valor: float
class Cofins(BaseModel):
cst: str
baseCalculo: BaseCalculoCofins
aliquota: float
valor: float
class Tributos(BaseModel):
icms: Optional[Icms] = None
pis: Optional[Pis] = None
cofins: Optional[Cofins] = None
class ItemNFe(BaseModel):
codigo: str
descricao: str
ncm: str
cest: Optional[str] = None
cfop: str
valorUnitario: ValorTributavel
valor: float
tributos: Tributos
class Pagamento(BaseModel):
aVista: Optional[bool] = True
meio: Optional[str] = "01"
valor: float
class Telefone(BaseModel):
ddd: str
numero: str
class ResponsavelTecnico(BaseModel):
cpfCnpj: str
nome: str
email: str
telefone: Telefone
class NFe(BaseModel):
idIntegracao: str
presencial: bool
consumidorFinal: bool
natureza: str
emitente: Emitente
destinatario: Destinatario
itens: List[ItemNFe]
pagamentos: List[Pagamento]
responsavelTecnico: Optional[ResponsavelTecnico] = None

View file

@ -0,0 +1,49 @@
from pydantic import BaseModel
from typing import Optional
class Prestador(BaseModel):
cpfCnpj: str
class Endereco(BaseModel):
descricaoCidade: str
cep: str
tipoLogradouro: str
logradouro: str
tipoBairro: Optional[str] = None
codigoCidade: str
complemento: Optional[str] = None
estado: str
numero: str
bairro: str
class Tomador(BaseModel):
cpfCnpj: str
razaoSocial: str
inscricaoMunicipal: Optional[str] = None
email: str
endereco: dict
class ISS(BaseModel):
tipoTributacao: int
exigibilidade: int
aliquota: float
class Valor(BaseModel):
servico: float
descontoCondicionado: Optional[float] = 0
descontoIncondicionado: Optional[float] = 0
class Servico(BaseModel):
codigo: str
codigoTributacao: str
discriminacao: str
cnae: str
iss: ISS
valor: Valor
# Estrutura padrão de envio de uma NFSe contendo Prestador, Tomador e Serviço prestado
class NFSe(BaseModel):
idIntegracao: str
prestador: Prestador
tomador: Tomador
servico: Servico

View file

@ -0,0 +1,518 @@
from enum import Enum
from pydantic import BaseModel, ConfigDict, Field
from Models.config import Config
from Models.empresa import Empresa
from Models.nfe import NFe
from Models.nfse import NFSe
from Models.servico import Servico
from Models.tomador import Tomador
class Fila(str, Enum):
NFE = "nfe"
NFSE = "nfse"
CERTIFICADO_DIGITAL = "certificado_digital"
EMPRESA = "empresa"
SERVICO = "servico"
TOMADOR = "tomador"
RELATORIOS = "relatorios"
WEBHOOKS = "webhooks"
class NFeMetodo(str, Enum):
POST = "post"
GET = "get"
DOWNLOAD = "download"
DOWNLOAD_XML = "download_xml"
CANCELAR = "cancelar"
STATUS_CANCELAMENTO = "status_cancelamento"
CCE = "cce"
STATUS_CCE = "status_cce"
RESUMO = "resumo"
class NFSeMetodo(str, Enum):
POST = "post"
GET = "get"
DOWNLOAD = "download"
DOWNLOAD_XML = "download_xml"
CANCELAR = "cancelar"
STATUS_CANCELAMENTO = "status_cancelamento"
RESUMO = "resumo"
HISTORICO = "historico"
class CertificadoMetodo(str, Enum):
POST = "post"
GET = "get"
PUT = "put"
class EmpresaMetodo(str, Enum):
POST = "post"
GET = "get"
POST_FILE = "post_file"
PATCH = "patch"
class ServicoMetodo(str, Enum):
POST = "post"
GET = "get"
PUT = "put"
DELETE = "delete"
class TomadorMetodo(str, Enum):
POST = "post"
GET = "get"
PUT = "put"
DELETE = "delete"
class RelatorioMetodo(str, Enum):
NFE_EMITIDAS = "nfe_emitidas"
NFE_POR_CNPJ = "nfe_por_cnpj"
NFSE_EMITIDAS = "nfse_emitidas"
NFSE_POR_CNPJ = "nfse_por_cnpj"
class WebhookMetodo(str, Enum):
GET = "get"
POST = "post"
DELETE = "delete"
PUT = "put"
TEST = "test"
class BaseEnvelope(BaseModel):
cliente: str | None = None
config: Config
url_base: str | None = None
# --- Base Data Models ---
class BaseDados(BaseModel):
model_config = ConfigDict(populate_by_name=True)
id_usuario: int | None = 1
class NFePostDadosWrapped(BaseDados):
nfe: NFe
id_empresa: str | None = None
class NFePostDadosDirect(NFe):
id_usuario: int | None = 1
id_empresa: str | None = None
class NFeIdDados(BaseDados):
id: str
class NFeCancelarDados(NFeIdDados):
justificativa: str | None = None
class NFeCCEDados(NFeIdDados):
correcao: str
class NFeResumoDados(BaseDados):
cnpj: str
id_integracao: str | None = None
id: str | None = None
class NFSePostDados(NFSe):
id_usuario: int | None = 1
id_empresa: str | None = None
class NFSeIdDados(BaseDados):
id: str
class NFSeGetDados(BaseDados):
id: str | None = None
class NFSeResumoDados(NFSeIdDados):
cnpj: str
class NFSeHistoricoDados(NFSeIdDados):
cnpj: str | None = None
class CertificadoPostDados(BaseDados):
arquivo: str
senha: str
id_empresa: str | None = None
email: str | None = None
expiration_at: str | None = Field(None, alias="expirationAt")
data_expiracao: str | None = None
class CertificadoGetDados(BaseDados):
id: str | None = None
class CertificadoPutDados(CertificadoPostDados):
id: str
class EmpresaPostDados(Empresa):
id_usuario: int | None = 1
class EmpresaGetDados(BaseDados):
cnpj: str | None = None
class EmpresaPostFileDados(BaseDados):
file: str
cnpj: str
class EmpresaPatchDados(BaseDados):
cnpj: str
certificado: str
class ServicoPostDados(Servico):
id_usuario: int | None = 1
class ServicoGetDados(BaseDados):
id: str | None = None
class ServicoPutDados(Servico):
id: str
id_usuario: int | None = 1
class ServicoDeleteDados(BaseDados):
id: str
class TomadorPostDados(Tomador):
id_usuario: int | None = 1
class TomadorGetDados(BaseDados):
cpf_cnpj: str | None = None
class TomadorPutDados(Tomador):
cpf_cnpj: str
id_usuario: int | None = 1
class TomadorDeleteDados(BaseDados):
cpf_cnpj: str
class RelatorioVazioDados(BaseDados):
pass
class RelatorioCnpjDados(BaseDados):
cnpj: str
class RelatorioPeriodoDados(BaseDados):
data_inicial: str | None = None
data_final: str | None = None
class RelatorioCnpjPeriodoDados(RelatorioPeriodoDados):
cnpj: str
class WebhookGetDeleteTestDados(BaseDados):
cnpj: str
class WebhookPostPutDados(WebhookGetDeleteTestDados):
url: str
# --- Envelopes ---
class NFePostEnvelope(BaseEnvelope):
fila: Fila = Fila.NFE
metodo: NFeMetodo = NFeMetodo.POST
dados: NFePostDadosDirect | NFePostDadosWrapped
class NFeIdEnvelope(BaseEnvelope):
fila: Fila = Fila.NFE
metodo: NFeMetodo
dados: NFeIdDados
class NFeCancelarEnvelope(BaseEnvelope):
fila: Fila = Fila.NFE
metodo: NFeMetodo = NFeMetodo.CANCELAR
dados: NFeCancelarDados
class NFeCCEEnvelope(BaseEnvelope):
fila: Fila = Fila.NFE
metodo: NFeMetodo = NFeMetodo.CCE
dados: NFeCCEDados
class NFeResumoEnvelope(BaseEnvelope):
fila: Fila = Fila.NFE
metodo: NFeMetodo = NFeMetodo.RESUMO
dados: NFeResumoDados
class NFSePostEnvelope(BaseEnvelope):
fila: Fila = Fila.NFSE
metodo: NFSeMetodo = NFSeMetodo.POST
dados: NFSePostDados
class NFSeGetEnvelope(BaseEnvelope):
fila: Fila = Fila.NFSE
metodo: NFSeMetodo = NFSeMetodo.GET
dados: NFSeGetDados
class NFSeIdEnvelope(BaseEnvelope):
fila: Fila = Fila.NFSE
metodo: NFSeMetodo
dados: NFSeIdDados
class NFSeResumoEnvelope(BaseEnvelope):
fila: Fila = Fila.NFSE
metodo: NFSeMetodo = NFSeMetodo.RESUMO
dados: NFSeResumoDados
class NFSeHistoricoEnvelope(BaseEnvelope):
fila: Fila = Fila.NFSE
metodo: NFSeMetodo = NFSeMetodo.HISTORICO
dados: NFSeHistoricoDados
class CertificadoPostEnvelope(BaseEnvelope):
fila: Fila = Fila.CERTIFICADO_DIGITAL
metodo: CertificadoMetodo = CertificadoMetodo.POST
dados: CertificadoPostDados
class CertificadoGetEnvelope(BaseEnvelope):
fila: Fila = Fila.CERTIFICADO_DIGITAL
metodo: CertificadoMetodo = CertificadoMetodo.GET
dados: CertificadoGetDados
class CertificadoPutEnvelope(BaseEnvelope):
fila: Fila = Fila.CERTIFICADO_DIGITAL
metodo: CertificadoMetodo = CertificadoMetodo.PUT
dados: CertificadoPutDados
class EmpresaPostEnvelope(BaseEnvelope):
fila: Fila = Fila.EMPRESA
metodo: EmpresaMetodo = EmpresaMetodo.POST
dados: EmpresaPostDados
class EmpresaGetEnvelope(BaseEnvelope):
fila: Fila = Fila.EMPRESA
metodo: EmpresaMetodo = EmpresaMetodo.GET
dados: EmpresaGetDados
class EmpresaPostFileEnvelope(BaseEnvelope):
fila: Fila = Fila.EMPRESA
metodo: EmpresaMetodo = EmpresaMetodo.POST_FILE
dados: EmpresaPostFileDados
class EmpresaPatchEnvelope(BaseEnvelope):
fila: Fila = Fila.EMPRESA
metodo: EmpresaMetodo = EmpresaMetodo.PATCH
dados: EmpresaPatchDados
class ServicoPostEnvelope(BaseEnvelope):
fila: Fila = Fila.SERVICO
metodo: ServicoMetodo = ServicoMetodo.POST
dados: ServicoPostDados
class ServicoGetEnvelope(BaseEnvelope):
fila: Fila = Fila.SERVICO
metodo: ServicoMetodo = ServicoMetodo.GET
dados: ServicoGetDados
class ServicoPutEnvelope(BaseEnvelope):
fila: Fila = Fila.SERVICO
metodo: ServicoMetodo = ServicoMetodo.PUT
dados: ServicoPutDados
class ServicoDeleteEnvelope(BaseEnvelope):
fila: Fila = Fila.SERVICO
metodo: ServicoMetodo = ServicoMetodo.DELETE
dados: ServicoDeleteDados
class TomadorPostEnvelope(BaseEnvelope):
fila: Fila = Fila.TOMADOR
metodo: TomadorMetodo = TomadorMetodo.POST
dados: TomadorPostDados
class TomadorGetEnvelope(BaseEnvelope):
fila: Fila = Fila.TOMADOR
metodo: TomadorMetodo = TomadorMetodo.GET
dados: TomadorGetDados
class TomadorPutEnvelope(BaseEnvelope):
fila: Fila = Fila.TOMADOR
metodo: TomadorMetodo = TomadorMetodo.PUT
dados: TomadorPutDados
class TomadorDeleteEnvelope(BaseEnvelope):
fila: Fila = Fila.TOMADOR
metodo: TomadorMetodo = TomadorMetodo.DELETE
dados: TomadorDeleteDados
class RelatorioNFeEmitidasEnvelope(BaseEnvelope):
fila: Fila = Fila.RELATORIOS
metodo: RelatorioMetodo = RelatorioMetodo.NFE_EMITIDAS
dados: RelatorioVazioDados
class RelatorioNFeCnpjEnvelope(BaseEnvelope):
fila: Fila = Fila.RELATORIOS
metodo: RelatorioMetodo = RelatorioMetodo.NFE_POR_CNPJ
dados: RelatorioCnpjDados
class RelatorioNFSeEmitidasEnvelope(BaseEnvelope):
fila: Fila = Fila.RELATORIOS
metodo: RelatorioMetodo = RelatorioMetodo.NFSE_EMITIDAS
dados: RelatorioPeriodoDados
class RelatorioNFSeCnpjEnvelope(BaseEnvelope):
fila: Fila = Fila.RELATORIOS
metodo: RelatorioMetodo = RelatorioMetodo.NFSE_POR_CNPJ
dados: RelatorioCnpjPeriodoDados
class WebhookGetDeleteTestEnvelope(BaseEnvelope):
fila: Fila = Fila.WEBHOOKS
metodo: WebhookMetodo
dados: WebhookGetDeleteTestDados
class WebhookPostPutEnvelope(BaseEnvelope):
fila: Fila = Fila.WEBHOOKS
metodo: WebhookMetodo
dados: WebhookPostPutDados
# --- Generic Envelope for basic structural validation ---
class GenericEnvelope(BaseEnvelope):
"""Validates that minimum required fields exist before routing."""
fila: Fila
metodo: str
dados: dict | None = None
# --- Payload Validation Router ---
# Maps (fila, metodo) -> Envelope class
_ENVELOPE_MAP: dict[tuple[str, str], type[BaseEnvelope]] = {
# NFe
("nfe", "post"): NFePostEnvelope,
("nfe", "get"): NFeIdEnvelope,
("nfe", "download"): NFeIdEnvelope,
("nfe", "download_xml"): NFeIdEnvelope,
("nfe", "cancelar"): NFeCancelarEnvelope,
("nfe", "status_cancelamento"): NFeIdEnvelope,
("nfe", "cce"): NFeCCEEnvelope,
("nfe", "status_cce"): NFeIdEnvelope,
("nfe", "resumo"): NFeResumoEnvelope,
# NFSe
("nfse", "post"): NFSePostEnvelope,
("nfse", "get"): NFSeGetEnvelope,
("nfse", "download"): NFSeIdEnvelope,
("nfse", "download_xml"): NFSeIdEnvelope,
("nfse", "cancelar"): NFSeIdEnvelope,
("nfse", "status_cancelamento"): NFSeIdEnvelope,
("nfse", "resumo"): NFSeResumoEnvelope,
("nfse", "historico"): NFSeHistoricoEnvelope,
# Certificado Digital
("certificado_digital", "post"): CertificadoPostEnvelope,
("certificado_digital", "get"): CertificadoGetEnvelope,
("certificado_digital", "put"): CertificadoPutEnvelope,
# Empresa
("empresa", "post"): EmpresaPostEnvelope,
("empresa", "get"): EmpresaGetEnvelope,
("empresa", "post_file"): EmpresaPostFileEnvelope,
("empresa", "patch"): EmpresaPatchEnvelope,
# Serviço
("servico", "post"): ServicoPostEnvelope,
("servico", "get"): ServicoGetEnvelope,
("servico", "put"): ServicoPutEnvelope,
("servico", "delete"): ServicoDeleteEnvelope,
# Tomador
("tomador", "post"): TomadorPostEnvelope,
("tomador", "get"): TomadorGetEnvelope,
("tomador", "put"): TomadorPutEnvelope,
("tomador", "delete"): TomadorDeleteEnvelope,
# Relatórios
("relatorios", "nfe_emitidas"): RelatorioNFeEmitidasEnvelope,
("relatorios", "nfe_por_cnpj"): RelatorioNFeCnpjEnvelope,
("relatorios", "nfse_emitidas"): RelatorioNFSeEmitidasEnvelope,
("relatorios", "nfse_por_cnpj"): RelatorioNFSeCnpjEnvelope,
# Webhooks
("webhooks", "get"): WebhookGetDeleteTestEnvelope,
("webhooks", "post"): WebhookPostPutEnvelope,
("webhooks", "delete"): WebhookGetDeleteTestEnvelope,
("webhooks", "put"): WebhookPostPutEnvelope,
("webhooks", "test"): WebhookGetDeleteTestEnvelope,
}
def validate_payload(payload_dict: dict) -> BaseEnvelope:
# Step 1: Basic structural validation
generic = GenericEnvelope(**payload_dict)
fila = generic.fila.value
metodo = generic.metodo.lower()
# Step 2: Find specific envelope
key = (fila, metodo)
envelope_cls = _ENVELOPE_MAP.get(key)
if envelope_cls is None:
raise ValueError(f"Combinação fila/metodo desconhecida: {fila}/{metodo}")
# Step 3: Normalize metodo in the dict to match enum lowercase values
payload_dict['metodo'] = metodo
# Step 4: Full typed validation
return envelope_cls(**payload_dict)

View file

@ -0,0 +1,150 @@
from pydantic import BaseModel
from enum import IntEnum
class TipoTributacao(IntEnum):
NAO_DEFINIDO = 0
ISENTO_ISS = 1
IMUNE = 2
NAO_INCIDENCIA = 3
NAO_TRIBUTAVEL = 4
RETIDO = 5
TRIBUTAVEL_MUNICIPIO = 6
TRIBUTAVEL_FORA_MUNICIPIO = 7
TRIBUTAVEL_MUNICIPIO_TOMADOR = 8
class TipoExigibilidade(IntEnum):
EXIGIVEL = 1
NAO_INCIDENCIA = 2
ISENCAO = 3
EXPORTACAO = 4
IMUNIDADE = 5
SUSPENSO_ACAO_JUDICIAL = 6
SUSPENSO_ACAO_ADMINISTRATIVA = 7
class ISS(BaseModel):
tipoTributacao: int #TipoTributacao
exigibilidade: int #TipoExigibilidade
retido: bool | None = None # Se o ISS foi retido na fonte
aliquota: float # Aliquota do ISS
valor: float # Valor do ISS
valorRetido: float # Valor do ISS retido
processoSuspensao: str # Numero processo de suspensão do ISS
##############################################################################
# tipoTributacao integer
# Default: 6
# Enum: 0 1 2 3 4 5 6 7 8
# Tipo de Tributação do Serviço, valores:
# 0 - Não definido
# 1 - Isento de ISS
# 2 - Imune
# 3 - Não Incidência no Município
# 4 - Não Tributável
# 5 - Retido
# 6 - Tributável Dentro do Município
# 7 - Tributável Fora do Município
# 8 - Tributável Dentro do Município pelo tomador
##############################################################################
# exigibilidade integer
# Default: 1
# Enum: 1 2 3 4 5 6 7
# Exigibilidade do ISS, valores:
# 1 - Exigível
# 2 - Não Incidência
# 3 - Isenção
# 4 - Exportação
# 5 - Imunidade
# 6 - Supenso por Ação Judicial
# 7 - Suspenso por Ação Administrativa
##############################################################################
# responsavelRetencao string
# Valores aceitos:
# 1 - Tomador
# 2 - Intermediário
##############################################################################
class Obra(BaseModel):
art: str # Anotação de Responsabilidade Técnica (Codigo ART)
codigo: str # Código da obra
class Valor(BaseModel):
servico: float # Valor bruto do serviço prestado.
baseCalculo: float # Valor da base de cálculo dos impostos.
deducoes: float # Valor deduções.
descontoCondicionado: float # Valor do desconto condicionado.
descontoIncondicionado: float # Valor do desconto incondicionado.
liquido: float # Valor liquido do serviço, caso você não informe esse campo, vamos calcular automáticamente.
unitario: float # Valor unitário do serviço.
valorAproximadoTributos: float # Valor aproximado dos tributos.
class Deducao(BaseModel):
tipo: int
descricao: str # Descrição da dedução
##############################################################################
# tipo integer
# Tipo de dedução:
# 0 - Sem Deduções
# 1 - Materiais
# 2 - Subempreitada de Mão de Obra
# 3 - Serviços
# 4 - Produção Externa
# 5 - Alimentação e Bebidas
# 6 - Reembolso de Despesas
# 7 - Repasse Consorciado
# 8 - Repasse Plano de Saúde
# 99 - Outras Deduções
##############################################################################
class RetencaoInterno(BaseModel):
aliquota: float # Aliquota da retenção
valor: float # Valor da retenção
class Retencao(BaseModel):
pis: RetencaoInterno
cofins: RetencaoInterno
csll: RetencaoInterno
inss: RetencaoInterno
irrf: RetencaoInterno
outrasRetencoes: float # Valor de outras retenções
cpp: RetencaoInterno
class Aliquota(BaseModel):
municipal: float # Aliquota municipal
estadual: float # Aliquota estadual
federal: float # Aliquota federal
class Detalhado(BaseModel):
aliquota: Aliquota
class Simplificado(BaseModel):
aliquota: float # Aliquota geral de impostos
class IBPT(BaseModel):
simplificado: Simplificado
detalhado: Detalhado
# Representa o detalhamento do serviço prestado na NFSe, impostos incidentes e deduções
class Servico(BaseModel):
codigo: str # Código de serviço, também conhecido como Código Item Lista Serviço, geralmente segue a LC116, podendo váriar de acordo com a prefeitura.Você pode encontrar esse dado no portal da prefeitura, em uma nota emitida, ou junto ao contador.
idIntegracao: str # Código EXTERNO de integração do serviço. Utilizado SOMENTE quando se cadastra previamente um serviço.
discriminacao: str # Descrição do serviço prestado
codigoTributacao: str # Código tributação no Município
cnae: str # O código CNAE ou, por extenso, Classificação Nacional de Atividades Econômicas, é uma forma de padronizar, em todo o território nacional, os códigos de atividades econômicas e os critérios de enquadramento usados pelos mais diversos órgãos da administração tributária do Brasil.
codigoCidadeIncidencia: str # Código IBGE da cidade de incidência do ISS.
descricaoCidadeIncidencia: str # Descrição da cidade de incidência do ISS.
unidade: str # Unidade do serviço prestado.
quantidade: int # Quantidade dos serviços prestados.
iss: ISS
obra: Obra
valor: Valor
deducao: Deducao
retencao: Retencao
tributavel: bool # Indica se o serviço está sujeito a tributação.
ibpt: IBPT
responsavelRetencao: str
##############################################################################
# responsavelRetencao Valores aceitos:
# 1 - Tomador
# 2 - Intermediário
# Caso o responsável pela retenção seja o próprio prestador, não gerar o campo.
##############################################################################

View file

@ -0,0 +1,34 @@
from pydantic import BaseModel
class Endereco(BaseModel):
tipoLogradouro: str
logradouro: str
numero: str
complemento: str
tipoBairro: str
bairro: str
codigoPais: str
descricaoPais: str
codigoCidade: str
descricaoCidade: str
estado: str
cep: str
class Telefone(BaseModel):
ddd: str
numero: str
# Representa o cliente (Tomador do Serviço) vinculado a uma nota fiscal de serviço
class Tomador(BaseModel):
cpfCnpj: str
inscricaoMunicipal: str
inscricaoEstadual: str
inscricaoSuframa: str
indicadorInscricaoEstadual: int
razaoSocial: str
nomeFantasia: str
endereco: Endereco
telefone: Telefone
email: str
orgaoPublico: bool

View file

@ -0,0 +1,105 @@
from Models.servico import Servico
from utils.api import Api
from utils.banco import Banco, ServicoTable
from utils.Exchange2 import LoggerExchange2
from utils.exchangeModels import RealmType
from sqlalchemy.exc import SQLAlchemyError
import json
# Controller responsável pelos cadastros e busca de Serviços a serem vinculados em NFSe
class ServicoController:
def __init__(self, url_base, api_key, banco: Banco, logger: LoggerExchange2 = None):
self.api = Api(api_key)
self.url = f"{url_base}/nfse/servico"
self.banco = banco
self.logger = logger
def _log(self, realm: RealmType, msg: str):
if self.logger:
self.logger.logIC2(realm, (msg,))
def cadastrar_servico(self, servico: Servico, data: dict):
try:
resposta = self.api.post(self.url, servico.model_dump())
if 'error' in resposta:
self._log(RealmType.ERRORS, f"Erro na API ao cadastrar serviço")
return resposta
servico_novo = ServicoTable(
descricao=servico.discriminacao,
id_tecnospeed=resposta['data']['id'],
id_usuario=data.get('id_usuario', 1),
status=True
)
self.banco.add(servico_novo)
self._log(RealmType.OPERATIONAL, f"Serviço cadastrado com sucesso - ID: {resposta['data']['id']}")
return resposta
except SQLAlchemyError as e:
self.banco.rollback()
self._log(RealmType.ERRORS, f"Erro no banco ao cadastrar serviço: {e}")
return {"error": f"Erro ao cadastrar serviço: {str(e)}"}
except Exception as e:
return {"error": f"Erro inesperado: {str(e)}"}
def buscar_servico(self, id_servico, data: dict = None):
try:
resposta = self.api.get(f"{self.url}/{id_servico}")
self._log(RealmType.OPERATIONAL, f"Serviço consultado - ID: {id_servico}")
return resposta
except Exception as e:
self._log(RealmType.ERRORS, f"Erro ao buscar serviço: {e}")
return {"error": f"Erro ao buscar serviço: {str(e)}"}
def listar_servicos(self, data: dict = None):
try:
servicos = self.banco.session.query(ServicoTable.id_tecnospeed, ServicoTable.descricao, ServicoTable.status).all()
resultado = [{"id_tecnospeed": servico.id_tecnospeed, "descricao": servico.descricao, "status": servico.status} for servico in servicos]
self._log(RealmType.OPERATIONAL, f"Listagem de serviços - {len(resultado)} registros")
return resultado
except SQLAlchemyError as e:
self._log(RealmType.ERRORS, f"Erro ao listar serviços: {e}")
return {"error": f"Erro ao listar serviços: {str(e)}"}
except Exception as e:
return {"error": f"Erro inesperado: {str(e)}"}
def atualizar_servico(self, id_servico, servico: Servico):
try:
resposta = self.api.patch(f"{self.url}/{id_servico}", servico.model_dump())
servico_existente = self.banco.session.query(ServicoTable).filter_by(id=id_servico).first()
if not servico_existente:
self._log(RealmType.ERRORS, f"Serviço não encontrado - ID: {id_servico}")
return {"error": "Serviço não encontrado"}
servico_existente.descricao = servico.discriminacao
self.banco.session.commit()
self._log(RealmType.OPERATIONAL, f"Serviço atualizado com sucesso - ID: {id_servico}")
return resposta
except SQLAlchemyError as e:
self.banco.rollback()
self._log(RealmType.ERRORS, f"Erro ao atualizar serviço: {e}")
return {"error": f"Erro ao atualizar serviço: {str(e)}"}
except Exception as e:
return {"error": f"Erro inesperado: {str(e)}"}
def deletar_servico(self, id_servico):
try:
resposta = self.api.delete(f"{self.url}/{id_servico}")
servico_existente = self.banco.session.query(ServicoTable).filter_by(id=id_servico).first()
if not servico_existente:
self._log(RealmType.ERRORS, f"Serviço não encontrado para deletar - ID: {id_servico}")
return {"error": "Serviço não encontrado"}
self.banco.session.delete(servico_existente)
self.banco.session.commit()
self._log(RealmType.OPERATIONAL, f"Serviço deletado com sucesso - ID: {id_servico}")
return resposta
except SQLAlchemyError as e:
self.banco.rollback()
self._log(RealmType.ERRORS, f"Erro ao deletar serviço: {e}")
return {"error": f"Erro ao deletar serviço: {str(e)}"}
except Exception as e:
return {"error": f"Erro inesperado: {str(e)}"}

View file

@ -0,0 +1,91 @@
from utils.api import Api
from utils.banco import Banco, TomadorTable
from utils.Exchange2 import LoggerExchange2
from utils.exchangeModels import RealmType
from Models.tomador import Tomador
import json
# Controller para gerenciamento no banco e na API dos Tomadores (clientes finais) das notas
class TomadorController:
def __init__(self, url_base, api_key, banco: Banco, logger: LoggerExchange2 = None):
self.api = Api(api_key)
self.url = f"{url_base}/nfse/tomador"
self.banco = banco
self.logger = logger
def _log(self, realm: RealmType, msg: str):
if self.logger:
self.logger.logIC2(realm, (msg,))
def cadastrar_tomador(self, tomador: Tomador, data: dict):
try:
retorno = self.api.post(self.url, tomador.model_dump())
if 'error' in retorno:
self._log(RealmType.ERRORS, f"Erro na API ao cadastrar tomador")
return retorno
tomador_novo = TomadorTable(
id_tecnospeed=retorno['id'],
nome=tomador.razaoSocial,
cpf_cnpj=tomador.cpfCnpj,
email=tomador.email,
telefone=f"({tomador.telefone.ddd}) {tomador.telefone.numero}",
id_usuario=data.get('id_usuario', 1),
status=True
)
self.banco.add(tomador_novo)
self._log(RealmType.OPERATIONAL, f"Tomador cadastrado com sucesso - CPF/CNPJ: {tomador.cpfCnpj}")
return retorno
except Exception as e:
self._log(RealmType.ERRORS, f"Erro ao cadastrar tomador: {e}")
return {"error": str(e)}
def buscar_tomador(self, cpf_cnpj):
try:
tomador = self.api.get(f"{self.url}/{cpf_cnpj}")
self._log(RealmType.OPERATIONAL, f"Tomador consultado - CPF/CNPJ: {cpf_cnpj}")
return tomador
except Exception as e:
self._log(RealmType.ERRORS, f"Erro ao buscar tomador: {e}")
return {"error": str(e)}
def atualizar_tomador(self, cpf_cnpj, tomador: Tomador):
try:
tomador_existente = self.banco.session.query(TomadorTable).filter_by(cpf_cnpj=cpf_cnpj).first()
if not tomador_existente:
self._log(RealmType.ERRORS, f"Tomador não encontrado - CPF/CNPJ: {cpf_cnpj}")
return {"error": "Tomador não encontrado"}
resposta = self.api.patch(f"{self.url}/{cpf_cnpj}", tomador.model_dump())
tomador_existente.nome = tomador.razaoSocial
tomador_existente.email = tomador.email
tomador_existente.telefone = f"({tomador.telefone.ddd}) {tomador.telefone.numero}"
self.banco.session.commit()
self._log(RealmType.OPERATIONAL, f"Tomador atualizado com sucesso - CPF/CNPJ: {cpf_cnpj}")
return resposta
except Exception as e:
self.banco.session.rollback()
self._log(RealmType.ERRORS, f"Erro ao atualizar tomador: {e}")
return {"error": str(e)}
def deletar_tomador(self, cpf_cnpj):
try:
tomador_existente = self.banco.session.query(TomadorTable).filter_by(cpf_cnpj=cpf_cnpj).first()
if not tomador_existente:
self._log(RealmType.ERRORS, f"Tomador não encontrado para deletar - CPF/CNPJ: {cpf_cnpj}")
return {"error": "Tomador não encontrado"}
resposta = self.api.delete(f"{self.url}/{cpf_cnpj}")
self.banco.session.delete(tomador_existente)
self.banco.session.commit()
self._log(RealmType.OPERATIONAL, f"Tomador deletado com sucesso - CPF/CNPJ: {cpf_cnpj}")
return resposta
except Exception as e:
self.banco.session.rollback()
self._log(RealmType.ERRORS, f"Erro ao deletar tomador: {e}")
return {"error": str(e)}

View file

@ -0,0 +1,167 @@
from Models.nfse import NFSe
from utils.api import Api
from utils.banco import Banco, NFSeTable
from utils.Exchange2 import LoggerExchange2
from utils.exchangeModels import RealmType
from utils.notificador import NotificadorInterno
import json
# Serviço responsável pelo controle, emissão e cancelamento das Notas Fiscais de Serviço Eletrônicas (NFSe)
class NFSeService:
def __init__(self, api_key, url_base, banco: Banco, logger: LoggerExchange2 = None):
self.api = Api(api_key)
self.url = f"{url_base}/nfse"
self.banco = banco
self.logger = logger
self.notificador = NotificadorInterno(logger=logger) if logger else None
def _log(self, realm: RealmType, msg: str):
if self.logger:
self.logger.logIC2(realm, (msg,))
def cadastrar_nfse(self, nfse: NFSe, data: dict):
try:
id_empresa = data.get('id_empresa', "0")
resposta = self.api.post(self.url, [nfse.model_dump()])
if 'error' in resposta:
self._log(RealmType.ERRORS, f"Erro na API ao cadastrar NFSe: {resposta.get('error')}")
return resposta
# A API da Plugnotas retorna 'documents' contendo uma lista com os sucessos
documentos = resposta.get('documents', [])
if not documentos:
self._log(RealmType.ERRORS, f"Nenhum documento retornado pela API. Resposta: {resposta}")
return resposta
id_recebido = documentos[0].get('id')
if not id_recebido:
raise Exception("ID do documento não encontrado na resposta.")
nfse_nova = NFSeTable(
id_tecnospeed=id_recebido,
id_empresa=id_empresa,
id_usuario=data.get('id_usuario', 1),
status=True
)
self.banco.add(nfse_nova)
self._log(RealmType.OPERATIONAL, f"NFSe cadastrada com sucesso - ID: {id_recebido}")
# Notifica o listener interno para sincronização imediata
if self.notificador:
self.notificador.notificar_emissao(id_tecnospeed=id_recebido, documento="nfse", cnpj=id_empresa)
return resposta
except Exception as e:
self._log(RealmType.ERRORS, f"Erro ao cadastrar NFSe: {e}")
raise Exception(f"Erro ao cadastrar NFSe: {e}")
def buscar_nfse(self, id_nfse):
try:
resposta = self.api.get(f"{self.url}/{id_nfse}")
self._log(RealmType.OPERATIONAL, f"NFSe consultada com sucesso - ID: {id_nfse}")
return resposta
except Exception as e:
self._log(RealmType.ERRORS, f"Erro ao buscar NFSe: {e}")
raise Exception(f"Erro ao buscar NFSe: {e}")
def listar_nfse(self):
try:
nfse = self.banco.session.query(NFSeTable.id, NFSeTable.id_tecnospeed).all()
resultado = [{"id": n[0], "descricao": n[1]} for n in nfse]
self._log(RealmType.OPERATIONAL, f"Listagem de NFSe - {len(resultado)} registros")
return resultado
except Exception as e:
self._log(RealmType.ERRORS, f"Erro ao listar NFSe: {e}")
raise Exception(f"Erro ao listar NFSe: {e}")
def baixar_nfse(self, id_nfse):
try:
resposta = self.api.get(f"{self.url}/pdf/{id_nfse}")
self._log(RealmType.OPERATIONAL, f"PDF NFSe baixado com sucesso - ID: {id_nfse}")
return resposta
except Exception as e:
self._log(RealmType.ERRORS, f"Erro ao baixar NFSe: {e}")
raise Exception(f"Erro ao baixar NFSe: {e}")
def baixar_xml_nfse(self, id_nfse):
try:
resposta = self.api.get(f"{self.url}/xml/{id_nfse}")
self._log(RealmType.OPERATIONAL, f"XML NFSe baixado com sucesso - ID: {id_nfse}")
return resposta
except Exception as e:
self._log(RealmType.ERRORS, f"Erro ao baixar XML NFSe: {e}")
raise Exception(f"Erro ao baixar XML NFSe: {e}")
def solicitar_cancelamento_nfse(self, id_nfse):
try:
resposta = self.api.post(f"{self.url}/cancelar/{id_nfse}", {})
self._log(RealmType.OPERATIONAL, f"Cancelamento NFSe solicitado - ID: {id_nfse}")
return resposta
except Exception as e:
self._log(RealmType.ERRORS, f"Erro ao solicitar cancelamento NFSe: {e}")
raise Exception(f"Erro ao solicitar cancelamento NFSe: {e}")
def consultar_status_cancelamento_nfse(self, id_nfse):
try:
resposta = self.api.get(f"{self.url}/cancelar/status/{id_nfse}")
self._log(RealmType.OPERATIONAL, f"Status cancelamento NFSe consultado - ID: {id_nfse}")
return resposta
except Exception as e:
self._log(RealmType.ERRORS, f"Erro ao consultar status cancelamento NFSe: {e}")
raise Exception(f"Erro ao consultar status cancelamento NFSe: {e}")
def consultar_resumo_nfse(self, id_nfse, cnpj):
try:
resposta = self.api.get(f"{self.url}/consultar/{id_nfse}/{cnpj}")
self._log(RealmType.OPERATIONAL, f"Resumo NFSe consultado - ID: {id_nfse}")
return resposta
except Exception as e:
self._log(RealmType.ERRORS, f"Erro ao consultar resumo NFSe: {e}")
raise Exception(f"Erro ao consultar resumo NFSe: {e}")
def baixar_histotico_nfse(self, id_nfse, cnpj = None):
try:
resposta = self.api.get(f"{self.url}/historico/{id_nfse}") if cnpj is None else self.api.get(f"{self.url}/historico/{id_nfse}/{cnpj}")
self._log(RealmType.OPERATIONAL, f"Histórico NFSe baixado - ID: {id_nfse}")
return resposta
except Exception as e:
self._log(RealmType.ERRORS, f"Erro ao baixar histórico NFSe: {e}")
raise Exception(f"Erro ao baixar histórico NFSe: {e}")
def enviar_email_nfse(self, id_nfse, email:list[str], reenvio:bool = False):
try:
resposta = self.api.post(f"{self.url}/email/{id_nfse}", {"reenvio": reenvio,"destinatarios": email})
self._log(RealmType.OPERATIONAL, f"Email NFSe enviado para {email} - ID: {id_nfse}")
return resposta
except Exception as e:
self._log(RealmType.ERRORS, f"Erro ao enviar email NFSe: {e}")
raise Exception(f"Erro ao enviar email NFSe: {e}")
def resolver_nfse(self, id_nfse, identificacaoNota:str):
try:
resposta = self.api.post(f"{self.url}/resolve/{id_nfse}", {"identificacaoNota": identificacaoNota})
self._log(RealmType.OPERATIONAL, f"NFSe resolvida com sucesso - ID: {id_nfse}")
return resposta
except Exception as e:
self._log(RealmType.ERRORS, f"Erro ao resolver NFSe: {e}")
raise Exception(f"Erro ao resolver NFSe: {e}")
def consultar_nfse_periodo(self, data_inicio, data_fim):
try:
resposta = self.api.get_paginated(f"{self.url}/periodo/{data_inicio}/{data_fim}")
self._log(RealmType.OPERATIONAL, f"NFSe consultada por período: {data_inicio} a {data_fim}")
return resposta
except Exception as e:
self._log(RealmType.ERRORS, f"Erro ao consultar NFSe por período: {e}")
raise Exception(f"Erro ao consultar NFSe por período: {e}")
def baixar_recibo_provisorio_servico(self, id_nfse, cnpj = None):
try:
resposta = self.api.get(f"{self.url}/rps/pdf/{id_nfse}") if cnpj is None else self.api.get(f"{self.url}/rps/pdf/{id_nfse}/{cnpj}")
self._log(RealmType.OPERATIONAL, f"RPS baixado com sucesso - ID: {id_nfse}")
return resposta
except Exception as e:
self._log(RealmType.ERRORS, f"Erro ao baixar recibo provisório de serviço: {e}")
raise Exception(f"Erro ao baixar recibo provisório de serviço: {e}")

View file

@ -0,0 +1,126 @@
import json as json_lib
from utils.api import Api
from utils.banco import Banco, NFeTable
from Models.nfe import NFe
from typing import List
from utils.Exchange2 import LoggerExchange2
from utils.exchangeModels import RealmType
from utils.notificador import NotificadorInterno
class RegistroNFeService:
# Classe responsável por fornecer as operações de comunicação com a PlugNotas para o documento NFe
def __init__(self, api_key: str, url_base: str, banco: Banco, logger: LoggerExchange2 = None):
self.api = Api(api_key)
self.url = f"{url_base}/nfe"
self.banco = banco
self.logger = logger
self.notificador = NotificadorInterno(logger=logger) if logger else None
def cadastrar_nfe(self, nfe_list: List[NFe], data: dict):
# Envia uma lista de NFes em lote ou unitária para emissão e registra no banco de dados
try:
id_empresa = data.get('id_empresa', "0")
payload = [nfe.model_dump() for nfe in nfe_list]
resposta = self.api.post(self.url, payload)
if 'error' in resposta:
id_usr = data.get('id_usuario', 1)
if self.logger: self.logger.logIC2(RealmType.OPERATIONAL, ('INSERT', 'nfe', id_usr, '0', 'ERRO', f"Erro na API: {resposta.get('error')}", json_lib.dumps(resposta)))
return resposta
if 'documents' in resposta:
for doc in resposta['documents']:
id_tecnospeed = doc.get('id')
if id_tecnospeed:
nfe_nova = NFeTable(
id_tecnospeed=id_tecnospeed,
id_empresa=id_empresa,
id_usuario=data.get('id_usuario', 1),
status=True
)
self.banco.add(nfe_nova)
id_usr = data.get('id_usuario', 1)
if self.logger: self.logger.logIC2(RealmType.OPERATIONAL, ('INSERT', 'nfe', id_usr, str(nfe_nova.id), 'SUCESSO', 'NFe cadastrada com sucesso', json_lib.dumps(doc)))
# Notifica o listener interno para sincronização imediata
if self.notificador:
self.notificador.notificar_emissao(id_tecnospeed=id_tecnospeed, documento="nfe", cnpj=id_empresa)
return resposta
except Exception as e:
if self.logger: self.logger.logIC2(RealmType.OPERATIONAL, ('INSERT', 'nfe', data.get('id_usuario', 1), '0', 'ERRO', f"Erro ao cadastrar NFe: {e}", "{}"))
raise Exception(f"Erro ao cadastrar NFe: {e}")
def cancelar_nfe(self, id_nota: str, justificativa: str):
# Solicita o cancelamento de uma NFe existente mediante uma justificativa
try:
resposta = self.api.post(f"{self.url}/{id_nota}/cancelamento", {"justificativa": justificativa})
if self.logger: self.logger.logIC2(RealmType.OPERATIONAL, ('DELETE', 'nfe', 1, id_nota, 'SUCESSO', 'NFe cancelada com sucesso', json_lib.dumps(resposta)))
return resposta
except Exception as e:
if self.logger: self.logger.logIC2(RealmType.OPERATIONAL, ('DELETE', 'nfe', 1, id_nota, 'ERRO', f"Erro ao cancelar NFe: {e}", "{}"))
raise Exception(f"Erro ao cancelar NFe: {e}")
def enviar_cce(self, id_nota: str, correcao: str):
# Envia uma Carta de Correção Eletrônica (CCe) para retificar uma NFe já emitida
try:
resposta = self.api.post(f"{self.url}/{id_nota}/cce", {"correcao": correcao})
if self.logger: self.logger.logIC2(RealmType.OPERATIONAL, ('UPDATE', 'nfe', 1, id_nota, 'SUCESSO', 'CCe enviada com sucesso', json_lib.dumps(resposta)))
return resposta
except Exception as e:
if self.logger: self.logger.logIC2(RealmType.OPERATIONAL, ('UPDATE', 'nfe', 1, id_nota, 'ERRO', f"Erro ao enviar CCe NFe: {e}", "{}"))
raise Exception(f"Erro ao enviar CCe NFe: {e}")
def consultar_resumo(self, id_nota: str):
# Consulta o resumo e o status atual da NFe na PlugNotas pelo seu ID
try:
resposta = self.api.get(f"{self.url}/{id_nota}/resumo")
if self.logger: self.logger.logIC2(RealmType.OPERATIONAL, ('SELECT', 'nfe', 1, id_nota, 'SUCESSO', 'Resumo de NFe consultado', json_lib.dumps(resposta)))
return resposta
except Exception as e:
if self.logger: self.logger.logIC2(RealmType.OPERATIONAL, ('SELECT', 'nfe', 1, id_nota, 'ERRO', f"Erro ao consultar resumo NFe: {e}", "{}"))
raise Exception(f"Erro ao consultar resumo NFe: {e}")
def consultar_resumo_por_cnpj_integracao(self, cnpj: str, id_integracao: str):
try:
resposta = self.api.get(f"{self.url}/{cnpj}/{id_integracao}/resumo")
if self.logger: self.logger.logIC2(RealmType.OPERATIONAL, ('SELECT', 'nfe', id_integracao, id_integracao, 'SUCESSO', 'Resumo por CNPJ/Integração consultado', json_lib.dumps(resposta)))
return resposta
except Exception as e:
if self.logger: self.logger.logIC2(RealmType.OPERATIONAL, ('SELECT', 'nfe', id_integracao, id_integracao, 'ERRO', f"Erro ao consultar resumo NFe por CNPJ/Integração: {e}", "{}"))
raise Exception(f"Erro ao consultar resumo NFe por CNPJ/Integração: {e}")
def consultar_status_cancelamento(self, id_nota: str):
try:
resposta = self.api.get(f"{self.url}/{id_nota}/cancelamento/status")
if self.logger: self.logger.logIC2(RealmType.OPERATIONAL, ('SELECT', 'nfe', id_nota, id_nota, 'SUCESSO', 'Status de cancelamento consultado', json_lib.dumps(resposta)))
return resposta
except Exception as e:
if self.logger: self.logger.logIC2(RealmType.OPERATIONAL, ('SELECT', 'nfe', id_nota, id_nota, 'ERRO', f"Erro ao consultar status de cancelamento NFe: {e}", "{}"))
raise Exception(f"Erro ao consultar status de cancelamento NFe: {e}")
def consultar_status_cce(self, id_nota: str):
try:
resposta = self.api.get(f"{self.url}/{id_nota}/cce/status")
if self.logger: self.logger.logIC2(RealmType.OPERATIONAL, ('SELECT', 'nfe', id_nota, id_nota, 'SUCESSO', 'Status CCe consultado', json_lib.dumps(resposta)))
return resposta
except Exception as e:
if self.logger: self.logger.logIC2(RealmType.OPERATIONAL, ('SELECT', 'nfe', id_nota, id_nota, 'ERRO', f"Erro ao consultar status CCe NFe: {e}", "{}"))
raise Exception(f"Erro ao consultar status CCe NFe: {e}")
def baixar_pdf(self, id_nota: str):
# Obtém o link para download ou o stream do PDF da Nota Fiscal processada
try:
resposta = self.api.get(f"{self.url}/{id_nota}/pdf")
if self.logger: self.logger.logIC2(RealmType.OPERATIONAL, ('DOWNLOAD (PDF)', 'nfe', id_nota, id_nota, 'SUCESSO', 'PDF baixado com sucesso', json_lib.dumps(resposta) if isinstance(resposta, dict) else '{}'))
return resposta
except Exception as e:
if self.logger: self.logger.logIC2(RealmType.OPERATIONAL, ('DOWNLOAD (PDF)', 'nfe', id_nota, id_nota, 'ERRO', f"Erro ao baixar PDF NFe: {e}", "{}"))
raise Exception(f"Erro ao baixar PDF NFe: {e}")
def baixar_xml(self, id_nota: str):
try:
resposta = self.api.get(f"{self.url}/{id_nota}/xml")
if self.logger: self.logger.logIC2(RealmType.OPERATIONAL, ('DOWNLOAD (XML)', 'nfe', id_nota, id_nota, 'SUCESSO', 'XML baixado com sucesso', json_lib.dumps(resposta) if isinstance(resposta, dict) else '{}'))
return resposta
except Exception as e:
if self.logger: self.logger.logIC2(RealmType.OPERATIONAL, ('DOWNLOAD (XML)', 'nfe', id_nota, id_nota, 'ERRO', f"Erro ao baixar XML NFe: {e}", "{}"))
raise Exception(f"Erro ao baixar XML NFe: {e}")

View file

@ -0,0 +1,70 @@
from utils.api import Api
from utils.banco import Banco
from utils.Exchange2 import LoggerExchange2
from utils.exchangeModels import RealmType
from datetime import datetime, timedelta
import json
# Extração e consumo de relatórios massivos de emissões diretamente da API e consolidações em lotes
class Relatorios:
def __init__(self, url_base, api_key, banco: Banco, logger: LoggerExchange2 = None):
self.api = Api(api_key)
self.url = f"{url_base}/"
self.banco = banco
self.logger = logger
def _log(self, realm: RealmType, msg: str):
if self.logger:
self.logger.logIC2(realm, (msg,))
def nfe_emitidas(self, data: dict = None):
try:
resposta = self.api.get_paginated(f"{self.url}nfe/relatorio")
self._log(RealmType.OPERATIONAL, "Relatório de NFe emitidas consultado")
return resposta
except Exception as e:
msg_erro = f"Erro ao consultar relatório NFe emitidas: {e}"
self._log(RealmType.ERRORS, msg_erro)
return {"error": msg_erro}
def nfe_por_cnpj(self, cnpj, data: dict = None):
try:
resposta = self.api.get_paginated(f"{self.url}nfe/{cnpj}/relatorio")
self._log(RealmType.OPERATIONAL, f"Relatório de NFe por CNPJ: {cnpj}")
return resposta
except Exception as e:
msg_erro = f"Erro ao consultar relatório NFe por CNPJ: {e}"
self._log(RealmType.ERRORS, msg_erro)
return {"error": msg_erro}
def nfse_emitidas(self, data_inicial=None, data_final=None, data: dict = None):
try:
if not data_inicial:
data_inicial = (datetime.now() - timedelta(days=30)).strftime('%Y-%m-%d')
if not data_final:
data_final = datetime.now().strftime('%Y-%m-%d')
resposta = self.api.get_paginated(f"{self.url}nfse/relatorio?from={data_inicial}&to={data_final}")
self._log(RealmType.OPERATIONAL, f"Relatório NFSe emitidas: {data_inicial} a {data_final}")
return resposta
except Exception as e:
msg_erro = f"Erro ao consultar relatório NFSe emitidas: {e}"
self._log(RealmType.ERRORS, msg_erro)
return {"error": msg_erro}
def nfse_por_cnpj(self, cnpj, data_inicial=None, data_final=None, data: dict = None):
try:
if not data_inicial:
data_inicial = (datetime.now() - timedelta(days=30)).strftime('%Y-%m-%d')
if not data_final:
data_final = datetime.now().strftime('%Y-%m-%d')
resposta = self.api.get_paginated(f"{self.url}nfse/{cnpj}/relatorio?from={data_inicial}&to={data_final}")
self._log(RealmType.OPERATIONAL, f"Relatório NFSe por CNPJ: {cnpj}")
return resposta
except Exception as e:
msg_erro = f"Erro ao consultar relatório NFSe por CNPJ: {e}"
self._log(RealmType.ERRORS, msg_erro)
return {"error": msg_erro}

View file

@ -0,0 +1,165 @@
from utils.api import Api
from utils.banco import Banco, CertificadoDigitalTable
from datetime import date
from utils.Exchange2 import LoggerExchange2
from utils.exchangeModels import RealmType
import json
import base64
class CertificadoDigital:
# Serviço que gerencia o upload e atualização de certificados digitais A1/A3 na plataforma da PlugNotas
def __init__(self, url_base, banco: Banco, api_key: str, logger: LoggerExchange2 = None):
self.api = Api(api_key=api_key)
self.banco = banco
self.url = f"{url_base}/certificado"
self.logger = logger
def post_form_data(self, data):
# Realiza o envio multipart/form-data do arquivo físico do certificado .pfx para a API e o salva no banco
try:
files = {
'arquivo': (
'certificado.pfx',
base64.b64decode(data['arquivo']) if isinstance(data['arquivo'], str) else data['arquivo'],
'application/x-pkcs12'
),
'senha': (None, data['senha'])
}
if 'email' in data:
files['email'] = (None, data['email'])
response = self.api.post_form_data(self.url, files)
id_tecnospeed = response.get('data', {}).get('id')
id_usr = data.get('id_usuario', 0)
id_emp = data.get('id_empresa', "0")
if not id_tecnospeed:
if self.logger: self.logger.logIC2(RealmType.OPERATIONAL, ('INSERT', 'certificado_digital', id_usr, '0', 'ERRO', 'ID do certificado não encontrado na resposta da API', json.dumps(response)))
raise ValueError("Erro ao obter ID do certificado na resposta da API.")
expiration_value = data.get('expirationAt') or data.get('data_expiracao')
certificado = CertificadoDigitalTable(
id_tecnospeed=id_tecnospeed,
certificado=data['arquivo'],
expirationAt=date.fromisoformat(expiration_value) if expiration_value else date.today(),
senha=data['senha'],
status=True,
id_empresa=id_emp,
id_usuario=id_usr,
email=data.get('email')
)
self.banco.add(certificado)
if self.logger: self.logger.logIC2(RealmType.OPERATIONAL, ('INSERT', 'certificado_digital', id_usr, id_tecnospeed, 'SUCESSO', 'Certificado digital cadastrado com sucesso', json.dumps(response)))
return id_tecnospeed
except Exception as e:
if self.logger: self.logger.logIC2(RealmType.OPERATIONAL, ('INSERT', 'certificado_digital', data.get('id_usuario', 0), '0', 'ERRO', f"Erro ao adicionar certificado: {e}", "{}"))
raise
def contar_vencidos(self, lista_certificados):
# Auxiliar para contar certificados ativos e vencidos na listagem
if isinstance(lista_certificados, dict) and "data" in lista_certificados:
lista = lista_certificados["data"]
elif isinstance(lista_certificados, list):
lista = lista_certificados
else:
return {"certificados_ativos": 0, "certificados_vencidos": 0}
ativos = 0
vencidos = 0
hoje = date.today()
for cert in lista:
if not isinstance(cert, dict): continue
# Tenta encontrar o campo de expiração (vencimento ou outros nomes conhecidos)
exp_str = cert.get("vencimento") or cert.get("expirationAt") or cert.get("dataVencimento") or cert.get("data_expiracao")
if exp_str:
try:
# Tenta converter a string de data:
# 1. Formato DD/MM/YYYY (do snipped do usuário)
# 2. Formato ISO YYYY-MM-DD
if "/" in exp_str:
partes = exp_str.split(" ")[0].split("/")
if len(partes) == 3:
data_exp = date(int(partes[2]), int(partes[1]), int(partes[0]))
else:
data_exp = date.fromisoformat(exp_str[:10])
if data_exp >= hoje:
ativos += 1
else:
vencidos += 1
except:
continue
return {
"certificados_ativos": ativos,
"certificados_vencidos": vencidos
}
def get(self, id=None):
# Consulta a situação de um certificado digital específico pelo ID ou lista todos vinculados
try:
if id:
resultado = self.api.get(f"{self.url}/{id}")
if self.logger: self.logger.logIC2(RealmType.OPERATIONAL, ('SELECT', 'certificado_digital', id, id, 'SUCESSO', f"Certificado consultado - ID: {id}", json.dumps(resultado)))
return resultado
else:
resposta = self.api.get(self.url)
# Calcula totais
totais = self.contar_vencidos(resposta)
# Organiza a resposta unificada
resultado = {
"certificados": resposta if isinstance(resposta, list) else resposta.get("data", []),
"totais": totais
}
if self.logger: self.logger.logIC2(RealmType.OPERATIONAL, ('SELECT', 'certificado_digital', 'GERAL', '0', 'SUCESSO', 'Listagem de certificados digitais', json.dumps(resultado)))
return resultado
except Exception as e:
if self.logger: self.logger.logIC2(RealmType.OPERATIONAL, ('SELECT', 'certificado_digital', id or 'GERAL', '0', 'ERRO', f"Erro ao buscar certificado: {e}", "{}"))
raise
def put_certificado(self, id, data):
# Atualiza o arquivo físico ou a senha de um certificado já existente
try:
certificado = self.banco.session.query(CertificadoDigitalTable).filter_by(id_tecnospeed=id).first()
id_usr = data.get('id_usuario', 0)
if not certificado:
if self.logger: self.logger.logIC2(RealmType.OPERATIONAL, ('UPDATE', 'certificado_digital', id_usr, id, 'ERRO', f"Certificado com ID {id} não encontrado", "{}"))
raise ValueError(f"Certificado com ID {id} não encontrado no banco.")
files = {
'arquivo': (
'certificado.pfx',
base64.b64decode(data['arquivo']) if isinstance(data['arquivo'], str) else data['arquivo'],
'application/x-pkcs12'
),
'senha': (None, data['senha'])
}
if 'email' in data:
files['email'] = (None, data['email'])
self.api.put_form_data(f"{self.url}/{id}", files)
certificado.certificado = data['arquivo']
certificado.senha = data['senha']
expiration_value = data.get('expirationAt') or data.get('data_expiracao')
if expiration_value:
certificado.expirationAt = date.fromisoformat(expiration_value)
certificado.updatedAt = date.today()
self.banco.update(certificado)
if self.logger: self.logger.logIC2(RealmType.OPERATIONAL, ('UPDATE', 'certificado_digital', id_usr, id, 'SUCESSO', 'Certificado digital atualizado com sucesso', json.dumps({"id": id})))
return id
except Exception as e:
if self.logger: self.logger.logIC2(RealmType.OPERATIONAL, ('UPDATE', 'certificado_digital', data.get('id_usuario', 0), id, 'ERRO', f"Erro ao atualizar certificado: {e}", "{}"))
raise

View file

@ -0,0 +1,184 @@
from Models.empresa import Empresa
from utils.api import Api
from utils.banco import Banco, EnterpriseTable, LogoTable
from utils.Exchange2 import LoggerExchange2
from utils.exchangeModels import RealmType
from sqlalchemy.exc import SQLAlchemyError
import json
import string
import random
import time
def generate_cuid():
"""Gera um ID no formato aproximado de CUID (c + timestamp + random)."""
ts = hex(int(time.time() * 1000))[2:]
rand = ''.join(random.choices(string.ascii_lowercase + string.digits, k=12))
return f"c{ts}{rand}"[:25]
# Gerenciador de contas de Empresas emissoras de notas e seus respectivos certificados e logos
class EmpresaController:
def __init__(self, url_base, api_key: str, banco: Banco, logger: LoggerExchange2 = None):
self.api = Api(api_key=api_key)
self.banco = banco
self.url = f"{url_base}/empresa"
self.logger = logger
def _log(self, realm: RealmType, msg: str):
if self.logger:
self.logger.logIC2(realm, (msg,))
def post(self, empresa: Empresa, data: dict):
try:
dict_api = empresa.model_dump(exclude={'id', 'id_empresa', 'banco', 'email'}, exclude_none=True)
# Injeção das configurações fixas e obrigatórias para a PlugNotas
if dict_api.get("nfe") and dict_api["nfe"].get("ativo"):
nfe_config_front = dict_api["nfe"].get("config") or {}
dict_api["nfe"]["config"] = {
"producao": False,
"email": {"envio": False},
"dfe": {"primeiroNsu": False},
"impressaoFcp": False,
"impressaoPartilha": False,
**nfe_config_front
}
dict_api["nfe"]["tipoContrato"] = 0
if dict_api.get("nfse") and dict_api["nfse"].get("ativo"):
nfse_config_front = dict_api["nfse"].get("config") or {}
dict_api["nfse"]["config"] = {
"producao": False,
"email": {"envio": False},
"rps": {"lote": 1, "numeracao": [{"numero": 1, "serie": "1"}]},
"prefeitura": {"login": "12345", "senha": "123"},
**nfse_config_front
}
dict_api["nfse"]["tipoContrato"] = 0
print("===========================")
print("ENVIANDO PARA A API:", json.dumps(dict_api, indent=2))
print("===========================")
retorno = self.api.post(data=dict_api, url=self.url)
if 'error' in retorno:
err_data = retorno.get('response', {}).get('error', {})
msg = err_data.get('message', 'Erro desconhecido')
detalhes = err_data.get('data', [])
if detalhes:
msg += f" Detalhes: {detalhes}"
print(f"\n[ERRO API PLUGNOTAS] {msg}\n", flush=True)
self._log(RealmType.ERRORS, f"Erro na API ao cadastrar empresa: {msg}")
raise Exception(msg)
# Gerar ID CUID se não vier no objeto ou se for nulo
empresa_id = empresa.id if empresa.id else generate_cuid()
empresa_nova = EnterpriseTable(
id=empresa_id,
cnpj=empresa.cpfCnpj,
razao_social=empresa.razaoSocial,
nome_fantasia=empresa.nomeFantasia if empresa.nomeFantasia else empresa.razaoSocial,
banco=empresa.banco,
email=empresa.email,
status=True
)
self.banco.add(empresa_nova)
self._log(RealmType.OPERATIONAL, f"Empresa cadastrada com sucesso - CNPJ: {empresa.cpfCnpj} (ID: {empresa_id})")
if isinstance(retorno, dict) and 'data' in retorno:
retorno['data']['id'] = empresa_nova.id
retorno['data']['id_plugnotas'] = retorno['data'].get('_id')
retorno['data']['nome_fantasia'] = empresa_nova.nome_fantasia
retorno['data']['razao_social'] = empresa_nova.razao_social
retorno['data']['status'] = empresa_nova.status
retorno['data'].pop('razaoSocial', None)
retorno['data'].pop('nomeFantasia', None)
retorno['data'].pop('id_empresa', None)
return retorno
except SQLAlchemyError as e:
self.banco.rollback()
self._log(RealmType.ERRORS, f"Erro no banco ao cadastrar empresa: {e}")
return {"error": f"Erro ao cadastrar empresa: {str(e)}"}
except Exception as e:
return {"error": f"Erro inesperado: {str(e)}"}
def get(self, cnpj: str = None, data: dict = None):
try:
if cnpj:
resposta = self.api.get(f"{self.url}/{cnpj}")
if isinstance(resposta, dict) and "error" not in resposta:
empresa_local = self.banco.session.query(EnterpriseTable).filter_by(cnpj=cnpj).first()
if empresa_local:
resposta["id"] = empresa_local.id
resposta["nome_fantasia"] = empresa_local.nome_fantasia
resposta["razao_social"] = empresa_local.razao_social
resposta["status"] = empresa_local.status
resposta.pop("razaoSocial", None)
resposta.pop("nomeFantasia", None)
if "_id" in resposta:
resposta["id_plugnotas"] = resposta["_id"]
else:
resposta = self.api.get_paginated(self.url)
# Injetar o ID do banco local em cada empresa listada
if isinstance(resposta, dict) and "data" in resposta:
for emp in resposta["data"]:
empresa_local = self.banco.session.query(EnterpriseTable).filter_by(cnpj=emp.get("cpfCnpj")).first()
if empresa_local:
emp["id"] = empresa_local.id
emp["nome_fantasia"] = empresa_local.nome_fantasia
emp["razao_social"] = empresa_local.razao_social
emp["status"] = empresa_local.status
emp.pop("razaoSocial", None)
emp.pop("nomeFantasia", None)
if "_id" in emp:
emp["id_plugnotas"] = emp["_id"]
self._log(RealmType.OPERATIONAL, f"Empresa consultada - CNPJ: {cnpj or 'TODAS'}")
return resposta
except Exception as e:
self._log(RealmType.ERRORS, f"Erro ao buscar empresa: {e}")
return {"error": f"Erro ao buscar empresa: {str(e)}"}
def post_file(self, file, cnpj, data=None):
try:
empresa = self.banco.session.query(EnterpriseTable).filter_by(cnpj=cnpj).first()
if not empresa:
self._log(RealmType.ERRORS, f"Empresa não encontrada para upload de logo - CNPJ: {cnpj}")
return {"error": "Empresa não encontrada"}
logo = LogoTable(
id_empresa=empresa.id,
logo=file,
status=True
)
self.banco.add(logo)
files = {'arquivo': ('logo.png', file, 'image/png')}
resposta = self.api.post_file(file=files, url=f"{self.url}/{cnpj}/logotipo")
self._log(RealmType.OPERATIONAL, f"Logo enviada com sucesso - CNPJ: {cnpj}")
return resposta
except SQLAlchemyError as e:
self.banco.rollback()
self._log(RealmType.ERRORS, f"Erro no banco ao salvar logo: {e}")
return {"error": f"Erro ao salvar logo: {str(e)}"}
except Exception as e:
self._log(RealmType.ERRORS, f"Erro inesperado ao enviar logo: {e}")
return {"error": f"Erro inesperado: {str(e)}"}
def patch_certificado(self, cnpj: str, certificado: str, data: dict):
try:
empresa = self.banco.session.query(EnterpriseTable).filter_by(cnpj=cnpj).first()
if not empresa:
self._log(RealmType.ERRORS, f"Empresa não encontrada para atualizar certificado - CNPJ: {cnpj}")
return {"error": "Empresa não encontrada"}
resposta = self.api.patch(url=f"{self.url}/{cnpj}/certificado", data={"certificado": certificado})
self._log(RealmType.OPERATIONAL, f"Certificado da empresa atualizado - CNPJ: {cnpj}")
return resposta
except Exception as e:
self._log(RealmType.ERRORS, f"Erro ao atualizar certificado: {e}")
return {"error": f"Erro ao atualizar certificado: {str(e)}"}

View file

@ -0,0 +1,21 @@
import sys
import os
import uvicorn
sys.path.append(os.path.dirname(os.path.abspath(__file__)))
from listenerEndpoint import app
# O EndpointListener tem a função de manter a aplicação servindo de webhooks ativa recebendo HTTP e convertendo para RabbitMQ
class EndpointListener:
def __init__(self):
self.host = "0.0.0.0"
self.port = int(os.getenv("WEBHOOK_PORT", 8000))
def run(self):
print(f"[STATUS] Endpoint Listener rodando na porta {self.port}...")
uvicorn.run(app, host=self.host, port=self.port)
if __name__ == "__main__":
main = EndpointListener()
main.run()

View file

@ -0,0 +1,170 @@
import sys
import os
import json
from contextlib import asynccontextmanager
from fastapi import FastAPI, Request
from fastapi.responses import JSONResponse
import uvicorn
sys.path.append(os.path.dirname(os.path.abspath(__file__)))
from Models.config import SystemConfig
from utils.redis import RedisClient
from utils.Exchange2 import RPCExchange2, QueueExchange2, LoggerExchange2, AppIC2Builder
from utils.exchangeModels import RealmType
# Campos obrigatórios que o PlugNotas envia no Endpoint
CAMPOS_OBRIGATORIOS = ["id", "status", "documento"]
# Status válidos que o PlugNotas envia
STATUS_VALIDOS = ["CONCLUIDO", "REJEITADO", "CANCELADO", "PROCESSANDO"]
# Tipos de documento/entidades aceitos
DOCUMENTOS_VALIDOS = ["nfe", "nfse", "certificado_digital"]
# Variáveis globais
redis_client = None
system_config = None
exchange = None
logger = None
@asynccontextmanager
async def ciclo_app(app: FastAPI):
# Gerencia o ciclo de vida da aplicação, inicializando conexões com Redis e RabbitMQ (Exchange) na inicialização
global redis_client, system_config, exchange
try:
redis_client = RedisClient(host=os.getenv('REDIS_HOST', 'localhost'), port=6379)
system_config_data = redis_client.get("system_config")
if not system_config_data:
print("Erro: system_config não encontrado no Redis.")
else:
system_config = SystemConfig(**json.loads(system_config_data))
builder = AppIC2Builder(service_name="tecnospeed", versionstr="2.0.1", origin=system_config.origin)
global logger
logger = LoggerExchange2(ic2_builder=builder, amqps=system_config.amqps, system="integracao", service="tecnospeed", version="2.0.1", origin=system_config.origin)
logger.logIC2(RealmType.LOGS, (f"[{system_config.origin}] Configuração lida com sucesso pelo Listener Endpoint.",))
exchange = QueueExchange2(
amqps=system_config.amqps,
origin=system_config.origin,
system="integracao",
service="endpoint_listener",
version="2.0.1",
client="#"
)
servicekey = system_config.servicekey.ljust(32, '0')[:32]
exchange.setCrypto(serviceid=system_config.serviceid, servicekey=servicekey)
logger.logIC2(RealmType.LOGS, ("Exchange inicializado com sucesso para o Listener Endpoint.",))
except Exception as e:
if logger:
logger.logIC2(RealmType.ERRORS, (f"Erro ao inicializar o Listener Endpoint: {e}",))
else:
print(f"Erro ao inicializar o Listener Endpoint: {e}")
yield # Aplicação rodando
if logger:
logger.logIC2(RealmType.LOGS, ("Listener Endpoint encerrando...",))
logger.close()
app = FastAPI(
title="Listener Endpoint PlugNotas",
description="Endpoint para receber notificações Endpoint do PlugNotas",
version="2.0.1",
lifespan=ciclo_app
)
@app.get("/health")
async def healthy():
# Verifica a saúde da aplicação, validando se as dependências (Redis e Exchange) estão conectadas
status = {
"status": "ok",
"redis": redis_client is not None,
"exchange": exchange is not None,
"system_config": system_config is not None
}
if not exchange:
return JSONResponse(status_code=503, content={**status, "status": "degraded", "message": "Exchange não inicializado"})
return status
@app.post("/listenerPlugnotas")
@app.post("/listenerPlugnotas/{cnpj}")
async def receber_Endpoint_plugnotas(request: Request, cnpj: str = None):
# Rota principal que recebe a notificação da PlugNotas e repassa para processamento no RabbitMQ
try:
try:
payload = await request.json()
except Exception as e:
logger.logIC2(RealmType.ERRORS, (f"Payload inválido recebido (não é JSON válido): {e}",))
return JSONResponse(
status_code=400,
content={"status": "error", "message": "Payload não é um JSON válido"}
)
documento = payload.get("documento", "desconhecido")
status_nota = payload.get("status", "desconhecido")
id_nota = payload.get("id", "")
id_integracao = payload.get("idIntegracao", "")
emitente = payload.get("emitente", cnpj or "")
logger.logIC2(RealmType.LOGS, (
f"Endpoint recebido - Documento: {documento} | Status: {status_nota} | "
f"ID: {id_nota} | IDIntegracao: {id_integracao} | Emitente: {emitente}",
))
campos_faltantes = [campo for campo in CAMPOS_OBRIGATORIOS if campo not in payload]
if campos_faltantes:
logger.logIC2(RealmType.ERRORS, (
f"Endpoint com campos obrigatórios faltantes: {campos_faltantes} | "
f"Payload recebido: {json.dumps(payload)}",
))
return {"status": "warning", "message": f"Campos faltantes: {campos_faltantes}"}
if documento not in DOCUMENTOS_VALIDOS:
logger.logIC2(RealmType.ERRORS, (
f"Tipo de documento desconhecido: {documento} | Payload: {json.dumps(payload)}",
))
if status_nota not in STATUS_VALIDOS:
logger.logIC2(RealmType.ERRORS, (
f"Status desconhecido: {status_nota} | Documento: {documento} | ID: {id_nota}",
))
if not exchange:
logger.logIC2(RealmType.ERRORS, ("Exchange não inicializado no momento da chegada do Endpoint.",))
return JSONResponse(
status_code=500,
content={"status": "error", "message": "Serviço temporariamente indisponível"}
)
# Envia o EXATO objeto dict recebido do PlugNotas via Queue (assíncrono)
try:
exchange.sendMsg(json.dumps(payload), service="controller")
logger.logIC2(RealmType.LOGS, (
f"Endpoint enviado à Fila (Queue) Controller. "
f"Documento: {documento} | Status: {status_nota} | ID: {id_nota}",
))
return {"status": "ok", "message": "Enviado à Fila com sucesso"}
except Exception as e:
logger.logIC2(RealmType.ERRORS, (f"Erro ao postar mensagem na Fila (Controller): {e}",))
return JSONResponse(
status_code=500,
content={"status": "error", "message": "Falha ao enviar mensagem para processamento"}
)
except Exception as e:
logger.logIC2(RealmType.ERRORS, (f"Erro inesperado no Listener Endpoint: {e}",))
return JSONResponse(
status_code=500,
content={"status": "error", "message": "Erro interno no processamento do Endpoint"}
)

View file

@ -0,0 +1,24 @@
2026-04-01 09:09:33,082 - integracao - INFO - { 'caller': 'ciclo_app',
'client': '',
'logDate': '2026-04-01 09:09:33.082579-03:00',
'msg': '[local] Configuração lida com sucesso pelo Listener Webhook.',
'origin': '',
'service': 'tecnospeed',
'system': 'integracao',
'version': '2.0.1'}
2026-04-01 09:09:33,700 - integracao - INFO - { 'caller': 'ciclo_app',
'client': '',
'logDate': '2026-04-01 09:09:33.700242-03:00',
'msg': 'Exchange inicializado com sucesso para o Listener Webhook.',
'origin': '',
'service': 'tecnospeed',
'system': 'integracao',
'version': '2.0.1'}
2026-04-01 09:14:24,402 - integracao - INFO - { 'caller': 'ciclo_app',
'client': '',
'logDate': '2026-04-01 09:14:24.402193-03:00',
'msg': 'Listener Webhook encerrando...',
'origin': '',
'service': 'tecnospeed',
'system': 'integracao',
'version': '2.0.1'}

View file

@ -0,0 +1,194 @@
2026-04-02 09:14:16,362 - integracao - INFO - { 'caller': 'ciclo_app',
'client': '',
'logDate': '2026-04-02 09:14:16.362304-03:00',
'msg': '[local] Configuração lida com sucesso pelo Listener Webhook.',
'origin': '',
'service': 'tecnospeed',
'system': 'integracao',
'version': '2.0.1'}
2026-04-02 09:14:16,932 - integracao - INFO - { 'caller': 'ciclo_app',
'client': '',
'logDate': '2026-04-02 09:14:16.932459-03:00',
'msg': 'Exchange inicializado com sucesso para o Listener Webhook.',
'origin': '',
'service': 'tecnospeed',
'system': 'integracao',
'version': '2.0.1'}
2026-04-02 14:14:30,014 - integracao - INFO - { 'caller': 'ciclo_app',
'client': '',
'logDate': '2026-04-02 14:14:30.013914-03:00',
'msg': '[local] Configuração lida com sucesso pelo Listener Webhook.',
'origin': '',
'service': 'tecnospeed',
'system': 'integracao',
'version': '2.0.1'}
2026-04-02 14:14:31,040 - integracao - INFO - { 'caller': 'ciclo_app',
'client': '',
'logDate': '2026-04-02 14:14:31.039753-03:00',
'msg': 'Exchange inicializado com sucesso para o Listener Webhook.',
'origin': '',
'service': 'tecnospeed',
'system': 'integracao',
'version': '2.0.1'}
2026-04-02 14:22:20,024 - integracao - INFO - { 'caller': 'receber_webhook_plugnotas',
'client': '',
'logDate': '2026-04-02 14:22:20.024167-03:00',
'msg': 'Webhook recebido - Documento: nfe | Status: CONCLUIDO | ID: '
'69cab453... | IDIntegracao: | Emitente:',
'origin': '',
'service': 'tecnospeed',
'system': 'integracao',
'version': '2.0.1'}
2026-04-02 14:22:20,263 - integracao - INFO - { 'caller': 'receber_webhook_plugnotas',
'client': '',
'logDate': '2026-04-02 14:22:20.263204-03:00',
'msg': 'Webhook repassado ao worker com sucesso - Documento: nfe | Status: '
'CONCLUIDO | ID: 69cab453...',
'origin': '',
'service': 'tecnospeed',
'system': 'integracao',
'version': '2.0.1'}
2026-04-02 14:23:45,608 - integracao - INFO - { 'caller': 'receber_webhook_plugnotas',
'client': '',
'logDate': '2026-04-02 14:23:45.608300-03:00',
'msg': 'Webhook recebido - Documento: nfe | Status: CONCLUIDO | ID: '
'69cab45329f21a7de7691961 | IDIntegracao: XXXYY99999_TESTE445 | '
'Emitente: 08187168000160',
'origin': '',
'service': 'tecnospeed',
'system': 'integracao',
'version': '2.0.1'}
2026-04-02 14:23:46,107 - integracao - INFO - { 'caller': 'receber_webhook_plugnotas',
'client': '',
'logDate': '2026-04-02 14:23:46.107170-03:00',
'msg': 'Webhook repassado ao worker com sucesso - Documento: nfe | Status: '
'CONCLUIDO | ID: 69cab45329f21a7de7691961',
'origin': '',
'service': 'tecnospeed',
'system': 'integracao',
'version': '2.0.1'}
2026-04-02 14:26:07,402 - integracao - INFO - { 'caller': 'ciclo_app',
'client': '',
'logDate': '2026-04-02 14:26:07.401683-03:00',
'msg': 'Listener Webhook encerrando...',
'origin': '',
'service': 'tecnospeed',
'system': 'integracao',
'version': '2.0.1'}
2026-04-02 14:37:36,191 - integracao - INFO - { 'caller': 'ciclo_app',
'client': '',
'logDate': '2026-04-02 14:37:36.190896-03:00',
'msg': '[local] Configuração lida com sucesso pelo Listener Webhook.',
'origin': '',
'service': 'tecnospeed',
'system': 'integracao',
'version': '2.0.1'}
2026-04-02 14:37:36,969 - integracao - INFO - { 'caller': 'ciclo_app',
'client': '',
'logDate': '2026-04-02 14:37:36.969622-03:00',
'msg': 'Exchange inicializado com sucesso para o Listener Webhook.',
'origin': '',
'service': 'tecnospeed',
'system': 'integracao',
'version': '2.0.1'}
2026-04-02 14:37:37,459 - integracao - INFO - { 'caller': 'ciclo_app',
'client': '',
'logDate': '2026-04-02 14:37:37.459047-03:00',
'msg': 'Listener Webhook encerrando...',
'origin': '',
'service': 'tecnospeed',
'system': 'integracao',
'version': '2.0.1'}
2026-04-02 14:37:42,582 - integracao - INFO - { 'caller': 'ciclo_app',
'client': '',
'logDate': '2026-04-02 14:37:42.582792-03:00',
'msg': '[local] Configuração lida com sucesso pelo Listener Webhook.',
'origin': '',
'service': 'tecnospeed',
'system': 'integracao',
'version': '2.0.1'}
2026-04-02 14:37:43,391 - integracao - INFO - { 'caller': 'ciclo_app',
'client': '',
'logDate': '2026-04-02 14:37:43.391071-03:00',
'msg': 'Exchange inicializado com sucesso para o Listener Webhook.',
'origin': '',
'service': 'tecnospeed',
'system': 'integracao',
'version': '2.0.1'}
2026-04-02 14:37:43,782 - integracao - INFO - { 'caller': 'ciclo_app',
'client': '',
'logDate': '2026-04-02 14:37:43.781995-03:00',
'msg': 'Listener Webhook encerrando...',
'origin': '',
'service': 'tecnospeed',
'system': 'integracao',
'version': '2.0.1'}
2026-04-02 16:03:48,010 - integracao - INFO - { 'caller': 'run',
'client': '',
'logDate': '2026-04-02 16:03:48.009993-03:00',
'msg': 'Conectando ao RabbitMQ',
'origin': '',
'service': 'tecnospeed',
'system': 'integracao',
'version': '2.0.1'}
2026-04-02 16:03:48,217 - integracao - INFO - { 'caller': 'run',
'client': '',
'logDate': '2026-04-02 16:03:48.216535-03:00',
'msg': "Configuração do sistema carregada - serviceid='local' "
"servicekey='local' origin='local' "
"amqps='amqp://guest:guest@localhost:5672/'",
'origin': '',
'service': 'tecnospeed',
'system': 'integracao',
'version': '2.0.1'}
2026-04-02 16:12:17,812 - integracao - INFO - { 'caller': 'run',
'client': '',
'logDate': '2026-04-02 16:12:17.812369-03:00',
'msg': 'Conectando ao RabbitMQ',
'origin': '',
'service': 'tecnospeed',
'system': 'integracao',
'version': '2.0.1'}
2026-04-02 16:12:18,488 - integracao - INFO - { 'caller': 'run',
'client': '',
'logDate': '2026-04-02 16:12:18.487913-03:00',
'msg': "Configuração do sistema carregada - serviceid='local' "
"servicekey='local' origin='local' "
"amqps='amqp://guest:guest@localhost:5672/'",
'origin': '',
'service': 'tecnospeed',
'system': 'integracao',
'version': '2.0.1'}
2026-04-02 16:12:18,864 - integracao - INFO - { 'caller': 'run',
'client': '',
'logDate': '2026-04-02 16:12:18.863889-03:00',
'msg': 'Erro fatal ao conectar ao RabbitMQ - VERIFY PARAMETERS: '
'is_amqp_url: True\n'
' amqps[amqp://guest:guest@localhost:5672/]\n'
' origin[local]\n'
' system[integracao]\n'
' service[tecnospeed]\n'
' version[2.0.1\n'
' client[]',
'origin': '',
'service': 'tecnospeed',
'system': 'integracao',
'version': '2.0.1'}
2026-04-02 16:12:30,426 - integracao - INFO - { 'caller': 'run',
'client': '',
'logDate': '2026-04-02 16:12:30.426681-03:00',
'msg': 'Conectando ao RabbitMQ',
'origin': '',
'service': 'tecnospeed',
'system': 'integracao',
'version': '2.0.1'}
2026-04-02 16:12:30,947 - integracao - INFO - { 'caller': 'run',
'client': '',
'logDate': '2026-04-02 16:12:30.947199-03:00',
'msg': "Configuração do sistema carregada - serviceid='local' "
"servicekey='local' origin='local' "
"amqps='amqp://guest:guest@localhost:5672/'",
'origin': '',
'service': 'tecnospeed',
'system': 'integracao',
'version': '2.0.1'}

View file

@ -0,0 +1,56 @@
2026-04-06 08:25:17,847 - teste_wrapper.integracao - INFO - params: <URLParameters host=jackal.rmq.cloudamqp.com port=5671 virtual_host=iawkzmhh ssl=True> amqps: amqps://iawkzmhh:4iIVXCgLDsLy2oVyVWeV-o6shg-BZ89z@jackal.rmq.cloudamqp.com/iawkzmhh
2026-04-06 08:25:18,327 - teste_wrapper.integracao - INFO - Logger Messages topics: logs.teste_wrapper.integracao.
2026-04-06 08:25:18,361 - teste_wrapper.integracao - INFO - {'caller': 'IC2',
'client': '',
'hmac': '862c786853960277c4d518d1eb01143b2fe7782092dbf5403fb61b52127c7829',
'logDate': '2026-04-06 08:25:18.361486-03:00',
'msg': "LOGIC2 LOGS [('Testando o novo fo ...]",
'origin': '',
'payload': '44102feb05c76aac7b2b',
'service': 'teste_wrapper',
'serviceid': 'AF1699F719F1B6C81DE2BD38AB811994E3089B35C8E9314813A770AC7AE25D8D',
'system': 'integracao',
'target': '',
'type': 'logger',
'version': ''}
2026-04-06 08:25:18,368 - teste_wrapper.integracao - INFO - {'caller': 'logMsg',
'client': '',
'hmac': '',
'logDate': '2026-04-06 08:25:18.367868-03:00',
'msg': 'Testando o novo formato IC2',
'origin': '',
'payload': '',
'service': 'teste_wrapper',
'serviceid': 'AF1699F719F1B6C81DE2BD38AB811994E3089B35C8E9314813A770AC7AE25D8D',
'system': 'integracao',
'target': '',
'type': 'logger',
'version': ''}
2026-04-06 08:28:37,324 - teste_wrapper.integracao - INFO - params: <URLParameters host=jackal.rmq.cloudamqp.com port=5671 virtual_host=iawkzmhh ssl=True> amqps: amqps://iawkzmhh:4iIVXCgLDsLy2oVyVWeV-o6shg-BZ89z@jackal.rmq.cloudamqp.com/iawkzmhh
2026-04-06 08:28:37,834 - teste_wrapper.integracao - INFO - Logger Messages topics: logs.teste_wrapper.integracao.
2026-04-06 08:28:37,855 - teste_wrapper.integracao - INFO - {'caller': 'IC2',
'client': '',
'hmac': '841681411efce4ae99ff2602ae79da6284e6c3ca247a27da3a87dedf3771244e',
'logDate': '2026-04-06 08:28:37.855637-03:00',
'msg': "LOGIC2 LOGS [('Testando o formato ...]",
'origin': '',
'payload': '353fd214129406d78301',
'service': 'teste_wrapper',
'serviceid': 'AF1699F719F1B6C81DE2BD38AB811994E3089B35C8E9314813A770AC7AE25D8D',
'system': 'integracao',
'target': '',
'type': 'logger',
'version': ''}
2026-04-06 08:28:37,865 - teste_wrapper.integracao - INFO - {'caller': 'logMsg',
'client': '',
'hmac': '',
'logDate': '2026-04-06 08:28:37.865799-03:00',
'msg': 'Testando o formato IC2',
'origin': '',
'payload': '',
'service': 'teste_wrapper',
'serviceid': 'AF1699F719F1B6C81DE2BD38AB811994E3089B35C8E9314813A770AC7AE25D8D',
'system': 'integracao',
'target': '',
'type': 'logger',
'version': ''}

View file

View file

View file

@ -0,0 +1,140 @@
2026-03-27 14:57:57,807 - integracao - INFO - { 'caller': 'run',
'client': '',
'logDate': '2026-03-27 14:57:57.807604-03:00',
'msg': 'Conectando ao RabbitMQ',
'origin': '',
'service': 'tecnospeed',
'system': 'integracao',
'version': '2.0.1'}
2026-03-27 14:57:58,282 - integracao - INFO - { 'caller': 'run',
'client': '',
'logDate': '2026-03-27 14:57:58.282491-03:00',
'msg': "Configuração do sistema carregada - serviceid='local' "
"servicekey='local' origin='local' "
"amqps='amqp://guest:guest@localhost:5672/'",
'origin': '',
'service': 'tecnospeed',
'system': 'integracao',
'version': '2.0.1'}
2026-03-27 14:59:07,273 - integracao - INFO - { 'caller': 'callback',
'client': 'Test',
'logDate': '2026-03-27 14:59:07.273291-03:00',
'msg': "Configuração recebida - {'api_key': None, 'x_api_key': "
"'2da392a6-79d2-4304-a8b7-959572c7e44d', 'user_postgres': "
"'postgres', 'pass_postgres': 'postgres', 'host_postgres': "
"'localhost', 'port_postgres': '5432', 'database_postgres': "
"'tecnospeed'}",
'origin': '',
'service': 'tecnospeed',
'system': 'integracao',
'version': '2.0.1'}
2026-03-27 14:59:08,015 - integracao - INFO - { 'caller': 'callback',
'client': 'Test',
'logDate': '2026-03-27 14:59:08.014993-03:00',
'msg': 'Configuração incompleta',
'origin': '',
'service': 'tecnospeed',
'system': 'integracao',
'version': '2.0.1'}
2026-03-27 15:00:56,891 - integracao - INFO - { 'caller': 'callback',
'client': 'Test',
'logDate': '2026-03-27 15:00:56.890566-03:00',
'msg': "Configuração recebida - {'api_key': None, 'x_api_key': "
"'2da392a6-79d2-4304-a8b7-959572c7e44d', 'user_postgres': "
"'postgres', 'pass_postgres': 'postgres', 'host_postgres': "
"'localhost', 'port_postgres': '5432', 'database_postgres': "
"'sg_servico_tecnospeed'}",
'origin': '',
'service': 'tecnospeed',
'system': 'integracao',
'version': '2.0.1'}
2026-03-27 15:00:57,787 - integracao - INFO - { 'caller': 'callback',
'client': 'Test',
'logDate': '2026-03-27 15:00:57.787472-03:00',
'msg': 'Configuração incompleta',
'origin': '',
'service': 'tecnospeed',
'system': 'integracao',
'version': '2.0.1'}
2026-03-27 15:02:14,102 - integracao - INFO - { 'caller': 'callback',
'client': 'Test',
'logDate': '2026-03-27 15:02:14.101859-03:00',
'msg': "Configuração recebida - {'api_key': None, 'x_api_key': "
"'2da392a6-79d2-4304-a8b7-959572c7e44d', 'user_postgres': "
"'postgres', 'pass_postgres': 'postgres', 'host_postgres': "
"'localhost', 'port_postgres': '5432', 'database_postgres': "
"'tecnospeed'}",
'origin': '',
'service': 'tecnospeed',
'system': 'integracao',
'version': '2.0.1'}
2026-03-27 15:02:14,792 - integracao - INFO - { 'caller': 'callback',
'client': 'Test',
'logDate': '2026-03-27 15:02:14.791942-03:00',
'msg': 'Configuração incompleta',
'origin': '',
'service': 'tecnospeed',
'system': 'integracao',
'version': '2.0.1'}
2026-03-27 15:09:33,329 - integracao - INFO - { 'caller': 'run',
'client': '',
'logDate': '2026-03-27 15:09:33.329870-03:00',
'msg': 'Conectando ao RabbitMQ',
'origin': '',
'service': 'tecnospeed',
'system': 'integracao',
'version': '2.0.1'}
2026-03-27 15:09:33,698 - integracao - INFO - { 'caller': 'run',
'client': '',
'logDate': '2026-03-27 15:09:33.698183-03:00',
'msg': "Configuração do sistema carregada - serviceid='local' "
"servicekey='local' origin='local' "
"amqps='amqp://guest:guest@localhost:5672/'",
'origin': '',
'service': 'tecnospeed',
'system': 'integracao',
'version': '2.0.1'}
2026-03-27 15:09:39,159 - integracao - INFO - { 'caller': 'callback',
'client': 'Test',
'logDate': '2026-03-27 15:09:39.158573-03:00',
'msg': "Configuração recebida - {'api_key': None, 'x_api_key': "
"'2da392a6-79d2-4304-a8b7-959572c7e44d', 'user_postgres': "
"'postgres', 'pass_postgres': 'postgres', 'host_postgres': "
"'localhost', 'port_postgres': '5432', 'database_postgres': "
"'tecnospeed'}",
'origin': '',
'service': 'tecnospeed',
'system': 'integracao',
'version': '2.0.1'}
2026-03-27 15:09:44,975 - integracao - INFO - { 'caller': 'callback',
'client': 'Test',
'logDate': '2026-03-27 15:09:44.975168-03:00',
'msg': 'Configuração completa para o cliente Test',
'origin': '',
'service': 'tecnospeed',
'system': 'integracao',
'version': '2.0.1'}
2026-03-27 15:09:45,763 - integracao - INFO - { 'caller': 'nfe',
'client': 'Test',
'logDate': '2026-03-27 15:09:45.763614-03:00',
'msg': 'Iniciando serviço de NFe - Método: GET',
'origin': '',
'service': 'tecnospeed',
'system': 'integracao',
'version': '2.0.1'}
2026-03-27 15:09:46,610 - integracao - INFO - { 'caller': 'nfe',
'client': 'Test',
'logDate': '2026-03-27 15:09:46.610671-03:00',
'msg': 'Consultando resumo de NFe',
'origin': '',
'service': 'tecnospeed',
'system': 'integracao',
'version': '2.0.1'}
2026-03-27 15:09:47,761 - integracao - INFO - { 'caller': 'nfe',
'client': 'Test',
'logDate': '2026-03-27 15:09:47.761417-03:00',
'msg': 'Operação realizada com sucesso',
'origin': '',
'service': 'tecnospeed',
'system': 'integracao',
'version': '2.0.1'}

View file

@ -0,0 +1 @@
# tests package

View file

@ -0,0 +1,119 @@
"""
Fixtures compartilhados para os testes E2E do servico_plugnotas.
Cria um banco SQLite in-memory, mocks do Logger e da Api,
e uma instância pré-configurada da classe Main para cada teste.
"""
import pytest
import json
import sys
from unittest.mock import MagicMock, patch
mock_pika = MagicMock()
sys.modules["pika"] = mock_pika
mock_redis_module = MagicMock()
sys.modules["redis"] = mock_redis_module
from utils.banco import Base, Banco, LogTable
from utils.api import Api
@pytest.fixture
def banco_teste():
"""Cria um Banco real com SQLite in-memory para cada teste (isolado)."""
from sqlalchemy import create_engine
from sqlalchemy.orm import sessionmaker
engine = create_engine("sqlite:///:memory:")
Base.metadata.create_all(engine)
Session = sessionmaker(bind=engine)
banco = Banco.__new__(Banco)
banco.engine = engine
banco.session = Session()
yield banco
banco.session.close()
engine.dispose()
@pytest.fixture
def mock_logger():
"""Logger que grava todas as chamadas de log em uma lista para inspeção."""
logger = MagicMock()
logger.logs = []
logger.client = "TestClient"
def capture_log(realm, msg_tuple):
logger.logs.append({"realm": str(realm), "message": msg_tuple[0] if msg_tuple else ""})
logger.logIC2.side_effect = capture_log
return logger
@pytest.fixture
def mock_api():
"""Api que retorna respostas configuráveis sem fazer chamadas HTTP reais."""
api = MagicMock(spec=Api)
api.get.return_value = {"status": "ok"}
api.post.return_value = {"documents": [{"id": "PLUG-001"}]}
api.put.return_value = {"status": "ok"}
api.patch.return_value = {"status": "ok"}
api.delete.return_value = {"status": "ok"}
api.post_file.return_value = {"status": "ok"}
api.post_form_data.return_value = {"data": {"id": "CERT-001"}}
api.get_paginated.return_value = {"data": [], "total": 0}
return api
@pytest.fixture
def mock_exchange():
"""Exchange que simula setPayload sem enviar nada."""
exchange = MagicMock()
exchange.payload = ""
exchange.payloads_set = []
def capture_payload(p):
exchange.payloads_set.append(p)
exchange.payload = p
exchange.setPayload.side_effect = capture_payload
return exchange
@pytest.fixture
def main_instance(banco_teste, mock_logger, mock_exchange):
with patch("utils.Exchange2.RPCExchange2"), \
patch("utils.Exchange2.QueueExchange2"), \
patch("utils.Exchange2.LoggerExchange2"), \
patch("utils.redis.RedisClient"):
from main import Main
with patch.object(Main, '__init__', lambda self: None):
main = Main()
main.client = "TestClient"
main.api_key = "test-api-key-fake"
main.exchange = mock_exchange
main.banco = banco_teste
main.system_config = MagicMock()
main.system_config.serviceid = "test-id"
main.system_config.servicekey = "test-key-32-chars-long-12345678"
main.redis = MagicMock()
main.service = "tecnospeed"
main.system = "integracao"
main.version = "2.0.1-test"
main.logger = mock_logger
main.url_base = "https://api.sandbox.plugnotas.com.br"
main.funcoes = {
"nfe": main.nfe,
"nfse": main.nfse,
"certificado_digital": main.certificado_digital,
"empresa": main.empresa,
"servico": main.servico,
"tomador": main.tomador,
"relatorios": main.relatorios,
"webhooks": main.webhooks,
}
return main

View file

@ -0,0 +1,67 @@
import json
import os
import sys
# Garante que o Python encontre os módulos do projeto
sys.path.append(os.path.dirname(os.path.dirname(os.path.abspath(__file__))))
from Models.payload import validate_payload
sample_payload = {
"fila": "nfse",
"cliente": "MassiveNFSeTest",
"metodo": "POST",
"dados": {
"id_empresa": "some-uuid",
"id_usuario": 1,
"idIntegracao": "TEST_123",
"prestador": {
"cpfCnpj": "00000000000000"
},
"tomador": {
"cpfCnpj": "08114280956",
"razaoSocial": "NFSE EMITIDA EM HOMOLOGACAO",
"email": "contato@tecnospeed.com.br",
"endereco": {
"logradouro": "AVENIDA DUQUE DE CAXIAS",
"numero": "882",
"bairro": "CENTRO",
"codigoCidade": "4115200",
"descricaoCidade": "MARINGA",
"estado": "PR",
"cep": "87020025",
"tipoLogradouro": "Avenida"
}
},
"servico": {
"codigo": "1.01",
"codigoTributacao": "1.01",
"discriminacao": "SERVICO DE TESTE DE CARGA MASSIVA",
"cnae": "6201501",
"iss": {
"tipoTributacao": 1,
"exigibilidade": 1,
"aliquota": 2.0
},
"valor": {
"servico": 100.0
}
}
},
"config": {
"x_api_key": "2da392a6-79d2-4304-a8b7-959572c7e44d",
"user_postgres": "tecnospeed",
"pass_postgres": "u7QimG7E!SF//JC2c8",
"host_postgres": "banco",
"port_postgres": "5432",
"database_postgres": "tecnospeed"
},
"url_base": "https://api.sandbox.plugnotas.com.br"
}
try:
print("Tentando validar payload...")
validated = validate_payload(sample_payload)
print("Sucesso! Payload válido.")
except Exception as e:
print(f"Erro de validação: {e}")

View file

@ -0,0 +1,33 @@
import requests
import os
import json
def debug_report():
api_key = "2da392a6-79d2-4304-a8b7-959572c7e44d" # From src/utils/api.py comment
url = "https://api.sandbox.plugnotas.com.br/nfse/relatorio?from=2026-04-01&to=2026-04-30"
headers = {
"x-api-key": api_key
}
print(f"Testing URL: {url}")
response = requests.get(url, headers=headers)
print(f"Status Code: {response.status_code}")
try:
print(f"Response Body: {json.dumps(response.json(), indent=2)}")
except:
print(f"Response Body: {response.text}")
# Test with ISO format
url_iso = "https://api.sandbox.plugnotas.com.br/nfse/relatorio?from=2026-04-01T00:00:00&to=2026-04-30T23:59:59"
print(f"\nTesting URL (ISO): {url_iso}")
response = requests.get(url_iso, headers=headers)
print(f"Status Code: {response.status_code}")
try:
print(f"Response Body: {json.dumps(response.json(), indent=2)}")
except:
print(f"Response Body: {response.text}")
if __name__ == "__main__":
debug_report()

View file

@ -0,0 +1,46 @@
import pika
import os
import sys
import json
# Garante que o Python encontre os módulos do projeto
sys.path.append(os.path.dirname(os.path.dirname(os.path.abspath(__file__))))
from utils.redis import RedisClient
def purge_queue():
# 1. Obter URL do RabbitMQ do Redis
redis_host = os.getenv('REDIS_HOST', 'localhost')
redis_client = RedisClient(host=redis_host, port=6379)
try:
system_config_data = redis_client.get("system_config")
if not system_config_data:
print("Erro: system_config não encontrado no Redis.")
return
system_config = json.loads(system_config_data)
amqps_url = system_config.get("amqps")
except Exception as e:
print(f"Erro ao ler configs do Redis: {e}")
return
if not amqps_url:
print("Erro: AMQPS URL não encontrada na configuração.")
return
# 2. Conectar e limpar a fila
queue_name = "integracao_tecnospeed"
try:
params = pika.URLParameters(amqps_url)
connection = pika.BlockingConnection(params)
channel = connection.channel()
# Tenta realizar o purge
result = channel.queue_purge(queue=queue_name)
print(f"Sucesso! Fila [{queue_name}] limpa. Mensagens removidas: {result.method.message_count}")
connection.close()
except Exception as e:
print(f"Erro ao limpar a fila: {e}")
if __name__ == "__main__":
purge_queue()

View file

@ -0,0 +1,134 @@
import sys
import os
import json
sys.path.append(os.path.dirname(os.path.abspath(__file__)))
from Models.config import SystemConfig
from utils.redis import RedisClient
from utils.Exchange2 import RPCExchange2
def main():
print("Iniciando script de testes da mensageria...")
# Carregando configuração do Redis
redis_client = RedisClient(host=os.getenv('REDIS_HOST', 'localhost'), port=6379)
try:
system_config_data = redis_client.get("system_config")
if not system_config_data:
print("Erro: system_config não encontrado no Redis. Certifique-se de que a variável existe.")
return
system_config = SystemConfig(**json.loads(system_config_data))
print(f"[{system_config.origin}] Configuração do sistema lida com sucesso.")
except Exception as e:
print(f"Erro ao ler configs do Redis: {e}")
return
# Inicializando mensageria RPC
try:
amqps_local = system_config.amqps.replace("host.docker.internal", "localhost")
exchange = RPCExchange2(
amqps=amqps_local,
origin=system_config.origin,
system="integracao",
service="tecnospeed",
version="2.0.1",
client="#"
)
except Exception as e:
print(f"Falha ao inicializar mensageria: {e}")
return
# Configurando criptografia e payload
servicekey = system_config.servicekey.ljust(32, '0')[:32]
exchange.setCrypto(serviceid=system_config.serviceid, servicekey=servicekey)
# Payload da mensageria
payload_interno = {
"fila": "nfse",
"cliente": "Test",
"metodo": "POST",
"dados": {
"idIntegracao": "INTEGRACAO-NFSE-002",
"prestador": {
"cpfCnpj": "08187168000160"
},
"tomador": {
"cpfCnpj": "08114280956",
"razaoSocial": "João Tomador da Silva",
"inscricaoMunicipal": "isento",
"email": "tomador@email.com",
"endereco": {
"descricaoCidade": "São Paulo",
"cep": "00000000",
"tipoLogradouro": "Rua",
"logradouro": "Dois de Ouro",
"tipoBairro": "Bairro",
"codigoCidade": "3550308",
"complemento": "Apto 13",
"estado": "SP",
"numero": "44",
"bairro": "Centro"
}
},
"servico": {
"codigo": "1.01",
"codigoTributacao": "1.01",
"discriminacao": "Serviços prestados de tecnologia",
"cnae": "6204000",
"iss": {
"tipoTributacao": 6,
"exigibilidade": 1,
"aliquota": 2.5
},
"valor": {
"servico": 1500.50,
"descontoCondicionado": 0.0,
"descontoIncondicionado": 0.0
}
}
},
"config": {
"x_api_key": os.getenv('PLUGNOTAS_API_KEY', ''),
"user_postgres": os.getenv('POSTGRES_USER', 'tecnospeed'),
"pass_postgres": os.getenv('POSTGRES_PASSWORD', 'tecnospeed'),
"host_postgres": os.getenv('POSTGRES_HOST', 'localhost'),
"port_postgres": os.getenv('POSTGRES_PORT', '5432'),
"database_postgres": os.getenv('POSTGRES_DB', 'tecnospeed')
},
"url_base": "https://api.sandbox.plugnotas.com.br"
}
# Criptografando e enviando tarefa
exchange.setPayload(json.dumps(payload_interno))
print("Disparando chamada RPC para o RabbitMQ...")
# Executando chamada RPC
response = exchange.callRPC("Disparo de testes executado pelo test.py")
print("\n[RAW RPC RESPONSE]:")
print(response)
# Descriptografando a resposta
try:
resp_obj = json.loads(response)
hmac_resp = resp_obj.get('hmac')
payload_resp = resp_obj.get('payload')
if hmac_resp and payload_resp:
if exchange.getPayload(hmac_resp, payload_resp):
print("\n[DECRYPTED RPC DATA]:")
print(json.dumps(json.loads(exchange.payload), indent=2))
else:
print("\n[ERROR]: Failed to decrypt response.")
else:
print("\n[INFO]: Response does not contain an encrypted payload.")
except Exception as e:
print(f"\n[ERROR]: Error processing response: {e}")
if __name__ == "__main__":
main()

View file

@ -0,0 +1,58 @@
import pytest
from unittest.mock import patch, MagicMock
from fastapi.testclient import TestClient
from src.listenerEndpoint import app
client = TestClient(app)
PAYLOAD_VALIDO = {
"id": "60a7c4f1c9d2a00015...",
"status": "CONCLUIDO",
"documento": "nfe"
}
HEADERS_VALIDOS = {"x-plugnotas-signature": "assinatura_hmac_valida"}
class TestPlugNotasWebhookEndpoint:
@patch("src.listenerEndpoint.exchange")
def test_deve_receber_payload_e_enfileirar_com_sucesso(self, mock_exchange):
"""Critério: Caminho feliz. Valida, enfileira e responde com sucesso (200)."""
mock_exchange.sendTask.return_value = True
response = client.post("/listenerPlugnotas", json=PAYLOAD_VALIDO, headers=HEADERS_VALIDOS)
assert response.status_code == 200
assert response.json() == {"status": "ok", "message": "Webhook recebido e encaminhado para processamento"}
mock_exchange.setPayload.assert_called_once()
mock_exchange.sendTask.assert_called_once_with("Webhook PlugNotas Recebido")
@patch("src.listenerEndpoint.exchange")
def test_deve_rejeitar_payload_invalido_antes_de_processar(self, mock_exchange):
"""Critério: Integridade de Dados. Valida campos obrigatórios recebidos do Hook."""
payload_invalido = {"status": "CONCLUIDO"} # Faltando ID e documento
response = client.post("/listenerPlugnotas", json=payload_invalido, headers=HEADERS_VALIDOS)
assert response.status_code == 200
assert response.json()["status"] == "warning"
assert "Campos faltantes" in response.json()["message"]
@patch("src.listenerEndpoint.exchange")
def test_deve_retornar_erro_se_internal_error_no_rabbitmq(self, mock_exchange):
"""Critério: Resiliência. Retorna 500 em caso de erro ao enviar para fila."""
# Simulando uma queda ao enviar task
mock_exchange.sendTask.side_effect = Exception("RabbitMQ is down")
response = client.post("/listenerPlugnotas", json=PAYLOAD_VALIDO, headers=HEADERS_VALIDOS)
assert response.status_code == 500
assert response.json()["message"] == "Erro interno no processamento do webhook"
def test_deve_retornar_erro_se_exchange_nao_inicializado(self):
"""Critério: Resiliência. Retorna 500 se o exchange global for nulo."""
# Sem patch de exchange, então pegará o exchange default (None) em client sem lifespan completo
response = client.post("/listenerPlugnotas", json=PAYLOAD_VALIDO)
assert response.status_code == 500
assert response.json()["message"] == "Serviço temporariamente indisponível"

Some files were not shown because too many files have changed in this diff Show more