/** * Fiscal bounded context (Master Prompt §6.11, ADR-0011, eden.md módulo 5 * "Módulo Fiscal (NCM, CFOP, Municípios)"). The legacy OrçaFácil has a * mature ~19-table catalog layer + automatic (Siscomex/IBGE fetch) and * manual (CSV/XLSX/JSON upload) importers + a sync log (`fiscal_catalog_sync`) * + `product_fiscal_profiles` linking `products` to fiscal classification. * * This MVP builds a coherent, faithfully-shaped SUBSET, end-to-end: * - fiscal_ncm (this migration) — "simple catalog" shape, eden.md §2.2 * - fiscal_cfop (this migration) — simple catalog + direction/scope, eden.md §2.3/§4 * - fiscal_municipality (this migration) — distinct shape, eden.md §2.3 * - fiscal_operation_nature (this migration) — internal enum-like catalog, eden.md §2.3, seeded * - product_fiscal_profiles (this migration) — eden.md §2.5, reduced to the * 4 catalogs actually created here (ncm/cfop/municipality/operation_nature) * * Explicitly DEFERRED, not forgotten: * - The other ~9 "simple catalog" tables (eden.md §2.2): fiscal_product_origin, * fiscal_icms_cst, fiscal_icms_csosn, fiscal_pis_cst, fiscal_cofins_cst, * fiscal_ipi_cst, fiscal_lc116_service, fiscal_nbs, fiscal_ibs_cbs_cst, * fiscal_ibs_cbs_credit_class — same shape as fiscal_ncm/fiscal_cfop, can * be added later without a redesign. * - The "extended catalog" tables (eden.md §2.3): fiscal_cest, fiscal_cbenef, * fiscal_nfcom_cclass, fiscal_nfse_trib_nacional, fiscal_nfse_trib_municipal, * fiscal_ibs_cbs_classification, fiscal_ibs_cbs_standard_rate. * - `fiscal_catalog_sync` (append-only sync log, eden.md §2.4) and the * automatic importers (NCM/Siscomex, Municipality/IBGE — eden.md §3/§5) * + the generic manual file importer with dry-run and the mass- * inactivation safety threshold (eden.md §7) — substantial, isolated * work (network fetch adapters, checksum/threshold logic, file parsing), * deferred to its own leva. * - `fiscal_rules` (eden.md §2.6) — schema-ready-but-unconsumed even in the * legacy itself ("estrutura pronta, sem UI/consumo ainda"); not recreated * here, stays future work until the fiscal resolution engine is built. * - `products.ixc_product_code` / `products.product_code` (eden.md §6) — * belong to the Products migration, not this one. * - product_fiscal_profiles columns for catalogs not yet created here: * nfcom_cclass_id, nfse_trib_nacional_id, nfse_trib_municipal_id, * lc116_service_id, nbs_id, cest_id, product_origin_id, ibs_cbs_cst_id, * ibs_cbs_classification_id, ibs_cbs_credit_class_id (eden.md §2.5) — * added when their respective catalogs land. * * `product_fiscal_profiles.product_id` is deliberately nullable-FK-free: * NO `REFERENCES products(id)`, same cross-module "FK cruzada adiada" * pattern used by `contracts.quote_id`/`contracts.client_registration_id` * (see 1788447802216_contracts.cjs header) and * `support_tickets.client_registration_id`/`.contract_id` * (see 1788451374900_support.cjs header) — this worktree may not always * carry every other module's migration in the same merge order; wiring the * FK is integration-step work at merge time, not this migration's. * * Naming convention: all fiscal tables use `created_at`/`updated_at` * (not `created_date`/`updated_date`) — this is already EDEN's own * repo-wide convention (see every other migration in this directory), so * there is no deviation to reconcile here. eden.md §2.1 calls this "a * deliberate deviation" only relative to the *legacy* OrçaFácil schema, * not relative to EDEN. */ exports.shorthands = undefined; exports.up = (pgm) => { pgm.sql(` -- ===================================================================== -- fiscal_ncm — Nomenclatura Comum do Mercosul. "Simple catalog" shape, -- eden.md §2.2. Source: Receita Federal / Siscomex (import deferred). -- ===================================================================== CREATE TABLE fiscal_ncm ( id UUID PRIMARY KEY DEFAULT gen_random_uuid(), code TEXT NOT NULL, description TEXT NOT NULL, valid_from DATE, valid_until DATE, active BOOLEAN NOT NULL DEFAULT true, source TEXT, source_version TEXT, source_updated_at TIMESTAMPTZ, metadata JSONB, created_at TIMESTAMPTZ NOT NULL DEFAULT now(), updated_at TIMESTAMPTZ NOT NULL DEFAULT now() ); COMMENT ON TABLE fiscal_ncm IS 'Nomenclatura Comum do Mercosul (eden.md §2.2, §3). "Catálogo simples" — mesmo shape reutilizável para as ~9 outras tabelas de catálogo simples ainda não portadas (fiscal_product_origin, ICMS CST/CSOSN, PIS/COFINS/IPI CST, LC116, NBS, IBS-CBS CST/credit_class). Importação automática (Siscomex, checksum, trava de volume mínimo, upsert com inactivateMissing=true) é trabalho DEFERIDO — ver cabeçalho da migration.'; CREATE UNIQUE INDEX idx_fiscal_ncm_code ON fiscal_ncm(code); CREATE INDEX idx_fiscal_ncm_active ON fiscal_ncm(active); CREATE TRIGGER trg_fiscal_ncm_updated_at BEFORE UPDATE ON fiscal_ncm FOR EACH ROW EXECUTE FUNCTION set_updated_at(); -- ===================================================================== -- fiscal_cfop — Código Fiscal de Operações e Prestações. "Simple -- catalog" shape + direction/scope (eden.md §2.3, §4). Fonte MANUAL -- (sem endpoint estruturado confiável testado no legado). -- ===================================================================== CREATE TABLE fiscal_cfop ( id UUID PRIMARY KEY DEFAULT gen_random_uuid(), code TEXT NOT NULL, description TEXT NOT NULL, direction TEXT, scope TEXT, valid_from DATE, valid_until DATE, active BOOLEAN NOT NULL DEFAULT true, source TEXT, source_version TEXT, source_updated_at TIMESTAMPTZ, metadata JSONB, created_at TIMESTAMPTZ NOT NULL DEFAULT now(), updated_at TIMESTAMPTZ NOT NULL DEFAULT now() ); COMMENT ON TABLE fiscal_cfop IS 'Código Fiscal de Operações e Prestações (eden.md §2.3, §4). "Catálogo simples" + direction/scope. Fonte declarada "Portal Nacional NF-e" mas tipo MANUAL no legado (sem endpoint estruturado confiável testado) — cadastro por upload de arquivo ou tela, ambos DEFERIDOS nesta leva (ver cabeçalho da migration).'; COMMENT ON COLUMN fiscal_cfop.direction IS 'Direção da operação (entrada/saída) — TEXT livre, sem CHECK/enum, exatamente como o legado (eden.md §4). O 1º dígito do código CFOP codifica direção+âmbito na prática brasileira, mas isso NUNCA é derivado automaticamente do código no legado nem aqui — direction/scope são só campos preenchidos manualmente/via importação.'; COMMENT ON COLUMN fiscal_cfop.scope IS 'Âmbito da operação (estadual/interestadual/exterior) — TEXT livre, sem CHECK/enum, mesma razão de direction (eden.md §4).'; CREATE UNIQUE INDEX idx_fiscal_cfop_code ON fiscal_cfop(code); CREATE INDEX idx_fiscal_cfop_active ON fiscal_cfop(active); CREATE TRIGGER trg_fiscal_cfop_updated_at BEFORE UPDATE ON fiscal_cfop FOR EACH ROW EXECUTE FUNCTION set_updated_at(); -- ===================================================================== -- fiscal_municipality — shape DIFERENTE do catálogo simples: sem -- valid_from/valid_until/metadata ("não faz sentido um município -- expirar", eden.md §2.3). Fonte: IBGE (import deferido). -- ===================================================================== CREATE TABLE fiscal_municipality ( id UUID PRIMARY KEY DEFAULT gen_random_uuid(), code TEXT NOT NULL, name TEXT NOT NULL, state TEXT NOT NULL, active BOOLEAN NOT NULL DEFAULT true, source TEXT, source_version TEXT, created_at TIMESTAMPTZ NOT NULL DEFAULT now(), updated_at TIMESTAMPTZ NOT NULL DEFAULT now() ); COMMENT ON TABLE fiscal_municipality IS 'Municípios/IBGE (eden.md §2.3, §5) — base para ISS/NFS-e. Shape deliberadamente distinto do catálogo simples: sem valid_from/valid_until/metadata ("não faz sentido um município expirar"). code = código IBGE, state = UF. Importação automática (IBGE, checksum, trava de volume mínimo de 5000 linhas) é DEFERIDA — ver cabeçalho da migration.'; CREATE UNIQUE INDEX idx_fiscal_municipality_code ON fiscal_municipality(code); CREATE INDEX idx_fiscal_municipality_state ON fiscal_municipality(state); CREATE TRIGGER trg_fiscal_municipality_updated_at BEFORE UPDATE ON fiscal_municipality FOR EACH ROW EXECUTE FUNCTION set_updated_at(); -- ===================================================================== -- fiscal_operation_nature — cadastro interno, sem fonte externa -- (eden.md §2.3). Seed fixo via ON CONFLICT DO NOTHING. -- ===================================================================== CREATE TABLE fiscal_operation_nature ( id UUID PRIMARY KEY DEFAULT gen_random_uuid(), code TEXT UNIQUE NOT NULL, name TEXT NOT NULL, description TEXT, active BOOLEAN NOT NULL DEFAULT true, created_at TIMESTAMPTZ NOT NULL DEFAULT now(), updated_at TIMESTAMPTZ NOT NULL DEFAULT now() ); COMMENT ON TABLE fiscal_operation_nature IS 'Naturezas de operação (eden.md §2.3) — cadastro interno de negócio, não uma fonte externa sincronizável. Seed fixo abaixo, único endpoint da API para este catálogo é leitura (GET /v1/fiscal/operation-natures) — sem criação via API nesta leva.'; CREATE TRIGGER trg_fiscal_operation_nature_updated_at BEFORE UPDATE ON fiscal_operation_nature FOR EACH ROW EXECUTE FUNCTION set_updated_at(); INSERT INTO fiscal_operation_nature (code, name) VALUES ('VENDA', 'Venda'), ('REMESSA_LOCACAO', 'Remessa em locação'), ('RETORNO_LOCACAO', 'Retorno de locação'), ('REMESSA_MANUTENCAO', 'Remessa para manutenção'), ('RETORNO_MANUTENCAO', 'Retorno de manutenção'), ('STFC_RECORRENTE', 'STFC recorrente'), ('SVA_RECORRENTE', 'SVA recorrente'), ('PABX_RECORRENTE', 'PABX recorrente'), ('PABX_IMPLANTACAO', 'PABX implantação'), ('SOFTWARE_RECORRENTE', 'Software recorrente'), ('SOFTWARE_IMPLANTACAO', 'Software implantação') ON CONFLICT (code) DO NOTHING; -- ===================================================================== -- product_fiscal_profiles — liga products à classificação fiscal -- (eden.md §2.5). Reduzida aos 4 catálogos criados nesta leva. -- ===================================================================== CREATE TABLE product_fiscal_profiles ( id UUID PRIMARY KEY DEFAULT gen_random_uuid(), product_id UUID NOT NULL, billing_component TEXT NOT NULL CHECK (billing_component IN ('RECURRING', 'IMPLEMENTATION')), document_type TEXT NOT NULL DEFAULT 'NONE' CHECK (document_type IN ('NFCOM', 'NFSE', 'NFE', 'NONE')), ncm_id UUID REFERENCES fiscal_ncm(id), cfop_id UUID REFERENCES fiscal_cfop(id), municipality_id UUID REFERENCES fiscal_municipality(id), operation_nature_id UUID REFERENCES fiscal_operation_nature(id), valid_from DATE, valid_until DATE, active BOOLEAN NOT NULL DEFAULT true, created_at TIMESTAMPTZ NOT NULL DEFAULT now(), updated_at TIMESTAMPTZ NOT NULL DEFAULT now(), created_by UUID REFERENCES users(id), updated_by UUID REFERENCES users(id) ); COMMENT ON TABLE product_fiscal_profiles IS 'Configuração fiscal de produto (eden.md §2.5) — deliberadamente separada de products, nunca duplica campos comerciais (price_*, impl_unit_price, metered, tariff_rates, has_ldi continuam só em products). product_id SEM FK (products pode não existir neste worktree isolado — ver cabeçalho da migration, padrão "FK cruzada adiada" de contracts.cjs/support.cjs). Colunas de referência a catálogos ainda não portados (nfcom_cclass_id, nfse_trib_nacional_id, nfse_trib_municipal_id, lc116_service_id, nbs_id, cest_id, product_origin_id, ibs_cbs_cst_id, ibs_cbs_classification_id, ibs_cbs_credit_class_id) ficam para quando os catálogos correspondentes existirem. Sem backfill automático nesta leva (eden.md §2.5 backfill dependia de products.service_type/impl_unit_price já populados em produção real) — profiles são criados sob demanda via API.'; COMMENT ON COLUMN product_fiscal_profiles.product_id IS 'Sem REFERENCES products(id) nesta leva — products pode não existir neste worktree isolado. FK cruzada é trabalho de integração no merge (mesmo padrão de contracts.quote_id/client_registration_id e support_tickets.client_registration_id/contract_id).'; -- Invariante central (eden.md §2.5): um produto tem no máximo um -- profile ATIVO por billing_component. Nunca DELETE/UPDATE destrutivo -- de um profile antigo para "trocar" a classificação — cria-se um novo -- profile ativo e inativa-se o anterior (active=false), preservando -- histórico. CREATE UNIQUE INDEX idx_product_fiscal_profiles_active_component ON product_fiscal_profiles(product_id, billing_component) WHERE active = true; CREATE INDEX idx_product_fiscal_profiles_product ON product_fiscal_profiles(product_id); CREATE TRIGGER trg_product_fiscal_profiles_updated_at BEFORE UPDATE ON product_fiscal_profiles FOR EACH ROW EXECUTE FUNCTION set_updated_at(); `); }; exports.down = (pgm) => { pgm.sql(` DROP TABLE IF EXISTS product_fiscal_profiles; DROP TABLE IF EXISTS fiscal_operation_nature; DROP TABLE IF EXISTS fiscal_municipality; DROP TABLE IF EXISTS fiscal_cfop; DROP TABLE IF EXISTS fiscal_ncm; `); };