nfse-br 0.0.0-stage → 2.0.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/LICENSE +21 -0
- package/README.md +202 -2
- package/assets/fonts/LICENSE-LiberationSans.txt +102 -0
- package/assets/fonts/LiberationSans-Bold.ttf +0 -0
- package/assets/fonts/LiberationSans-Regular.ttf +0 -0
- package/completions/_nfse +200 -0
- package/completions/nfse.bash +69 -0
- package/dist/cli/index.d.ts +3 -0
- package/dist/cli/index.d.ts.map +1 -0
- package/dist/cli/index.js +1355 -0
- package/dist/cli/index.js.map +1 -0
- package/dist/client.d.ts +39 -0
- package/dist/client.d.ts.map +1 -0
- package/dist/client.js +82 -0
- package/dist/client.js.map +1 -0
- package/dist/config/index.d.ts +45 -0
- package/dist/config/index.d.ts.map +1 -0
- package/dist/config/index.js +80 -0
- package/dist/config/index.js.map +1 -0
- package/dist/domain/errors.d.ts +111 -0
- package/dist/domain/errors.d.ts.map +1 -0
- package/dist/domain/errors.js +133 -0
- package/dist/domain/errors.js.map +1 -0
- package/dist/domain/signature-policy.d.ts +55 -0
- package/dist/domain/signature-policy.d.ts.map +1 -0
- package/dist/domain/signature-policy.js +84 -0
- package/dist/domain/signature-policy.js.map +1 -0
- package/dist/domain/types.d.ts +295 -0
- package/dist/domain/types.d.ts.map +1 -0
- package/dist/domain/types.js +9 -0
- package/dist/domain/types.js.map +1 -0
- package/dist/index.d.ts +40 -0
- package/dist/index.d.ts.map +1 -0
- package/dist/index.js +24 -0
- package/dist/index.js.map +1 -0
- package/dist/infra/certificate.d.ts +42 -0
- package/dist/infra/certificate.d.ts.map +1 -0
- package/dist/infra/certificate.js +106 -0
- package/dist/infra/certificate.js.map +1 -0
- package/dist/infra/transport-errors.d.ts +24 -0
- package/dist/infra/transport-errors.d.ts.map +1 -0
- package/dist/infra/transport-errors.js +95 -0
- package/dist/infra/transport-errors.js.map +1 -0
- package/dist/infra/xml-signer.d.ts +21 -0
- package/dist/infra/xml-signer.d.ts.map +1 -0
- package/dist/infra/xml-signer.js +55 -0
- package/dist/infra/xml-signer.js.map +1 -0
- package/dist/infra/xml.d.ts +32 -0
- package/dist/infra/xml.d.ts.map +1 -0
- package/dist/infra/xml.js +73 -0
- package/dist/infra/xml.js.map +1 -0
- package/dist/providers/giss/http-client.d.ts +11 -0
- package/dist/providers/giss/http-client.d.ts.map +1 -0
- package/dist/providers/giss/http-client.js +61 -0
- package/dist/providers/giss/http-client.js.map +1 -0
- package/dist/providers/giss/messages/parser.d.ts +72 -0
- package/dist/providers/giss/messages/parser.d.ts.map +1 -0
- package/dist/providers/giss/messages/parser.js +160 -0
- package/dist/providers/giss/messages/parser.js.map +1 -0
- package/dist/providers/giss/messages/provided-services.d.ts +64 -0
- package/dist/providers/giss/messages/provided-services.d.ts.map +1 -0
- package/dist/providers/giss/messages/provided-services.js +415 -0
- package/dist/providers/giss/messages/provided-services.js.map +1 -0
- package/dist/providers/giss/messages/taken-services.d.ts +32 -0
- package/dist/providers/giss/messages/taken-services.d.ts.map +1 -0
- package/dist/providers/giss/messages/taken-services.js +241 -0
- package/dist/providers/giss/messages/taken-services.js.map +1 -0
- package/dist/providers/giss/municipalities.d.ts +38 -0
- package/dist/providers/giss/municipalities.d.ts.map +1 -0
- package/dist/providers/giss/municipalities.js +225 -0
- package/dist/providers/giss/municipalities.js.map +1 -0
- package/dist/providers/giss/nfsc-service.d.ts +61 -0
- package/dist/providers/giss/nfsc-service.d.ts.map +1 -0
- package/dist/providers/giss/nfsc-service.js +88 -0
- package/dist/providers/giss/nfsc-service.js.map +1 -0
- package/dist/providers/giss/nfse-service.d.ts +130 -0
- package/dist/providers/giss/nfse-service.d.ts.map +1 -0
- package/dist/providers/giss/nfse-service.js +224 -0
- package/dist/providers/giss/nfse-service.js.map +1 -0
- package/dist/providers/giss/portal-service.d.ts +300 -0
- package/dist/providers/giss/portal-service.d.ts.map +1 -0
- package/dist/providers/giss/portal-service.js +444 -0
- package/dist/providers/giss/portal-service.js.map +1 -0
- package/dist/providers/giss/soap-client.d.ts +46 -0
- package/dist/providers/giss/soap-client.d.ts.map +1 -0
- package/dist/providers/giss/soap-client.js +131 -0
- package/dist/providers/giss/soap-client.js.map +1 -0
- package/dist/providers/nacional/danfse/danfse-data.d.ts +132 -0
- package/dist/providers/nacional/danfse/danfse-data.d.ts.map +1 -0
- package/dist/providers/nacional/danfse/danfse-data.js +436 -0
- package/dist/providers/nacional/danfse/danfse-data.js.map +1 -0
- package/dist/providers/nacional/danfse/danfse-pdf.d.ts +24 -0
- package/dist/providers/nacional/danfse/danfse-pdf.d.ts.map +1 -0
- package/dist/providers/nacional/danfse/danfse-pdf.js +466 -0
- package/dist/providers/nacional/danfse/danfse-pdf.js.map +1 -0
- package/dist/providers/nacional/danfse/ibge-municipalities.d.ts +5 -0
- package/dist/providers/nacional/danfse/ibge-municipalities.d.ts.map +1 -0
- package/dist/providers/nacional/danfse/ibge-municipalities.js +19 -0
- package/dist/providers/nacional/danfse/ibge-municipalities.js.map +1 -0
- package/dist/providers/nacional/danfse/logo.d.ts +6 -0
- package/dist/providers/nacional/danfse/logo.d.ts.map +1 -0
- package/dist/providers/nacional/danfse/logo.js +6 -0
- package/dist/providers/nacional/danfse/logo.js.map +1 -0
- package/dist/providers/nacional/messages.d.ts +99 -0
- package/dist/providers/nacional/messages.d.ts.map +1 -0
- package/dist/providers/nacional/messages.js +277 -0
- package/dist/providers/nacional/messages.js.map +1 -0
- package/dist/providers/nacional/national-client.d.ts +26 -0
- package/dist/providers/nacional/national-client.d.ts.map +1 -0
- package/dist/providers/nacional/national-client.js +56 -0
- package/dist/providers/nacional/national-client.js.map +1 -0
- package/dist/providers/nacional/national-service.d.ts +123 -0
- package/dist/providers/nacional/national-service.d.ts.map +1 -0
- package/dist/providers/nacional/national-service.js +233 -0
- package/dist/providers/nacional/national-service.js.map +1 -0
- package/dist/services/lookup-service.d.ts +41 -0
- package/dist/services/lookup-service.d.ts.map +1 -0
- package/dist/services/lookup-service.js +122 -0
- package/dist/services/lookup-service.js.map +1 -0
- package/dist/storage/contact-repository.d.ts +46 -0
- package/dist/storage/contact-repository.d.ts.map +1 -0
- package/dist/storage/contact-repository.js +105 -0
- package/dist/storage/contact-repository.js.map +1 -0
- package/dist/storage/invoice-sync.d.ts +12 -0
- package/dist/storage/invoice-sync.d.ts.map +1 -0
- package/dist/storage/invoice-sync.js +71 -0
- package/dist/storage/invoice-sync.js.map +1 -0
- package/dist/storage/profile-repository.d.ts +76 -0
- package/dist/storage/profile-repository.d.ts.map +1 -0
- package/dist/storage/profile-repository.js +135 -0
- package/dist/storage/profile-repository.js.map +1 -0
- package/dist/validation/schema-validator.d.ts +27 -0
- package/dist/validation/schema-validator.d.ts.map +1 -0
- package/dist/validation/schema-validator.js +68 -0
- package/dist/validation/schema-validator.js.map +1 -0
- package/package.json +90 -4
- package/schemas/giss/prestados/cabecalho-v2_04.xsd +25 -0
- package/schemas/giss/prestados/cancelar-nfse-envio-v2_04.xsd +23 -0
- package/schemas/giss/prestados/cancelar-nfse-reposta-v2_04.xsd +26 -0
- package/schemas/giss/prestados/consultar-lote-rps-envio-v2_04.xsd +29 -0
- package/schemas/giss/prestados/consultar-lote-rps-resposta-v2_04.xsd +40 -0
- package/schemas/giss/prestados/consultar-nfse-faixa-envio-v2_04.xsd +38 -0
- package/schemas/giss/prestados/consultar-nfse-faixa-resposta-v2_04.xsd +34 -0
- package/schemas/giss/prestados/consultar-nfse-rps-envio-v2_04.xsd +28 -0
- package/schemas/giss/prestados/consultar-nfse-rps-resposta-v2_04.xsd +26 -0
- package/schemas/giss/prestados/consultar-nfse-servico-prestado-envio-v2_04.xsd +58 -0
- package/schemas/giss/prestados/consultar-nfse-servico-prestado-resposta-v2_04.xsd +34 -0
- package/schemas/giss/prestados/consultar-nfse-servico-tomado-envio-v2_04.xsd +58 -0
- package/schemas/giss/prestados/consultar-nfse-servico-tomado-resposta-v2_04.xsd +34 -0
- package/schemas/giss/prestados/enviar-lote-rps-envio-v2_04.xsd +26 -0
- package/schemas/giss/prestados/enviar-lote-rps-resposta-v2_04.xsd +32 -0
- package/schemas/giss/prestados/enviar-lote-rps-sincrono-envio-v2_04.xsd +26 -0
- package/schemas/giss/prestados/enviar-lote-rps-sincrono-resposta-v2_04.xsd +44 -0
- package/schemas/giss/prestados/gerar-nfse-envio-v2_04.xsd +24 -0
- package/schemas/giss/prestados/gerar-nfse-resposta-v2_04.xsd +36 -0
- package/schemas/giss/prestados/nfse_v2-04.xsd +3492 -0
- package/schemas/giss/prestados/substituir-nfse-envio-v2_04.xsd +37 -0
- package/schemas/giss/prestados/substituir-nfse-resposta-v2_04.xsd +51 -0
- package/schemas/giss/prestados/tipos-v2_04.xsd +3176 -0
- package/schemas/giss/prestados/xmldsig-core-schema20020212.xsd +316 -0
- package/schemas/giss/tomados/cancelar-nota-servico-comprado-envio-v1_00.xsd +26 -0
- package/schemas/giss/tomados/cancelar-nota-servico-comprado-resposta-v1_00.xsd +33 -0
- package/schemas/giss/tomados/consultar-lote-nota-servico-comprado-envio-v1_00.xsd +33 -0
- package/schemas/giss/tomados/consultar-lote-nota-servico-comprado-resposta-v1_00.xsd +51 -0
- package/schemas/giss/tomados/consultar-nota-servico-comprado-envio-v1_00.xsd +38 -0
- package/schemas/giss/tomados/consultar-nota-servico-comprado-resposta-v1_00.xsd +33 -0
- package/schemas/giss/tomados/consultar-protocolo-nota-servico-comprado-envio-v1_00.xsd +35 -0
- package/schemas/giss/tomados/consultar-protocolo-nota-servico-comprado-resposta-v1_00.xsd +34 -0
- package/schemas/giss/tomados/emitir-nota-servico-comprado-envio-v1_00.xsd +28 -0
- package/schemas/giss/tomados/emitir-nota-servico-comprado-resposta-v1_00.xsd +32 -0
- package/schemas/giss/tomados/enviar-lote-nota-servico-comprado-envio-v1_00.xsd +31 -0
- package/schemas/giss/tomados/enviar-lote-nota-servico-comprado-resposta-v1_00.xsd +40 -0
- package/schemas/giss/tomados/tipos-servicos-comprados-v1_00.xsd +3370 -0
- package/schemas/giss/tomados/tipos-servicos-comprados-v1_01.xsd +540 -0
- package/schemas/giss/tomados/vigente/LEIA-ME.md +10 -0
- package/schemas/giss/tomados/vigente/cancelar-nota-servico-comprado-envio-v1_00.xsd +26 -0
- package/schemas/giss/tomados/vigente/cancelar-nota-servico-comprado-resposta-v1_00.xsd +33 -0
- package/schemas/giss/tomados/vigente/consultar-lote-nota-servico-comprado-envio-v1_00.xsd +33 -0
- package/schemas/giss/tomados/vigente/consultar-lote-nota-servico-comprado-resposta-v1_00.xsd +51 -0
- package/schemas/giss/tomados/vigente/consultar-nota-servico-comprado-envio-v1_00.xsd +38 -0
- package/schemas/giss/tomados/vigente/consultar-nota-servico-comprado-resposta-v1_00.xsd +33 -0
- package/schemas/giss/tomados/vigente/consultar-protocolo-nota-servico-comprado-envio-v1_00.xsd +35 -0
- package/schemas/giss/tomados/vigente/consultar-protocolo-nota-servico-comprado-resposta-v1_00.xsd +34 -0
- package/schemas/giss/tomados/vigente/emitir-nota-servico-comprado-envio-v1_00.xsd +28 -0
- package/schemas/giss/tomados/vigente/emitir-nota-servico-comprado-resposta-v1_00.xsd +32 -0
- package/schemas/giss/tomados/vigente/enviar-lote-nota-servico-comprado-envio-v1_00.xsd +31 -0
- package/schemas/giss/tomados/vigente/enviar-lote-nota-servico-comprado-resposta-v1_00.xsd +40 -0
- package/schemas/giss/tomados/vigente/tipos-servicos-comprados-v1_00.xsd +3221 -0
- package/schemas/giss/tomados/vigente/xmldsig-core-schema20020212.xsd +316 -0
- package/schemas/giss/tomados/xmldsig-core-schema20020212.xsd +316 -0
- package/schemas/nacional/CNC_v1.00.xsd +14 -0
- package/schemas/nacional/DPS_v1.00.xsd +13 -0
- package/schemas/nacional/DPS_v1.01.xsd +14 -0
- package/schemas/nacional/NFSe_v1.00.xsd +14 -0
- package/schemas/nacional/NFSe_v1.01.xsd +15 -0
- package/schemas/nacional/evento_v1.00.xsd +15 -0
- package/schemas/nacional/evento_v1.01.xsd +15 -0
- package/schemas/nacional/pedRegEvento_v1.00.xsd +15 -0
- package/schemas/nacional/pedRegEvento_v1.01.xsd +15 -0
- package/schemas/nacional/tiposCnc_v1.00.xsd +166 -0
- package/schemas/nacional/tiposComplexos_v1.00.xsd +1828 -0
- package/schemas/nacional/tiposComplexos_v1.01.xsd +2709 -0
- package/schemas/nacional/tiposEventos_v1.00.xsd +733 -0
- package/schemas/nacional/tiposEventos_v1.01.xsd +780 -0
- package/schemas/nacional/tiposSimples_v1.00.xsd +1658 -0
- package/schemas/nacional/tiposSimples_v1.01.xsd +1844 -0
- package/schemas/nacional/xmldsig-core-schema.xsd +318 -0
- package/schemas/nacional-restrita/CNC_v1.00.xsd +14 -0
- package/schemas/nacional-restrita/DPS_v1.01.xsd +14 -0
- package/schemas/nacional-restrita/NFSe_v1.01.xsd +15 -0
- package/schemas/nacional-restrita/evento_v1.01.xsd +15 -0
- package/schemas/nacional-restrita/pedRegEvento_v1.01.xsd +15 -0
- package/schemas/nacional-restrita/tiposCnc_v1.00.xsd +166 -0
- package/schemas/nacional-restrita/tiposComplexos_v1.01.xsd +2747 -0
- package/schemas/nacional-restrita/tiposEventos_v1.01.xsd +780 -0
- package/schemas/nacional-restrita/tiposSimples_v1.01.xsd +1865 -0
- package/schemas/nacional-restrita/xmldsig-core-schema.xsd +309 -0
|
@@ -0,0 +1,1355 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
import { existsSync, mkdirSync, readFileSync, writeFileSync } from "node:fs";
|
|
3
|
+
import { dirname, resolve } from "node:path";
|
|
4
|
+
import { parseArgs } from "node:util";
|
|
5
|
+
import { loadPortalCredentials, resolveCityCode, } from "../config/index.js";
|
|
6
|
+
import { GissError, NationalError, NfseError, } from "../domain/errors.js";
|
|
7
|
+
import { exportPem } from "../infra/certificate.js";
|
|
8
|
+
import { isoDate } from "../infra/xml.js";
|
|
9
|
+
import { nfseNumberFromKey, parseNationalEvent, parseNationalNfse, } from "../providers/nacional/messages.js";
|
|
10
|
+
import { NfseClient } from "../client.js";
|
|
11
|
+
import { renderDanfse } from "../providers/nacional/danfse/danfse-pdf.js";
|
|
12
|
+
import { lookupParty, lookupZip, } from "../services/lookup-service.js";
|
|
13
|
+
import { buildPortalParty, PortalService, } from "../providers/giss/portal-service.js";
|
|
14
|
+
import { findMunicipalityByCode, MUNICIPALITIES } from "../providers/giss/municipalities.js";
|
|
15
|
+
import { ContactRepository, taxIdOf, } from "../storage/contact-repository.js";
|
|
16
|
+
import { syncFromInvoices } from "../storage/invoice-sync.js";
|
|
17
|
+
import { buildRps, ProfileRepository } from "../storage/profile-repository.js";
|
|
18
|
+
import { SCHEMA_DIRECTORIES, validateAgainstSchema, } from "../validation/schema-validator.js";
|
|
19
|
+
/** `nfse ...` quando instalado (`giss` segue como apelido); `npm run nfse -- ...` no repositório. */
|
|
20
|
+
const INVOCATION = process.env["npm_lifecycle_event"] ? "npm run nfse --" : "nfse";
|
|
21
|
+
const HELP = `
|
|
22
|
+
nfse — Brazilian NFS-e client: GissOnline (ABRASF 2.04) and the Sistema Nacional NFS-e
|
|
23
|
+
|
|
24
|
+
Usage: ${INVOCATION} <command> [options]
|
|
25
|
+
|
|
26
|
+
CERTIFICATE
|
|
27
|
+
cert [--export [--out DIR]] A1 certificate details; --export writes the PEM files
|
|
28
|
+
|
|
29
|
+
QUERIES (services provided)
|
|
30
|
+
latest [--limit N] [--months N] Most recent invoices (default: 10, last 12 months)
|
|
31
|
+
issued --from D --to D Invoices by issue period
|
|
32
|
+
[--competence] use the competence period instead
|
|
33
|
+
[--number N] [--page N] [--all]
|
|
34
|
+
range --first N --last N [--page N] Invoices by number range
|
|
35
|
+
rps --number N --series S [--type 1] Invoice generated from an RPS
|
|
36
|
+
batch --protocol P Status of an RPS batch
|
|
37
|
+
|
|
38
|
+
QUERIES (services received)
|
|
39
|
+
received --from D --to D Invoices where you are the customer
|
|
40
|
+
[--competence] [--number N] [--page N] [--all]
|
|
41
|
+
purchased-batch --protocol P Invoices declared in a batch (nfsc)
|
|
42
|
+
purchased-protocol --protocol P Status of a protocol (nfsc)
|
|
43
|
+
purchased-number --from D --to D --number N --series S
|
|
44
|
+
|
|
45
|
+
ISSUING
|
|
46
|
+
issue --customer X --amount V [--description T] Issues an invoice
|
|
47
|
+
[--rps N] [--series S] [--competence D]
|
|
48
|
+
[--item 01.09] [--cnae N] [--nbs N] [--rate 3.07]
|
|
49
|
+
[--csll V] [--inss V] [--income-tax V] [--notes T] [--confirm]
|
|
50
|
+
cancel --number N --reason 1..5 [--confirm] Cancels an invoice
|
|
51
|
+
replace --number N --reason 1..5 --customer X --amount V
|
|
52
|
+
[--description T] [--confirm] Cancels and reissues
|
|
53
|
+
|
|
54
|
+
DOCUMENTS (portal REST — the Web Service issues no files)
|
|
55
|
+
pdf --number N [--out DIR|FILE] Downloads the invoice PDF
|
|
56
|
+
xml --number N [--out DIR|FILE] Downloads the invoice XML
|
|
57
|
+
|
|
58
|
+
LOCAL DIRECTORY
|
|
59
|
+
customers [--sync --from D --to D] Lists/updates customers
|
|
60
|
+
suppliers [--sync --from D --to D] Lists/updates suppliers
|
|
61
|
+
customer-add --tax-id D --name N [--alias A] [--registration N] [--email E] [--phone T]
|
|
62
|
+
[--street L --number N --district B --city IBGE --state UF --zip C]
|
|
63
|
+
[--complement C] [--trade-name F] [--simples 1|2]
|
|
64
|
+
supplier-add (same options as customer-add)
|
|
65
|
+
customer-rm --tax-id D
|
|
66
|
+
supplier-rm --tax-id D
|
|
67
|
+
|
|
68
|
+
PORTAL (REST API — the actual GissOnline directory, via CPF/password login)
|
|
69
|
+
portal-list [--type 1|2] Portal directory (1=customer, 2=supplier)
|
|
70
|
+
portal-add --tax-id D --name N [--type 1|2] Registers in the portal
|
|
71
|
+
[--trade-name F] [--registration N] [--simples] [--mei]
|
|
72
|
+
[--street L --number N --district B --city IBGE --state UF --zip C]
|
|
73
|
+
[--complement C] [--street-type Rua] [--confirm]
|
|
74
|
+
portal-rm --tax-id D [--type 1|2] [--confirm] Removes from the portal
|
|
75
|
+
portal-import [--type 1|2] Imports the portal directory locally
|
|
76
|
+
|
|
77
|
+
LOOKUPS (BrasilAPI — convenience, not a source of truth)
|
|
78
|
+
zip <cep> Street, district, city and state for a postal code
|
|
79
|
+
cnpj <cnpj> Company details, address and IBGE code
|
|
80
|
+
|
|
81
|
+
Both customer-add and portal-add accept --lookup, which fills the missing
|
|
82
|
+
fields from the CNPJ before registering. Anything you pass explicitly wins.
|
|
83
|
+
|
|
84
|
+
MUNICIPALITIES
|
|
85
|
+
cities [--state SP] Cities known to publish the Web Service
|
|
86
|
+
|
|
87
|
+
PORTAL MESSAGES (Fale Conosco)
|
|
88
|
+
messages [--id N] [--unread] Messages you opened, or one with its reply
|
|
89
|
+
[--attachment N] [--out DIR] Downloads an attachment of that message
|
|
90
|
+
message --subject S --text T [--confirm] Opens a message to the city hall
|
|
91
|
+
|
|
92
|
+
NATIONAL ISSUER (Sistema Nacional NFS-e — mandatory for Simples from 2026-11-01)
|
|
93
|
+
national-status City adhesion and whether issuing is possible
|
|
94
|
+
national-get --key K Invoice by its 50-digit access key
|
|
95
|
+
national-pdf --key K [--out DIR|FILE] DANFSe (the national PDF)
|
|
96
|
+
national-xml --key K [--out DIR|FILE] Invoice XML
|
|
97
|
+
national-docs [--from NSU] Invoices and events where your CNPJ appears
|
|
98
|
+
danfse <nfse.xml> [--event FILE]... [--out DIR|FILE] DANFSe from a saved XML (offline)
|
|
99
|
+
[--status cancelled|replaced] Watermark; otherwise taken from the events
|
|
100
|
+
With NFSE_EMISSOR=nacional (or --issuer nacional), issue and cancel go
|
|
101
|
+
through the national API: issue takes [--dps N] to make a retry safe, and
|
|
102
|
+
cancel takes --key K --reason 1|2|9 --text "15 to 255 characters".
|
|
103
|
+
|
|
104
|
+
MUNICIPAL ACTIVITIES (the source of CodigoTributacaoMunicipio)
|
|
105
|
+
activities [term] [--item 1.09] [--city IBGE] City activity table (no login)
|
|
106
|
+
[--company] [--date YYYY-MM-DD] Only the ones your company is bound to
|
|
107
|
+
|
|
108
|
+
TAX PROFILE
|
|
109
|
+
profile [--save] Shows (or writes to data/profile.json) the defaults
|
|
110
|
+
|
|
111
|
+
Global options:
|
|
112
|
+
--env producao|homologacao Environment (default: GISS_ENV from .env)
|
|
113
|
+
--issuer giss|nacional Who issues (default: NFSE_EMISSOR; else nacional for issue, by argument for cancel)
|
|
114
|
+
--json | --xml | --debug Output format / diagnostics
|
|
115
|
+
`;
|
|
116
|
+
const options = {
|
|
117
|
+
env: { type: "string" },
|
|
118
|
+
issuer: { type: "string" },
|
|
119
|
+
key: { type: "string" },
|
|
120
|
+
dps: { type: "string" },
|
|
121
|
+
from: { type: "string" },
|
|
122
|
+
to: { type: "string" },
|
|
123
|
+
competence: { type: "string" },
|
|
124
|
+
number: { type: "string" },
|
|
125
|
+
series: { type: "string" },
|
|
126
|
+
type: { type: "string" },
|
|
127
|
+
page: { type: "string" },
|
|
128
|
+
limit: { type: "string" },
|
|
129
|
+
months: { type: "string" },
|
|
130
|
+
all: { type: "boolean", default: false },
|
|
131
|
+
first: { type: "string" },
|
|
132
|
+
last: { type: "string" },
|
|
133
|
+
protocol: { type: "string" },
|
|
134
|
+
export: { type: "boolean", default: false },
|
|
135
|
+
out: { type: "string" },
|
|
136
|
+
customer: { type: "string" },
|
|
137
|
+
amount: { type: "string" },
|
|
138
|
+
description: { type: "string" },
|
|
139
|
+
notes: { type: "string" },
|
|
140
|
+
rps: { type: "string" },
|
|
141
|
+
csll: { type: "string" },
|
|
142
|
+
rate: { type: "string" },
|
|
143
|
+
item: { type: "string" },
|
|
144
|
+
cnae: { type: "string" },
|
|
145
|
+
nbs: { type: "string" },
|
|
146
|
+
inss: { type: "string" },
|
|
147
|
+
"income-tax": { type: "string" },
|
|
148
|
+
reason: { type: "string" },
|
|
149
|
+
event: { type: "string", multiple: true },
|
|
150
|
+
status: { type: "string" },
|
|
151
|
+
confirm: { type: "boolean", default: false },
|
|
152
|
+
sync: { type: "boolean", default: false },
|
|
153
|
+
"tax-id": { type: "string" },
|
|
154
|
+
name: { type: "string" },
|
|
155
|
+
"trade-name": { type: "string" },
|
|
156
|
+
registration: { type: "string" },
|
|
157
|
+
email: { type: "string" },
|
|
158
|
+
phone: { type: "string" },
|
|
159
|
+
alias: { type: "string" },
|
|
160
|
+
state: { type: "string" },
|
|
161
|
+
street: { type: "string" },
|
|
162
|
+
district: { type: "string" },
|
|
163
|
+
complement: { type: "string" },
|
|
164
|
+
city: { type: "string" },
|
|
165
|
+
zip: { type: "string" },
|
|
166
|
+
simples: { type: "string" },
|
|
167
|
+
company: { type: "boolean", default: false },
|
|
168
|
+
date: { type: "string" },
|
|
169
|
+
id: { type: "string" },
|
|
170
|
+
attachment: { type: "string" },
|
|
171
|
+
subject: { type: "string" },
|
|
172
|
+
text: { type: "string" },
|
|
173
|
+
unread: { type: "boolean", default: false },
|
|
174
|
+
save: { type: "boolean", default: false },
|
|
175
|
+
"street-type": { type: "string" },
|
|
176
|
+
mei: { type: "boolean", default: false },
|
|
177
|
+
lookup: { type: "boolean", default: false },
|
|
178
|
+
json: { type: "boolean", default: false },
|
|
179
|
+
xml: { type: "boolean", default: false },
|
|
180
|
+
debug: { type: "boolean", default: false },
|
|
181
|
+
help: { type: "boolean", default: false },
|
|
182
|
+
};
|
|
183
|
+
/**
|
|
184
|
+
* Carrega o `.env` do diretório atual quando existe. Instalado como binário
|
|
185
|
+
* global não há `--env-file`, e as credenciais precisam vir de algum lugar;
|
|
186
|
+
* variáveis já definidas no ambiente têm precedência e não são sobrescritas.
|
|
187
|
+
*/
|
|
188
|
+
function loadDotEnv() {
|
|
189
|
+
const file = resolve(process.cwd(), ".env");
|
|
190
|
+
if (!existsSync(file))
|
|
191
|
+
return;
|
|
192
|
+
try {
|
|
193
|
+
process.loadEnvFile(file);
|
|
194
|
+
}
|
|
195
|
+
catch {
|
|
196
|
+
// Node < 20.12 não tem loadEnvFile; nesse caso o .env é ignorado
|
|
197
|
+
}
|
|
198
|
+
}
|
|
199
|
+
async function main() {
|
|
200
|
+
loadDotEnv();
|
|
201
|
+
const { values, positionals } = parseArgs({
|
|
202
|
+
args: process.argv.slice(2),
|
|
203
|
+
options,
|
|
204
|
+
allowPositionals: true,
|
|
205
|
+
});
|
|
206
|
+
const command = positionals[0];
|
|
207
|
+
if (!command || values.help) {
|
|
208
|
+
console.log(HELP);
|
|
209
|
+
return;
|
|
210
|
+
}
|
|
211
|
+
// Comandos de cadastro local e perfil não tocam a rede nem o certificado.
|
|
212
|
+
if (await runLocalCommand(command, values, positionals))
|
|
213
|
+
return;
|
|
214
|
+
// A tabela de atividades vem do portal, não do Web Service: pede município,
|
|
215
|
+
// não certificado. Fica antes do cliente SOAP para não exigir o .pfx.
|
|
216
|
+
if (command === "activities") {
|
|
217
|
+
await runActivitiesCommand(values, positionals);
|
|
218
|
+
return;
|
|
219
|
+
}
|
|
220
|
+
// O Fale Conosco também vive no portal; o certificado só entra quando é ele
|
|
221
|
+
// que abre a sessão.
|
|
222
|
+
if (command === "messages" || command === "message") {
|
|
223
|
+
await runMessageCommand(command, values);
|
|
224
|
+
return;
|
|
225
|
+
}
|
|
226
|
+
const client = new NfseClient({
|
|
227
|
+
environment: values.env,
|
|
228
|
+
issuer: values.issuer,
|
|
229
|
+
debug: values.debug,
|
|
230
|
+
});
|
|
231
|
+
warnCertificateExpiry(client.certificate);
|
|
232
|
+
if (await runNationalCommand(command, values, client))
|
|
233
|
+
return;
|
|
234
|
+
const asNumber = (v) => (v === undefined ? undefined : Number(v));
|
|
235
|
+
switch (command) {
|
|
236
|
+
case "cert": {
|
|
237
|
+
const { subject, validFrom, validTo } = client.certificate;
|
|
238
|
+
console.log(`Holder: ${subject}`);
|
|
239
|
+
console.log(`Valid from: ${validFrom.toISOString().slice(0, 10)}`);
|
|
240
|
+
console.log(`Valid until: ${validTo.toISOString().slice(0, 10)}`);
|
|
241
|
+
console.log(`Environment: ${client.config.environment}`);
|
|
242
|
+
console.log(`Host: ${client.config.host}`);
|
|
243
|
+
console.log(`Provider: CNPJ ${client.config.cnpj} / IM ${client.config.municipalRegistration}`);
|
|
244
|
+
if (values.export) {
|
|
245
|
+
const target = values.out ?? dirname(client.config.certificatePath);
|
|
246
|
+
const files = exportPem(client.certificate, target);
|
|
247
|
+
console.log("\nPEM files written:");
|
|
248
|
+
console.log(` certificate: ${files.certificate}`);
|
|
249
|
+
console.log(` key: ${files.key}`);
|
|
250
|
+
if (files.chain)
|
|
251
|
+
console.log(` chain: ${files.chain}`);
|
|
252
|
+
console.log(` bundle: ${files.bundle}`);
|
|
253
|
+
console.log("\nThe key has no passphrase — keep these files out of version control.");
|
|
254
|
+
}
|
|
255
|
+
return;
|
|
256
|
+
}
|
|
257
|
+
case "latest": {
|
|
258
|
+
const limit = asNumber(values.limit) ?? 10;
|
|
259
|
+
const months = asNumber(values.months) ?? 12;
|
|
260
|
+
const to = new Date();
|
|
261
|
+
const from = new Date(to);
|
|
262
|
+
from.setMonth(from.getMonth() - months);
|
|
263
|
+
const invoices = [];
|
|
264
|
+
for await (const page of client.paginate((page) => client.nfse.queryProvidedServices({
|
|
265
|
+
issuePeriod: { from: isoDate(from), to: isoDate(to) },
|
|
266
|
+
page,
|
|
267
|
+
}))) {
|
|
268
|
+
invoices.push(...page.invoices);
|
|
269
|
+
}
|
|
270
|
+
// O serviço devolve em ordem crescente; as mais recentes ficam no fim.
|
|
271
|
+
const latest = invoices.slice(-limit).reverse();
|
|
272
|
+
printInvoices({ invoices: latest, warnings: [], xml: "" }, values);
|
|
273
|
+
if (!values.json && !values.xml) {
|
|
274
|
+
console.log(`\n${latest.length} of ${invoices.length} invoice(s) in the last ${months} months`);
|
|
275
|
+
}
|
|
276
|
+
return;
|
|
277
|
+
}
|
|
278
|
+
case "issued":
|
|
279
|
+
case "received": {
|
|
280
|
+
const taken = command === "received";
|
|
281
|
+
const filter = values.number
|
|
282
|
+
? { nfseNumber: values.number }
|
|
283
|
+
: dateFilter(values.from, values.to, Boolean(values.competence));
|
|
284
|
+
const query = (page) => taken
|
|
285
|
+
? client.nfse.queryTakenServices({ ...filter, page })
|
|
286
|
+
: client.nfse.queryProvidedServices({ ...filter, page });
|
|
287
|
+
if (values.all && !values.number) {
|
|
288
|
+
let total = 0;
|
|
289
|
+
for await (const page of client.paginate(query)) {
|
|
290
|
+
total += page.invoices.length;
|
|
291
|
+
printInvoices(page, values, taken);
|
|
292
|
+
}
|
|
293
|
+
if (!values.json && !values.xml)
|
|
294
|
+
console.log(`\nTotal: ${total} invoice(s)`);
|
|
295
|
+
return;
|
|
296
|
+
}
|
|
297
|
+
printInvoices(await query(asNumber(values.page) ?? 1), values, taken);
|
|
298
|
+
return;
|
|
299
|
+
}
|
|
300
|
+
case "range": {
|
|
301
|
+
if (!values.first || !values.last) {
|
|
302
|
+
throw new Error("Provide --first and --last with the invoice number range");
|
|
303
|
+
}
|
|
304
|
+
printInvoices(await client.nfse.queryNfseRange({
|
|
305
|
+
firstNumber: values.first,
|
|
306
|
+
lastNumber: values.last,
|
|
307
|
+
page: asNumber(values.page),
|
|
308
|
+
}), values);
|
|
309
|
+
return;
|
|
310
|
+
}
|
|
311
|
+
case "rps": {
|
|
312
|
+
if (!values.number || !values.series) {
|
|
313
|
+
throw new Error("Provide the RPS --number and --series");
|
|
314
|
+
}
|
|
315
|
+
printInvoices(await client.nfse.queryNfseByRps({
|
|
316
|
+
number: values.number,
|
|
317
|
+
series: values.series,
|
|
318
|
+
type: values.type ? Number(values.type) : undefined,
|
|
319
|
+
}), values);
|
|
320
|
+
return;
|
|
321
|
+
}
|
|
322
|
+
case "batch": {
|
|
323
|
+
if (!values.protocol)
|
|
324
|
+
throw new Error("Provide the batch --protocol");
|
|
325
|
+
const result = await client.nfse.queryRpsBatch(values.protocol);
|
|
326
|
+
if (values.xml)
|
|
327
|
+
return void console.log(result.xml);
|
|
328
|
+
if (values.json)
|
|
329
|
+
return void console.log(JSON.stringify(result, null, 2));
|
|
330
|
+
console.log(`Status: ${result.status} — ${result.statusLabel}`);
|
|
331
|
+
if (result.batchNumber)
|
|
332
|
+
console.log(`Batch: ${result.batchNumber}`);
|
|
333
|
+
if (result.receivedAt)
|
|
334
|
+
console.log(`Received: ${result.receivedAt}`);
|
|
335
|
+
printInvoices(result, values);
|
|
336
|
+
return;
|
|
337
|
+
}
|
|
338
|
+
case "purchased-batch": {
|
|
339
|
+
if (!values.protocol)
|
|
340
|
+
throw new Error("Provide the --protocol");
|
|
341
|
+
printInvoices(await client.nfsc.queryPurchasedByBatch(values.protocol), values);
|
|
342
|
+
return;
|
|
343
|
+
}
|
|
344
|
+
case "purchased-protocol": {
|
|
345
|
+
if (!values.protocol)
|
|
346
|
+
throw new Error("Provide the --protocol");
|
|
347
|
+
const result = await client.nfsc.queryPurchasedByProtocol(values.protocol);
|
|
348
|
+
if (values.xml)
|
|
349
|
+
return void console.log(result.xml);
|
|
350
|
+
console.log(`Status: ${result.status} — ${result.statusLabel}`);
|
|
351
|
+
printInvoices(result, values);
|
|
352
|
+
return;
|
|
353
|
+
}
|
|
354
|
+
case "purchased-number": {
|
|
355
|
+
if (!values.from || !values.to || !values.number || !values.series) {
|
|
356
|
+
throw new Error("Provide --from, --to, --number and --series");
|
|
357
|
+
}
|
|
358
|
+
const range = { from: values.from, to: values.to };
|
|
359
|
+
printInvoices(await client.nfsc.queryPurchasedByNumber({
|
|
360
|
+
issuePeriod: range,
|
|
361
|
+
competencePeriod: range,
|
|
362
|
+
declaredNumber: values.number,
|
|
363
|
+
declaredSeries: values.series,
|
|
364
|
+
}), values, true);
|
|
365
|
+
return;
|
|
366
|
+
}
|
|
367
|
+
case "issue": {
|
|
368
|
+
const rps = buildRpsFromCli(values);
|
|
369
|
+
warnSimplesDeadline(rps);
|
|
370
|
+
if (!values.confirm) {
|
|
371
|
+
const preview = client.nfse.previewIssueNfse(rps);
|
|
372
|
+
if (values.xml)
|
|
373
|
+
return void console.log(preview);
|
|
374
|
+
console.log(issueSummary(values, rps));
|
|
375
|
+
printValidation(preview);
|
|
376
|
+
console.log("\nNothing was sent. Repeat with --confirm to actually issue.");
|
|
377
|
+
return;
|
|
378
|
+
}
|
|
379
|
+
// GerarNfse e RecepcionarLoteRpsSincrono respondem A01 mesmo com o XML
|
|
380
|
+
// correto; o lote assíncrono é o único caminho que emite. `issueRps`
|
|
381
|
+
// cuida do protocolo e da espera, e é ele que garante que repetir o
|
|
382
|
+
// comando com o mesmo --rps não gere uma segunda nota.
|
|
383
|
+
const rpsNumber = values.rps ?? String(Date.now() % 100000);
|
|
384
|
+
if (!values.rps) {
|
|
385
|
+
console.log(`RPS ${rpsNumber} (generated). Pass --rps to make a retry safe to repeat.`);
|
|
386
|
+
}
|
|
387
|
+
const withRps = rps.identification
|
|
388
|
+
? rps
|
|
389
|
+
: {
|
|
390
|
+
...rps,
|
|
391
|
+
identification: {
|
|
392
|
+
number: rpsNumber,
|
|
393
|
+
series: values.series ?? "A",
|
|
394
|
+
type: 1,
|
|
395
|
+
},
|
|
396
|
+
issueDate: new Date(),
|
|
397
|
+
status: 1,
|
|
398
|
+
};
|
|
399
|
+
const outcome = await client.nfse.issueRps(withRps);
|
|
400
|
+
if (outcome.status === "already-issued") {
|
|
401
|
+
console.log("This RPS had already been issued:");
|
|
402
|
+
printInvoices({ invoices: [outcome.invoice], warnings: [], xml: "" }, values);
|
|
403
|
+
return;
|
|
404
|
+
}
|
|
405
|
+
if (outcome.invoice) {
|
|
406
|
+
console.log("Invoice issued:");
|
|
407
|
+
printInvoices({ invoices: [outcome.invoice], warnings: outcome.warnings, xml: "" }, values);
|
|
408
|
+
return;
|
|
409
|
+
}
|
|
410
|
+
for (const warning of outcome.warnings) {
|
|
411
|
+
console.log(`⚠ [${warning.code}] ${warning.message}`);
|
|
412
|
+
}
|
|
413
|
+
console.log(outcome.status === "rejected"
|
|
414
|
+
? `Rejected. Protocol: ${outcome.protocol}`
|
|
415
|
+
: `Still processing. Query it with: ${INVOCATION} batch --protocol ${outcome.protocol}`);
|
|
416
|
+
console.log(`Repeating with --rps ${rpsNumber} is safe: an already-issued RPS is not issued twice.`);
|
|
417
|
+
return;
|
|
418
|
+
}
|
|
419
|
+
case "cancel": {
|
|
420
|
+
if (!values.number || !values.reason) {
|
|
421
|
+
throw new Error("Provide the invoice --number and --reason (1 to 5)");
|
|
422
|
+
}
|
|
423
|
+
if (!values.confirm) {
|
|
424
|
+
console.log(`Would cancel invoice ${values.number}, reason ${values.reason} (${REASONS[values.reason] ?? "?"}).`);
|
|
425
|
+
console.log("Nothing was sent. Repeat with --confirm.");
|
|
426
|
+
return;
|
|
427
|
+
}
|
|
428
|
+
const result = await client.nfse.cancelNfse({
|
|
429
|
+
nfseNumber: values.number,
|
|
430
|
+
cancellationCode: Number(values.reason),
|
|
431
|
+
});
|
|
432
|
+
console.log(`Invoice ${values.number} cancelled.`);
|
|
433
|
+
if (result.cancelledAt)
|
|
434
|
+
console.log(`Timestamp: ${result.cancelledAt}`);
|
|
435
|
+
return;
|
|
436
|
+
}
|
|
437
|
+
case "replace": {
|
|
438
|
+
if (!values.number || !values.reason) {
|
|
439
|
+
throw new Error("Provide the replaced invoice --number and --reason (1 to 5)");
|
|
440
|
+
}
|
|
441
|
+
const rps = buildRpsFromCli(values);
|
|
442
|
+
if (!values.confirm) {
|
|
443
|
+
console.log(`Would replace invoice ${values.number} (reason ${values.reason}) with:\n`);
|
|
444
|
+
console.log(issueSummary(values, rps));
|
|
445
|
+
console.log("\nNothing was sent. Repeat with --confirm.");
|
|
446
|
+
return;
|
|
447
|
+
}
|
|
448
|
+
const result = await client.nfse.replaceNfse({
|
|
449
|
+
nfseNumber: values.number,
|
|
450
|
+
cancellationCode: Number(values.reason),
|
|
451
|
+
}, rps);
|
|
452
|
+
console.log(`Invoice ${values.number} replaced by:`);
|
|
453
|
+
printInvoices(result, values);
|
|
454
|
+
return;
|
|
455
|
+
}
|
|
456
|
+
case "pdf":
|
|
457
|
+
case "xml": {
|
|
458
|
+
if (!values.number)
|
|
459
|
+
throw new Error("Provide the invoice --number");
|
|
460
|
+
// O arquivo é identificado pelo id interno da nota, que só a consulta
|
|
461
|
+
// conhece — o número impresso não serve na rota do portal.
|
|
462
|
+
const { invoices } = await client.nfse.queryProvidedServices({
|
|
463
|
+
nfseNumber: values.number,
|
|
464
|
+
});
|
|
465
|
+
const invoice = invoices[0];
|
|
466
|
+
if (!invoice)
|
|
467
|
+
throw new Error(`Invoice ${values.number} was not found`);
|
|
468
|
+
if (!invoice.internalId) {
|
|
469
|
+
throw new Error(`Invoice ${values.number} has no internal id to download`);
|
|
470
|
+
}
|
|
471
|
+
const portal = await PortalService.authenticate(portalCredentials(client.config, client.certificate));
|
|
472
|
+
const file = await portal.invoiceDocument(invoice.internalId, command);
|
|
473
|
+
const target = documentTarget(values.out, invoice.number, command);
|
|
474
|
+
mkdirSync(dirname(target), { recursive: true });
|
|
475
|
+
writeFileSync(target, file);
|
|
476
|
+
console.log(`Invoice ${invoice.number} | ${invoice.taker?.legalName ?? ""}`);
|
|
477
|
+
console.log(`${target} (${(file.length / 1024).toFixed(1)} KB)`);
|
|
478
|
+
return;
|
|
479
|
+
}
|
|
480
|
+
case "portal-list":
|
|
481
|
+
case "portal-add":
|
|
482
|
+
case "portal-rm":
|
|
483
|
+
case "portal-import":
|
|
484
|
+
await runPortalCommand(command, values, client.config);
|
|
485
|
+
return;
|
|
486
|
+
case "customers":
|
|
487
|
+
case "suppliers": {
|
|
488
|
+
const role = command === "customers" ? "customer" : "supplier";
|
|
489
|
+
const repository = new ContactRepository();
|
|
490
|
+
if (values.sync) {
|
|
491
|
+
const filter = dateFilter(values.from, values.to, Boolean(values.competence));
|
|
492
|
+
const invoices = [];
|
|
493
|
+
const query = (page) => role === "customer"
|
|
494
|
+
? client.nfse.queryProvidedServices({ ...filter, page })
|
|
495
|
+
: client.nfse.queryTakenServices({ ...filter, page });
|
|
496
|
+
for await (const page of client.paginate(query)) {
|
|
497
|
+
invoices.push(...page.invoices);
|
|
498
|
+
}
|
|
499
|
+
const { saved } = syncFromInvoices(repository, role, invoices);
|
|
500
|
+
console.log(`${saved} ${roleLabel(role)}(s) synced from ${invoices.length} invoice(s).\n`);
|
|
501
|
+
}
|
|
502
|
+
printContacts(repository, role, values);
|
|
503
|
+
return;
|
|
504
|
+
}
|
|
505
|
+
default:
|
|
506
|
+
throw new Error(`Unknown command: ${command}`);
|
|
507
|
+
}
|
|
508
|
+
}
|
|
509
|
+
/** Avisa com 30 dias de antecedência: certificado vencido para toda emissão. */
|
|
510
|
+
function warnCertificateExpiry(certificate, now = new Date()) {
|
|
511
|
+
const days = Math.floor((certificate.validTo.getTime() - now.getTime()) / 86_400_000);
|
|
512
|
+
if (days < 0 || days > 30)
|
|
513
|
+
return;
|
|
514
|
+
console.error(`⚠ The A1 certificate expires in ${days} day(s), on ${isoDate(certificate.validTo)}. ` +
|
|
515
|
+
"Renew it before then: an expired certificate stops every request.\n");
|
|
516
|
+
}
|
|
517
|
+
/** Data em que o Simples Nacional deixa de emitir pelo sistema municipal. */
|
|
518
|
+
const SIMPLES_NATIONAL_DEADLINE = "2026-11-01";
|
|
519
|
+
/**
|
|
520
|
+
* A Res. CGSN 191/2026 tira o Simples dos emissores municipais em 01/11/2026.
|
|
521
|
+
* O aviso não bloqueia: o município pode aceitar por mais tempo, e quem decide
|
|
522
|
+
* é a resposta do serviço — mas ninguém deveria descobrir isso pela recusa.
|
|
523
|
+
*/
|
|
524
|
+
function warnSimplesDeadline(rps) {
|
|
525
|
+
if (rps.simplesNacionalOptant !== 1)
|
|
526
|
+
return;
|
|
527
|
+
if (isoDate(new Date()) < SIMPLES_NATIONAL_DEADLINE)
|
|
528
|
+
return;
|
|
529
|
+
// stderr: o aviso não pode entrar na saída de `--xml`/`--json`, que é para pipe
|
|
530
|
+
console.error(`⚠ Simples Nacional issues through the national system since ${SIMPLES_NATIONAL_DEADLINE} ` +
|
|
531
|
+
"(Res. CGSN 191/2026). Set NFSE_EMISSOR=nacional — see docs/national.md.\n");
|
|
532
|
+
}
|
|
533
|
+
/**
|
|
534
|
+
* Quem emite quando o usuário não disse: `NFSE_EMISSOR` e `--issuer` sempre mandam.
|
|
535
|
+
* Sem eles, `issue` vai ao nacional — é o padrão do país e o único que o Simples
|
|
536
|
+
* ainda tem desde 01/11/2026. `cancel` segue o argumento: `--key` é do nacional,
|
|
537
|
+
* `--number` é do GissOnline. `replace` só existe no GissOnline, salvo para o
|
|
538
|
+
* optante do Simples, que não tem mais o que substituir lá (o erro explica).
|
|
539
|
+
*/
|
|
540
|
+
function resolveIssuer(config, command, values) {
|
|
541
|
+
if (config.issuerExplicit)
|
|
542
|
+
return { issuer: config.issuer, source: "explicit" };
|
|
543
|
+
if (command === "issue")
|
|
544
|
+
return { issuer: "nacional", source: "automatic" };
|
|
545
|
+
if (command === "cancel") {
|
|
546
|
+
return { issuer: values.key ? "nacional" : "giss", source: "automatic" };
|
|
547
|
+
}
|
|
548
|
+
if (command === "replace") {
|
|
549
|
+
let optant = false;
|
|
550
|
+
try {
|
|
551
|
+
optant = new ProfileRepository().load().simplesNacionalOptant === 1;
|
|
552
|
+
}
|
|
553
|
+
catch {
|
|
554
|
+
// sem perfil legível, fica no GissOnline
|
|
555
|
+
}
|
|
556
|
+
if (optant && isoDate(new Date()) >= SIMPLES_NATIONAL_DEADLINE) {
|
|
557
|
+
return { issuer: "nacional", source: "automatic" };
|
|
558
|
+
}
|
|
559
|
+
}
|
|
560
|
+
return { issuer: "giss", source: "automatic" };
|
|
561
|
+
}
|
|
562
|
+
const NATIONAL_REASONS = {
|
|
563
|
+
"1": "issuing error",
|
|
564
|
+
"2": "service not provided",
|
|
565
|
+
"9": "other",
|
|
566
|
+
};
|
|
567
|
+
/**
|
|
568
|
+
* Comandos do Sistema Nacional NFS-e. `issue` e `cancel` só passam por aqui
|
|
569
|
+
* quando o emissor configurado é o nacional; os `national-*` sempre.
|
|
570
|
+
*/
|
|
571
|
+
async function runNationalCommand(command, values, client) {
|
|
572
|
+
const { national, config } = client;
|
|
573
|
+
const { issuer, source } = resolveIssuer(config, command, values);
|
|
574
|
+
const viaNational = issuer === "nacional";
|
|
575
|
+
const key = () => {
|
|
576
|
+
const value = values.key?.replace(/\D/g, "");
|
|
577
|
+
if (!value || value.length !== 50)
|
|
578
|
+
throw new Error("Provide the 50-digit access --key");
|
|
579
|
+
return value;
|
|
580
|
+
};
|
|
581
|
+
switch (command) {
|
|
582
|
+
case "national-status": {
|
|
583
|
+
const agreement = await national.agreement();
|
|
584
|
+
const yes = (v) => (v ? "yes" : "no");
|
|
585
|
+
console.log(`City (IBGE): ${config.cityCode}`);
|
|
586
|
+
console.log(`Environment: ${config.environment} (${national.hosts.sefin})`);
|
|
587
|
+
console.log(`Shares with national: ${yes(agreement.sharesWithNational)}`);
|
|
588
|
+
console.log(`Accepts national issuer: ${yes(agreement.usesNationalIssuer)}`);
|
|
589
|
+
const forIssue = resolveIssuer(config, "issue", values);
|
|
590
|
+
console.log(`Issuer for issue: ${forIssue.issuer}${forIssue.source === "automatic" ? " (automatic)" : ""}`);
|
|
591
|
+
if (values.json)
|
|
592
|
+
console.log(JSON.stringify(agreement.raw, null, 2));
|
|
593
|
+
if (!agreement.usesNationalIssuer) {
|
|
594
|
+
console.log("\nThe city has not enabled the national issuer in the ADN: a DPS is refused with E0039 —\n" +
|
|
595
|
+
"except for ME/EPP who apportion through the Simples (regApTribSN = 1) with competence on or\n" +
|
|
596
|
+
"after 2026-11-01 (RN 239), which issues anyway. See docs/national.md.");
|
|
597
|
+
}
|
|
598
|
+
return true;
|
|
599
|
+
}
|
|
600
|
+
case "national-get": {
|
|
601
|
+
printNational(await national.get(key()), values);
|
|
602
|
+
return true;
|
|
603
|
+
}
|
|
604
|
+
case "national-pdf":
|
|
605
|
+
case "national-xml": {
|
|
606
|
+
const accessKey = key();
|
|
607
|
+
const format = command === "national-pdf" ? "pdf" : "xml";
|
|
608
|
+
const file = format === "pdf"
|
|
609
|
+
? await national.pdf(accessKey).catch((error) => {
|
|
610
|
+
// O DANFSe do ADN cai com frequência (503). Nota emitida por um
|
|
611
|
+
// município do GissOnline tem o PDF da prefeitura como saída.
|
|
612
|
+
const city = findMunicipalityByCode(accessKey.slice(0, 7));
|
|
613
|
+
if (error instanceof NfseError && error.code === "UNAVAILABLE" && city) {
|
|
614
|
+
console.error(`The national PDF is unavailable. This invoice was issued in ${city.name} (GissOnline): ` +
|
|
615
|
+
`try \`${INVOCATION} pdf --number ${nfseNumberFromKey(accessKey)}\`.`);
|
|
616
|
+
}
|
|
617
|
+
throw error;
|
|
618
|
+
})
|
|
619
|
+
: Buffer.from((await national.get(accessKey)).xml, "utf8");
|
|
620
|
+
const target = documentTarget(values.out, accessKey, format);
|
|
621
|
+
mkdirSync(dirname(target), { recursive: true });
|
|
622
|
+
writeFileSync(target, file);
|
|
623
|
+
console.log(`${target} (${(file.length / 1024).toFixed(1)} KB)`);
|
|
624
|
+
return true;
|
|
625
|
+
}
|
|
626
|
+
case "national-docs": {
|
|
627
|
+
const documents = await national.distribution(Number(values.from ?? 0));
|
|
628
|
+
if (values.json) {
|
|
629
|
+
console.log(JSON.stringify(documents.map(({ xml, ...rest }) => rest), null, 2));
|
|
630
|
+
return true;
|
|
631
|
+
}
|
|
632
|
+
for (const document of documents) {
|
|
633
|
+
const head = [String(document.nsu).padStart(5), document.type.padEnd(6)];
|
|
634
|
+
if (document.type !== "NFSE") {
|
|
635
|
+
const event = parseNationalEvent(document.xml);
|
|
636
|
+
const replaced = event.replacementKey
|
|
637
|
+
? ` → replaced by ${nfseNumberFromKey(event.replacementKey) ?? event.replacementKey}`
|
|
638
|
+
: "";
|
|
639
|
+
console.log([
|
|
640
|
+
...head,
|
|
641
|
+
(event.code ?? "").padEnd(8),
|
|
642
|
+
(event.nfseNumber ?? "").padStart(9),
|
|
643
|
+
(event.requestedAt ?? event.processedAt)?.slice(0, 10) ?? "",
|
|
644
|
+
`${event.description ?? "event"}${replaced}`,
|
|
645
|
+
].join(" "));
|
|
646
|
+
console.log(`${"".padStart(15)}key ${event.accessKey ?? document.accessKey}`);
|
|
647
|
+
continue;
|
|
648
|
+
}
|
|
649
|
+
const nfse = parseNationalNfseSafe(document.xml);
|
|
650
|
+
const role = nfse?.issuerCnpj === config.cnpj ? "issued" : "received";
|
|
651
|
+
const party = role === "issued" ? nfse?.takerName : nfse?.issuerName;
|
|
652
|
+
console.log([
|
|
653
|
+
...head,
|
|
654
|
+
nfse ? role.padEnd(8) : "".padEnd(8),
|
|
655
|
+
nfse?.number?.padStart(9) ?? "".padStart(9),
|
|
656
|
+
nfse?.processedAt?.slice(0, 10) ?? "",
|
|
657
|
+
nfse?.serviceAmount?.toFixed(2).padStart(10) ?? "",
|
|
658
|
+
`${nfse?.issuingCity ?? ""}${party ? ` · ${party}` : ""}`,
|
|
659
|
+
].join(" "));
|
|
660
|
+
console.log(`${"".padStart(15)}key ${document.accessKey}`);
|
|
661
|
+
}
|
|
662
|
+
const last = documents.at(-1);
|
|
663
|
+
console.log(last
|
|
664
|
+
? `\n${documents.length} document(s). Continue with --from ${last.nsu}`
|
|
665
|
+
: "No documents after this NSU.");
|
|
666
|
+
return true;
|
|
667
|
+
}
|
|
668
|
+
case "issue": {
|
|
669
|
+
if (!viaNational)
|
|
670
|
+
return false;
|
|
671
|
+
const rps = buildRpsFromCli(values);
|
|
672
|
+
const profile = new ProfileRepository().load();
|
|
673
|
+
const number = values.dps ?? String(Math.floor(Date.now() / 1000));
|
|
674
|
+
const issueOptions = {
|
|
675
|
+
series: values.series ?? profile.nationalSeries ?? "1",
|
|
676
|
+
number,
|
|
677
|
+
simplesOption: profile.simplesOption ?? (rps.simplesNacionalOptant === 1 ? 3 : 1),
|
|
678
|
+
simplesApportionment: profile.simplesApportionment,
|
|
679
|
+
};
|
|
680
|
+
if (!values.confirm) {
|
|
681
|
+
const preview = national.previewDps(rps, issueOptions);
|
|
682
|
+
if (values.xml)
|
|
683
|
+
return void console.log(preview), true;
|
|
684
|
+
console.log(issueSummary(values, rps));
|
|
685
|
+
console.log(`DPS: ${number} series ${issueOptions.series} (national issuer${source === "automatic" ? ", the default; --issuer giss for GissOnline" : ""})`);
|
|
686
|
+
printValidation(preview, "DPS_v1.01.xsd", SCHEMA_DIRECTORIES.national);
|
|
687
|
+
console.log("\nNothing was sent. Repeat with --confirm to actually issue.");
|
|
688
|
+
return true;
|
|
689
|
+
}
|
|
690
|
+
if (!values.dps) {
|
|
691
|
+
console.log(`DPS ${number} (generated). Pass --dps ${number} to retry safely.`);
|
|
692
|
+
}
|
|
693
|
+
const outcome = await national.issue(rps, issueOptions);
|
|
694
|
+
console.log(outcome.status === "already-issued"
|
|
695
|
+
? "This DPS had already been issued:"
|
|
696
|
+
: "Invoice issued:");
|
|
697
|
+
printNational(outcome.nfse, values);
|
|
698
|
+
for (const alert of outcome.alerts)
|
|
699
|
+
console.log(`⚠ [${alert.code}] ${alert.message}`);
|
|
700
|
+
return true;
|
|
701
|
+
}
|
|
702
|
+
case "cancel": {
|
|
703
|
+
if (!viaNational)
|
|
704
|
+
return false;
|
|
705
|
+
const accessKey = key();
|
|
706
|
+
if (!values.reason || !NATIONAL_REASONS[values.reason]) {
|
|
707
|
+
throw new Error("Provide --reason 1 (issuing error), 2 (not provided) or 9 (other)");
|
|
708
|
+
}
|
|
709
|
+
if (!values.text)
|
|
710
|
+
throw new Error("Provide the justification in --text (15 to 255 chars)");
|
|
711
|
+
if (!values.confirm) {
|
|
712
|
+
console.log(`Would cancel ${accessKey}, reason ${values.reason} (${NATIONAL_REASONS[values.reason]}).`);
|
|
713
|
+
console.log("Nothing was sent. Repeat with --confirm.");
|
|
714
|
+
return true;
|
|
715
|
+
}
|
|
716
|
+
const event = await national.cancel(accessKey, Number(values.reason), values.text);
|
|
717
|
+
console.log(`Invoice ${accessKey} cancelled.`);
|
|
718
|
+
if (values.xml)
|
|
719
|
+
console.log(event);
|
|
720
|
+
return true;
|
|
721
|
+
}
|
|
722
|
+
case "replace":
|
|
723
|
+
if (viaNational) {
|
|
724
|
+
throw new Error("replace is not available on the national issuer yet: cancel, then issue again");
|
|
725
|
+
}
|
|
726
|
+
return false;
|
|
727
|
+
default:
|
|
728
|
+
return false;
|
|
729
|
+
}
|
|
730
|
+
}
|
|
731
|
+
function parseNationalNfseSafe(xml) {
|
|
732
|
+
try {
|
|
733
|
+
return parseNationalNfse(xml);
|
|
734
|
+
}
|
|
735
|
+
catch {
|
|
736
|
+
return null;
|
|
737
|
+
}
|
|
738
|
+
}
|
|
739
|
+
function printNational(nfse, values) {
|
|
740
|
+
if (values.xml)
|
|
741
|
+
return void console.log(nfse.xml);
|
|
742
|
+
if (values.json) {
|
|
743
|
+
const { xml, ...rest } = nfse;
|
|
744
|
+
return void console.log(JSON.stringify(rest, null, 2));
|
|
745
|
+
}
|
|
746
|
+
console.log(`Number: ${nfse.number ?? "—"}`);
|
|
747
|
+
console.log(`Access key: ${nfse.accessKey}`);
|
|
748
|
+
console.log(`Processed: ${nfse.processedAt ?? "—"}`);
|
|
749
|
+
console.log(`Customer: ${nfse.takerName ?? "—"} (${nfse.takerTaxId ?? "—"})`);
|
|
750
|
+
console.log(`Amount: R$ ${nfse.serviceAmount?.toFixed(2) ?? "—"}`);
|
|
751
|
+
console.log(`Description: ${nfse.description ?? "—"}`);
|
|
752
|
+
}
|
|
753
|
+
/** Rótulo em português para as mensagens ao usuário. */
|
|
754
|
+
const roleLabel = (role) => role === "customer" ? "customer" : "supplier";
|
|
755
|
+
/** Aguarda o processamento do lote, que é assíncrono. */
|
|
756
|
+
const REASONS = {
|
|
757
|
+
"1": "issuing error",
|
|
758
|
+
"2": "service not provided",
|
|
759
|
+
"3": "signature error",
|
|
760
|
+
"4": "duplicate invoice",
|
|
761
|
+
"5": "processing error",
|
|
762
|
+
};
|
|
763
|
+
/** Comandos que não precisam de rede nem de certificado. */
|
|
764
|
+
async function runLocalCommand(command, values, positionals) {
|
|
765
|
+
// eslint-disable-next-line no-param-reassign -- --lookup enriches the input
|
|
766
|
+
switch (command) {
|
|
767
|
+
case "customer-add":
|
|
768
|
+
case "supplier-add": {
|
|
769
|
+
const role = command === "customer-add" ? "customer" : "supplier";
|
|
770
|
+
values = await withLookup(values);
|
|
771
|
+
if (!values["tax-id"] || !values.name) {
|
|
772
|
+
throw new Error("Provide --tax-id and --name");
|
|
773
|
+
}
|
|
774
|
+
const contact = new ContactRepository().save(role, {
|
|
775
|
+
taxId: values["tax-id"],
|
|
776
|
+
legalName: values.name,
|
|
777
|
+
tradeName: values["trade-name"],
|
|
778
|
+
municipalRegistration: values.registration,
|
|
779
|
+
email: values.email,
|
|
780
|
+
phone: values.phone,
|
|
781
|
+
alias: values.alias,
|
|
782
|
+
address: buildAddress(values),
|
|
783
|
+
simplesNacionalOptant: values.simples
|
|
784
|
+
? Number(values.simples)
|
|
785
|
+
: undefined,
|
|
786
|
+
});
|
|
787
|
+
console.log(`${roleLabel(role)} saved: ${contact.legalName} (${contact.taxId})`);
|
|
788
|
+
if (!contact.address) {
|
|
789
|
+
console.log("Warning: no address. Issuing an invoice requires the customer address.");
|
|
790
|
+
}
|
|
791
|
+
return true;
|
|
792
|
+
}
|
|
793
|
+
case "customer-rm":
|
|
794
|
+
case "supplier-rm": {
|
|
795
|
+
const role = command === "customer-rm" ? "customer" : "supplier";
|
|
796
|
+
if (!values["tax-id"])
|
|
797
|
+
throw new Error("Provide --tax-id");
|
|
798
|
+
const removed = new ContactRepository().remove(role, values["tax-id"]);
|
|
799
|
+
console.log(removed ? `${roleLabel(role)} removed.` : `${roleLabel(role)} not found.`);
|
|
800
|
+
return true;
|
|
801
|
+
}
|
|
802
|
+
case "zip": {
|
|
803
|
+
const code = positionals[1] ?? values.zip;
|
|
804
|
+
if (!code)
|
|
805
|
+
throw new Error(`Provide the postal code: ${INVOCATION} zip 01310-100`);
|
|
806
|
+
const found = await lookupZip(code);
|
|
807
|
+
if (values.json)
|
|
808
|
+
return void console.log(JSON.stringify(found, null, 2)), true;
|
|
809
|
+
console.log(` zip: ${found.zipCode}`);
|
|
810
|
+
console.log(` street: ${found.street ?? "—"}`);
|
|
811
|
+
console.log(` district: ${found.district ?? "—"}`);
|
|
812
|
+
console.log(` city: ${found.city}/${found.state}`);
|
|
813
|
+
return true;
|
|
814
|
+
}
|
|
815
|
+
case "cnpj": {
|
|
816
|
+
const taxId = positionals[1] ?? values["tax-id"];
|
|
817
|
+
if (!taxId)
|
|
818
|
+
throw new Error(`Provide the CNPJ: ${INVOCATION} cnpj 00000000000191`);
|
|
819
|
+
const found = await lookupParty(taxId);
|
|
820
|
+
if (values.json)
|
|
821
|
+
return void console.log(JSON.stringify(found, null, 2)), true;
|
|
822
|
+
console.log(` name: ${found.legalName}`);
|
|
823
|
+
if (found.tradeName)
|
|
824
|
+
console.log(` trade: ${found.tradeName}`);
|
|
825
|
+
console.log(` status: ${found.status ?? "—"}${found.simplesNacionalOptant ? " · Simples Nacional" : ""}`);
|
|
826
|
+
console.log(` address: ${[found.street, found.number].filter(Boolean).join(", ") || "—"}`);
|
|
827
|
+
if (found.complement)
|
|
828
|
+
console.log(` complement:${found.complement}`);
|
|
829
|
+
console.log(` district: ${found.district ?? "—"}`);
|
|
830
|
+
console.log(` city: ${found.city}/${found.state} IBGE ${found.cityCode ?? "—"}`);
|
|
831
|
+
console.log(` zip: ${found.zipCode ?? "—"}`);
|
|
832
|
+
if (found.email)
|
|
833
|
+
console.log(` email: ${found.email}`);
|
|
834
|
+
if (found.phone)
|
|
835
|
+
console.log(` phone: ${found.phone}`);
|
|
836
|
+
console.log(`\n ${INVOCATION} customer-add --tax-id ${found.taxId} --lookup`);
|
|
837
|
+
return true;
|
|
838
|
+
}
|
|
839
|
+
case "danfse": {
|
|
840
|
+
// Offline: o PDF sai do XML que o projeto já guardou, sem certificado.
|
|
841
|
+
const file = positionals[1];
|
|
842
|
+
if (!file)
|
|
843
|
+
throw new Error(`Usage: ${INVOCATION} danfse <nfse.xml> [--event FILE]... [--out DIR|FILE]`);
|
|
844
|
+
const status = values.status;
|
|
845
|
+
if (status !== undefined && status !== "cancelled" && status !== "replaced") {
|
|
846
|
+
throw new Error("--status must be cancelled or replaced");
|
|
847
|
+
}
|
|
848
|
+
const xml = readFileSync(file, "utf8");
|
|
849
|
+
const pdf = Buffer.from(await renderDanfse(xml, { status, events: (values.event ?? []).map((path) => readFileSync(path, "utf8")) }));
|
|
850
|
+
const accessKey = /Id="NFS(\d{50})"/.exec(xml)?.[1] ?? "danfse";
|
|
851
|
+
const target = documentTarget(values.out, accessKey, "pdf");
|
|
852
|
+
mkdirSync(dirname(target), { recursive: true });
|
|
853
|
+
writeFileSync(target, pdf);
|
|
854
|
+
console.log(`${target} (${(pdf.length / 1024).toFixed(1)} KB)`);
|
|
855
|
+
return true;
|
|
856
|
+
}
|
|
857
|
+
case "cities": {
|
|
858
|
+
const state = values.state?.toUpperCase();
|
|
859
|
+
const list = state
|
|
860
|
+
? MUNICIPALITIES.filter((m) => m.state === state)
|
|
861
|
+
: MUNICIPALITIES;
|
|
862
|
+
if (values.json)
|
|
863
|
+
return void console.log(JSON.stringify(list, null, 2)), true;
|
|
864
|
+
for (const city of list) {
|
|
865
|
+
console.log(`${city.slug.padEnd(18)} ${city.cityCode} ${city.name}/${city.state}`);
|
|
866
|
+
}
|
|
867
|
+
console.log(`\n${list.length} municipalit${list.length === 1 ? "y" : "ies"}`);
|
|
868
|
+
console.log("Set GISS_MUNICIPIO to the first column; the IBGE code follows from it.");
|
|
869
|
+
return true;
|
|
870
|
+
}
|
|
871
|
+
case "profile": {
|
|
872
|
+
const repository = new ProfileRepository();
|
|
873
|
+
const profile = repository.load();
|
|
874
|
+
if (values.save)
|
|
875
|
+
console.log(`Profile written to ${repository.save(profile)}`);
|
|
876
|
+
console.log(JSON.stringify(profile, null, 2));
|
|
877
|
+
return true;
|
|
878
|
+
}
|
|
879
|
+
default:
|
|
880
|
+
return false;
|
|
881
|
+
}
|
|
882
|
+
}
|
|
883
|
+
/**
|
|
884
|
+
* Aceita um diretório ou o caminho completo do arquivo em `--out`; sem nada,
|
|
885
|
+
* grava no diretório atual com o número da nota no nome.
|
|
886
|
+
*/
|
|
887
|
+
function documentTarget(out, number, format) {
|
|
888
|
+
const name = `nfse-${number}.${format}`;
|
|
889
|
+
if (!out)
|
|
890
|
+
return resolve(process.cwd(), name);
|
|
891
|
+
return out.toLowerCase().endsWith(`.${format}`) ? resolve(out) : resolve(out, name);
|
|
892
|
+
}
|
|
893
|
+
/** Comandos que falam com a API REST do portal, não com o Web Service. */
|
|
894
|
+
/**
|
|
895
|
+
* Como entrar no portal: CPF e senha quando estão no ambiente, senão o próprio
|
|
896
|
+
* A1 — o mesmo certificado que assina os RPS também abre o REST, o que dispensa
|
|
897
|
+
* guardar a senha de uma pessoa física para um processo automatizado.
|
|
898
|
+
*
|
|
899
|
+
* A ordem preserva o comportamento de quem já tem `GISS_LOGIN`/`GISS_PASS`
|
|
900
|
+
* configurados. Passe o certificado já carregado quando houver um: reabrir o
|
|
901
|
+
* PKCS#12 é a parte cara.
|
|
902
|
+
*/
|
|
903
|
+
function portalCredentials(config, certificate) {
|
|
904
|
+
if (process.env.GISS_LOGIN && process.env.GISS_PASS) {
|
|
905
|
+
return loadPortalCredentials(config);
|
|
906
|
+
}
|
|
907
|
+
const source = certificate ?? process.env.CERT_PATH;
|
|
908
|
+
if (!source) {
|
|
909
|
+
throw new Error("The portal needs GISS_LOGIN and GISS_PASS, or CERT_PATH to log in with the certificate");
|
|
910
|
+
}
|
|
911
|
+
return {
|
|
912
|
+
certificate: source,
|
|
913
|
+
certificatePassword: process.env.CERT_PASSWORD,
|
|
914
|
+
cityCode: config.cityCode,
|
|
915
|
+
cnpj: config.cnpj,
|
|
916
|
+
};
|
|
917
|
+
}
|
|
918
|
+
/**
|
|
919
|
+
* Fale Conosco: lista, abre e baixa anexos das mensagens trocadas com a
|
|
920
|
+
* prefeitura. É o canal de atendimento que a API expõe ao contribuinte — o
|
|
921
|
+
* módulo de chamados do fisco nega acesso a quem entra como empresa.
|
|
922
|
+
*/
|
|
923
|
+
async function runMessageCommand(command, values) {
|
|
924
|
+
const cityCode = resolveCityCode(undefined, values.city);
|
|
925
|
+
const portal = await PortalService.authenticate(portalCredentials({ cityCode, cnpj: process.env.GISS_CNPJ }));
|
|
926
|
+
if (command === "message") {
|
|
927
|
+
if (!values.subject || !values.text) {
|
|
928
|
+
throw new Error("Provide --subject and --text");
|
|
929
|
+
}
|
|
930
|
+
if (!values.confirm) {
|
|
931
|
+
console.log(`Subject: ${values.subject}`);
|
|
932
|
+
console.log(`Message: ${values.text}`);
|
|
933
|
+
console.log(`To: ${portal.session.legalName} → city ${cityCode}`);
|
|
934
|
+
console.log("\nNothing was sent. Repeat with --confirm to open it.");
|
|
935
|
+
return;
|
|
936
|
+
}
|
|
937
|
+
await portal.sendMessage({ subject: values.subject, body: values.text });
|
|
938
|
+
console.log("Message sent.");
|
|
939
|
+
return;
|
|
940
|
+
}
|
|
941
|
+
if (values.id) {
|
|
942
|
+
const message = await portal.getMessage(Number(values.id));
|
|
943
|
+
// O anexo é uma faceta da mensagem, não um comando à parte: o `--id` já
|
|
944
|
+
// deu o contexto, e os ids dos arquivos saem na própria listagem dela.
|
|
945
|
+
if (values.attachment) {
|
|
946
|
+
const attachment = message.attachments.find((a) => a.id === Number(values.attachment));
|
|
947
|
+
if (!attachment) {
|
|
948
|
+
throw new Error(`Message ${message.id} has no attachment ${values.attachment}. Available: ` +
|
|
949
|
+
(message.attachments.map((a) => `${a.id} (${a.name})`).join(", ") || "none"));
|
|
950
|
+
}
|
|
951
|
+
const file = await portal.messageAttachment(message.id, attachment.id);
|
|
952
|
+
const target = values.out
|
|
953
|
+
? resolve(values.out, attachment.name)
|
|
954
|
+
: resolve(attachment.name);
|
|
955
|
+
mkdirSync(dirname(target), { recursive: true });
|
|
956
|
+
writeFileSync(target, file);
|
|
957
|
+
console.log(`${target} (${(file.length / 1024).toFixed(1)} KB)`);
|
|
958
|
+
return;
|
|
959
|
+
}
|
|
960
|
+
if (values.json)
|
|
961
|
+
return void console.log(JSON.stringify(message, null, 2));
|
|
962
|
+
printMessage(message);
|
|
963
|
+
return;
|
|
964
|
+
}
|
|
965
|
+
const messages = await portal.listMessages();
|
|
966
|
+
const shown = values.unread ? messages.filter((m) => m.answered && !m.read) : messages;
|
|
967
|
+
if (values.json)
|
|
968
|
+
return void console.log(JSON.stringify(shown, null, 2));
|
|
969
|
+
for (const message of shown) {
|
|
970
|
+
const status = message.answered ? (message.read ? "answered" : "NEW REPLY") : "waiting";
|
|
971
|
+
console.log([
|
|
972
|
+
String(message.id).padStart(5),
|
|
973
|
+
message.sentAt.slice(0, 10),
|
|
974
|
+
status.padEnd(9),
|
|
975
|
+
message.subject,
|
|
976
|
+
].join(" "));
|
|
977
|
+
}
|
|
978
|
+
console.log(`\n${shown.length} message(s) — ${await portal.unreadMessages()} unread`);
|
|
979
|
+
const first = shown[0];
|
|
980
|
+
if (first)
|
|
981
|
+
console.log(`See one with: ${INVOCATION} messages --id ${first.id}`);
|
|
982
|
+
}
|
|
983
|
+
/** Uma mensagem inteira, com a resposta do auditor quando existe. */
|
|
984
|
+
function printMessage(message) {
|
|
985
|
+
console.log(`#${message.id} ${message.subject}`);
|
|
986
|
+
console.log(`Sent: ${message.sentAt.slice(0, 19).replace("T", " ")}`);
|
|
987
|
+
console.log(`\n${message.body}`);
|
|
988
|
+
if (message.answer) {
|
|
989
|
+
console.log(`\n--- reply${message.auditor ? ` from ${message.auditor}` : ""}` +
|
|
990
|
+
`${message.answeredAt ? ` on ${message.answeredAt.slice(0, 10)}` : ""} ---`);
|
|
991
|
+
console.log(message.answer);
|
|
992
|
+
}
|
|
993
|
+
else {
|
|
994
|
+
console.log("\n(no reply yet)");
|
|
995
|
+
}
|
|
996
|
+
for (const attachment of message.attachments) {
|
|
997
|
+
console.log(`\nAttachment ${attachment.id}: ${attachment.name}` +
|
|
998
|
+
`${attachment.fromAuditor ? " (from the auditor)" : ""}` +
|
|
999
|
+
`\n ${INVOCATION} messages --id ${message.id} --attachment ${attachment.id}`);
|
|
1000
|
+
}
|
|
1001
|
+
}
|
|
1002
|
+
/**
|
|
1003
|
+
* Lista a tabela de atividades do município, ou só as da empresa.
|
|
1004
|
+
*
|
|
1005
|
+
* O `CodigoTributacaoMunicipio` não existe no WSDL — nenhuma das 16 operações
|
|
1006
|
+
* devolve tabela de apoio —, então a origem é a API REST do portal. A lista da
|
|
1007
|
+
* cidade é pública; a da empresa exige o login do portal.
|
|
1008
|
+
*/
|
|
1009
|
+
async function runActivitiesCommand(values, positionals) {
|
|
1010
|
+
const cityCode = resolveCityCode(undefined, values.city);
|
|
1011
|
+
const activities = values.company
|
|
1012
|
+
? await (await PortalService.authenticate(portalCredentials({ cityCode, cnpj: process.env.GISS_CNPJ }))).companyActivities(values.date ?? new Date())
|
|
1013
|
+
: await PortalService.listActivities(cityCode);
|
|
1014
|
+
// O item da LC 116 muda de forma conforme a cidade: Suzano grava `1.09`,
|
|
1015
|
+
// Guarulhos grava `101` para 1.01. Comparar só os dígitos faz `--item 1.09`
|
|
1016
|
+
// valer nas duas.
|
|
1017
|
+
const digits = (value) => value.replace(/\D/g, "").replace(/^0+/, "");
|
|
1018
|
+
const term = positionals[1]?.toLowerCase();
|
|
1019
|
+
const item = values.item ? digits(values.item) : undefined;
|
|
1020
|
+
const found = activities.filter((activity) => (!term ||
|
|
1021
|
+
activity.code.includes(term) ||
|
|
1022
|
+
activity.description.toLowerCase().includes(term)) &&
|
|
1023
|
+
(!item || digits(activity.serviceListItem) === item));
|
|
1024
|
+
if (values.json)
|
|
1025
|
+
return void console.log(JSON.stringify(found, null, 2));
|
|
1026
|
+
const width = Math.max(4, ...found.map((a) => a.code.length));
|
|
1027
|
+
for (const activity of found) {
|
|
1028
|
+
console.log([
|
|
1029
|
+
activity.code.padEnd(width),
|
|
1030
|
+
activity.serviceListItem.padEnd(6),
|
|
1031
|
+
activity.rate === undefined ? "" : `${activity.rate.toFixed(2)}%`.padStart(7),
|
|
1032
|
+
activity.description,
|
|
1033
|
+
]
|
|
1034
|
+
.filter(Boolean)
|
|
1035
|
+
.join(" "));
|
|
1036
|
+
}
|
|
1037
|
+
const scope = values.company ? "bound to the company" : `in city ${cityCode}`;
|
|
1038
|
+
console.log(`\n${found.length} of ${activities.length} activity(ies) ${scope}`);
|
|
1039
|
+
console.log("The rate above is the municipal one — under Simples Nacional the annex rate applies.");
|
|
1040
|
+
}
|
|
1041
|
+
async function runPortalCommand(command, values, config) {
|
|
1042
|
+
// eslint-disable-next-line no-param-reassign -- --lookup enriches the input
|
|
1043
|
+
const role = (values.type ? Number(values.type) : 1);
|
|
1044
|
+
const label = role === 1 ? "customer" : "supplier";
|
|
1045
|
+
const portal = await PortalService.authenticate(portalCredentials(config));
|
|
1046
|
+
if (command === "portal-list") {
|
|
1047
|
+
const parties = await portal.list(role);
|
|
1048
|
+
if (values.json)
|
|
1049
|
+
return void console.log(JSON.stringify(parties, null, 2));
|
|
1050
|
+
for (const party of parties) {
|
|
1051
|
+
console.log([party.documento.padEnd(14), party.razaoSocial, party.nomeFantasia ?? ""]
|
|
1052
|
+
.filter(Boolean)
|
|
1053
|
+
.join(" | "));
|
|
1054
|
+
}
|
|
1055
|
+
console.log(`\n${parties.length} ${label}(s) — ${portal.session.legalName}`);
|
|
1056
|
+
return;
|
|
1057
|
+
}
|
|
1058
|
+
if (command === "portal-import") {
|
|
1059
|
+
const repository = new ContactRepository();
|
|
1060
|
+
const parties = await portal.list(role);
|
|
1061
|
+
for (const party of parties) {
|
|
1062
|
+
const full = party.endereco ? party : await portal.get(party.id);
|
|
1063
|
+
repository.save(role === 1 ? "customer" : "supplier", {
|
|
1064
|
+
taxId: full.documento,
|
|
1065
|
+
legalName: full.razaoSocial,
|
|
1066
|
+
tradeName: full.nomeFantasia,
|
|
1067
|
+
municipalRegistration: full.inscricaoMunicipal,
|
|
1068
|
+
address: full.endereco
|
|
1069
|
+
? {
|
|
1070
|
+
street: `${full.endereco.tipoLogradouro} ${full.endereco.logradouro}`.trim(),
|
|
1071
|
+
number: full.endereco.numero,
|
|
1072
|
+
complement: full.endereco.complemento,
|
|
1073
|
+
district: full.endereco.bairro,
|
|
1074
|
+
cityCode: String(full.endereco.idIbge),
|
|
1075
|
+
state: full.endereco.estado,
|
|
1076
|
+
zipCode: full.endereco.cep,
|
|
1077
|
+
}
|
|
1078
|
+
: undefined,
|
|
1079
|
+
source: "portal",
|
|
1080
|
+
});
|
|
1081
|
+
}
|
|
1082
|
+
console.log(`${parties.length} ${label}(s) imported from the portal.`);
|
|
1083
|
+
return;
|
|
1084
|
+
}
|
|
1085
|
+
if (command === "portal-rm") {
|
|
1086
|
+
if (!values["tax-id"])
|
|
1087
|
+
throw new Error("Provide --tax-id");
|
|
1088
|
+
const existing = await portal.findByTaxId(values["tax-id"], role);
|
|
1089
|
+
if (!existing) {
|
|
1090
|
+
console.log(`${label} ${values["tax-id"]} is not registered in the portal.`);
|
|
1091
|
+
return;
|
|
1092
|
+
}
|
|
1093
|
+
if (!values.confirm) {
|
|
1094
|
+
console.log(`Would remove: ${existing.razaoSocial} (${existing.documento})`);
|
|
1095
|
+
console.log("Nothing was sent. Repeat with --confirm.");
|
|
1096
|
+
return;
|
|
1097
|
+
}
|
|
1098
|
+
await portal.remove(await portal.get(existing.id));
|
|
1099
|
+
console.log(`${label} removed from the portal: ${existing.razaoSocial}`);
|
|
1100
|
+
return;
|
|
1101
|
+
}
|
|
1102
|
+
// portal-add
|
|
1103
|
+
values = await withLookup(values);
|
|
1104
|
+
if (!values["tax-id"] || !values.name) {
|
|
1105
|
+
throw new Error("Provide --tax-id and --name");
|
|
1106
|
+
}
|
|
1107
|
+
const existing = await portal.findByTaxId(values["tax-id"], role);
|
|
1108
|
+
if (existing) {
|
|
1109
|
+
console.log(`Already registered in the portal: ${existing.razaoSocial} (${existing.documento}).`);
|
|
1110
|
+
return;
|
|
1111
|
+
}
|
|
1112
|
+
const address = buildAddress(values);
|
|
1113
|
+
const cityName = address ? await portal.cityName(address.cityCode) : undefined;
|
|
1114
|
+
const party = buildPortalParty(portal.session, {
|
|
1115
|
+
taxId: values["tax-id"],
|
|
1116
|
+
legalName: values.name,
|
|
1117
|
+
tradeName: values["trade-name"],
|
|
1118
|
+
municipalRegistration: values.registration,
|
|
1119
|
+
role,
|
|
1120
|
+
mei: values.mei,
|
|
1121
|
+
simplesNacional: values.simples === "1",
|
|
1122
|
+
email: values.email,
|
|
1123
|
+
phone: values.phone,
|
|
1124
|
+
address: address
|
|
1125
|
+
? { ...address, streetType: values["street-type"], cityName }
|
|
1126
|
+
: undefined,
|
|
1127
|
+
});
|
|
1128
|
+
if (!values.confirm) {
|
|
1129
|
+
console.log(`Would register in the portal (${portal.session.legalName}):\n`);
|
|
1130
|
+
console.log(JSON.stringify(party, null, 2));
|
|
1131
|
+
console.log("\nNothing was sent. Repeat with --confirm.");
|
|
1132
|
+
return;
|
|
1133
|
+
}
|
|
1134
|
+
const created = await portal.create(party);
|
|
1135
|
+
console.log(`${label} registered in the portal: ${created.razaoSocial} (id ${created.id})`);
|
|
1136
|
+
}
|
|
1137
|
+
/**
|
|
1138
|
+
* Fills the gaps from the CNPJ registry. Anything passed explicitly wins — the
|
|
1139
|
+
* lookup only supplies what the command line left out.
|
|
1140
|
+
*/
|
|
1141
|
+
async function withLookup(values) {
|
|
1142
|
+
if (!values.lookup)
|
|
1143
|
+
return values;
|
|
1144
|
+
const taxId = values["tax-id"];
|
|
1145
|
+
if (!taxId)
|
|
1146
|
+
throw new Error("--lookup needs --tax-id");
|
|
1147
|
+
const found = await lookupParty(taxId);
|
|
1148
|
+
console.log(`Looked up ${found.legalName}${found.status ? ` (${found.status})` : ""}`);
|
|
1149
|
+
const filled = { ...values };
|
|
1150
|
+
const fill = (key, value) => {
|
|
1151
|
+
if (value && !filled[key])
|
|
1152
|
+
filled[key] = value;
|
|
1153
|
+
};
|
|
1154
|
+
fill("name", found.legalName);
|
|
1155
|
+
fill("trade-name", found.tradeName);
|
|
1156
|
+
fill("street", found.street);
|
|
1157
|
+
fill("number", found.number);
|
|
1158
|
+
fill("complement", found.complement);
|
|
1159
|
+
fill("district", found.district);
|
|
1160
|
+
fill("city", found.cityCode);
|
|
1161
|
+
fill("state", found.state);
|
|
1162
|
+
fill("zip", found.zipCode);
|
|
1163
|
+
fill("email", found.email);
|
|
1164
|
+
fill("phone", found.phone);
|
|
1165
|
+
if (found.simplesNacionalOptant && !filled["simples"])
|
|
1166
|
+
filled["simples"] = "1";
|
|
1167
|
+
return filled;
|
|
1168
|
+
}
|
|
1169
|
+
/** Monta o endereço; o XSD exige o grupo completo ou nenhum. */
|
|
1170
|
+
function buildAddress(values) {
|
|
1171
|
+
if (!values.street)
|
|
1172
|
+
return undefined;
|
|
1173
|
+
const missing = ["district", "city", "state", "zip"].filter((field) => !values[field]);
|
|
1174
|
+
if (missing.length > 0) {
|
|
1175
|
+
throw new Error(`Incomplete address — missing: ${missing.map((f) => `--${f}`).join(", ")}. ` +
|
|
1176
|
+
"--city takes the 7-digit IBGE code.");
|
|
1177
|
+
}
|
|
1178
|
+
return {
|
|
1179
|
+
street: values.street,
|
|
1180
|
+
number: values.number ?? "S/N",
|
|
1181
|
+
complement: values.complement,
|
|
1182
|
+
district: values.district,
|
|
1183
|
+
cityCode: values.city,
|
|
1184
|
+
state: values.state.toUpperCase(),
|
|
1185
|
+
zipCode: values.zip.replace(/\D/g, ""),
|
|
1186
|
+
};
|
|
1187
|
+
}
|
|
1188
|
+
function buildRpsFromCli(values) {
|
|
1189
|
+
if (!values.customer || !values.amount) {
|
|
1190
|
+
throw new Error("Provide --customer (tax id or alias) and --amount");
|
|
1191
|
+
}
|
|
1192
|
+
const repository = new ContactRepository();
|
|
1193
|
+
const contact = repository.find("customer", values.customer);
|
|
1194
|
+
if (!contact) {
|
|
1195
|
+
throw new Error(`Customer "${values.customer}" is not in the local directory. Use customer-add, portal-import or customers --sync.`);
|
|
1196
|
+
}
|
|
1197
|
+
return buildRps(new ProfileRepository().load(), {
|
|
1198
|
+
taker: ContactRepository.asServiceTaker(contact),
|
|
1199
|
+
serviceAmount: Number(values.amount),
|
|
1200
|
+
description: values.description,
|
|
1201
|
+
competenceDate: values.competence,
|
|
1202
|
+
rpsNumber: values.rps,
|
|
1203
|
+
series: values.series,
|
|
1204
|
+
rate: values.rate ? Number(values.rate) : undefined,
|
|
1205
|
+
csll: values.csll ? Number(values.csll) : undefined,
|
|
1206
|
+
inss: values.inss ? Number(values.inss) : undefined,
|
|
1207
|
+
incomeTax: values["income-tax"] ? Number(values["income-tax"]) : undefined,
|
|
1208
|
+
additionalInformation: values.notes,
|
|
1209
|
+
profile: {
|
|
1210
|
+
...(values.item ? { serviceListItem: values.item } : {}),
|
|
1211
|
+
...(values.cnae ? { cnaeCode: values.cnae, municipalTaxCode: values.cnae } : {}),
|
|
1212
|
+
...(values.nbs ? { nbsCode: values.nbs } : {}),
|
|
1213
|
+
},
|
|
1214
|
+
});
|
|
1215
|
+
}
|
|
1216
|
+
function issueSummary(values, rps) {
|
|
1217
|
+
const lines = [
|
|
1218
|
+
`Customer: ${rps.taker?.legalName} (${rps.taker?.cnpj ?? rps.taker?.cpf})`,
|
|
1219
|
+
`Amount: R$ ${Number(values.amount).toFixed(2)}`,
|
|
1220
|
+
`Competence: ${isoDate(rps.competenceDate)}`,
|
|
1221
|
+
`Description: ${rps.service.description}`,
|
|
1222
|
+
`LC 116 item: ${rps.service.serviceListItem}`,
|
|
1223
|
+
`CNAE: ${rps.service.cnaeCode ?? "—"}`,
|
|
1224
|
+
`NBS: ${rps.service.nbsCode ?? "—"}`,
|
|
1225
|
+
`Rate: ${rps.service.amounts.rate ?? "—"}%`,
|
|
1226
|
+
`ISS withheld: ${rps.service.issWithheld === 1 ? "yes" : "no"}`,
|
|
1227
|
+
];
|
|
1228
|
+
if (rps.identification) {
|
|
1229
|
+
lines.push(`RPS: ${rps.identification.number} series ${rps.identification.series}`);
|
|
1230
|
+
}
|
|
1231
|
+
return lines.join("\n");
|
|
1232
|
+
}
|
|
1233
|
+
function printValidation(xml, schema = "gerar-nfse-envio-v2_04.xsd", directory) {
|
|
1234
|
+
const result = validateAgainstSchema(xml, schema, directory);
|
|
1235
|
+
if (result === null) {
|
|
1236
|
+
console.log("\nSchema: not checked (xmllint not installed).");
|
|
1237
|
+
}
|
|
1238
|
+
else if (result.valid) {
|
|
1239
|
+
console.log(`\nSchema: XML valid against ${schema}.`);
|
|
1240
|
+
for (const d of result.knownDivergences) {
|
|
1241
|
+
console.log(` (known XSD divergence, ignored) ${d.slice(0, 90)}`);
|
|
1242
|
+
}
|
|
1243
|
+
}
|
|
1244
|
+
else {
|
|
1245
|
+
console.log("\nSchema: XML INVALID —");
|
|
1246
|
+
for (const error of result.errors)
|
|
1247
|
+
console.log(` ${error}`);
|
|
1248
|
+
}
|
|
1249
|
+
}
|
|
1250
|
+
function dateFilter(from, to, byCompetence) {
|
|
1251
|
+
if (!from || !to) {
|
|
1252
|
+
throw new Error("Provide --from and --to as YYYY-MM-DD (or use --number)");
|
|
1253
|
+
}
|
|
1254
|
+
const range = { from, to };
|
|
1255
|
+
return byCompetence ? { competencePeriod: range } : { issuePeriod: range };
|
|
1256
|
+
}
|
|
1257
|
+
function printInvoices(result, values, showProvider = false) {
|
|
1258
|
+
if (values.xml)
|
|
1259
|
+
return void console.log(result.xml);
|
|
1260
|
+
if (values.json)
|
|
1261
|
+
return void console.log(JSON.stringify(result.invoices, null, 2));
|
|
1262
|
+
for (const warning of result.warnings) {
|
|
1263
|
+
console.log(`⚠ [${warning.code}] ${warning.message}`);
|
|
1264
|
+
}
|
|
1265
|
+
if (result.invoices.length === 0) {
|
|
1266
|
+
console.log("No invoice found.");
|
|
1267
|
+
return;
|
|
1268
|
+
}
|
|
1269
|
+
for (const invoice of result.invoices) {
|
|
1270
|
+
const party = showProvider ? invoice.provider : invoice.taker;
|
|
1271
|
+
console.log([
|
|
1272
|
+
`NFS-e ${invoice.number}`,
|
|
1273
|
+
invoice.issueDate?.slice(0, 10),
|
|
1274
|
+
party?.legalName ?? party?.taxId ?? "",
|
|
1275
|
+
invoice.serviceAmount ? `R$ ${invoice.serviceAmount}` : "",
|
|
1276
|
+
invoice.verificationCode,
|
|
1277
|
+
]
|
|
1278
|
+
.filter(Boolean)
|
|
1279
|
+
.join(" | "));
|
|
1280
|
+
}
|
|
1281
|
+
}
|
|
1282
|
+
function printContacts(repository, role, values) {
|
|
1283
|
+
const contacts = repository.list(role);
|
|
1284
|
+
if (values.json)
|
|
1285
|
+
return void console.log(JSON.stringify(contacts, null, 2));
|
|
1286
|
+
if (contacts.length === 0) {
|
|
1287
|
+
console.log(`No ${roleLabel(role)} in the local directory (${repository.path}).`);
|
|
1288
|
+
return;
|
|
1289
|
+
}
|
|
1290
|
+
for (const contact of contacts) {
|
|
1291
|
+
const id = taxIdOf(contact.taxId);
|
|
1292
|
+
console.log([
|
|
1293
|
+
(id.cnpj ?? id.cpf ?? "").padEnd(14),
|
|
1294
|
+
contact.legalName,
|
|
1295
|
+
contact.alias ? `(${contact.alias})` : "",
|
|
1296
|
+
contact.email ?? "",
|
|
1297
|
+
]
|
|
1298
|
+
.filter(Boolean)
|
|
1299
|
+
.join(" | "));
|
|
1300
|
+
}
|
|
1301
|
+
console.log(`\n${contacts.length} ${roleLabel(role)}(s) — ${repository.path}`);
|
|
1302
|
+
}
|
|
1303
|
+
/** O que fazer depois de cada tipo de erro — a parte que a mensagem do serviço não diz. */
|
|
1304
|
+
const NEXT_STEP = {
|
|
1305
|
+
CONFIG: "Check .env (see .env.example).",
|
|
1306
|
+
CERTIFICATE: `Check the certificate with: ${INVOCATION} cert`,
|
|
1307
|
+
UNAVAILABLE: "The service is unavailable right now. Try again in a few minutes.",
|
|
1308
|
+
TRANSPORT: "Network failure. Try again; check connectivity or a proxy if it persists.",
|
|
1309
|
+
AUTHENTICATION: "The credentials were refused. Check the certificate or the portal login.",
|
|
1310
|
+
};
|
|
1311
|
+
function printError(error, debug) {
|
|
1312
|
+
if (!(error instanceof NfseError)) {
|
|
1313
|
+
console.error(error instanceof Error ? error.message : error);
|
|
1314
|
+
if (debug && error instanceof Error)
|
|
1315
|
+
console.error(error.stack);
|
|
1316
|
+
return;
|
|
1317
|
+
}
|
|
1318
|
+
const origin = [error.provider, error.operation].filter(Boolean).join(" · ");
|
|
1319
|
+
const messages = error instanceof GissError || error instanceof NationalError ? error.messages : [];
|
|
1320
|
+
if (messages.length > 0) {
|
|
1321
|
+
console.error(`\n${origin} returned an error:`);
|
|
1322
|
+
for (const message of messages) {
|
|
1323
|
+
console.error(` [${message.code}] ${message.message}${message.correction ? ` — ${message.correction}` : ""}`);
|
|
1324
|
+
}
|
|
1325
|
+
}
|
|
1326
|
+
else {
|
|
1327
|
+
console.error(`\n${error.message}`);
|
|
1328
|
+
}
|
|
1329
|
+
console.error(`\n code: ${error.code}${origin ? ` (${origin})` : ""}`);
|
|
1330
|
+
if (error.outcomeUnknown) {
|
|
1331
|
+
console.error("\n⚠ The request may have reached the service, and the answer did not come back.");
|
|
1332
|
+
const rps = error.details["rps"];
|
|
1333
|
+
const id = error.details["dpsId"]
|
|
1334
|
+
? `DPS ${String(error.details["dpsId"])}`
|
|
1335
|
+
: rps
|
|
1336
|
+
? `RPS ${String(rps.number)} series ${String(rps.series)}`
|
|
1337
|
+
: undefined;
|
|
1338
|
+
console.error(id
|
|
1339
|
+
? ` Repeat with the same number (${id}): it will not issue twice.`
|
|
1340
|
+
: " Repeat with the same RPS/DPS number: it will not issue twice.");
|
|
1341
|
+
}
|
|
1342
|
+
else if (NEXT_STEP[error.code]) {
|
|
1343
|
+
console.error(` ${NEXT_STEP[error.code]}`);
|
|
1344
|
+
}
|
|
1345
|
+
if (messages.some((m) => m.code === "E0039")) {
|
|
1346
|
+
console.error(`\nThe city has not enabled the national issuer for this taxpayer. Check with: ${INVOCATION} national-status — or issue through GissOnline with --issuer giss`);
|
|
1347
|
+
}
|
|
1348
|
+
if (debug && error.cause)
|
|
1349
|
+
console.error("\ncause:", error.cause);
|
|
1350
|
+
}
|
|
1351
|
+
main().catch((error) => {
|
|
1352
|
+
printError(error, process.argv.includes("--debug"));
|
|
1353
|
+
process.exitCode = 1;
|
|
1354
|
+
});
|
|
1355
|
+
//# sourceMappingURL=index.js.map
|