Repository navigation
CT e Emissao NFePHP
Documentação técnica do fluxo de emissão real de CT-e (modelo 57) no módulo api-platform-accounting.
Implementação principal: issue #16 (NFePHP + DACTE).
Correção de validação de quantidade de NFs: issue #26 (mínimo 1 NF, alinhado à prática fiscal).
Vínculo imediato NF → task no emit-cte: issue #27 (invoice_task_id / InvoiceTax::$integration na criação da task).
Consumir a fila de emissão de CT-e (cte_emission / invoice_task tipo cte_emission), montar o XML com NFePHP (nfephp-org/sped-cte), assinar e transmitir à SEFAZ com o certificado digital da empresa (configuração fiscal), persistir o XML autorizado, vincular o CT-e às NFs de origem (modelo 55) e disponibilizar o DACTE em PDF a partir do XML armazenado (sem reconsulta à SEFAZ só para visualização).
Dentro do escopo (esta issue):
- Worker/comando de consumo da fila
- Montagem do CT-e a partir das NFs vinculadas
- Assinatura e transmissão SEFAZ
- Persistência do XML e vínculo
cte_id - Atualização de status via
StatusService::discoveryStatus(nunca por ID fixo) - Incremento de
receita-federal-cte-last-numberapós autorização - Geração de PDF (DACTE) no endpoint de download a partir do XML persistido
Fora de escopo desta issue:
- Campos da tela de emissão e listagem
without-cte(trabalho paralelo de UI/staging) - Alterações de formulário de configuração fiscal (já existentes em
ui-common)
| Visão | Papel neste fluxo |
|---|---|
| MANAGER / LOGISTIC | Operador dispara a emissão a partir da tela /cte/emit?ids= (UI em ui-logistic ou equivalente). Visualiza CT-e emitidos e abre o DACTE. |
| API (accounting) | Backend único responsável pela montagem, SEFAZ, persistência e download. |
| Outras visões (CRM, POS, SHOP, …) | Não emitem CT-e nem consomem a fila. Podem apenas visualizar documentos fiscais já emitidos se a UI compartilhada permitir. |
O módulo não deve assumir regras de tela, seleção de NFs ou layout de listagem — isso permanece na UI. A API só recebe invoiceTaxIds + cfop (+ extra opcional) e processa de forma assíncrona.
UI de consulta (não emissão): CT-e — Detalhe e PDF (DACTE) — lista /cte aba CTE, modal PDF e página /cte/detail read-only (ui-logistic#34). O endpoint de download continua neste módulo.
Lida por CteFiscalConfig a partir das configs da empresa emissora (aba Fiscal / Receita Federal):
| Chave | Uso |
|---|---|
receita-federal-certificate-file |
Arquivo .pfx/.p12 (conteúdo binário ou referência a File) |
receita-federal-certificate-password |
Senha do certificado |
receita-federal-environment |
1 = produção, 2 = homologação |
receita-federal-tax-regime |
Regime tributário (contexto fiscal) |
receita-federal-ibge-code |
Código IBGE do município do emitente |
receita-federal-cte-enabled |
Flag de habilitação de CT-e |
receita-federal-cte-serie |
Série do CT-e |
receita-federal-cte-last-number |
Último número autorizado (incrementado somente após autorização SEFAZ) |
receita-federal-cte-rntrc (opcional) |
RNTRC; se ausente, pode vir de documento da transportadora no payload extra
|
Sem certificado + senha válidos a emissão falha com mensagem clara e status de erro na tarefa.
sequenceDiagram
participant UI as UI (/cte/emit)
participant API as EmitCteAction
participant Task as Integration / invoice_task
participant Cmd as app:cte:emit
participant Proc as CteEmissionProcessor
participant SEFAZ as SEFAZ (sped-cte)
participant Store as File + InvoiceTax
UI->>API: POST /invoice_tasks/emit-cte<br/>{invoiceTaxIds, cfop, extra?}
API->>Task: Cria Integration queue CteEmission<br/>vincula NFs (integration)
Note over Task: status pending
Cmd->>Proc: processPending / processMessengerIntegration
Proc->>Proc: status → processing (StatusDiscovery)
Proc->>Proc: CteXmlBuilder.build (mod 57)
Proc->>SEFAZ: CteSefazClient.signAndSend (PFX + Tools)
SEFAZ-->>Proc: XML autorizado (cStat 100) ou erro
Proc->>Store: File (XML) + InvoiceTax modelo 57
Proc->>Store: NFs origem.cte_id = CT-e
Proc->>Proc: incrementa receita-federal-cte-last-number
Proc->>Task: status → closed | error
-
Endpoint:
POST /invoice_tasks/emit-cte(EmitCteAction→EmitCteService). -
Payload:
-
invoiceTaxIdsouselected: lista de IDs deinvoice_tax(modelo 55) -
cfop(obrigatório) -
extra(opcional):natOp,rntrc,xMunEnv,UFEnv, etc.
-
- Cria
IntegrationcomqueueName/taskTypede emissão CT-e e vincula as NFs imediatamente (InvoiceTax::setIntegration($integration)+flush) — ver seção Vínculo imediato NF → task (#27). -
Validações de negócio (
EmitCteService::emit):-
Mínimo 1 NF (
count($ids) < 1→ 400 com mensagem "Selecione ao menos uma NF para emitir o CT-e."). Um CT-e pode referenciar uma única NF-e (caso mais comum na legislação e na prática); agrupar várias NFs do mesmo remetente/destinatário é opcional, não obrigatório. Correção da validação rígida anterior (< 2) em #26. - Lista não vazia após normalização (
intval+ unique + filter). - CFOP obrigatório (não vazio).
- Todas as NFs existem no repositório; caso contrário 400.
- NFs não possuem
ctenemintegrationem andamento (já ocupadas). - Isolamento multi-tenant:
assertTenantCanEmit(excetoROLE_SUPER). - Payload de bind de arrays usa
Doctrine\DBAL\ArrayParameterType::INTEGER(compatível DBAL 3+/4; evitaConnection::PARAM_INT_ARRAYremovido).
-
Mínimo 1 NF (
-
Comando:
php bin/console app:cte:emit [--limit=20] -
Messenger / cron
tenant:integration:start:IntegrationService::executeIntegrationresolve o handler peloqueueNamee chamaCteEmissionService::integrate(). -
CteEmissionProcessor:- Processa
Integrationcom filacte_emission/CteEmissione tarefasinvoice_tasktipocte_emissionem status pending. - Status somente via
StatusService::discoveryStatus('processing'|'closed'|'error', …, 'invoice_task'|'integration').
- Processa
Antes do hotfix, CteEmissionService::integrate() era um stub: criava InvoiceTax model 57 com chave/número fabricados e status closed, sem XML, sem assinatura e sem transmissão SEFAZ. A UI (/cte) mostrava Closed falso.
Contrato atual (obrigatório):
| Artefato | Responsabilidade |
|---|---|
CteEmissionService |
Thin handler: apenas return $this->processor->processMessengerIntegration($integration)
|
CteEmissionProcessor::processMessengerIntegration |
Normaliza body/payload (invoiceTaxIds/selected + cfop + extra); exige ids e CFOP; delega a processTask
|
processTask |
processing → build → signAndSend → persistCte (Closed somente após XML autorizado) |
processIntegrations |
Em sucesso marca Integration closed; em qualquer exceção marca error (nunca closed silencioso) |
app:cte:emit e o caminho messenger compartilham a mesma implementação (CteEmissionProcessor). Não deve existir segundo caminho que invente Closed.
Referência de produto: app-community#689.
CteXmlBuilder::build($invoices, $fiscal, $cfop, $extra)- Modelo 57, versão 4.00, modal rodoviário (
01). - Partes: emitente (empresa), remetente, destinatário, tomador, chaves das NFs 55 (
infNFe), totais, municípios/UF, RNTRC. - Número/série derivados da config fiscal; chave de acesso gerada conforme regras do layout.
CteSefazClient::signAndSend- Pacotes:
nfephp-org/sped-cte(Certificate::readPfx,Tools::signCTe,sefazEnviaCTe, consulta de recibo secStat103/104). - Ambiente e UF a partir da config da empresa.
- Sucesso:
cStat/ protocolo 100; XML com protocolo anexado quando possível. - Falha: exception com motivo SEFAZ; tarefa marcada como
errore payload atualizado com mensagem.
- XML autorizado salvo via
FileService(tipoinvoice_tax, nomecte-{chave}.xml). - Novo
InvoiceTaxcominvoice_model = 57, chave, número, totais, vínculos de pessoas/endereços,integrationda tarefa, status closed. - Cada NF de origem recebe
cte(cte_id) apontando para o CT-e. -
receita-federal-cte-last-numberatualizado para o número autorizado.
-
Endpoint existente:
GET /invoice_taxes/{id}/download-nf(formatospdf,xml,base64). -
DownloadNFService::getPdf:- Se
invoice_model === 57(ou XML detectado como CT-e) →NFePHP\DA\CTe\Dacte(sped-da). - Caso contrário → DANFE (NF-e modelo 55).
- Se
-
Não reconsulta SEFAZ: usa exclusivamente o XML já armazenado em
InvoiceTax/File. - A tela de CT-es emitidos deve abrir esse endpoint para o PDF do DACTE.
| Componente | Responsabilidade |
|---|---|
EmitCteAction / EmitCteService
|
API de enfileiramento e validações de negócio |
CteEmitCommand (app:cte:emit) |
Entrada de cron/worker |
CteEmissionProcessor |
Orquestra status, build, SEFAZ, persistência |
CteFiscalConfig |
Carrega e atualiza configs fiscais da empresa |
CteXmlBuilder |
Monta XML CT-e 4.00 |
CteSefazClient |
Assina e transmite com NFePHP |
DownloadNFService |
PDF/XML do documento (DACTE ou DANFE) |
ListInvoicesWithoutCteAction / InvoicesWithoutCteService
|
Listagem de NFs elegíveis (paralelo) |
Presentes no runtime do pai api-community:
nfephp-org/sped-cte-
nfephp-org/sped-da(DACTE) -
nfephp-org/sped-common(Certificate, etc.)
Antes do fix, POST /invoice_tasks/emit-cte criava a Integration / invoice_task com status: open|pending, mas não preenchia invoice_tax.invoice_task_id (mapeado como InvoiceTax::$integration → Integration). O vínculo só ocorria depois, em:
-
CteEmissionProcessor::persistCte($invoice->setIntegration($task)) -
CteEmissionService::integrate($nf->setIntegration($integration))
Enquanto a task estava em andamento (open / pending / processing), GET /invoice_taxes/without-cte continuava listando as NFs e o operador podia reenviar o mesmo conjunto.
Após integrationService->addIntegration(...):
- Para cada
InvoiceTaxselecionada:$invoice->setIntegration($integration)+persist. -
EntityManager::flush()antes de retornar a Integration (HTTP 201). - Resposta da API não muda (id,
@id, status).
Com isso, no mesmo instante do 201:
- cada NF tem
invoice_task_id= id da task criada; -
GET /invoice_taxes/without-ctenão retorna mais essas NFs, mesmo comcte_idaindaNULLe task aindaopen.
A listagem de NFs elegíveis exclui qualquer NF que já esteja “ocupada”:
| Critério | Origem |
|---|---|
cte_id IS NULL |
CT-e ainda não emitido / vinculado |
invoice_task_id IS NULL (coluna presente) |
SQL path |
exclusão de IDs com invoice_task_id IS NOT NULL (busyInvoiceIds) |
Doctrine path (invoiceTax.cte IS NULL + filtro pós-query) |
Modelos listáveis: 55 e 65 (NF_MODELS). Modelo CT-e = 57.
- Tentativa de emitir de novo NFs já com
integration→400"Uma ou mais NFs já estão em emissão ou emitidas." - NF com
ctejá preenchido →400"NF … já possui CT-e vinculado." - Isolamento multi-tenant (
assertTenantCanEmit) permanece inalterado.
Em caso de erro posterior (SEFAZ, validação no processor, etc.):
- o vínculo
invoice_task_idnão é limpo automaticamente nesta issue (evita reentrada silenciosa na filawithout-cte); - reprocesso / cancelamento com liberação do vínculo deve ser tratado em issue separada de retry/cancelamento;
- o processor continua podendo setar
cte+integrationno sucesso sem conflito com o vínculo já gravado no emit.
| Componente | Papel no vínculo |
|---|---|
EmitCteService |
Seta integration + flush na criação da task |
InvoicesWithoutCteService |
Filtra por cte null e ausência de invoice_task_id
|
ListInvoicesWithoutCteAction |
Endpoint HTTP da listagem |
CteEmissionProcessor / CteEmissionService
|
No sucesso, persistem CT-e e reforçam vínculos; não dependem de limpar o bind do emit |
-
tests/Service/EmitCteServiceTest.php— assert de bind imediato das NFs na Integration criada. -
tests/Service/InvoicesWithoutCteServiceTest.php— exclusão de NFs cominvoice_task_idpreenchido.
- Transições de
invoice_task/ integração: pending → processing → closed | error. - Sempre via
StatusDiscovery(real status + context), nunca IDs hard-coded. - Em erro: mensagem SEFAZ ou de validação gravada no payload/body da tarefa; NFs podem permanecer vinculadas à integração para auditoria (comportamento de reprocesso depende de operação).
Em tests/Service/Cte/:
CteXmlBuilderTestCteFiscalConfigTestCteEmissionProcessorTest
Em tests/Service/:
-
EmitCteServiceTest— validações de emit + bind imediatosetIntegration(#27) -
InvoicesWithoutCteServiceTest— exclusão de NFs cominvoice_task_id/ctepreenchidos
Cobrem montagem, carga de config, fluxo do processor (com mocks de SEFAZ/persistência) e o contrato de fila without-cte.
| Destino | URL |
|---|---|
| Issue (NFePHP + DACTE) | https://github.com/ControleOnline/api-platform-accounting/issues/16 |
| Issue (mínimo 1 NF no emit-cte) | https://github.com/ControleOnline/api-platform-accounting/issues/26 |
| Issue (vínculo imediato invoice_task_id no emit) | https://github.com/ControleOnline/api-platform-accounting/issues/27 |
| Issue (worker stub Closed falso) | https://github.com/ControleOnline/app-community/issues/689 |
| Home do módulo | https://github.com/ControleOnline/api-platform-accounting/wiki |
| Wiki API (pai) | https://github.com/ControleOnline/api-community/wiki |
| Wiki App | https://github.com/ControleOnline/app-community/wiki |
| Config fiscal (UI) | Módulo ui-common — aba Fiscal / Receita Federal da empresa |
| Download NF/CT-e | GET /invoice_taxes/{id}/download-nf |
- Novos campos de layout CT-e: preferir extensão em
CteXmlBuildere testes unitários; evitar crescer o processor. - Mudança de versão de schema SEFAZ: alinhar
schemes/versaoemCteSefazCliente validar em homologação (tpAmb=2). - Certificado: nunca logar senha ou conteúdo binário do PFX; apenas existência/ausência.
A coleção GET /invoice_taxes?invoiceModel=57 usa normalização invoice_tax:read. O Status embutido só serializa id/status/realStatus/color se esses campos declararem o grupo invoice_tax:read na entidade compartilhada Status (api-platform-common).
Documentação canônica: Status — grupos de serialização para recursos aninhados (fix #32).