M4 — Gateway/ACL (ErpPort, CrmPort, GatewayPagamentoPort) + adapters em memória
§4.1, §10.6
src/observability/
M5 — Observabilidade & Alertas
§4.1
src/shared/
Result/Either, erros de domínio
§6.1
Fluxo do código (entrada → saída)
O core é um pipeline dirigido por evento: um título criado no Dataverse vira uma
mensagem de fila; a Saga (src/app/saga-processar-titulo.ts) processa de ponta a
ponta e devolve uma decisão para a fila (completar / abandonar / dlq), tendo
como efeitos externos as escritas no (SOAP) e no (Dataverse).
Carregando diagrama...
Passo a passo (com os arquivos)
Portas — a fronteira injetável (nada abre conexão real neste repositório)
Direção
Porta
Papel
Adapter real (último passo, TI)
entrada
FilaPort
consumidor do evento
Azure Service Bus
entrada
FonteDadosPort
relê o título/inscrição/PF no CRM
fonte-dados-dataverse.ts (Web API, GET-only — Parte 5A)
saída
ErpPort
cria pedido/título no ERP
SapiensSoapAdapter (SOAP/TLS)
saída
CrmEscritaPort / CrmPort
patches e status de integração
Dataverse Web API
saída
NotificacaoPort
O meio é 100% puro (matriz fiscal, MontadorPedido, planos de pós-processamento — sem I/O). O shadow-run (src/app/shadow-run.ts) pluga todos os adapters em memória + um transporte que captura o XML, rodando o pipeline inteiro offline para o diff Node × legado.
Regras de arquitetura (invariantes)
Plano de entrega em partes (cada parte fecha com revisão)
Consistência (M1) — o que a Saga garante
A fila entrega at-least-once e o Sapiens não tem campo de idempotência confirmado, então a segurança contra pedido duplicado vive na Saga:
Nada aqui abre conexão real: os adapters de fila, CRM e e-mail reais entram só no fim (executados pelo responsável de infra); os testes e o shadow-run usam os adapters em memória de src/integration/memoria/.
Comandos
npm install
npm run typecheck
npm test# 227 testes (domínio, adapters, Saga, montador, shadow-run, leitura Dataverse)# verificação da extração do legado contra o export original do fluxo:
node docs/extracao-legado/verifica.cjs <export.json> # matriz fiscal (Partes 1-2)
node docs/extracao-legado/verifica-parte3.cjs <export.json> # orquestração M1 (Parte 3)
node docs/extracao-legado/verifica-montador.cjs <export.json> # montador de pedido (Parte 4)
node docs/extracao-legado/extrai-leituras.cjs # leituras/escritas Dataverse (Parte 5)
Observação somente-leitura sobre um título REAL (requer az login; ver
docs/observacao-somente-leitura.md):
npm run observar -- listar --dias 7
npm run observar -- titulo <guid-do-titulo>
Estado: o core é offline. A única fronteira real ligada até aqui é a
leitura do Dataverse, usada pelo modo observação e protegida por uma trava
que recusa qualquer escrita. Sapiens, fila, e-mail e escrita no CRM continuam
desligados — entram no último passo, executado pela equipe de infra/TI.
Sapiens
CRM
Entrada — fila (src/app/ports/fila-port.ts): o runtime entrega MensagemFila<EventoTituloCriado> (só o tituloId) ao ConsumidorMensagem = a Saga. O adapter real da fila entra só no fim; os testes/shadow-run usam a fila em memória.
Idempotent Receiver (registro-idempotencia-port.ts): iniciar(eventId) → concluído→completar (duplicata); em_andamento→dlq (execução anterior morreu no meio, nunca reprocessa sozinha); novo→segue.
Leitura (fonte-dados-port.ts): buscarDados(tituloId) devolve o agregado DadosProcessamento (os ~10 lookups do legado numa porta só) — troca a "espera cega" de 7 min por releitura + reentrega quando os dados ainda não chegaram.
Gates fiéis (topo do fluxo legado): kill-switch e trâmite → completar; PIX não confirmado → abandonar (a fila reentrega até a confirmação, em vez do Terminate "Succeeded" do legado).
Contexto + matriz (src/domain/, puro): montarContextoPrecificacao mapeia os optionsets crus → domínio; no caminho principal, calcularAjusteFiscalLegado roda a matriz fiscal (transação / representante / conta / SKU) — sem regra casando ⇒ MATRIZ_NAO_COBRE, nunca valor silencioso.
Roteamento (método × parcela): cupom-100 e carta de crédito → só CRM; parcela > 1 → só o título CR referenciando o pedido da parcela 1 (criarTituloParcela); 1ª parcela → monta o pedido.
Montagem (montador-pedido.ts, Parte 4): montar(dados, contexto, ajuste) → {pedido, titulo} fiel aos 4 Composes do legado (perfil por modo: estrangeiro / OnDemand / nacional-com-cep / nacional-sem-cep).
Efeito no ERP (src/integration/sapiens/): criarPedidoETitulo monta o XML SOAP e envia — inserepedido sempre; EntradaTitulosLoteCRsó para boleto (fidelidade). Retry off por padrão (sem idempotência confirmada no Sapiens).
Pós-processamento CRM (pos-processamento.ts, src/app/fluxos/): planos puros devolvem uma lista de OperacaoCrm; a Saga as executa por crmEscrita.executar (patches de título/inscrição/contato/solicitação, tarefas de NF/carta).
Fechamento (executarPlanoEConcluir): notifica, marca statusIntegracao = efetivada, concluir a chave de idempotência → completar.
Saída em falha (rotearFalha + classificacao-erros.ts): classifica efeito externo × natureza → sem efeito/transitória = liberar + abandonar; permanente ou efeito parcial = marca status = erro, notifica e dlq (com a chave presa quando o efeito é incerto — nunca reprocessa um possível pedido).
os 10 e-mails inline viram 1 porta
Graph/SMTP
dedupe
RegistroIdempotenciaPort
chave por eventId
UNIQUE em SQL
O domínio não importa nada de fora de domain/ e shared/ — sem I/O, sem SDK, sem Dataverse/Sapiens.
A matriz fiscal é tabela de decisão versionada (domain/rules/), extraída do fluxo legado com evidência por regra (nome das actions do Power Automate). Sem regra casando → erro explícito MATRIZ_NAO_COBRE, nunca valor silencioso.
Fronteira com o Sapiens é sempre assíncrona — o ErpPort nunca é chamado em caminho HTTP síncrono.
Idempotência: toda escrita no ERP carrega chaveIdempotencia (= id do título) propagada como referência externa.
Sem PII em log sem mascaramento.
Parte 1 (CONCLUÍDA — revisada) — scaffold + M2: modelo de domínio, motor de tabela de decisão, matriz fiscal extraída do fluxo legado real (extração multiagente com evidência por regra + verificação mecânica contra o JSON — ver docs/extracao-legado/), estratégias M3 (geraPedido). Revisão adversarial: 1 achado ALTO corrigido (OnDemand-boleto fora da janela mantém a matriz). Gate: revisão + docs/questoes-aceite-contabil.md validado pelo financeiro.
Parte 2 (CONCLUÍDA — revisada) — M5 (pino + correlation id + mascaramento de PII) + M4 SapiensSoapAdapter (XMLs fiéis aos templates extraídos do legado, TLS obrigatório, circuit breaker, retry desligado por padrão até haver idempotência confirmada no Sapiens) + DataverseAdapter (Web API + de-para optionset→domínio) + pipeline CI de quality gates (typecheck, testes, SCA, gitleaks). Tudo OFFLINE: nenhum endpoint real configurado; transporte injetável testado com dublês. Revisão adversarial: 4 achados ALTOS corrigidos. Gate: revisão + contrato TI (endpoints https, campo de idempotência, valores de fase do Dataverse).
Parte 3 (CONCLUÍDA — revisada) — M1: Saga ProcessarTítulo que substitui a orquestração do monólito legado (src/app/saga-processar-titulo.ts). Reproduz fielmente os gates do fluxo (kill-switch, gate do PIX no topo, trâmite, cupom-100, carta de crédito, parcela subsequente, caminho Sapiens) e o pós-processamento CRM completo (os 3 switches por área de negócio, Condição_renovação, os 7 produtos de certificação CC120xx, tarefas de NF/carta de crédito, patches de inscrição/contato/solicitação — src/app/pos-processamento.ts e src/app/fluxos/). Onde o legado tinha Terminate silencioso e esperas fixas de 7/4 min, agora há fila com reentrega, DLQ rastreável e Idempotent Receiver. Ver a seção Consistência (M1) abaixo. Extração da orquestração em docs/extracao-legado/orquestracao_m1.json, verificada mecanicamente por docs/extracao-legado/verifica-parte3.cjs (53/53 contra o export original). Revisão adversarial multiagente (4 dimensões + verificação cética por achado, em duas rodadas): o candidato ALTA de pedido duplicado (CIRCUITO_ABERTO após o pedido) foi refutado 2× — o modelo de event-loop garante que o breaker não muda de estado entre a criação do pedido e o gate do título na mesma chamada (ainda assim o adapter foi endurecido: falha do título pós-pedido = efeito parcial, nunca libera a chave). Correções aplicadas com testes: roteamento de ibgc_numero_parcela nulo como parcela subsequente (fechava um caminho de pedido duplicado), gate de sucesso do título EXATO (=== "OK"), nº do pedido da parcela por parse real (corrige o substring fixo), codCli estrito em result.gridResult, scrub de PII no log e na DLQ, e verificação de checksum do gitleaks no CI. Detalhe completo e itens de decisão TI (questões 21–35) em docs/questoes-aceite-contabil.md. Gate: revisão + confirmar GUIDs de fase e optionsets com o TI.
Parte 4 (CONCLUÍDA — revisada) — MontadorPedido fiel (src/app/montador-pedido.ts): réplica dos 4 Composes do legado (cada um um perfil completo por modo — endereço, e-mail, cobrança, inscrição estadual, observação, transação e codccu variam por modo), com seleção estrangeiro → OnDemand → nacional-com-cep → nacional-sem-cep, o guard de cadastro incompleto (nome/CPF/logradouro/cidade do pagador nulos → CADASTRO_PAGADOR_INCOMPLETO, no lugar do Terminate Cancelled silencioso; bloco de dados ausente → DADOS_INDISPONIVEIS/reentrega), valorCurso = soma das parcelas ativas (resolve a questão 12) e o TEF do cartão (com erro explícito se os dados do cartão vierem ilegíveis). Shadow-run harness (src/app/shadow-run.ts + test/shadow/): roda o pipeline completo OFFLINE e captura o que seria enviado ao Sapiens (XML SOAP) e escrito no CRM, para o diff Node × legado da Fase 2 — sem abrir conexão. Extração em docs/extracao-legado/montador_pedido.json, verificada por verifica-montador.cjs (27/27). Revisão adversarial (docs/revisao-parte4.md): um cluster de divergência de fidelidade (os 4 Composes estavam achatados num só — CEP/número fixos, transação 9799A do estrangeiro, cobrança/e-mail/IE/obsPed/codccu por modo) foi encontrado e corrigido, além de TEF sem drop silencioso, guard × bloco ausente e o codCcu do rateio do título. Gate: aceite contábil de equivalência (shadow-run com corpus real de títulos) + contrato TI.
Parte 5A (EM VALIDAÇÃO) — modo observação, somente leitura (src/cli/observar.ts + docs/observacao-somente-leitura.md): primeiro contato com o mundo real, e só de leitura. Implementa a FonteDadosPort real sobre a Web API (src/integration/dataverse/fonte-dados-dataverse.ts — as 14 leituras do legado, extraídas em docs/extracao-legado/leituras-dataverse.json pelo extrai-leituras.cjs), o transporte HTTP com trava de escrita (http-transport.ts: qualquer método ≠ GET é recusado antes de abrir conexão) e o token vindo do Azure CLI/ambiente (provedor-token.ts, nenhum segredo no repositório). A CLI roda o pipeline sobre um título real e imprime leituras, gates, XML que iria ao Sapiens (não vai), patches que iriam ao CRM (não vão) e o estado atual do registro — a comparação coluna a coluna com o que o legado gravou. Rastrear as variáveis do legado até a origem revelou 3 divergências corrigidas aqui: <numpsp> é passaporte (contacts.governmentid, vazio no nacional) e não o nº da solicitação; <razaoSocialCobranca> leva documento (CPF/CNPJ limpo, passaporte no exterior, ibgc_cpf do contato no OnDemand); e <cpfcnpj> vai vazio no exterior. Gate: rodar sobre um corpus de títulos reais (boleto, cartão, PIX, parcelado, estrangeiro, OnDemand, cupom, carta) e fechar o diff.
Idempotent Receiver (src/app/ports/registro-idempotencia-port.ts): antes de qualquer efeito externo, registra a chave (= eventId). concluído → duplicata benigna (completa sem reprocessar); em_andamento → estado ambíguo (execução anterior morreu no meio) → DLQ, nunca reprocessa no automático. Implementação real = UNIQUE em SQL; em teste/shadow-run = em memória.
Classificação de falha (src/app/classificacao-erros.ts) por dois eixos — efeito externo (nenhum/incerto/parcial) × natureza (transitória/permanente):
sem efeito + transitória → libera a chave e abandona (fila reentrega);
sem efeito + permanente → marca erro no título, notifica, DLQ + libera (reenvio manual é seguro);
efeito parcial/incerto (ex.: timeout após criar o pedido) → DLQ com a chave presa em em_andamento + notificação de compensação manual. Nunca reexecuta algo que pode ter criado pedido.
DLQ com motivo rastreável em vez do Terminate "Cancelled" invisível do legado — os "pedidos órfãos" do parecer deixam de ser silenciosos.