@fayz-ai/plugin-fiscal-br 0.3.1 → 0.3.4
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.
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"registries.d.ts","sourceRoot":"","sources":["../../src/data/registries.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAa,iBAAiB,EAAE,MAAM,eAAe,CAAA;AA4FjE,eAAO,MAAM,gBAAgB,EAAE,iBAAiB,EAQ/C,CAAA"}
|
package/dist/index.d.ts.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../src/index.ts"],"names":[],"mappings":"AAAA,OAAO,EAAgB,KAAK,cAAc,EAAE,MAAM,eAAe,CAAA;AACjE,OAAO,EAA4B,KAAK,+BAA+B,EAAE,MAAM,wBAAwB,CAAA;
|
|
1
|
+
{"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../src/index.ts"],"names":[],"mappings":"AAAA,OAAO,EAAgB,KAAK,cAAc,EAAE,MAAM,eAAe,CAAA;AACjE,OAAO,EAA4B,KAAK,+BAA+B,EAAE,MAAM,wBAAwB,CAAA;AAIvG,cAAc,wBAAwB,CAAA;AACtC,cAAc,0BAA0B,CAAA;AACxC,cAAc,SAAS,CAAA;AAEvB,MAAM,WAAW,4BAA6B,SAAQ,+BAA+B;CAAG;AAExF;;;GAGG;AACH,wBAAgB,qBAAqB,CAAC,OAAO,GAAE,4BAAiC,GAAG,cAAc,CAoFhG"}
|
package/dist/index.js
CHANGED
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import { connectionSettings, connectionStatus, definePlugin, createConnectionStore, settingsStatus } from '@fayz-ai/core';
|
|
1
|
+
import { recordRailFields, RECORD_GROUP, activeFacet, connectionSettings, connectionStatus, definePlugin, createConnectionStore, settingsStatus } from '@fayz-ai/core';
|
|
2
2
|
|
|
3
3
|
// src/index.ts
|
|
4
4
|
var PLUGNOTAS_CONNECTOR_ID = "plugnotas";
|
|
@@ -1576,12 +1576,357 @@ DROP INDEX IF EXISTS public.plg_fiscalbr_tax_groups_name_uidx;
|
|
|
1576
1576
|
CREATE INDEX IF NOT EXISTS plg_fiscalbr_tax_groups_name_idx
|
|
1577
1577
|
ON public.plg_fiscalbr_tax_groups USING btree (tenant_id, lower(name));
|
|
1578
1578
|
`;
|
|
1579
|
+
var MIGRATION_005_A_NOTA_EMITIDA_E_UM_DOCUMENTO = `-- A nota emitida \xE9 um DOCUMENTO, e o documento j\xE1 tem arqu\xE9tipo.
|
|
1580
|
+
--
|
|
1581
|
+
-- O V1 do Pertinho do C\xE9u emitiu 496 notas (todas NFC-e: 440 conclu\xEDdas, 51
|
|
1582
|
+
-- rejeitadas, 5 com erro) e nenhuma delas tem onde morar no V2. O que existe
|
|
1583
|
+
-- hoje aqui \xE9 a caixa de ENTRADA (\`plg_fiscalbr_inbox_*\`, a nota que chega do
|
|
1584
|
+
-- fornecedor) e o JOB de sa\xEDda (\`plg_fiscalbr_nfce_jobs\`, a tentativa de
|
|
1585
|
+
-- emitir). Falta o resultado: a nota que a casa emitiu, com chave, protocolo e
|
|
1586
|
+
-- XML.
|
|
1587
|
+
--
|
|
1588
|
+
-- \u2500\u2500 Por que pendurar em \`public.documents\` e n\xE3o numa tabela solta \u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500
|
|
1589
|
+
--
|
|
1590
|
+
-- Porque a pergunta que o operador faz n\xE3o \xE9 "quais notas fiscais existem",
|
|
1591
|
+
-- \xE9 "o que foi emitido sobre ESTE pedido". Hoje a resposta est\xE1 espalhada:
|
|
1592
|
+
-- o anexo do t\xEDtulo est\xE1 em \`documents\`, o formul\xE1rio est\xE1 em \`documents\`, a
|
|
1593
|
+
-- foto do ponto est\xE1 em \`documents\` \u2014 e a nota fiscal ficaria de fora, sendo
|
|
1594
|
+
-- o documento mais documento de todos.
|
|
1595
|
+
--
|
|
1596
|
+
-- \`documents\` \xE9 o arqu\xE9tipo: \`kind\` diz a esp\xE9cie, \`subject_type\`/\`subject_id\`
|
|
1597
|
+
-- dizem sobre o qu\xEA, e o plugin pendura o que s\xF3 ele entende numa extens\xE3o 1:1
|
|
1598
|
+
-- pela chave \u2014 exatamente como \`plg_forms_documents\` j\xE1 faz. Esta migra\xE7\xE3o
|
|
1599
|
+
-- segue esse padr\xE3o, n\xE3o inventa outro.
|
|
1600
|
+
--
|
|
1601
|
+
-- \u2500\u2500 O que entra e o que N\xC3O entra \u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500
|
|
1602
|
+
--
|
|
1603
|
+
-- ENTRA o artefato legal e a trilha:
|
|
1604
|
+
-- xml_content 113 de 496 preenchidos, 618 kB no total (5,6 kB cada).
|
|
1605
|
+
-- \xC9 a prova da emiss\xE3o e n\xE3o se regenera. Fica.
|
|
1606
|
+
-- os quatro payloads 108 kB para as 496 notas inteiras. \xC9 a conversa com a
|
|
1607
|
+
-- SEFAZ, e \xE9 ela que explica as 51 rejei\xE7\xF5es.
|
|
1608
|
+
-- pdf_storage_path 84 de 496. Vira \`documents.storage_path\` com
|
|
1609
|
+
-- \`storage_provider = 'v1-pending'\`, a mesma conven\xE7\xE3o
|
|
1610
|
+
-- que \`upsert_document\` usa: o REGISTRO chegou, os bytes
|
|
1611
|
+
-- ainda n\xE3o.
|
|
1612
|
+
--
|
|
1613
|
+
-- N\xC3O entra nada que o V2 j\xE1 saiba responder por conta pr\xF3pria, e a emiss\xE3o
|
|
1614
|
+
-- nova do V2 continua nascendo por \`plg_fiscalbr_nfce_jobs\` \u2014 este arquivo \xE9 o
|
|
1615
|
+
-- destino do resultado, n\xE3o um segundo caminho de emiss\xE3o.
|
|
1616
|
+
--
|
|
1617
|
+
-- \u2500\u2500 Os dois ponteiros da nota do V1, que n\xE3o s\xE3o o mesmo \u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500
|
|
1618
|
+
--
|
|
1619
|
+
-- fiscal_documents.order_id 496 de 496 \u2014 a COMANDA (\`table_orders\`)
|
|
1620
|
+
-- fiscal_documents.invoice_id 118 de 496 \u2014 a VENDA (\`invoices\`)
|
|
1621
|
+
--
|
|
1622
|
+
-- Os dois chegam em \`public.orders\` no V2, e \xE0s vezes na MESMA linha (a
|
|
1623
|
+
-- comanda que virou nota adota a venda). Guardar os dois na mesma coluna
|
|
1624
|
+
-- perderia justamente a distin\xE7\xE3o: \`order_id\` na extens\xE3o \xE9 a comanda; a venda
|
|
1625
|
+
-- vai para \`plg_fiscalbr_document_orders\`, que \xE9 N:N porque uma nota pode
|
|
1626
|
+
-- juntar v\xE1rias (511 v\xEDnculos para 496 notas).
|
|
1627
|
+
--
|
|
1628
|
+
-- Idempotente.
|
|
1629
|
+
|
|
1630
|
+
-- \u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550
|
|
1631
|
+
-- 1. A esp\xE9cie entra no vocabul\xE1rio
|
|
1632
|
+
-- \u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550
|
|
1633
|
+
-- \`migration.upsert_document\` RECUSA esp\xE9cie que n\xE3o esteja aqui, e faz certo:
|
|
1634
|
+
-- \xE9 o registro que impede \`kind\` de virar campo livre.
|
|
1635
|
+
|
|
1636
|
+
INSERT INTO public.document_kinds (kind, plugin, description, statuses, status)
|
|
1637
|
+
VALUES ('fiscal_document', 'fiscal_br',
|
|
1638
|
+
'A nota fiscal que a casa EMITIU (NFC-e, NF-e, NFS-e), com chave, protocolo e XML',
|
|
1639
|
+
ARRAY['draft','processing','authorized','rejected','error','cancelled','substituted'],
|
|
1640
|
+
'declared')
|
|
1641
|
+
ON CONFLICT (kind) DO UPDATE
|
|
1642
|
+
SET plugin = EXCLUDED.plugin,
|
|
1643
|
+
description = EXCLUDED.description,
|
|
1644
|
+
statuses = EXCLUDED.statuses,
|
|
1645
|
+
updated_at = now();
|
|
1646
|
+
|
|
1647
|
+
-- \u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550
|
|
1648
|
+
-- 2. A extens\xE3o: o que s\xF3 o fiscal entende
|
|
1649
|
+
-- \u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550
|
|
1650
|
+
|
|
1651
|
+
CREATE TABLE IF NOT EXISTS public.plg_fiscalbr_documents (
|
|
1652
|
+
-- A chave \xC9 o documento. N\xE3o h\xE1 id pr\xF3prio: a nota e o documento s\xE3o a mesma
|
|
1653
|
+
-- coisa vista de dois lugares, e um segundo id s\xF3 criaria a chance de eles
|
|
1654
|
+
-- discordarem.
|
|
1655
|
+
document_id uuid PRIMARY KEY REFERENCES public.documents(id) ON DELETE CASCADE,
|
|
1656
|
+
tenant_id uuid NOT NULL REFERENCES public.tenants(id) ON DELETE CASCADE,
|
|
1657
|
+
unit_id uuid,
|
|
1658
|
+
-- A comanda/pedido que originou a emiss\xE3o.
|
|
1659
|
+
order_id uuid REFERENCES public.orders(id) ON DELETE SET NULL,
|
|
1660
|
+
model text NOT NULL,
|
|
1661
|
+
environment text,
|
|
1662
|
+
status text NOT NULL,
|
|
1663
|
+
series text,
|
|
1664
|
+
number text,
|
|
1665
|
+
-- 44 d\xEDgitos. Sem CHECK de formato: uma nota REJEITADA n\xE3o tem chave, e uma
|
|
1666
|
+
-- chave que a SEFAZ devolveu torta \xE9 informa\xE7\xE3o, n\xE3o erro de digita\xE7\xE3o.
|
|
1667
|
+
access_key text,
|
|
1668
|
+
verification_code text,
|
|
1669
|
+
protocol text,
|
|
1670
|
+
provider text,
|
|
1671
|
+
provider_document_id text,
|
|
1672
|
+
provider_integration_id text,
|
|
1673
|
+
total_value numeric(14,2),
|
|
1674
|
+
iss_value numeric(14,2),
|
|
1675
|
+
recipient_snapshot jsonb,
|
|
1676
|
+
request_payload jsonb,
|
|
1677
|
+
response_payload jsonb,
|
|
1678
|
+
status_payload jsonb,
|
|
1679
|
+
rejection_payload jsonb,
|
|
1680
|
+
source_snapshot jsonb,
|
|
1681
|
+
-- O artefato legal. 5,6 kB por nota nas 113 que o t\xEAm; n\xE3o se regenera.
|
|
1682
|
+
xml text,
|
|
1683
|
+
authorized_at timestamptz,
|
|
1684
|
+
last_polled_at timestamptz,
|
|
1685
|
+
emitted_by_user_id uuid,
|
|
1686
|
+
cancelled_at timestamptz,
|
|
1687
|
+
cancel_reason text,
|
|
1688
|
+
cancelled_by_user_id uuid,
|
|
1689
|
+
-- A cadeia de substitui\xE7\xE3o. Auto-refer\xEAncia, ent\xE3o SET NULL: apagar a nota
|
|
1690
|
+
-- substitu\xEDda n\xE3o pode arrastar a que a substituiu.
|
|
1691
|
+
substitutes_document_id uuid REFERENCES public.plg_fiscalbr_documents(document_id) ON DELETE SET NULL,
|
|
1692
|
+
substituted_by_document_id uuid REFERENCES public.plg_fiscalbr_documents(document_id) ON DELETE SET NULL,
|
|
1693
|
+
observations text,
|
|
1694
|
+
metadata jsonb NOT NULL DEFAULT '{}'::jsonb,
|
|
1695
|
+
created_at timestamptz NOT NULL DEFAULT now(),
|
|
1696
|
+
updated_at timestamptz NOT NULL DEFAULT now()
|
|
1697
|
+
);
|
|
1698
|
+
|
|
1699
|
+
DO $$ BEGIN
|
|
1700
|
+
ALTER TABLE public.plg_fiscalbr_documents
|
|
1701
|
+
ADD CONSTRAINT plg_fiscalbr_documents_unit_fkey
|
|
1702
|
+
FOREIGN KEY (tenant_id, unit_id) REFERENCES app.units(tenant_id, id) ON DELETE SET NULL;
|
|
1703
|
+
EXCEPTION WHEN duplicate_object THEN NULL; END $$;
|
|
1704
|
+
|
|
1705
|
+
-- A chave de acesso \xE9 \xFAnica POR INQUILINO e s\xF3 quando existe: 56 das 496 notas
|
|
1706
|
+
-- do Pertinho n\xE3o t\xEAm chave nenhuma (rejeitadas e com erro), e um UNIQUE
|
|
1707
|
+
-- comum as empilharia todas num conflito s\xF3.
|
|
1708
|
+
CREATE UNIQUE INDEX IF NOT EXISTS plg_fiscalbr_documents_chave_idx
|
|
1709
|
+
ON public.plg_fiscalbr_documents (tenant_id, access_key) WHERE access_key IS NOT NULL;
|
|
1710
|
+
CREATE INDEX IF NOT EXISTS plg_fiscalbr_documents_order_idx
|
|
1711
|
+
ON public.plg_fiscalbr_documents (tenant_id, order_id);
|
|
1712
|
+
CREATE INDEX IF NOT EXISTS plg_fiscalbr_documents_status_idx
|
|
1713
|
+
ON public.plg_fiscalbr_documents (tenant_id, unit_id, status, authorized_at DESC);
|
|
1714
|
+
|
|
1715
|
+
-- \u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550
|
|
1716
|
+
-- 3. A trilha da emiss\xE3o
|
|
1717
|
+
-- \u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550
|
|
1718
|
+
-- 1.886 eventos para 496 notas \u2014 quase quatro por nota. \xC9 aqui que a rejei\xE7\xE3o
|
|
1719
|
+
-- diz o motivo, e \xE9 o \xFAnico lugar em que ela diz.
|
|
1720
|
+
|
|
1721
|
+
CREATE TABLE IF NOT EXISTS public.plg_fiscalbr_document_events (
|
|
1722
|
+
id uuid PRIMARY KEY DEFAULT gen_random_uuid(),
|
|
1723
|
+
tenant_id uuid NOT NULL REFERENCES public.tenants(id) ON DELETE CASCADE,
|
|
1724
|
+
unit_id uuid,
|
|
1725
|
+
document_id uuid NOT NULL REFERENCES public.plg_fiscalbr_documents(document_id) ON DELETE CASCADE,
|
|
1726
|
+
event_type text NOT NULL,
|
|
1727
|
+
status text,
|
|
1728
|
+
justification text,
|
|
1729
|
+
request_payload jsonb,
|
|
1730
|
+
response_payload jsonb,
|
|
1731
|
+
created_by_user_id uuid,
|
|
1732
|
+
metadata jsonb NOT NULL DEFAULT '{}'::jsonb,
|
|
1733
|
+
created_at timestamptz NOT NULL DEFAULT now()
|
|
1734
|
+
);
|
|
1735
|
+
|
|
1736
|
+
CREATE INDEX IF NOT EXISTS plg_fiscalbr_document_events_doc_idx
|
|
1737
|
+
ON public.plg_fiscalbr_document_events (document_id, created_at DESC);
|
|
1738
|
+
|
|
1739
|
+
-- \u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550
|
|
1740
|
+
-- 4. Quais vendas a nota cobre
|
|
1741
|
+
-- \u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550
|
|
1742
|
+
|
|
1743
|
+
CREATE TABLE IF NOT EXISTS public.plg_fiscalbr_document_orders (
|
|
1744
|
+
id uuid PRIMARY KEY DEFAULT gen_random_uuid(),
|
|
1745
|
+
tenant_id uuid NOT NULL REFERENCES public.tenants(id) ON DELETE CASCADE,
|
|
1746
|
+
document_id uuid NOT NULL REFERENCES public.plg_fiscalbr_documents(document_id) ON DELETE CASCADE,
|
|
1747
|
+
order_id uuid NOT NULL REFERENCES public.orders(id) ON DELETE CASCADE,
|
|
1748
|
+
created_at timestamptz NOT NULL DEFAULT now(),
|
|
1749
|
+
UNIQUE (document_id, order_id)
|
|
1750
|
+
);
|
|
1751
|
+
|
|
1752
|
+
CREATE INDEX IF NOT EXISTS plg_fiscalbr_document_orders_order_idx
|
|
1753
|
+
ON public.plg_fiscalbr_document_orders (tenant_id, order_id);
|
|
1754
|
+
|
|
1755
|
+
-- \u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550
|
|
1756
|
+
-- 5. updated_at e a cerca
|
|
1757
|
+
-- \u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550
|
|
1758
|
+
|
|
1759
|
+
DO $$ BEGIN
|
|
1760
|
+
CREATE TRIGGER plg_fiscalbr_documents_updated_at BEFORE UPDATE
|
|
1761
|
+
ON public.plg_fiscalbr_documents FOR EACH ROW EXECUTE FUNCTION public.handle_updated_at();
|
|
1762
|
+
EXCEPTION WHEN duplicate_object THEN NULL; END $$;
|
|
1763
|
+
|
|
1764
|
+
-- \`app.current_tenant_id()\` e n\xE3o \`user_tenant_ids()\`, pela raz\xE3o que a 013 do
|
|
1765
|
+
-- plugin-tables teve de consertar em produ\xE7\xE3o: a segunda devolve a UNI\xC3O das
|
|
1766
|
+
-- contas do chamador e mostraria a nota fiscal de um cliente para outro.
|
|
1767
|
+
DO $$
|
|
1768
|
+
DECLARE t text;
|
|
1769
|
+
BEGIN
|
|
1770
|
+
FOREACH t IN ARRAY ARRAY['plg_fiscalbr_documents','plg_fiscalbr_document_events',
|
|
1771
|
+
'plg_fiscalbr_document_orders'] LOOP
|
|
1772
|
+
EXECUTE format('ALTER TABLE public.%I ENABLE ROW LEVEL SECURITY', t);
|
|
1773
|
+
EXECUTE format('DROP POLICY IF EXISTS %I ON public.%I', t || '_select', t);
|
|
1774
|
+
EXECUTE format('DROP POLICY IF EXISTS %I ON public.%I', t || '_insert', t);
|
|
1775
|
+
EXECUTE format('DROP POLICY IF EXISTS %I ON public.%I', t || '_update', t);
|
|
1776
|
+
EXECUTE format('DROP POLICY IF EXISTS %I ON public.%I', t || '_delete', t);
|
|
1777
|
+
EXECUTE format('CREATE POLICY %I ON public.%I FOR SELECT USING (tenant_id = (select app.current_tenant_id()))', t || '_select', t);
|
|
1778
|
+
EXECUTE format('CREATE POLICY %I ON public.%I FOR INSERT WITH CHECK (tenant_id = (select app.current_tenant_id()))', t || '_insert', t);
|
|
1779
|
+
EXECUTE format('CREATE POLICY %I ON public.%I FOR UPDATE USING (tenant_id = (select app.current_tenant_id()))', t || '_update', t);
|
|
1780
|
+
EXECUTE format('CREATE POLICY %I ON public.%I FOR DELETE USING (tenant_id = (select app.current_tenant_id()))', t || '_delete', t);
|
|
1781
|
+
EXECUTE format('GRANT SELECT, INSERT, UPDATE, DELETE ON public.%I TO authenticated', t);
|
|
1782
|
+
END LOOP;
|
|
1783
|
+
END $$;
|
|
1784
|
+
`;
|
|
1785
|
+
var MIGRATION_006_O_GRUPO_FISCAL_GANHA_O_RAIL = `-- O grupo fiscal ganha observa\xE7\xE3o e etiqueta.
|
|
1786
|
+
--
|
|
1787
|
+
-- A tabela existe desde a 002 e nunca teve TELA: o grupo fiscal de toda conta
|
|
1788
|
+
-- migrada estava no banco sem porta. Estas duas colunas s\xE3o o que falta para
|
|
1789
|
+
-- ele ter o mesmo rail de todo cadastro do produto \u2014 e a etiqueta tem uso
|
|
1790
|
+
-- imediato aqui, porque \xABSimples\xBB, \xABmonof\xE1sico\xBB e \xABsubstitui\xE7\xE3o tribut\xE1ria\xBB
|
|
1791
|
+
-- s\xE3o recortes que nenhum campo do grupo responde e que decidem qual grupo
|
|
1792
|
+
-- algu\xE9m procura.
|
|
1793
|
+
--
|
|
1794
|
+
-- Idempotente.
|
|
1795
|
+
|
|
1796
|
+
ALTER TABLE public.plg_fiscalbr_tax_groups
|
|
1797
|
+
ADD COLUMN IF NOT EXISTS notes text,
|
|
1798
|
+
ADD COLUMN IF NOT EXISTS tags text[] DEFAULT '{}'::text[],
|
|
1799
|
+
ADD COLUMN IF NOT EXISTS unit_id uuid;
|
|
1800
|
+
|
|
1801
|
+
CREATE INDEX IF NOT EXISTS plg_fiscalbr_tax_groups_tags_gin
|
|
1802
|
+
ON public.plg_fiscalbr_tax_groups USING gin (tags);
|
|
1803
|
+
`;
|
|
1804
|
+
var MIGRATION_007_A_RLS_DO_DOCUMENTO_PASSA_A_SER_LEGIVEL = `-- ---------------------------------------------------------------------------
|
|
1805
|
+
-- 007_a_rls_do_documento_passa_a_ser_legivel.sql
|
|
1806
|
+
--
|
|
1807
|
+
-- MESMO caso da 036 do plugin-tables, e a mesma medi\xE7\xE3o antes de escrever.
|
|
1808
|
+
--
|
|
1809
|
+
-- \`check-plugin-review\` reprovou estas tabelas com *\xABcria public.X com
|
|
1810
|
+
-- tenant_id e nunca liga row security\xBB*. Fui ao banco: **todas t\xEAm RLS ligada,
|
|
1811
|
+
-- com pol\xEDticas** \u2014 medido no cluster do Pertinho em 29-09:
|
|
1812
|
+
--
|
|
1813
|
+
-- plg_fiscalbr_documents ........ rls=t, 4 pol\xEDticas
|
|
1814
|
+
-- plg_fiscalbr_document_events .. rls=t, 4
|
|
1815
|
+
-- plg_fiscalbr_document_orders .. rls=t, 4
|
|
1816
|
+
--
|
|
1817
|
+
-- N\xE3o h\xE1 vazamento. O \`ENABLE\` dessas tabelas mora dentro de um
|
|
1818
|
+
-- \`DO $$ \u2026 EXECUTE format(...)\`, e o port\xE3o l\xEA o ARQUIVO, n\xE3o o banco \u2014 uma
|
|
1819
|
+
-- garantia que o revisor n\xE3o consegue ler \xE9 uma garantia que ningu\xE9m confere.
|
|
1820
|
+
--
|
|
1821
|
+
-- As migrations que as criam J\xC1 FORAM APLICADAS; reescrev\xEA-las trocaria o
|
|
1822
|
+
-- checksum, que \xE9 a deriva de ledger pela qual este reposit\xF3rio para o mundo.
|
|
1823
|
+
-- Ent\xE3o a legibilidade entra por arquivo NOVO \u2014 o port\xE3o pergunta ao SQL
|
|
1824
|
+
-- INTEIRO do plugin.
|
|
1825
|
+
--
|
|
1826
|
+
-- Ligar RLS j\xE1 ligada \xE9 no-op: isto n\xE3o muda comportamento, torna confer\xEDvel o
|
|
1827
|
+
-- que j\xE1 era verdade.
|
|
1828
|
+
--
|
|
1829
|
+
-- Idempotente e replay-safe.
|
|
1830
|
+
-- ---------------------------------------------------------------------------
|
|
1831
|
+
|
|
1832
|
+
ALTER TABLE public.plg_fiscalbr_documents ENABLE ROW LEVEL SECURITY;
|
|
1833
|
+
ALTER TABLE public.plg_fiscalbr_document_events ENABLE ROW LEVEL SECURITY;
|
|
1834
|
+
ALTER TABLE public.plg_fiscalbr_document_orders ENABLE ROW LEVEL SECURITY;
|
|
1835
|
+
`;
|
|
1579
1836
|
var MIGRATIONS = [
|
|
1580
1837
|
{ id: "000_baseline", sql: MIGRATION_000_BASELINE },
|
|
1581
1838
|
{ id: "001_preserve_terminal_results_and_recover_leases", sql: MIGRATION_001_PRESERVE_TERMINAL_RESULTS_AND_RECOVER_LEASES },
|
|
1582
1839
|
{ id: "002_a_nota_que_chega_tem_onde_esperar", sql: MIGRATION_002_A_NOTA_QUE_CHEGA_TEM_ONDE_ESPERAR },
|
|
1583
1840
|
{ id: "003_a_caixa_de_entrada_atravessa", sql: MIGRATION_003_A_CAIXA_DE_ENTRADA_ATRAVESSA },
|
|
1584
|
-
{ id: "004_o_grupo_fiscal_do_v1_repete_nome", sql: MIGRATION_004_O_GRUPO_FISCAL_DO_V1_REPETE_NOME }
|
|
1841
|
+
{ id: "004_o_grupo_fiscal_do_v1_repete_nome", sql: MIGRATION_004_O_GRUPO_FISCAL_DO_V1_REPETE_NOME },
|
|
1842
|
+
{ id: "005_a_nota_emitida_e_um_documento", sql: MIGRATION_005_A_NOTA_EMITIDA_E_UM_DOCUMENTO },
|
|
1843
|
+
{ id: "006_o_grupo_fiscal_ganha_o_rail", sql: MIGRATION_006_O_GRUPO_FISCAL_GANHA_O_RAIL },
|
|
1844
|
+
{ id: "007_a_rls_do_documento_passa_a_ser_legivel", sql: MIGRATION_007_A_RLS_DO_DOCUMENTO_PASSA_A_SER_LEGIVEL }
|
|
1845
|
+
];
|
|
1846
|
+
var taxGroupEntity = {
|
|
1847
|
+
name: "Grupo fiscal",
|
|
1848
|
+
namePlural: "Grupos fiscais",
|
|
1849
|
+
icon: "ReceiptText",
|
|
1850
|
+
displayField: "name",
|
|
1851
|
+
defaultSort: "name",
|
|
1852
|
+
formColumns: 2,
|
|
1853
|
+
facets: [activeFacet({ active: "Ativos", inactive: "Inativos", all: "Todos" })],
|
|
1854
|
+
fieldGroups: [
|
|
1855
|
+
{
|
|
1856
|
+
id: "identity",
|
|
1857
|
+
label: "Identifica\xE7\xE3o",
|
|
1858
|
+
columns: 2,
|
|
1859
|
+
description: "O nome \xE9 como a casa chama o grupo (\xABBebida alco\xF3lica\xBB, \xABServi\xE7o isento\xBB) \u2014 o resto \xE9 o que a nota exige."
|
|
1860
|
+
},
|
|
1861
|
+
{ id: "icms", label: "ICMS", columns: 3 },
|
|
1862
|
+
{ id: "pis_cofins", label: "PIS e COFINS", columns: 2 },
|
|
1863
|
+
{
|
|
1864
|
+
id: "ibs_cbs",
|
|
1865
|
+
label: "IBS e CBS",
|
|
1866
|
+
columns: 2,
|
|
1867
|
+
description: "A reforma tribut\xE1ria. Em branco enquanto n\xE3o for exigido da sua opera\xE7\xE3o."
|
|
1868
|
+
},
|
|
1869
|
+
{ ...RECORD_GROUP }
|
|
1870
|
+
],
|
|
1871
|
+
fields: [
|
|
1872
|
+
{ key: "name", label: "Nome", type: "text", required: true, showInTable: true, searchable: true, group: "identity", span: 2 },
|
|
1873
|
+
// O CFOP classifica a OPERAÇÃO (venda dentro do estado, fora, devolução) e
|
|
1874
|
+
// é o primeiro campo que o contador confere.
|
|
1875
|
+
{ key: "cfop", label: "CFOP", type: "text", showInTable: true, searchable: true, group: "identity" },
|
|
1876
|
+
// CEST anda com a substituição tributária: só existe para quem está nela.
|
|
1877
|
+
{ key: "cest", label: "CEST", type: "text", showInTable: false, group: "identity" },
|
|
1878
|
+
{ key: "benefitCode", label: "C\xF3digo de benef\xEDcio", type: "text", showInTable: false, group: "identity" },
|
|
1879
|
+
// CST é do regime normal e CSOSN é do Simples — a casa usa UM dos dois, e
|
|
1880
|
+
// qual depende do regime dela, não deste grupo. Os dois ficam à vista de
|
|
1881
|
+
// propósito: esconder o que a conta não usa exigiria saber o regime aqui.
|
|
1882
|
+
{ key: "cst", label: "CST", type: "text", showInTable: true, group: "icms" },
|
|
1883
|
+
{ key: "csosn", label: "CSOSN (Simples)", type: "text", showInTable: false, group: "icms" },
|
|
1884
|
+
{
|
|
1885
|
+
key: "icmsOrigin",
|
|
1886
|
+
label: "Origem da mercadoria",
|
|
1887
|
+
type: "select",
|
|
1888
|
+
showInTable: false,
|
|
1889
|
+
group: "icms",
|
|
1890
|
+
options: [
|
|
1891
|
+
{ value: "0", label: "0 \u2014 Nacional" },
|
|
1892
|
+
{ value: "1", label: "1 \u2014 Estrangeira, importa\xE7\xE3o direta" },
|
|
1893
|
+
{ value: "2", label: "2 \u2014 Estrangeira, mercado interno" },
|
|
1894
|
+
{ value: "3", label: "3 \u2014 Nacional, conte\xFAdo de importa\xE7\xE3o > 40%" },
|
|
1895
|
+
{ value: "4", label: "4 \u2014 Nacional, processos produtivos b\xE1sicos" },
|
|
1896
|
+
{ value: "5", label: "5 \u2014 Nacional, conte\xFAdo de importa\xE7\xE3o \u2264 40%" },
|
|
1897
|
+
{ value: "6", label: "6 \u2014 Estrangeira, sem similar nacional" },
|
|
1898
|
+
{ value: "7", label: "7 \u2014 Estrangeira, mercado interno sem similar" },
|
|
1899
|
+
{ value: "8", label: "8 \u2014 Nacional, conte\xFAdo de importa\xE7\xE3o > 70%" }
|
|
1900
|
+
]
|
|
1901
|
+
},
|
|
1902
|
+
{ key: "icmsRate", label: "Al\xEDquota ICMS (%)", type: "number", min: 0, max: 100, showInTable: true, group: "icms" },
|
|
1903
|
+
{ key: "icmsBcMode", label: "Modalidade da base", type: "text", showInTable: false, group: "icms" },
|
|
1904
|
+
{ key: "baseCalculationMode", label: "C\xE1lculo da base", type: "text", showInTable: false, group: "icms" },
|
|
1905
|
+
// O fundo de combate à pobreza é adicional ao ICMS em alguns estados.
|
|
1906
|
+
{ key: "povertyFundRate", label: "Fundo de pobreza (%)", type: "number", min: 0, max: 100, showInTable: false, group: "icms" },
|
|
1907
|
+
{ key: "pisSituation", label: "Situa\xE7\xE3o PIS", type: "text", showInTable: false, group: "pis_cofins" },
|
|
1908
|
+
{ key: "pisRate", label: "Al\xEDquota PIS (%)", type: "number", min: 0, max: 100, showInTable: false, group: "pis_cofins" },
|
|
1909
|
+
{ key: "cofinsSituation", label: "Situa\xE7\xE3o COFINS", type: "text", showInTable: false, group: "pis_cofins" },
|
|
1910
|
+
{ key: "cofinsRate", label: "Al\xEDquota COFINS (%)", type: "number", min: 0, max: 100, showInTable: false, group: "pis_cofins" },
|
|
1911
|
+
{ key: "ibsCbsSituation", label: "Situa\xE7\xE3o IBS/CBS", type: "text", showInTable: false, group: "ibs_cbs" },
|
|
1912
|
+
{ key: "ibsCbsClassification", label: "Classifica\xE7\xE3o IBS/CBS", type: "text", showInTable: false, group: "ibs_cbs" },
|
|
1913
|
+
...recordRailFields({ tagsTable: "plg_fiscalbr_tax_groups", activeLabel: "Ativo" })
|
|
1914
|
+
],
|
|
1915
|
+
data: {
|
|
1916
|
+
table: "plg_fiscalbr_tax_groups",
|
|
1917
|
+
schema: "public",
|
|
1918
|
+
tenantScoped: true,
|
|
1919
|
+
searchColumns: ["name", "cfop"]
|
|
1920
|
+
}
|
|
1921
|
+
};
|
|
1922
|
+
var fiscalRegistries = [
|
|
1923
|
+
{
|
|
1924
|
+
id: "taxGroups",
|
|
1925
|
+
icon: "ReceiptText",
|
|
1926
|
+
entity: taxGroupEntity,
|
|
1927
|
+
description: "A tributa\xE7\xE3o que um produto herda: CFOP, ICMS, PIS/COFINS e, quando exigido, IBS/CBS.",
|
|
1928
|
+
agentHint: "Regras de imposto que um produto herda \u2014 N\xC3O \xE9 a nota emitida nem o imposto de uma venda espec\xEDfica."
|
|
1929
|
+
}
|
|
1585
1930
|
];
|
|
1586
1931
|
|
|
1587
1932
|
// src/lib/financial-provider.ts
|
|
@@ -1689,6 +2034,9 @@ function createPlugNotasPlugin(options = {}) {
|
|
|
1689
2034
|
dependencies: ["financial"],
|
|
1690
2035
|
navigation: [],
|
|
1691
2036
|
routes: [],
|
|
2037
|
+
// O grupo fiscal é CADASTRO, e por isso entra como registry e não como
|
|
2038
|
+
// rota própria: ele aparece onde os outros cadastros do produto aparecem.
|
|
2039
|
+
registries: fiscalRegistries,
|
|
1692
2040
|
connectors: [createPlugNotasConnector(options)],
|
|
1693
2041
|
functions: [{
|
|
1694
2042
|
slug: "plugnotas-bridge",
|
package/dist/index.js.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"sources":["../src/connectors/plugnotas.ts","../src/migrations/index.ts","../src/lib/financial-provider.ts","../src/index.ts"],"names":[],"mappings":"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;AAAA;AAAA;AAAA;AAAA;AAAA;;AAAA;AAAA;AAAA;;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;;AAAA;AAAA;;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;;AAAA;AAAA;AAAA;AAAA;;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;;AAAA;AAAA;AAAA;;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;;AAAA;AAAA;AAAA;AAAA;;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;;AAAA;AAAA;AAAA;AAAA;AAAA;;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA,CAAA;AA6jB/B,IAAM,0DAAA,GAA6D,CAAA;AAAA;AAAA;;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;;AAAA;AAAA;AAAA;AAAA;AAAA;;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA,CAAA;AAqMnE,IAAM,+CAAA,GAAkD,CAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;;AAAA;AAAA;AAAA;AAAA;;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;;AAAA;AAAA;;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;;AAAA;AAAA;;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;;AAAA;AAAA;;AAAA;AAAA;AAAA;;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;;AAAA;AAAA;AAAA;AAAA;AAAA,CAAA;AA2WxD,IAAM,0CAAA,GAA6C,CAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;;AAAA;;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;;AAAA;AAAA;AAAA;;AAAA;AAAA,CAAA;AAkPnD,IAAM,8CAAA,GAAiD,CAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;;AAAA;AAAA;AAAA;AAA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","file":"index.js","sourcesContent":["import type React from 'react'\nimport {\n connectionSettings,\n connectionStatus,\n createConnectionStore,\n settingsStatus,\n type ConnectionConfig,\n type ConnectorDefinition,\n type ConnectorField,\n type ConnectorSetting,\n} from '@fayz-ai/core'\nimport type { PlugNotasConnectorClient, PlugNotasEnvironment, PlugNotasSetupState } from '../types'\n\nexport const PLUGNOTAS_CONNECTOR_ID = 'plugnotas'\n\nconst PLUGNOTAS_MARK = \"data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' width='32' height='32' viewBox='0 0 32 32'%3E%3Crect width='32' height='32' rx='8' fill='%230F766E'/%3E%3Cpath d='M10 7h12v18l-2-1.5-2 1.5-2-1.5-2 1.5-2-1.5-2 1.5V7Z' fill='white'/%3E%3Cpath d='M13 12h6M13 16h6M13 20h4' stroke='%230F766E' stroke-width='1.8' stroke-linecap='round'/%3E%3C/svg%3E\"\n\nexport const PLUGNOTAS_FIELDS = [\n {\n key: 'emitterCnpj',\n label: 'CNPJ do emitente',\n type: 'text',\n placeholder: '00.000.000/0000-00',\n },\n] as const satisfies readonly ConnectorField[]\n\nexport const PLUGNOTAS_SETTINGS = [{\n key: 'environment',\n label: 'Ambiente fiscal',\n description: 'Sandbox simula o emissor. Homologação envia para a SEFAZ sem valor fiscal.',\n type: 'select',\n options: [\n { value: 'sandbox', label: 'Sandbox simulado' },\n { value: 'homologation', label: 'Homologação SEFAZ' },\n ],\n requiredToOperate: true,\n defaultValue: 'sandbox',\n}] as const satisfies readonly ConnectorSetting[]\n\nconst INTERNAL_SETTINGS = [\n 'certificateReady',\n 'cscReady',\n 'providerEmitterId',\n 'certificateExpiresAt',\n 'certificateFingerprint',\n // Non-secret operational data written by the host's server-side setup flow.\n // Declaring these keys keeps the shared connection store authoritative while\n // credentials (API key, A1 bytes/password, CSC) remain outside public JSON.\n 'stateRegistration',\n 'corporateName',\n 'tradeName',\n 'email',\n 'taxRegime',\n 'address',\n 'series',\n 'paymentMethod',\n]\nconst DAY_MS = 24 * 60 * 60 * 1000\n\nfunction digits(value: string): string {\n return value.replace(/\\D/g, '')\n}\n\nexport function isValidCnpj(value: string): boolean {\n const normalized = digits(value)\n if (!/^\\d{14}$/.test(normalized) || /^(\\d)\\1{13}$/.test(normalized)) return false\n const digit = (base: string, weights: number[]) => {\n const sum = weights.reduce((total, weight, index) => total + Number(base[index]) * weight, 0)\n const remainder = sum % 11\n return remainder < 2 ? 0 : 11 - remainder\n }\n const first = digit(normalized.slice(0, 12), [5, 4, 3, 2, 9, 8, 7, 6, 5, 4, 3, 2])\n const second = digit(`${normalized.slice(0, 12)}${first}`, [6, 5, 4, 3, 2, 9, 8, 7, 6, 5, 4, 3, 2])\n return normalized.endsWith(`${first}${second}`)\n}\n\nfunction environment(value: unknown): PlugNotasEnvironment {\n if (value === undefined || value === null || value === '' || value === 'sandbox') return 'sandbox'\n if (value === 'homologation') return value\n throw new Error('Produção não está disponível neste conector preview.')\n}\n\nfunction store() {\n return createConnectionStore(PLUGNOTAS_CONNECTOR_ID, {\n fields: [...PLUGNOTAS_FIELDS],\n settings: [...PLUGNOTAS_SETTINGS],\n settingsKeys: INTERNAL_SETTINGS,\n })\n}\n\nfunction deriveStatus(connection: ConnectionConfig | null) {\n if (!connection?.active) return { connected: false }\n const readiness = settingsStatus([...PLUGNOTAS_SETTINGS], connection)\n const certificateReady = connection.settings.certificateReady === true\n const cscReady = connection.settings.cscReady === true\n const expiry = typeof connection.settings.certificateExpiresAt === 'string'\n ? Date.parse(connection.settings.certificateExpiresAt)\n : Number.NaN\n const daysUntilExpiry = Number.isFinite(expiry) ? Math.ceil((expiry - Date.now()) / DAY_MS) : null\n const expired = daysUntilExpiry !== null && daysUntilExpiry <= 0\n const expiringSoon = daysUntilExpiry !== null && daysUntilExpiry > 0 && daysUntilExpiry <= 30\n const missingSettings = [...new Set([\n ...(readiness.missingSettings ?? []),\n ...(!certificateReady || expired ? ['certificate'] : []),\n ...(!cscReady ? ['csc'] : []),\n ])]\n\n return {\n connected: true,\n needsSetup: missingSettings.length > 0,\n ...(missingSettings.length ? { missingSettings } : {}),\n detail: expired\n ? 'Certificado A1 vencido'\n : !certificateReady\n ? 'Envie o certificado A1 do emitente.'\n : !cscReady\n ? 'Confirme o CSC e o ID Token do emitente.'\n : connection.settings.environment === 'homologation'\n ? 'Emitente pronto em homologação'\n : 'Emitente pronto no sandbox',\n ...(expiringSoon\n ? { issues: [`O certificado A1 vence em ${daysUntilExpiry} dia${daysUntilExpiry === 1 ? '' : 's'}.`] }\n : {}),\n plane: 'sync' as const,\n }\n}\n\nexport interface CreatePlugNotasConnectorOptions {\n client?: PlugNotasConnectorClient\n SetupPanel?: React.ComponentType\n}\n\nexport function createPlugNotasConnector(options: CreatePlugNotasConnectorOptions = {}): ConnectorDefinition {\n const connectionStore = store()\n return {\n id: PLUGNOTAS_CONNECTOR_ID,\n // PlugBank and PlugNotas are different products from the same vendor. The\n // directory key names the product, otherwise one silently replaces the\n // other in the generated connector map.\n catalogSlug: 'plugnotas',\n hostPluginId: 'financial',\n name: 'PlugNotas · NFC-e',\n description: 'Emita NFC-e pela TecnoSpeed sem expor certificado, senha ou API key no navegador.',\n icon: 'ReceiptText',\n logoUrl: PLUGNOTAS_MARK,\n authKind: 'api-key',\n category: 'erp',\n direction: 'outbound',\n planes: ['sync'],\n docsUrl: 'https://docs.plugnotas.com.br/',\n fields: [...PLUGNOTAS_FIELDS],\n settings: [...PLUGNOTAS_SETTINGS],\n permissions: [{\n kind: 'data',\n resource: 'fiscal-document',\n actions: ['read', 'write'],\n reason: 'Enviar a NFC-e e reconciliar situação, chave de acesso e artefatos no Financeiro.',\n sensitive: true,\n }],\n invokedAt: [{\n key: 'financial.nfce.issue',\n label: 'Quando um operador confirma a emissão da NFC-e de um pedido concluído',\n plane: 'sync',\n trigger: 'manual',\n touches: ['fiscal-document'],\n }],\n howItSyncs: [\n 'A emissão cria primeiro um comando durável e só depois chama o provedor.',\n 'Webhook e recuperação consultiva convergem o mesmo comando, sem reenviar no escuro.',\n ],\n ...connectionStatus(PLUGNOTAS_CONNECTOR_ID, deriveStatus, { store }),\n ...connectionSettings(connectionStore),\n async testConnection(values) {\n const emitterCnpj = digits(values.emitterCnpj ?? '')\n if (!isValidCnpj(emitterCnpj)) return { ok: false, message: 'Informe um CNPJ válido.' }\n let selected: PlugNotasEnvironment\n try {\n selected = environment(values.environment)\n } catch (error) {\n return { ok: false, message: error instanceof Error ? error.message : 'Ambiente inválido.' }\n }\n return options.client?.testEmitter\n ? options.client.testEmitter({ emitterCnpj, environment: selected })\n : { ok: true }\n },\n async saveConnection(values) {\n const emitterCnpj = digits(values.emitterCnpj ?? '')\n if (!isValidCnpj(emitterCnpj)) throw new Error('Informe um CNPJ válido.')\n await connectionStore.save({\n active: true,\n displayName: `Emitente ${emitterCnpj.slice(-6)}`,\n providerAccountKey: emitterCnpj,\n settings: {\n emitterCnpj,\n environment: environment(values.environment),\n certificateReady: false,\n cscReady: false,\n },\n })\n },\n ...(options.client?.disconnectEmitter\n ? { disconnect: () => options.client!.disconnectEmitter!() }\n : {}),\n disconnectNote: 'As notas existentes permanecem no Financeiro; somente novas emissões são interrompidas.',\n ExtraPanel: options.SetupPanel,\n }\n}\n\n/** Persist only server-confirmed readiness and non-secret certificate metadata. */\nexport async function markPlugNotasEmitterReady(input: PlugNotasSetupState): Promise<void> {\n const emitterCnpj = digits(input.emitterCnpj)\n if (!isValidCnpj(emitterCnpj)) throw new Error('Informe um CNPJ válido.')\n await store().save({\n active: true,\n providerAccountKey: emitterCnpj,\n settings: {\n emitterCnpj,\n environment: environment(input.environment),\n certificateReady: input.certificateReady,\n cscReady: input.cscReady,\n providerEmitterId: input.providerEmitterId,\n certificateExpiresAt: input.certificateExpiresAt,\n certificateFingerprint: input.certificateFingerprint,\n },\n })\n}\n","// AUTO-GENERATED from 000_baseline.sql, 001_preserve_terminal_results_and_recover_leases.sql, 002_a_nota_que_chega_tem_onde_esperar.sql, 003_a_caixa_de_entrada_atravessa.sql, 004_o_grupo_fiscal_do_v1_repete_nome.sql — regenerate with scripts/embed-migrations.mjs\n// SQL files are the source of truth; this inline copy lets the manifest declare\n// migrations as data. Do not edit by hand — run the embed script instead.\n\nexport const MIGRATION_000_BASELINE = `-- Fiscal Brasil / PlugNotas baseline.\n--\n-- This addon does not create a second fiscal ledger. \\`plg_financial_nfe\\`\n-- remains the authoritative document owned by plugin-financial. The tables\n-- below describe delivery to an external issuer: durable commands, attempts,\n-- webhooks and emitter ownership.\n--\n-- The file is additive and idempotent because the current restaurant pool is\n-- live. Applying it is deliberately outside this draft PR.\n\nCREATE TABLE IF NOT EXISTS public.plg_fiscalbr_nfce_jobs (\n id uuid PRIMARY KEY DEFAULT gen_random_uuid(),\n tenant_id uuid NOT NULL REFERENCES public.tenants(id) ON DELETE CASCADE,\n -- Logical reference to plugin-financial's fiscal ledger. Cross-plugin\n -- physical foreign keys are intentionally forbidden: installation and\n -- removal of this addon must never rewrite another plugin's schema.\n nota_id uuid NOT NULL,\n order_id uuid NOT NULL REFERENCES public.orders(id) ON DELETE RESTRICT,\n connection_id uuid NOT NULL REFERENCES public.connections(id) ON DELETE RESTRICT,\n operation text NOT NULL CHECK (operation IN ('issue', 'cancel')),\n status text NOT NULL DEFAULT 'queued'\n CHECK (status IN ('queued', 'running', 'waiting', 'succeeded', 'failed', 'manual_review')),\n idempotency_key text NOT NULL,\n provider_document_id text,\n integration_id text,\n request_snapshot jsonb NOT NULL DEFAULT '{}'::jsonb,\n provider_response jsonb NOT NULL DEFAULT '{}'::jsonb,\n attempts integer NOT NULL DEFAULT 0 CHECK (attempts >= 0),\n next_attempt_at timestamptz,\n locked_until timestamptz,\n last_error text,\n created_at timestamptz NOT NULL DEFAULT now(),\n updated_at timestamptz NOT NULL DEFAULT now(),\n UNIQUE (tenant_id, idempotency_key)\n);\n\nCREATE INDEX IF NOT EXISTS plg_fiscalbr_nfce_jobs_recovery_idx\n ON public.plg_fiscalbr_nfce_jobs (status, next_attempt_at, locked_until)\n WHERE status IN ('queued', 'waiting');\n\nCREATE TABLE IF NOT EXISTS public.plg_fiscalbr_nfce_events (\n id bigint GENERATED BY DEFAULT AS IDENTITY PRIMARY KEY,\n tenant_id uuid NOT NULL REFERENCES public.tenants(id) ON DELETE CASCADE,\n nota_id uuid NOT NULL,\n job_id uuid REFERENCES public.plg_fiscalbr_nfce_jobs(id) ON DELETE SET NULL,\n event_type text NOT NULL,\n payload jsonb NOT NULL DEFAULT '{}'::jsonb,\n created_at timestamptz NOT NULL DEFAULT now()\n);\n\nCREATE INDEX IF NOT EXISTS plg_fiscalbr_nfce_events_document_idx\n ON public.plg_fiscalbr_nfce_events (tenant_id, nota_id, created_at DESC);\n\nCREATE TABLE IF NOT EXISTS public.plg_fiscalbr_webhook_receipts (\n id bigint GENERATED BY DEFAULT AS IDENTITY PRIMARY KEY,\n provider text NOT NULL DEFAULT 'plugnotas',\n provider_event_id text NOT NULL,\n tenant_id uuid REFERENCES public.tenants(id) ON DELETE SET NULL,\n nota_id uuid,\n payload_sha256 text NOT NULL,\n received_at timestamptz NOT NULL DEFAULT now(),\n processed_at timestamptz,\n processing_error text,\n UNIQUE (provider, provider_event_id)\n);\n\nCREATE TABLE IF NOT EXISTS public.plg_fiscalbr_emitter_ownership (\n emitter_cnpj text PRIMARY KEY CHECK (emitter_cnpj ~ '^[0-9]{14}$'),\n tenant_id uuid NOT NULL REFERENCES public.tenants(id) ON DELETE CASCADE,\n connection_id uuid NOT NULL REFERENCES public.connections(id) ON DELETE CASCADE,\n environment text NOT NULL CHECK (environment IN ('sandbox', 'homologation')),\n provider_emitter_id text,\n certificate_expires_at timestamptz,\n certificate_fingerprint text,\n csc_ready boolean NOT NULL DEFAULT false,\n created_at timestamptz NOT NULL DEFAULT now(),\n updated_at timestamptz NOT NULL DEFAULT now(),\n UNIQUE (tenant_id, connection_id)\n);\n\nALTER TABLE public.plg_fiscalbr_nfce_jobs ENABLE ROW LEVEL SECURITY;\nALTER TABLE public.plg_fiscalbr_nfce_events ENABLE ROW LEVEL SECURITY;\nALTER TABLE public.plg_fiscalbr_webhook_receipts ENABLE ROW LEVEL SECURITY;\nALTER TABLE public.plg_fiscalbr_emitter_ownership ENABLE ROW LEVEL SECURITY;\n\nDROP POLICY IF EXISTS plg_fiscalbr_nfce_jobs_read ON public.plg_fiscalbr_nfce_jobs;\nCREATE POLICY plg_fiscalbr_nfce_jobs_read ON public.plg_fiscalbr_nfce_jobs\n FOR SELECT TO authenticated\n USING (\n tenant_id = (SELECT app.current_tenant_id())\n AND (SELECT app.has_permission('financial.read', NULL))\n );\n\nDROP POLICY IF EXISTS plg_fiscalbr_nfce_events_read ON public.plg_fiscalbr_nfce_events;\nCREATE POLICY plg_fiscalbr_nfce_events_read ON public.plg_fiscalbr_nfce_events\n FOR SELECT TO authenticated\n USING (\n tenant_id = (SELECT app.current_tenant_id())\n AND (SELECT app.has_permission('financial.read', NULL))\n );\n\nDROP POLICY IF EXISTS plg_fiscalbr_emitter_ownership_read ON public.plg_fiscalbr_emitter_ownership;\nCREATE POLICY plg_fiscalbr_emitter_ownership_read ON public.plg_fiscalbr_emitter_ownership\n FOR SELECT TO authenticated\n USING (\n tenant_id = (SELECT app.current_tenant_id())\n AND (SELECT app.has_permission('financial.read', NULL))\n );\n\nREVOKE ALL ON public.plg_fiscalbr_nfce_jobs FROM anon, authenticated;\nREVOKE ALL ON public.plg_fiscalbr_nfce_events FROM anon, authenticated;\nREVOKE ALL ON public.plg_fiscalbr_webhook_receipts FROM anon, authenticated;\nREVOKE ALL ON public.plg_fiscalbr_emitter_ownership FROM anon, authenticated;\nGRANT SELECT ON public.plg_fiscalbr_nfce_jobs TO authenticated;\nGRANT SELECT ON public.plg_fiscalbr_nfce_events TO authenticated;\nGRANT SELECT ON public.plg_fiscalbr_emitter_ownership TO authenticated;\nGRANT ALL ON public.plg_fiscalbr_nfce_jobs TO service_role;\nGRANT ALL ON public.plg_fiscalbr_nfce_events TO service_role;\nGRANT ALL ON public.plg_fiscalbr_webhook_receipts TO service_role;\nGRANT ALL ON public.plg_fiscalbr_emitter_ownership TO service_role;\n\nCREATE OR REPLACE FUNCTION public.plugnotas_register_emitter(\n p_tenant_id uuid,\n p_connection_id uuid,\n p_emitter_cnpj text,\n p_environment text,\n p_provider_emitter_id text DEFAULT NULL,\n p_certificate_expires_at timestamptz DEFAULT NULL,\n p_certificate_fingerprint text DEFAULT NULL,\n p_csc_ready boolean DEFAULT false\n) RETURNS jsonb\nLANGUAGE plpgsql SECURITY DEFINER SET search_path = '' AS $$\nDECLARE\n v_cnpj text := regexp_replace(coalesce(p_emitter_cnpj, ''), '[^0-9]', '', 'g');\nBEGIN\n IF v_cnpj !~ '^[0-9]{14}$' THEN\n RAISE EXCEPTION 'plugnotas_register_emitter: invalid CNPJ' USING ERRCODE = '22023';\n END IF;\n IF p_environment NOT IN ('sandbox', 'homologation') THEN\n RAISE EXCEPTION 'plugnotas_register_emitter: production is not enabled' USING ERRCODE = '22023';\n END IF;\n IF NOT EXISTS (\n SELECT 1 FROM public.connections c\n WHERE c.id = p_connection_id\n AND c.tenant_id = p_tenant_id\n AND c.connector_id = 'plugnotas'\n AND c.active\n ) THEN\n RAISE EXCEPTION 'plugnotas_register_emitter: active connection not found' USING ERRCODE = 'P0002';\n END IF;\n\n INSERT INTO public.plg_fiscalbr_emitter_ownership\n (emitter_cnpj, tenant_id, connection_id, environment, provider_emitter_id,\n certificate_expires_at, certificate_fingerprint, csc_ready, updated_at)\n VALUES\n (v_cnpj, p_tenant_id, p_connection_id, p_environment, p_provider_emitter_id,\n p_certificate_expires_at, p_certificate_fingerprint, p_csc_ready, now())\n ON CONFLICT (emitter_cnpj) DO UPDATE SET\n tenant_id = EXCLUDED.tenant_id,\n connection_id = EXCLUDED.connection_id,\n environment = EXCLUDED.environment,\n provider_emitter_id = EXCLUDED.provider_emitter_id,\n certificate_expires_at = EXCLUDED.certificate_expires_at,\n certificate_fingerprint = EXCLUDED.certificate_fingerprint,\n csc_ready = EXCLUDED.csc_ready,\n updated_at = now()\n WHERE public.plg_fiscalbr_emitter_ownership.tenant_id = EXCLUDED.tenant_id;\n\n IF NOT FOUND THEN\n RAISE EXCEPTION 'plugnotas_register_emitter: CNPJ belongs to another tenant' USING ERRCODE = '23505';\n END IF;\n\n UPDATE public.connections\n SET provider_account_key = v_cnpj,\n settings = settings || jsonb_build_object(\n 'environment', p_environment,\n 'certificateReady', p_certificate_expires_at IS NOT NULL,\n 'certificateExpiresAt', p_certificate_expires_at,\n 'certificateFingerprint', p_certificate_fingerprint,\n 'providerEmitterId', p_provider_emitter_id,\n 'cscReady', p_csc_ready\n ),\n status = CASE WHEN p_certificate_expires_at IS NOT NULL AND p_csc_ready THEN 'connected' ELSE 'pending' END,\n updated_at = now()\n WHERE id = p_connection_id AND tenant_id = p_tenant_id;\n\n RETURN jsonb_build_object('emitterCnpj', v_cnpj, 'environment', p_environment);\nEND $$;\n\nCREATE OR REPLACE FUNCTION public.plugnotas_disconnect_emitter(\n p_tenant_id uuid,\n p_connection_id uuid\n) RETURNS void\nLANGUAGE plpgsql SECURITY DEFINER SET search_path = '' AS $$\nBEGIN\n DELETE FROM public.plg_fiscalbr_emitter_ownership\n WHERE tenant_id = p_tenant_id AND connection_id = p_connection_id;\n UPDATE public.connections\n SET active = false, status = 'revoked', settings = '{}'::jsonb,\n provider_state = '{}'::jsonb, updated_at = now()\n WHERE tenant_id = p_tenant_id AND id = p_connection_id AND connector_id = 'plugnotas';\nEND $$;\n\nCREATE OR REPLACE FUNCTION public.plugnotas_create_nfce_draft_for_order(\n p_tenant_id uuid,\n p_order_id uuid\n) RETURNS uuid\nLANGUAGE plpgsql SECURITY DEFINER SET search_path = '' AS $$\nDECLARE\n v_order public.orders%ROWTYPE;\n v_nota_id uuid;\nBEGIN\n -- Serialize draft creation per tenant/order without adding a column or\n -- index to the financial plugin's table. The order itself remains the\n -- stable idempotency boundary for one NFC-e draft.\n PERFORM pg_advisory_xact_lock(hashtextextended(p_tenant_id::text || ':' || p_order_id::text, 0));\n\n SELECT * INTO v_order FROM public.orders\n WHERE tenant_id = p_tenant_id AND id = p_order_id;\n IF NOT FOUND THEN\n RAISE EXCEPTION 'plugnotas_create_nfce_draft_for_order: order not found' USING ERRCODE = 'P0002';\n END IF;\n IF v_order.status NOT IN ('completed', 'paid', 'invoiced') THEN\n RAISE EXCEPTION 'plugnotas_create_nfce_draft_for_order: order is not closed' USING ERRCODE = '23514';\n END IF;\n IF NOT EXISTS (SELECT 1 FROM public.items i WHERE i.tenant_id = p_tenant_id AND i.order_id = p_order_id) THEN\n RAISE EXCEPTION 'plugnotas_create_nfce_draft_for_order: order has no items' USING ERRCODE = '23514';\n END IF;\n\n SELECT n.id INTO v_nota_id FROM public.plg_financial_nfe n\n WHERE n.tenant_id = p_tenant_id AND n.order_id = p_order_id AND n.model = 'nfce'\n ORDER BY n.created_at DESC LIMIT 1;\n IF v_nota_id IS NOT NULL THEN RETURN v_nota_id; END IF;\n\n INSERT INTO public.plg_financial_nfe\n (tenant_id, unit_id, model, status, order_id, contact_id, total_amount,\n emissor, metadata)\n VALUES\n (p_tenant_id, v_order.unit_id, 'nfce', 'draft', p_order_id, v_order.party_id,\n coalesce(v_order.total, 0), 'plugnotas',\n jsonb_build_object('source', 'order', 'orderKind', v_order.kind, 'orderChannel', v_order.channel))\n RETURNING id INTO v_nota_id;\n RETURN v_nota_id;\nEND $$;\n\nCREATE OR REPLACE FUNCTION public.plugnotas_prepare_nfce(\n p_tenant_id uuid,\n p_nota_id uuid,\n p_connection_id uuid,\n p_idempotency_key text\n) RETURNS jsonb\nLANGUAGE plpgsql SECURITY DEFINER SET search_path = '' AS $$\nDECLARE\n v_nota public.plg_financial_nfe%ROWTYPE;\n v_job public.plg_fiscalbr_nfce_jobs%ROWTYPE;\n v_snapshot jsonb;\nBEGIN\n SELECT * INTO v_nota FROM public.plg_financial_nfe\n WHERE tenant_id = p_tenant_id AND id = p_nota_id FOR UPDATE;\n IF NOT FOUND OR v_nota.model <> 'nfce' OR v_nota.order_id IS NULL THEN\n RAISE EXCEPTION 'plugnotas_prepare_nfce: order-backed NFC-e not found' USING ERRCODE = 'P0002';\n END IF;\n IF v_nota.status NOT IN ('draft', 'error', 'processing') THEN\n RAISE EXCEPTION 'plugnotas_prepare_nfce: invalid document status %', v_nota.status USING ERRCODE = '23514';\n END IF;\n IF NOT EXISTS (\n SELECT 1 FROM public.plg_fiscalbr_emitter_ownership e\n WHERE e.tenant_id = p_tenant_id AND e.connection_id = p_connection_id\n AND e.csc_ready AND e.certificate_expires_at > now()\n ) THEN\n RAISE EXCEPTION 'plugnotas_prepare_nfce: emitter is not ready' USING ERRCODE = '23514';\n END IF;\n\n SELECT jsonb_build_object(\n 'nota', to_jsonb(v_nota),\n 'order', to_jsonb(o),\n 'items', coalesce((\n SELECT jsonb_agg(to_jsonb(i) ORDER BY i.sort_order, i.id)\n FROM public.items i\n WHERE i.tenant_id = p_tenant_id AND i.order_id = v_nota.order_id\n ), '[]'::jsonb),\n -- Product tax metadata must be frozen in the same transaction as the order\n -- lines. Looking products up in the Edge Function would let a retry emit\n -- with NCM/CFOP changed after the operator confirmed the document.\n 'products', coalesce((\n SELECT jsonb_agg(to_jsonb(p) ORDER BY p.id)\n FROM public.products p\n WHERE p.tenant_id = p_tenant_id\n AND p.id IN (\n SELECT i.product_id FROM public.items i\n WHERE i.tenant_id = p_tenant_id\n AND i.order_id = v_nota.order_id\n AND i.product_id IS NOT NULL\n )\n ), '[]'::jsonb)\n ) INTO v_snapshot\n FROM public.orders o\n WHERE o.tenant_id = p_tenant_id AND o.id = v_nota.order_id;\n\n IF jsonb_array_length(v_snapshot->'items') = 0 THEN\n RAISE EXCEPTION 'plugnotas_prepare_nfce: order has no items' USING ERRCODE = '23514';\n END IF;\n\n INSERT INTO public.plg_fiscalbr_nfce_jobs\n (tenant_id, nota_id, order_id, connection_id, operation, status,\n idempotency_key, request_snapshot, next_attempt_at, updated_at)\n VALUES\n (p_tenant_id, p_nota_id, v_nota.order_id, p_connection_id, 'issue', 'queued',\n p_idempotency_key, v_snapshot, now(), now())\n ON CONFLICT (tenant_id, idempotency_key) DO UPDATE\n SET updated_at = now()\n RETURNING * INTO v_job;\n\n UPDATE public.plg_financial_nfe\n SET status = 'processing', error_message = NULL,\n metadata = metadata || jsonb_build_object('plugnotas', jsonb_build_object(\n 'jobId', v_job.id, 'jobStatus', v_job.status, 'operation', 'issue'\n )), updated_at = now()\n WHERE id = p_nota_id AND tenant_id = p_tenant_id;\n\n INSERT INTO public.plg_fiscalbr_nfce_events (tenant_id, nota_id, job_id, event_type, payload)\n VALUES (p_tenant_id, p_nota_id, v_job.id, 'issue.queued', '{}'::jsonb);\n RETURN jsonb_build_object('jobId', v_job.id, 'snapshot', v_job.request_snapshot, 'replayed', v_job.attempts > 0);\nEND $$;\n\nCREATE OR REPLACE FUNCTION public.plugnotas_prepare_nfce_cancel(\n p_tenant_id uuid,\n p_nota_id uuid,\n p_connection_id uuid,\n p_reason text\n) RETURNS jsonb\nLANGUAGE plpgsql SECURITY DEFINER SET search_path = '' AS $$\nDECLARE\n v_nota public.plg_financial_nfe%ROWTYPE;\n v_job_id uuid;\n v_reason text := btrim(coalesce(p_reason, ''));\nBEGIN\n IF char_length(v_reason) NOT BETWEEN 15 AND 255 THEN\n RAISE EXCEPTION 'plugnotas_prepare_nfce_cancel: reason must contain 15 to 255 characters' USING ERRCODE = '22023';\n END IF;\n SELECT * INTO v_nota FROM public.plg_financial_nfe\n WHERE tenant_id = p_tenant_id AND id = p_nota_id FOR UPDATE;\n IF NOT FOUND OR v_nota.status <> 'issued' OR v_nota.access_key IS NULL THEN\n RAISE EXCEPTION 'plugnotas_prepare_nfce_cancel: authorized NFC-e not found' USING ERRCODE = '23514';\n END IF;\n\n INSERT INTO public.plg_fiscalbr_nfce_jobs\n (tenant_id, nota_id, order_id, connection_id, operation, status,\n idempotency_key, provider_document_id, integration_id,\n request_snapshot, next_attempt_at)\n VALUES\n (p_tenant_id, p_nota_id, v_nota.order_id, p_connection_id, 'cancel', 'queued',\n 'nfce-cancel:' || p_nota_id::text,\n v_nota.metadata #>> '{plugnotas,providerDocumentId}',\n v_nota.metadata #>> '{plugnotas,integrationId}',\n jsonb_build_object('reason', v_reason), now())\n ON CONFLICT (tenant_id, idempotency_key) DO UPDATE SET\n provider_document_id = coalesce(\n public.plg_fiscalbr_nfce_jobs.provider_document_id,\n EXCLUDED.provider_document_id\n ),\n integration_id = coalesce(\n public.plg_fiscalbr_nfce_jobs.integration_id,\n EXCLUDED.integration_id\n ),\n updated_at = now()\n RETURNING id INTO v_job_id;\n\n UPDATE public.plg_financial_nfe\n SET metadata = metadata || jsonb_build_object('plugnotas', jsonb_build_object(\n 'jobId', v_job_id, 'jobStatus', 'queued', 'operation', 'cancel', 'cancellationPending', true\n )), updated_at = now()\n WHERE id = p_nota_id AND tenant_id = p_tenant_id;\n RETURN jsonb_build_object('jobId', v_job_id, 'reason', v_reason);\nEND $$;\n\nCREATE OR REPLACE FUNCTION public.plugnotas_record_nfce_attempt(\n p_tenant_id uuid,\n p_job_id uuid,\n p_status text,\n p_provider_response jsonb DEFAULT '{}'::jsonb,\n p_error text DEFAULT NULL,\n p_next_attempt_at timestamptz DEFAULT NULL\n) RETURNS void\nLANGUAGE plpgsql SECURITY DEFINER SET search_path = '' AS $$\nBEGIN\n IF p_status NOT IN ('running', 'waiting', 'failed', 'manual_review') THEN\n RAISE EXCEPTION 'plugnotas_record_nfce_attempt: invalid status' USING ERRCODE = '22023';\n END IF;\n UPDATE public.plg_fiscalbr_nfce_jobs\n SET status = p_status,\n -- One provider dispatch is one attempt. Moving that same attempt from\n -- running to waiting/failed must not count it a second time.\n attempts = attempts + CASE WHEN p_status = 'running' THEN 1 ELSE 0 END,\n provider_response = coalesce(p_provider_response, '{}'::jsonb),\n last_error = p_error, next_attempt_at = p_next_attempt_at,\n locked_until = NULL, updated_at = now()\n WHERE tenant_id = p_tenant_id AND id = p_job_id;\nEND $$;\n\nCREATE OR REPLACE FUNCTION public.plugnotas_apply_nfce_result(\n p_tenant_id uuid,\n p_job_id uuid,\n p_status text,\n p_provider_document_id text DEFAULT NULL,\n p_integration_id text DEFAULT NULL,\n p_number text DEFAULT NULL,\n p_series text DEFAULT NULL,\n p_access_key text DEFAULT NULL,\n p_message text DEFAULT NULL,\n p_provider_response jsonb DEFAULT '{}'::jsonb\n) RETURNS uuid\nLANGUAGE plpgsql SECURITY DEFINER SET search_path = '' AS $$\nDECLARE\n v_job public.plg_fiscalbr_nfce_jobs%ROWTYPE;\n v_document_status text;\nBEGIN\n SELECT * INTO v_job FROM public.plg_fiscalbr_nfce_jobs\n WHERE tenant_id = p_tenant_id AND id = p_job_id FOR UPDATE;\n IF NOT FOUND THEN\n RAISE EXCEPTION 'plugnotas_apply_nfce_result: job not found' USING ERRCODE = 'P0002';\n END IF;\n IF p_status NOT IN ('processing', 'issued', 'cancelled', 'error') THEN\n RAISE EXCEPTION 'plugnotas_apply_nfce_result: invalid status' USING ERRCODE = '22023';\n END IF;\n\n v_document_status := CASE\n WHEN v_job.operation = 'cancel' AND p_status = 'processing' THEN 'issued'\n ELSE p_status\n END;\n UPDATE public.plg_fiscalbr_nfce_jobs\n SET status = CASE\n WHEN p_status IN ('issued', 'cancelled') THEN 'succeeded'\n WHEN p_status = 'error' THEN 'failed'\n ELSE 'waiting'\n END,\n provider_document_id = coalesce(p_provider_document_id, provider_document_id),\n integration_id = coalesce(p_integration_id, integration_id),\n provider_response = coalesce(p_provider_response, '{}'::jsonb),\n last_error = CASE WHEN p_status = 'error' THEN p_message ELSE NULL END,\n next_attempt_at = CASE WHEN p_status = 'processing' THEN now() + interval '2 minutes' ELSE NULL END,\n locked_until = NULL, updated_at = now()\n WHERE id = p_job_id;\n\n UPDATE public.plg_financial_nfe\n SET status = v_document_status,\n number = coalesce(p_number, number),\n series = coalesce(p_series, series),\n access_key = coalesce(p_access_key, access_key),\n issued_at = CASE WHEN p_status = 'issued' THEN coalesce(issued_at, now()) ELSE issued_at END,\n error_message = CASE WHEN p_status = 'error' THEN p_message ELSE NULL END,\n metadata = metadata || jsonb_build_object('plugnotas', jsonb_strip_nulls(jsonb_build_object(\n 'jobId', p_job_id,\n 'jobStatus', CASE WHEN p_status IN ('issued', 'cancelled') THEN 'succeeded' WHEN p_status = 'error' THEN 'failed' ELSE 'waiting' END,\n 'operation', v_job.operation,\n 'providerDocumentId', coalesce(p_provider_document_id, v_job.provider_document_id),\n 'integrationId', coalesce(p_integration_id, v_job.integration_id),\n 'cancellationPending', v_job.operation = 'cancel' AND p_status = 'processing'\n ))),\n updated_at = now()\n WHERE tenant_id = p_tenant_id AND id = v_job.nota_id;\n\n INSERT INTO public.plg_fiscalbr_nfce_events (tenant_id, nota_id, job_id, event_type, payload)\n VALUES (p_tenant_id, v_job.nota_id, p_job_id, v_job.operation || '.' || p_status,\n coalesce(p_provider_response, '{}'::jsonb));\n RETURN v_job.nota_id;\nEND $$;\n\nCREATE OR REPLACE FUNCTION public.plugnotas_apply_nfce_webhook(\n p_provider_event_id text,\n p_payload_sha256 text,\n p_provider_document_id text,\n p_status text,\n p_integration_id text DEFAULT NULL,\n p_number text DEFAULT NULL,\n p_series text DEFAULT NULL,\n p_access_key text DEFAULT NULL,\n p_message text DEFAULT NULL,\n p_payload jsonb DEFAULT '{}'::jsonb\n) RETURNS jsonb\nLANGUAGE plpgsql SECURITY DEFINER SET search_path = '' AS $$\nDECLARE\n v_job public.plg_fiscalbr_nfce_jobs%ROWTYPE;\n v_receipt_id bigint;\nBEGIN\n INSERT INTO public.plg_fiscalbr_webhook_receipts\n (provider_event_id, payload_sha256)\n VALUES (p_provider_event_id, p_payload_sha256)\n ON CONFLICT (provider, provider_event_id) DO NOTHING\n RETURNING id INTO v_receipt_id;\n IF v_receipt_id IS NULL THEN RETURN jsonb_build_object('duplicate', true); END IF;\n\n SELECT * INTO v_job FROM public.plg_fiscalbr_nfce_jobs\n WHERE provider_document_id = p_provider_document_id\n ORDER BY created_at DESC LIMIT 1;\n IF NOT FOUND THEN\n UPDATE public.plg_fiscalbr_webhook_receipts\n SET processed_at = now(), processing_error = 'job_not_found'\n WHERE id = v_receipt_id;\n RETURN jsonb_build_object('duplicate', false, 'matched', false);\n END IF;\n\n UPDATE public.plg_fiscalbr_webhook_receipts\n SET tenant_id = v_job.tenant_id, nota_id = v_job.nota_id, processed_at = now()\n WHERE id = v_receipt_id;\n PERFORM public.plugnotas_apply_nfce_result(\n v_job.tenant_id, v_job.id, p_status, p_provider_document_id,\n coalesce(p_integration_id, v_job.integration_id), p_number, p_series,\n p_access_key, p_message, p_payload\n );\n RETURN jsonb_build_object('duplicate', false, 'matched', true, 'jobId', v_job.id);\nEND $$;\n\nCREATE OR REPLACE FUNCTION public.plugnotas_get_artifact_locator(\n p_tenant_id uuid,\n p_nota_id uuid\n) RETURNS jsonb\nLANGUAGE sql SECURITY DEFINER SET search_path = '' STABLE AS $$\n SELECT jsonb_build_object(\n 'notaId', n.id,\n 'status', n.status,\n 'providerDocumentId', n.metadata #>> '{plugnotas,providerDocumentId}',\n 'integrationId', n.metadata #>> '{plugnotas,integrationId}'\n )\n FROM public.plg_financial_nfe n\n WHERE n.tenant_id = p_tenant_id AND n.id = p_nota_id\n AND n.status IN ('issued', 'cancelled');\n$$;\n\nCREATE OR REPLACE FUNCTION public.plugnotas_claim_nfce_recovery(\n p_limit integer DEFAULT 20\n) RETURNS SETOF public.plg_fiscalbr_nfce_jobs\nLANGUAGE plpgsql SECURITY DEFINER SET search_path = '' AS $$\nBEGIN\n RETURN QUERY\n WITH candidates AS (\n SELECT j.id FROM public.plg_fiscalbr_nfce_jobs j\n WHERE j.status IN ('queued', 'waiting')\n AND coalesce(j.next_attempt_at, j.created_at) <= now()\n AND coalesce(j.locked_until, '-infinity'::timestamptz) < now()\n ORDER BY coalesce(j.next_attempt_at, j.created_at), j.created_at\n FOR UPDATE SKIP LOCKED\n LIMIT greatest(1, least(coalesce(p_limit, 20), 100))\n )\n UPDATE public.plg_fiscalbr_nfce_jobs j\n SET status = 'running', locked_until = now() + interval '2 minutes', updated_at = now()\n FROM candidates c WHERE j.id = c.id\n RETURNING j.*;\nEND $$;\n\nREVOKE ALL ON FUNCTION public.plugnotas_register_emitter(uuid, uuid, text, text, text, timestamptz, text, boolean) FROM PUBLIC, anon, authenticated;\nREVOKE ALL ON FUNCTION public.plugnotas_disconnect_emitter(uuid, uuid) FROM PUBLIC, anon, authenticated;\nREVOKE ALL ON FUNCTION public.plugnotas_create_nfce_draft_for_order(uuid, uuid) FROM PUBLIC, anon, authenticated;\nREVOKE ALL ON FUNCTION public.plugnotas_prepare_nfce(uuid, uuid, uuid, text) FROM PUBLIC, anon, authenticated;\nREVOKE ALL ON FUNCTION public.plugnotas_prepare_nfce_cancel(uuid, uuid, uuid, text) FROM PUBLIC, anon, authenticated;\nREVOKE ALL ON FUNCTION public.plugnotas_record_nfce_attempt(uuid, uuid, text, jsonb, text, timestamptz) FROM PUBLIC, anon, authenticated;\nREVOKE ALL ON FUNCTION public.plugnotas_apply_nfce_result(uuid, uuid, text, text, text, text, text, text, text, jsonb) FROM PUBLIC, anon, authenticated;\nREVOKE ALL ON FUNCTION public.plugnotas_apply_nfce_webhook(text, text, text, text, text, text, text, text, text, jsonb) FROM PUBLIC, anon, authenticated;\nREVOKE ALL ON FUNCTION public.plugnotas_get_artifact_locator(uuid, uuid) FROM PUBLIC, anon, authenticated;\nREVOKE ALL ON FUNCTION public.plugnotas_claim_nfce_recovery(integer) FROM PUBLIC, anon, authenticated;\n\nGRANT EXECUTE ON FUNCTION public.plugnotas_register_emitter(uuid, uuid, text, text, text, timestamptz, text, boolean) TO service_role;\nGRANT EXECUTE ON FUNCTION public.plugnotas_disconnect_emitter(uuid, uuid) TO service_role;\nGRANT EXECUTE ON FUNCTION public.plugnotas_create_nfce_draft_for_order(uuid, uuid) TO service_role;\nGRANT EXECUTE ON FUNCTION public.plugnotas_prepare_nfce(uuid, uuid, uuid, text) TO service_role;\nGRANT EXECUTE ON FUNCTION public.plugnotas_prepare_nfce_cancel(uuid, uuid, uuid, text) TO service_role;\nGRANT EXECUTE ON FUNCTION public.plugnotas_record_nfce_attempt(uuid, uuid, text, jsonb, text, timestamptz) TO service_role;\nGRANT EXECUTE ON FUNCTION public.plugnotas_apply_nfce_result(uuid, uuid, text, text, text, text, text, text, text, jsonb) TO service_role;\nGRANT EXECUTE ON FUNCTION public.plugnotas_apply_nfce_webhook(text, text, text, text, text, text, text, text, text, jsonb) TO service_role;\nGRANT EXECUTE ON FUNCTION public.plugnotas_get_artifact_locator(uuid, uuid) TO service_role;\nGRANT EXECUTE ON FUNCTION public.plugnotas_claim_nfce_recovery(integer) TO service_role;\n`\n\nexport const MIGRATION_001_PRESERVE_TERMINAL_RESULTS_AND_RECOVER_LEASES = `-- Incremental recovery fix. Preserve 000_baseline.sql and its recorded checksum.\n-- Only existing fiscal functions change; no parallel ledger/schema or backfill.\n-- CREATE OR REPLACE retains the service-role-only ACL from the baseline.\n\nCREATE OR REPLACE FUNCTION public.plugnotas_record_nfce_attempt(\n p_tenant_id uuid,\n p_job_id uuid,\n p_status text,\n p_provider_response jsonb DEFAULT '{}'::jsonb,\n p_error text DEFAULT NULL,\n p_next_attempt_at timestamptz DEFAULT NULL\n) RETURNS void\nLANGUAGE plpgsql SECURITY DEFINER SET search_path = '' AS $$\nBEGIN\n IF p_status NOT IN ('running', 'waiting', 'failed', 'manual_review') THEN\n RAISE EXCEPTION 'plugnotas_record_nfce_attempt: invalid status' USING ERRCODE = '22023';\n END IF;\n UPDATE public.plg_fiscalbr_nfce_jobs\n SET status = p_status,\n -- One provider dispatch is one attempt. Moving that same attempt from\n -- running to waiting/failed must not count it a second time.\n attempts = attempts + CASE WHEN p_status = 'running' THEN 1 ELSE 0 END,\n provider_response = coalesce(p_provider_response, '{}'::jsonb),\n last_error = p_error, next_attempt_at = p_next_attempt_at,\n locked_until = CASE WHEN p_status = 'running'\n THEN greatest(coalesce(locked_until, now()), now() + interval '2 minutes') ELSE NULL END,\n updated_at = now()\n WHERE tenant_id = p_tenant_id AND id = p_job_id AND status <> 'succeeded';\nEND $$;\n\nCREATE OR REPLACE FUNCTION public.plugnotas_apply_nfce_result(\n p_tenant_id uuid,\n p_job_id uuid,\n p_status text,\n p_provider_document_id text DEFAULT NULL,\n p_integration_id text DEFAULT NULL,\n p_number text DEFAULT NULL,\n p_series text DEFAULT NULL,\n p_access_key text DEFAULT NULL,\n p_message text DEFAULT NULL,\n p_provider_response jsonb DEFAULT '{}'::jsonb\n) RETURNS uuid\nLANGUAGE plpgsql SECURITY DEFINER SET search_path = '' AS $$\nDECLARE\n v_job public.plg_fiscalbr_nfce_jobs%ROWTYPE;\n v_document_status text;\n v_existing_status text;\n v_job_status text;\nBEGIN\n SELECT * INTO v_job FROM public.plg_fiscalbr_nfce_jobs\n WHERE tenant_id = p_tenant_id AND id = p_job_id;\n IF NOT FOUND THEN\n RAISE EXCEPTION 'plugnotas_apply_nfce_result: job not found' USING ERRCODE = 'P0002';\n END IF;\n IF p_status NOT IN ('processing', 'issued', 'cancelled', 'error') THEN\n RAISE EXCEPTION 'plugnotas_apply_nfce_result: invalid status' USING ERRCODE = '22023';\n END IF;\n\n -- Match prepare/cancel's lock order: document, then job.\n SELECT status INTO v_existing_status FROM public.plg_financial_nfe\n WHERE tenant_id = p_tenant_id AND id = v_job.nota_id FOR UPDATE;\n IF NOT FOUND THEN\n RAISE EXCEPTION 'plugnotas_apply_nfce_result: document not found' USING ERRCODE = 'P0002';\n END IF;\n SELECT * INTO v_job FROM public.plg_fiscalbr_nfce_jobs\n WHERE tenant_id = p_tenant_id AND id = p_job_id FOR UPDATE;\n\n -- Late polling must not regress a concluded document or an active cancellation.\n IF v_existing_status = 'cancelled'\n OR (v_job.operation = 'issue' AND v_existing_status = 'issued' AND p_status <> 'cancelled')\n OR (v_job.status = 'succeeded' AND p_status IN ('processing', 'error')) THEN\n -- Old/interrupted jobs may still be running after the document concluded.\n -- Settle their lease too, otherwise recovery would claim them forever.\n UPDATE public.plg_fiscalbr_nfce_jobs\n SET status = 'succeeded', locked_until = NULL, next_attempt_at = NULL, updated_at = now()\n WHERE tenant_id = p_tenant_id AND id = p_job_id;\n RETURN v_job.nota_id;\n END IF;\n\n v_job_status := CASE\n WHEN p_status = 'cancelled' OR (v_job.operation = 'issue' AND p_status = 'issued') THEN 'succeeded'\n WHEN p_status = 'error' THEN 'failed'\n ELSE 'waiting' END;\n v_document_status := CASE\n WHEN v_job.operation = 'cancel' AND p_status <> 'cancelled' THEN v_existing_status\n ELSE p_status\n END;\n UPDATE public.plg_fiscalbr_nfce_jobs\n SET status = v_job_status,\n provider_document_id = coalesce(p_provider_document_id, provider_document_id),\n integration_id = coalesce(p_integration_id, integration_id),\n provider_response = coalesce(p_provider_response, '{}'::jsonb),\n last_error = CASE WHEN p_status = 'error' THEN p_message ELSE NULL END,\n next_attempt_at = CASE WHEN v_job_status = 'waiting' THEN now() + interval '2 minutes' ELSE NULL END,\n locked_until = NULL, updated_at = now()\n WHERE id = p_job_id;\n\n UPDATE public.plg_financial_nfe\n SET status = v_document_status,\n number = coalesce(p_number, number),\n series = coalesce(p_series, series),\n access_key = coalesce(p_access_key, access_key),\n issued_at = CASE WHEN p_status = 'issued' THEN coalesce(issued_at, now()) ELSE issued_at END,\n error_message = CASE WHEN v_document_status = 'error' THEN p_message ELSE NULL END,\n metadata = metadata || jsonb_build_object('plugnotas', jsonb_strip_nulls(jsonb_build_object(\n 'jobId', p_job_id,\n 'jobStatus', v_job_status,\n 'operation', v_job.operation,\n 'providerDocumentId', coalesce(p_provider_document_id, v_job.provider_document_id),\n 'integrationId', coalesce(p_integration_id, v_job.integration_id),\n 'cancellationPending', v_job.operation = 'cancel' AND v_job_status = 'waiting'\n ))),\n updated_at = now()\n WHERE tenant_id = p_tenant_id AND id = v_job.nota_id;\n\n INSERT INTO public.plg_fiscalbr_nfce_events (tenant_id, nota_id, job_id, event_type, payload)\n VALUES (p_tenant_id, v_job.nota_id, p_job_id, v_job.operation || '.' || p_status,\n coalesce(p_provider_response, '{}'::jsonb));\n RETURN v_job.nota_id;\nEND $$;\n\nCREATE OR REPLACE FUNCTION public.plugnotas_apply_nfce_webhook(\n p_provider_event_id text,\n p_payload_sha256 text,\n p_provider_document_id text,\n p_status text,\n p_integration_id text DEFAULT NULL,\n p_number text DEFAULT NULL,\n p_series text DEFAULT NULL,\n p_access_key text DEFAULT NULL,\n p_message text DEFAULT NULL,\n p_payload jsonb DEFAULT '{}'::jsonb\n) RETURNS jsonb\nLANGUAGE plpgsql SECURITY DEFINER SET search_path = '' AS $$\nDECLARE\n v_job public.plg_fiscalbr_nfce_jobs%ROWTYPE;\n v_receipt_id bigint;\n v_receipt public.plg_fiscalbr_webhook_receipts%ROWTYPE;\nBEGIN\n INSERT INTO public.plg_fiscalbr_webhook_receipts\n (provider_event_id, payload_sha256)\n VALUES (p_provider_event_id, p_payload_sha256)\n ON CONFLICT (provider, provider_event_id) DO NOTHING\n RETURNING id INTO v_receipt_id;\n SELECT * INTO v_receipt FROM public.plg_fiscalbr_webhook_receipts\n WHERE provider = 'plugnotas' AND provider_event_id = p_provider_event_id FOR UPDATE;\n v_receipt_id := v_receipt.id;\n IF v_receipt.payload_sha256 IS DISTINCT FROM p_payload_sha256 THEN\n RAISE EXCEPTION 'plugnotas_apply_nfce_webhook: conflicting event payload' USING ERRCODE = '22023';\n END IF;\n IF v_receipt.processed_at IS NOT NULL AND v_receipt.processing_error IS NULL THEN\n RETURN jsonb_build_object('duplicate', true, 'matched', true);\n END IF;\n\n SELECT * INTO v_job FROM public.plg_fiscalbr_nfce_jobs\n WHERE provider_document_id = p_provider_document_id\n ORDER BY created_at DESC LIMIT 1;\n IF NOT FOUND THEN\n UPDATE public.plg_fiscalbr_webhook_receipts\n SET processed_at = NULL, processing_error = 'job_not_found'\n WHERE id = v_receipt_id;\n RETURN jsonb_build_object('duplicate', false, 'matched', false);\n END IF;\n\n UPDATE public.plg_fiscalbr_webhook_receipts\n SET tenant_id = v_job.tenant_id, nota_id = v_job.nota_id, processed_at = now(), processing_error = NULL\n WHERE id = v_receipt_id;\n PERFORM public.plugnotas_apply_nfce_result(\n v_job.tenant_id, v_job.id, p_status, p_provider_document_id,\n coalesce(p_integration_id, v_job.integration_id), p_number, p_series,\n p_access_key, p_message, p_payload\n );\n RETURN jsonb_build_object('duplicate', false, 'matched', true, 'jobId', v_job.id);\nEND $$;\n\nCREATE OR REPLACE FUNCTION public.plugnotas_claim_nfce_recovery(\n p_limit integer DEFAULT 20\n) RETURNS SETOF public.plg_fiscalbr_nfce_jobs\nLANGUAGE plpgsql SECURITY DEFINER SET search_path = '' AS $$\nBEGIN\n RETURN QUERY\n WITH candidates AS (\n SELECT j.id FROM public.plg_fiscalbr_nfce_jobs j\n WHERE j.status IN ('queued', 'waiting', 'running')\n AND coalesce(j.next_attempt_at, j.created_at) <= now()\n AND coalesce(j.locked_until, '-infinity'::timestamptz) < now()\n ORDER BY coalesce(j.next_attempt_at, j.created_at), j.created_at\n FOR UPDATE SKIP LOCKED\n LIMIT greatest(1, least(coalesce(p_limit, 20), 100))\n )\n UPDATE public.plg_fiscalbr_nfce_jobs j\n SET status = 'running', locked_until = now() + interval '2 minutes', updated_at = now()\n FROM candidates c WHERE j.id = c.id\n RETURNING j.*;\nEND $$;\n`\n\nexport const MIGRATION_002_A_NOTA_QUE_CHEGA_TEM_ONDE_ESPERAR = `-- A nota que chega tem onde esperar.\n--\n-- O V1 recebe NF-e de fornecedor pela SEFAZ e guarda cada uma numa caixa de\n-- entrada antes de alguém decidir o que fazer com ela: manifestar, importar\n-- para o estoque, ou nada. Essa caixa não tinha destino no V2.\n--\n-- dfe_inbox 703 a nota recebida, com o XML inteiro\n-- dfe_inbox_items 3.215 o item da nota, com todo o tributo\n-- dfe_inbox_payments 739 a forma de pagamento declarada\n-- dfe_inbox_duplicatas 830 o vencimento da duplicata\n-- fiscal_groups 11 o grupo fiscal (CFOP, CST, alíquotas)\n--\n-- Os números somam os dois tenants: Espaço Facial e Pertinho do Céu. É o único\n-- domínio fiscal que os dois usam.\n--\n-- ── O XML é arquivo, e arquivo mora em public.documents ─────────────────────\n--\n-- \\`dfe_inbox.xml_content\\` é o XML inteiro numa coluna de texto, e\n-- \\`pdf_storage_path\\` aponta para o DANFE no storage do V1. Os dois são ARQUIVO,\n-- e o V2 já tem o arquétipo: \\`public.documents\\`, com kind, checksum, storage_*\n-- e a RLS de arquivo que uma coluna de texto nunca teria.\n--\n-- Então a extensão guarda um PONTEIRO — \\`document_id\\` — e os bytes vão para o\n-- arquétipo, pelo estágio de assets (\\`make mig-assets\\`), que é quem alcança\n-- object storage. Enquanto os bytes não chegam, \\`document_id\\` fica nulo e o\n-- caminho de origem fica em metadata: a linha é verdadeira nos dois momentos.\n--\n-- ── O tributo vem inteiro, e de propósito ───────────────────────────────────\n--\n-- 30 colunas de imposto por item (ICMS, ICMS-ST, PIS, COFINS, IPI, com base,\n-- alíquota e valor de cada). É muita coluna, e a tentação é jogar tudo num\n-- jsonb. Não: isto é documento fiscal recebido, e a pergunta que se faz dele é\n-- \"quanto de ICMS entrou neste mês\" — uma soma, sobre coluna, com índice. Um\n-- jsonb transformaria toda apuração numa varredura.\n--\n-- O que VAI para jsonb é o \\`raw_xml_item\\`, que já é jsonb na origem e existe\n-- para conferência, não para conta.\n--\n-- Idempotente.\n\nCREATE TABLE IF NOT EXISTS public.plg_fiscalbr_inbox_documents (\n id uuid DEFAULT gen_random_uuid() NOT NULL,\n tenant_id uuid NOT NULL,\n unit_id uuid,\n -- O ponteiro para o arquétipo. Nulo enquanto os bytes não subiram — o\n -- caminho de origem fica em metadata até lá.\n document_id uuid,\n access_key text,\n nfe_number text,\n nfe_series text,\n nfe_model text,\n issue_date timestamp with time zone,\n received_at timestamp with time zone,\n status text DEFAULT 'received'::text NOT NULL,\n -- A manifestação do destinatário é outro eixo que o status: uma nota pode\n -- estar recebida e não manifestada, e é a manifestação que a SEFAZ cobra.\n manifestation_status text,\n manifested_at timestamp with time zone,\n manifestation_attempts integer DEFAULT 0 NOT NULL,\n manifestation_response jsonb,\n supplier_party_id uuid,\n supplier_cnpj text,\n supplier_name text,\n supplier_ie text,\n total_value numeric(14,2),\n total_products numeric(14,2),\n total_discount numeric(14,2),\n total_freight numeric(14,2),\n total_insurance numeric(14,2),\n total_other numeric(14,2),\n total_icms_base numeric(14,2),\n total_icms_value numeric(14,2),\n total_icms_st_base numeric(14,2),\n total_icms_st_value numeric(14,2),\n total_pis_value numeric(14,2),\n total_cofins_value numeric(14,2),\n total_ipi_value numeric(14,2),\n nfe_protocol text,\n nfe_authorized_at timestamp with time zone,\n carrier_name text,\n carrier_cnpj text,\n freight_mode_code text,\n additional_info text,\n reviewed_at timestamp with time zone,\n -- O que essa nota virou no estoque, quando virou. Nulo é nota recebida e\n -- não importada, que é a maioria.\n inventory_operation_id uuid,\n owner_id uuid,\n metadata jsonb DEFAULT '{}'::jsonb NOT NULL,\n created_at timestamp with time zone DEFAULT now() NOT NULL,\n updated_at timestamp with time zone DEFAULT now() NOT NULL\n);\n\nCOMMENT ON TABLE public.plg_fiscalbr_inbox_documents IS\n 'A NF-e de fornecedor recebida da SEFAZ, esperando manifestação ou importação. V1: dfe_inbox.';\nCOMMENT ON COLUMN public.plg_fiscalbr_inbox_documents.document_id IS\n 'V1: xml_content + pdf_storage_path. O XML e o DANFE são arquivo; os bytes moram em public.documents e aqui fica o ponteiro.';\n\nCREATE TABLE IF NOT EXISTS public.plg_fiscalbr_inbox_items (\n id uuid DEFAULT gen_random_uuid() NOT NULL,\n tenant_id uuid NOT NULL,\n inbox_document_id uuid NOT NULL,\n item_number integer,\n supplier_product_code text,\n ean_barcode text,\n ean_taxable text,\n description text,\n ncm text,\n cest text,\n cfop text,\n unit_commercial text,\n unit_taxable text,\n quantity_commercial numeric(18,4),\n quantity_taxable numeric(18,4),\n unit_price_commercial numeric(18,6),\n unit_price_taxable numeric(18,6),\n total_price numeric(14,2),\n discount numeric(14,2),\n freight numeric(14,2),\n insurance numeric(14,2),\n other_expenses numeric(14,2),\n icms_cst text, icms_csosn text, icms_origin text,\n icms_base numeric(14,2), icms_rate numeric(9,4), icms_value numeric(14,2),\n icms_st_base numeric(14,2), icms_st_value numeric(14,2),\n pis_cst text, pis_base numeric(14,2), pis_rate numeric(9,4), pis_value numeric(14,2),\n cofins_cst text, cofins_base numeric(14,2), cofins_rate numeric(9,4), cofins_value numeric(14,2),\n ipi_cst text, ipi_rate numeric(9,4), ipi_value numeric(14,2),\n -- O produto do V2 que esta linha da nota representa. \\`reconciled\\` é a\n -- decisão de quem conferiu; o id é o resultado dela.\n product_id uuid,\n reconciled boolean DEFAULT false NOT NULL,\n raw_xml_item jsonb,\n metadata jsonb DEFAULT '{}'::jsonb NOT NULL,\n created_at timestamp with time zone DEFAULT now() NOT NULL,\n updated_at timestamp with time zone DEFAULT now() NOT NULL\n);\n\nCOMMENT ON TABLE public.plg_fiscalbr_inbox_items IS\n 'O item da NF-e recebida, com o tributo em coluna — a apuração soma coluna, não varre jsonb. V1: dfe_inbox_items.';\n\nCREATE TABLE IF NOT EXISTS public.plg_fiscalbr_inbox_payments (\n id uuid DEFAULT gen_random_uuid() NOT NULL,\n tenant_id uuid NOT NULL,\n inbox_document_id uuid NOT NULL,\n payment_index integer,\n payment_indicator text,\n payment_type_code text,\n payment_type_label text,\n value numeric(14,2),\n change_given numeric(14,2),\n card_brand text,\n card_acquirer_cnpj text,\n card_auth_code text,\n metadata jsonb DEFAULT '{}'::jsonb NOT NULL,\n created_at timestamp with time zone DEFAULT now() NOT NULL,\n updated_at timestamp with time zone DEFAULT now() NOT NULL\n);\n\nCREATE TABLE IF NOT EXISTS public.plg_fiscalbr_inbox_duplicates (\n id uuid DEFAULT gen_random_uuid() NOT NULL,\n tenant_id uuid NOT NULL,\n inbox_document_id uuid NOT NULL,\n duplicate_index integer,\n number text,\n due_date date,\n value numeric(14,2),\n -- O que essa duplicata virou no contas a pagar, se alguém a lançou.\n installment_id uuid,\n metadata jsonb DEFAULT '{}'::jsonb NOT NULL,\n created_at timestamp with time zone DEFAULT now() NOT NULL,\n updated_at timestamp with time zone DEFAULT now() NOT NULL\n);\n\nCOMMENT ON TABLE public.plg_fiscalbr_inbox_duplicates IS\n 'O vencimento declarado na NF-e recebida. Vira parcela do contas a pagar quando alguém lança a nota. V1: dfe_inbox_duplicatas.';\n\nCREATE TABLE IF NOT EXISTS public.plg_fiscalbr_tax_groups (\n id uuid DEFAULT gen_random_uuid() NOT NULL,\n tenant_id uuid NOT NULL,\n name text NOT NULL,\n cfop text,\n csosn text,\n cst text,\n icms_origin text,\n icms_rate numeric(9,4),\n icms_bc_mode text,\n pis_situation text,\n pis_rate numeric(9,4),\n cofins_situation text,\n cofins_rate numeric(9,4),\n -- A reforma tributária já está no cadastro do V1 e vem junto: deixar de\n -- fora obrigaria a recadastrar tudo no dia em que IBS/CBS valerem.\n ibs_cbs_situation text,\n ibs_cbs_classification text,\n base_calculation_mode text,\n benefit_code text,\n cest text,\n poverty_fund_rate numeric(9,4),\n is_active boolean DEFAULT true NOT NULL,\n metadata jsonb DEFAULT '{}'::jsonb NOT NULL,\n created_at timestamp with time zone DEFAULT now() NOT NULL,\n updated_at timestamp with time zone DEFAULT now() NOT NULL\n);\n\nCOMMENT ON TABLE public.plg_fiscalbr_tax_groups IS\n 'O grupo fiscal: CFOP, CST/CSOSN e alíquotas que o produto herda na emissão. V1: fiscal_groups. Traz IBS/CBS porque o cadastro do V1 já os tem.';\n\n-- ═══════════════════════════════════════════════════════════════════════════\n-- As chaves, os índices e a cerca\n-- ═══════════════════════════════════════════════════════════════════════════\n\nDO $$\nDECLARE t text;\nBEGIN\n FOREACH t IN ARRAY ARRAY['plg_fiscalbr_inbox_documents', 'plg_fiscalbr_inbox_items',\n 'plg_fiscalbr_inbox_payments', 'plg_fiscalbr_inbox_duplicates',\n 'plg_fiscalbr_tax_groups'] LOOP\n IF NOT EXISTS (SELECT 1 FROM pg_constraint WHERE conrelid = ('public.' || t)::regclass AND contype = 'p') THEN\n EXECUTE format('ALTER TABLE ONLY public.%I ADD CONSTRAINT %I PRIMARY KEY (id)', t, t || '_pkey');\n END IF;\n IF NOT EXISTS (SELECT 1 FROM pg_constraint WHERE conname = t || '_tenant_id_id_key') THEN\n EXECUTE format('ALTER TABLE ONLY public.%I ADD CONSTRAINT %I UNIQUE (tenant_id, id)', t, t || '_tenant_id_id_key');\n END IF;\n IF NOT EXISTS (SELECT 1 FROM pg_constraint WHERE conname = t || '_tenant_fkey') THEN\n EXECUTE format('ALTER TABLE ONLY public.%I ADD CONSTRAINT %I FOREIGN KEY (tenant_id) REFERENCES public.tenants(id) ON DELETE CASCADE', t, t || '_tenant_fkey');\n END IF;\n END LOOP;\nEND $$;\n\nDO $$ BEGIN\n IF NOT EXISTS (SELECT 1 FROM pg_constraint WHERE conname = 'plg_fiscalbr_inbox_documents_unit_fkey') THEN\n ALTER TABLE ONLY public.plg_fiscalbr_inbox_documents ADD CONSTRAINT plg_fiscalbr_inbox_documents_unit_fkey\n FOREIGN KEY (tenant_id, unit_id) REFERENCES app.units(tenant_id, id) ON DELETE SET NULL;\n END IF;\n -- O arquivo pode ser apagado sem levar a nota junto: a nota fiscal existe\n -- mesmo sem o XML à mão, e é ela que a SEFAZ conhece.\n IF NOT EXISTS (SELECT 1 FROM pg_constraint WHERE conname = 'plg_fiscalbr_inbox_documents_document_fkey') THEN\n ALTER TABLE ONLY public.plg_fiscalbr_inbox_documents ADD CONSTRAINT plg_fiscalbr_inbox_documents_document_fkey\n FOREIGN KEY (document_id) REFERENCES public.documents(id) ON DELETE SET NULL;\n END IF;\n IF NOT EXISTS (SELECT 1 FROM pg_constraint WHERE conname = 'plg_fiscalbr_inbox_documents_supplier_fkey') THEN\n ALTER TABLE ONLY public.plg_fiscalbr_inbox_documents ADD CONSTRAINT plg_fiscalbr_inbox_documents_supplier_fkey\n FOREIGN KEY (supplier_party_id) REFERENCES public.people(id) ON DELETE SET NULL;\n END IF;\n IF NOT EXISTS (SELECT 1 FROM pg_constraint WHERE conname = 'plg_fiscalbr_inbox_documents_operation_fkey') THEN\n ALTER TABLE ONLY public.plg_fiscalbr_inbox_documents ADD CONSTRAINT plg_fiscalbr_inbox_documents_operation_fkey\n FOREIGN KEY (inventory_operation_id) REFERENCES public.plg_inventory_operations(id) ON DELETE SET NULL;\n END IF;\n\n -- Item, pagamento e duplicata não existem fora da nota: CASCADE.\n IF NOT EXISTS (SELECT 1 FROM pg_constraint WHERE conname = 'plg_fiscalbr_inbox_items_document_fkey') THEN\n ALTER TABLE ONLY public.plg_fiscalbr_inbox_items ADD CONSTRAINT plg_fiscalbr_inbox_items_document_fkey\n FOREIGN KEY (tenant_id, inbox_document_id) REFERENCES public.plg_fiscalbr_inbox_documents(tenant_id, id) ON DELETE CASCADE;\n END IF;\n IF NOT EXISTS (SELECT 1 FROM pg_constraint WHERE conname = 'plg_fiscalbr_inbox_items_product_fkey') THEN\n ALTER TABLE ONLY public.plg_fiscalbr_inbox_items ADD CONSTRAINT plg_fiscalbr_inbox_items_product_fkey\n FOREIGN KEY (product_id) REFERENCES public.products(id) ON DELETE SET NULL;\n END IF;\n IF NOT EXISTS (SELECT 1 FROM pg_constraint WHERE conname = 'plg_fiscalbr_inbox_payments_document_fkey') THEN\n ALTER TABLE ONLY public.plg_fiscalbr_inbox_payments ADD CONSTRAINT plg_fiscalbr_inbox_payments_document_fkey\n FOREIGN KEY (tenant_id, inbox_document_id) REFERENCES public.plg_fiscalbr_inbox_documents(tenant_id, id) ON DELETE CASCADE;\n END IF;\n IF NOT EXISTS (SELECT 1 FROM pg_constraint WHERE conname = 'plg_fiscalbr_inbox_duplicates_document_fkey') THEN\n ALTER TABLE ONLY public.plg_fiscalbr_inbox_duplicates ADD CONSTRAINT plg_fiscalbr_inbox_duplicates_document_fkey\n FOREIGN KEY (tenant_id, inbox_document_id) REFERENCES public.plg_fiscalbr_inbox_documents(tenant_id, id) ON DELETE CASCADE;\n END IF;\n IF NOT EXISTS (SELECT 1 FROM pg_constraint WHERE conname = 'plg_fiscalbr_inbox_duplicates_installment_fkey') THEN\n ALTER TABLE ONLY public.plg_fiscalbr_inbox_duplicates ADD CONSTRAINT plg_fiscalbr_inbox_duplicates_installment_fkey\n FOREIGN KEY (installment_id) REFERENCES public.plg_financial_invoice_installments(id) ON DELETE SET NULL;\n END IF;\nEND $$;\n\n-- A chave de acesso é única por casa: a mesma nota chegando duas vezes da SEFAZ\n-- é a mesma nota, e é isso que impede a importação em dobro.\nCREATE UNIQUE INDEX IF NOT EXISTS plg_fiscalbr_inbox_documents_access_key_uidx\n ON public.plg_fiscalbr_inbox_documents USING btree (tenant_id, access_key)\n WHERE access_key IS NOT NULL;\nCREATE INDEX IF NOT EXISTS plg_fiscalbr_inbox_documents_pending_idx\n ON public.plg_fiscalbr_inbox_documents USING btree (tenant_id, unit_id, issue_date DESC)\n WHERE inventory_operation_id IS NULL;\nCREATE INDEX IF NOT EXISTS plg_fiscalbr_inbox_items_document_idx\n ON public.plg_fiscalbr_inbox_items USING btree (tenant_id, inbox_document_id);\nCREATE INDEX IF NOT EXISTS plg_fiscalbr_inbox_items_product_idx\n ON public.plg_fiscalbr_inbox_items USING btree (tenant_id, product_id) WHERE product_id IS NOT NULL;\nCREATE INDEX IF NOT EXISTS plg_fiscalbr_inbox_payments_document_idx\n ON public.plg_fiscalbr_inbox_payments USING btree (tenant_id, inbox_document_id);\nCREATE INDEX IF NOT EXISTS plg_fiscalbr_inbox_duplicates_document_idx\n ON public.plg_fiscalbr_inbox_duplicates USING btree (tenant_id, inbox_document_id);\nCREATE UNIQUE INDEX IF NOT EXISTS plg_fiscalbr_tax_groups_name_uidx\n ON public.plg_fiscalbr_tax_groups USING btree (tenant_id, lower(name));\n\nDO $$\nDECLARE t text;\nBEGIN\n FOREACH t IN ARRAY ARRAY['plg_fiscalbr_inbox_documents', 'plg_fiscalbr_inbox_items',\n 'plg_fiscalbr_inbox_payments', 'plg_fiscalbr_inbox_duplicates',\n 'plg_fiscalbr_tax_groups'] LOOP\n EXECUTE format('DROP TRIGGER IF EXISTS %I ON public.%I', t || '_updated_at', t);\n EXECUTE format('CREATE TRIGGER %I BEFORE UPDATE ON public.%I FOR EACH ROW EXECUTE FUNCTION public.handle_updated_at()', t || '_updated_at', t);\n END LOOP;\nEND $$;\n\n-- ═══════════════════════════════════════════════════════════════════════════\n-- Quem enxerga\n-- ═══════════════════════════════════════════════════════════════════════════\n--\n-- Nota fiscal é do FINANCEIRO, e a permissão é a que ele já usa. A caixa de\n-- entrada não tem unidade nos filhos — eles herdam a da nota —, então o escopo\n-- de unidade vale só no documento; o resto é do tenant, e é a CASCADE da FK que\n-- garante que ninguém veja filho de nota alheia.\n-- RLS, uma a uma e por extenso. O laço adiante já liga tudo isto — mas ele liga\n-- por \\`EXECUTE format(...)\\`, e o portão que revisa plugin lê o ARQUIVO, não o\n-- banco: para ele, tabela com tenant_id e sem um ENABLE literal é tabela\n-- aberta. Uma garantia que o revisor não consegue ler é uma garantia que\n-- ninguém confere.\nALTER TABLE public.plg_fiscalbr_inbox_documents ENABLE ROW LEVEL SECURITY;\nALTER TABLE public.plg_fiscalbr_inbox_items ENABLE ROW LEVEL SECURITY;\nALTER TABLE public.plg_fiscalbr_tax_groups ENABLE ROW LEVEL SECURITY;\nALTER TABLE public.plg_fiscalbr_inbox_payments ENABLE ROW LEVEL SECURITY;\nALTER TABLE public.plg_fiscalbr_inbox_duplicates ENABLE ROW LEVEL SECURITY;\n\nDO $$\nDECLARE\n t text;\n doc boolean;\nBEGIN\n FOREACH t IN ARRAY ARRAY['plg_fiscalbr_inbox_documents', 'plg_fiscalbr_inbox_items',\n 'plg_fiscalbr_inbox_payments', 'plg_fiscalbr_inbox_duplicates',\n 'plg_fiscalbr_tax_groups'] LOOP\n doc := t = 'plg_fiscalbr_inbox_documents';\n EXECUTE format('ALTER TABLE public.%I FORCE ROW LEVEL SECURITY', t);\n EXECUTE format('ALTER TABLE public.%I ENABLE ROW LEVEL SECURITY', t);\n EXECUTE format('GRANT SELECT, INSERT, UPDATE, DELETE ON TABLE public.%I TO authenticated', t);\n EXECUTE format('GRANT ALL ON TABLE public.%I TO service_role', t);\n\n EXECUTE format('DROP POLICY IF EXISTS %I ON public.%I', t || '_authz_select', t);\n EXECUTE format($f$CREATE POLICY %I ON public.%I FOR SELECT TO authenticated USING (\n tenant_id = (SELECT app.current_tenant_id())\n AND (%s)\n AND (SELECT app.has_permission('financial.read', %s)))$f$,\n t || '_authz_select', t,\n CASE WHEN doc THEN format('unit_id IS NULL OR (SELECT app.has_unit(%I.unit_id))', t) ELSE 'true' END,\n CASE WHEN doc THEN format('%I.unit_id', t) ELSE 'NULL::uuid' END);\n\n EXECUTE format('DROP POLICY IF EXISTS %I ON public.%I', t || '_authz_write', t);\n EXECUTE format($f$CREATE POLICY %I ON public.%I FOR ALL TO authenticated USING (\n tenant_id = (SELECT app.current_tenant_id())\n AND (SELECT app.has_permission('financial.edit', %s)))\n WITH CHECK (\n tenant_id = (SELECT app.current_tenant_id())\n AND (SELECT app.has_permission('financial.edit', %s)))$f$,\n t || '_authz_write', t,\n CASE WHEN doc THEN format('%I.unit_id', t) ELSE 'NULL::uuid' END,\n CASE WHEN doc THEN format('%I.unit_id', t) ELSE 'NULL::uuid' END);\n END LOOP;\nEND $$;\n\nINSERT INTO public.scoped_resources (resource_table, unit_column, owner_column, shareable, default_visibility, label)\nVALUES ('plg_fiscalbr_inbox_documents', 'unit_id', 'owner_id', false, 'unit', 'Notas recebidas')\nON CONFLICT (resource_table) DO UPDATE\n SET unit_column = EXCLUDED.unit_column, default_visibility = EXCLUDED.default_visibility, label = EXCLUDED.label;\n`\n\nexport const MIGRATION_003_A_CAIXA_DE_ENTRADA_ATRAVESSA = `-- A caixa de entrada atravessa.\n--\n-- A 002 fez as cinco tabelas. Esta escreve o de-para. As cinco usam o writer\n-- genérico \\`by_shape\\` (spine 123): as colunas do destino foram desenhadas\n-- olhando a origem, então o que falta é dizer qual nome vira qual — não uma\n-- função de upsert por tabela.\n--\n-- ── O arquivo não vem por aqui ──────────────────────────────────────────────\n--\n-- \\`xml_content\\` guarda o XML inteiro e \\`pdf_storage_path\\` aponta para o DANFE.\n-- Os dois saem do de-para com \\`-\\`: arquivo atravessa pelo estágio de assets,\n-- que é quem alcança object storage, e vira linha de \\`public.documents\\`. O\n-- caminho de origem fica em metadata para o estágio achar.\n--\n-- ── O fornecedor fica para a costura ────────────────────────────────────────\n--\n-- \\`supplier_party_id\\` não é preenchido aqui. A origem guarda CNPJ e nome, não\n-- um id de contato, e casar CNPJ com \\`public.people\\` é uma decisão de\n-- identidade — a mesma que já pôs 438 clientes em quarentena. Fica nulo, com o\n-- CNPJ na coluna ao lado, e uma passada de costura resolve quando alguém\n-- decidir a regra.\n--\n-- Idempotente.\n\nSET LOCAL migration.allowlist_unlock = 'on';\n\nSELECT migration._allow(\n 'fiscal_groups', 'public.plg_fiscalbr_tax_groups', 'fiscal_br', 'by_shape',\n jsonb_build_object('target', 'public.plg_fiscalbr_tax_groups'),\n NULL::text[],\n jsonb_build_object(\n 'uuid', 'id',\n 'name', 'name',\n 'cfop', 'cfop',\n 'csosn', 'csosn',\n 'cst', 'cst',\n 'icms_origin', 'icms_origin',\n 'icms_rate', 'icms_rate',\n 'icms_modalidade_bc', 'icms_bc_mode',\n 'pis_situation', 'pis_situation',\n 'pis_rate', 'pis_rate',\n 'cofins_situation', 'cofins_situation',\n 'cofins_rate', 'cofins_rate',\n 'ibs_cbs_situation', 'ibs_cbs_situation',\n 'ibs_cbs_classification', 'ibs_cbs_classification',\n 'base_calculation_mode', 'base_calculation_mode',\n 'cbenef', 'benefit_code',\n 'cest', 'cest',\n 'poverty_fund_rate', 'poverty_fund_rate',\n 'active', 'is_active',\n 'created_at', 'created_at',\n 'updated_at', 'updated_at',\n 'tenant_id', '-',\n 'id', 'jsonb:fiscalbr.v1_ref'),\n 'O grupo fiscal: CFOP, CST/CSOSN e alíquotas que o produto herda na emissão. '\n 'IBS/CBS vêm junto porque o cadastro do V1 já os tem — deixar de fora '\n 'obrigaria a recadastrar tudo no dia em que a reforma valer.'\n);\n\nSELECT migration._allow(\n 'dfe_inbox', 'public.plg_fiscalbr_inbox_documents', 'fiscal_br', 'by_shape',\n jsonb_build_object('target', 'public.plg_fiscalbr_inbox_documents'),\n NULL::text[],\n jsonb_build_object(\n 'uuid', 'id',\n 'company_id', 'unit_id@companies',\n 'access_key', 'access_key',\n 'nfe_number', 'nfe_number',\n 'nfe_series', 'nfe_series',\n 'nfe_model', 'nfe_model',\n 'issue_date', 'issue_date',\n 'received_at', 'received_at',\n 'status', 'status',\n 'manifestation_status', 'manifestation_status',\n 'manifested_at', 'manifested_at',\n 'manifestation_attempts', 'manifestation_attempts',\n 'manifestation_response', 'manifestation_response',\n 'supplier_cnpj', 'supplier_cnpj',\n 'supplier_name', 'supplier_name',\n 'supplier_ie', 'supplier_ie',\n 'total_value', 'total_value',\n 'total_products', 'total_products',\n 'total_discount', 'total_discount',\n 'total_freight', 'total_freight',\n 'total_insurance', 'total_insurance',\n 'total_other', 'total_other',\n 'total_icms_base', 'total_icms_base',\n 'total_icms_value', 'total_icms_value',\n 'total_icms_st_base', 'total_icms_st_base',\n 'total_icms_st_value', 'total_icms_st_value',\n 'total_pis_value', 'total_pis_value',\n 'total_cofins_value', 'total_cofins_value',\n 'total_ipi_value', 'total_ipi_value',\n 'nfe_protocol', 'nfe_protocol') ||\n -- \\`jsonb_build_object\\` aceita no máximo 100 argumentos, e este de-para tem\n -- 59 pares. Dois objetos concatenados, e não uma tabela a menos.\n jsonb_build_object(\n 'nfe_authorized_at', 'nfe_authorized_at',\n 'carrier_name', 'carrier_name',\n 'carrier_cnpj', 'carrier_cnpj',\n 'freight_mode_code', 'freight_mode_code',\n 'additional_info', 'additional_info',\n 'reviewed_at', 'reviewed_at',\n 'created_at', 'created_at',\n 'updated_at', 'updated_at',\n 'tenant_id', '-',\n 'reviewed_by', '-',\n 'xml_content', '-',\n 'pdf_storage_path', 'jsonb:fiscalbr.v1_pdf_path',\n 'pdf_fetched_at', 'jsonb:fiscalbr.v1_pdf_fetched_at',\n 'danfe_import_id', 'jsonb:fiscalbr.v1_danfe_import_ref',\n 'plugnotas_payload', 'jsonb:fiscalbr.v1_plugnotas_payload',\n 'plugnotas_received_at', 'jsonb:fiscalbr.v1_plugnotas_received_at',\n 'nfe_cnf', 'jsonb:fiscalbr.nfe.cnf',\n 'nfe_natureza_operacao', 'jsonb:fiscalbr.nfe.natureza_operacao',\n 'nfe_finalidade', 'jsonb:fiscalbr.nfe.finalidade',\n 'nfe_tipo_operacao', 'jsonb:fiscalbr.nfe.tipo_operacao',\n 'nfe_destination', 'jsonb:fiscalbr.nfe.destination',\n 'supplier_address', 'jsonb:fiscalbr.supplier.street',\n 'supplier_number', 'jsonb:fiscalbr.supplier.number',\n 'supplier_complement', 'jsonb:fiscalbr.supplier.complement',\n 'supplier_neighborhood', 'jsonb:fiscalbr.supplier.neighborhood',\n 'supplier_city', 'jsonb:fiscalbr.supplier.city',\n 'supplier_state', 'jsonb:fiscalbr.supplier.state',\n 'supplier_postal_code', 'jsonb:fiscalbr.supplier.postal_code',\n 'supplier_phone', 'jsonb:fiscalbr.supplier.phone',\n 'supplier_email', 'jsonb:fiscalbr.supplier.email',\n 'id', 'jsonb:fiscalbr.v1_ref'),\n 'A NF-e de fornecedor recebida da SEFAZ. \\`xml_content\\` sai com \\`-\\`: arquivo '\n 'atravessa pelo estágio de assets e vira public.documents, e o caminho do '\n 'DANFE fica em metadata para o estágio achar. O endereço do fornecedor vai '\n 'para metadata porque o fornecedor é \\`public.people\\`, e duplicá-lo em coluna '\n 'daria duas respostas para \"onde ele fica\".'\n);\n\nSELECT migration._allow(\n 'dfe_inbox_items', 'public.plg_fiscalbr_inbox_items', 'fiscal_br', 'by_shape',\n jsonb_build_object('target', 'public.plg_fiscalbr_inbox_items'),\n NULL::text[],\n jsonb_build_object(\n 'uuid', 'id',\n 'dfe_inbox_id', 'inbox_document_id@dfe_inbox',\n 'item_number', 'item_number',\n 'product_code_supplier', 'supplier_product_code',\n 'ean_barcode', 'ean_barcode',\n 'ean_trib', 'ean_taxable',\n 'description', 'description',\n 'ncm', 'ncm',\n 'cest', 'cest',\n 'cfop', 'cfop',\n 'unit_commercial', 'unit_commercial',\n 'unit_taxable', 'unit_taxable',\n 'quantity_commercial', 'quantity_commercial',\n 'quantity_taxable', 'quantity_taxable',\n 'unit_price_commercial', 'unit_price_commercial',\n 'unit_price_taxable', 'unit_price_taxable',\n 'total_price', 'total_price',\n 'discount', 'discount',\n 'freight', 'freight',\n 'insurance', 'insurance',\n 'other_expenses', 'other_expenses',\n 'icms_cst', 'icms_cst',\n 'icms_csosn', 'icms_csosn',\n 'icms_origin', 'icms_origin',\n 'icms_base', 'icms_base',\n 'icms_rate', 'icms_rate',\n 'icms_value', 'icms_value',\n 'icms_st_base', 'icms_st_base',\n 'icms_st_value', 'icms_st_value',\n 'pis_cst', 'pis_cst',\n 'pis_base', 'pis_base',\n 'pis_rate', 'pis_rate',\n 'pis_value', 'pis_value',\n 'cofins_cst', 'cofins_cst',\n 'cofins_base', 'cofins_base',\n 'cofins_rate', 'cofins_rate',\n 'cofins_value', 'cofins_value',\n 'ipi_cst', 'ipi_cst',\n 'ipi_rate', 'ipi_rate',\n 'ipi_value', 'ipi_value',\n 'linked_product_id', 'product_id@products',\n 'reconciled', 'reconciled',\n 'raw_xml_item', 'raw_xml_item',\n 'created_at', 'created_at',\n 'updated_at', 'updated_at',\n 'tenant_id', '-',\n 'id', 'jsonb:fiscalbr.v1_ref'),\n 'O item da nota, com o tributo em COLUNA — a apuração soma coluna, não varre '\n 'jsonb. \\`raw_xml_item\\` é a exceção: já é jsonb na origem e serve para '\n 'conferência, não para conta.'\n);\n\nSELECT migration._allow(\n 'dfe_inbox_payments', 'public.plg_fiscalbr_inbox_payments', 'fiscal_br', 'by_shape',\n jsonb_build_object('target', 'public.plg_fiscalbr_inbox_payments'),\n NULL::text[],\n jsonb_build_object(\n 'uuid', 'id',\n 'dfe_inbox_id', 'inbox_document_id@dfe_inbox',\n 'payment_index', 'payment_index',\n 'indicador_pagamento', 'payment_indicator',\n 'tipo_pagamento_code', 'payment_type_code',\n 'tipo_pagamento_label', 'payment_type_label',\n 'value', 'value',\n 'troco', 'change_given',\n 'card_flag', 'card_brand',\n 'card_acquirer_cnpj', 'card_acquirer_cnpj',\n 'card_auth_code', 'card_auth_code',\n 'created_at', 'created_at',\n 'updated_at', 'updated_at',\n 'tenant_id', '-',\n 'id', 'jsonb:fiscalbr.v1_ref'),\n 'A forma de pagamento declarada na nota recebida.'\n);\n\nSELECT migration._allow(\n 'dfe_inbox_duplicatas', 'public.plg_fiscalbr_inbox_duplicates', 'fiscal_br', 'by_shape',\n jsonb_build_object('target', 'public.plg_fiscalbr_inbox_duplicates'),\n NULL::text[],\n jsonb_build_object(\n 'uuid', 'id',\n 'dfe_inbox_id', 'inbox_document_id@dfe_inbox',\n 'duplicata_index', 'duplicate_index',\n 'numero', 'number',\n 'vencimento', 'due_date',\n 'valor', 'value',\n 'created_at', 'created_at',\n 'updated_at', 'updated_at',\n 'tenant_id', '-',\n 'id', 'jsonb:fiscalbr.v1_ref'),\n 'O vencimento declarado na nota. Vira parcela do contas a pagar quando alguém '\n 'lança a nota — \\`installment_id\\` fica nulo até lá, porque a parcela é '\n 'decisão de quem lança, não da SEFAZ.'\n);\n\nDELETE FROM migration.excluded_tables\n WHERE source_table IN ('dfe_inbox', 'dfe_inbox_items', 'dfe_inbox_payments',\n 'dfe_inbox_duplicatas', 'fiscal_groups');\n\nRESET migration.allowlist_unlock;\n`\n\nexport const MIGRATION_004_O_GRUPO_FISCAL_DO_V1_REPETE_NOME = `-- O grupo fiscal do V1 repete nome.\n--\n-- A 002 criou índice ÚNICO em \\`(tenant_id, lower(name))\\`. O V1 tem, no tenant\n-- 8, \"Bebidas Alcoólicas\", \"Bebidas Não Alcoólicas\" e \"Vinhos e Espumantes\"\n-- DUAS vezes cada — grupos fiscais diferentes com o mesmo rótulo, e o V1\n-- permite. Três das onze linhas foram para a quarentena por uma regra que eu\n-- inventei, não que a origem tenha.\n--\n-- O índice vira comum: serve para buscar, não para proibir. Quem quiser proibir\n-- decide isso na tela, com o dado já dentro — e sabendo quais três são.\n--\n-- Esta correção nasceu dentro de \\`financial/083\\` e mudou de arquivo: o\n-- financeiro roda ANTES do fiscal_br na corrente, e um arquivo do financeiro\n-- que mexe numa tabela do fiscal_br falha do zero com\n-- \\`relation \"public.plg_fiscalbr_tax_groups\" does not exist\\`. Cada plugin mexe\n-- na tabela que é dele.\n--\n-- Idempotente.\n\nDROP INDEX IF EXISTS public.plg_fiscalbr_tax_groups_name_uidx;\nCREATE INDEX IF NOT EXISTS plg_fiscalbr_tax_groups_name_idx\n ON public.plg_fiscalbr_tax_groups USING btree (tenant_id, lower(name));\n`\n\nexport const MIGRATIONS: Array<{ id: string; sql: string }> = [\n { id: \"000_baseline\", sql: MIGRATION_000_BASELINE },\n { id: \"001_preserve_terminal_results_and_recover_leases\", sql: MIGRATION_001_PRESERVE_TERMINAL_RESULTS_AND_RECOVER_LEASES },\n { id: \"002_a_nota_que_chega_tem_onde_esperar\", sql: MIGRATION_002_A_NOTA_QUE_CHEGA_TEM_ONDE_ESPERAR },\n { id: \"003_a_caixa_de_entrada_atravessa\", sql: MIGRATION_003_A_CAIXA_DE_ENTRADA_ATRAVESSA },\n { id: \"004_o_grupo_fiscal_do_v1_repete_nome\", sql: MIGRATION_004_O_GRUPO_FISCAL_DO_V1_REPETE_NOME },\n]\n","import type { FinancialDataProvider } from '@fayz-ai/plugin-financial'\nimport type { PlugNotasFinancialProviderOptions } from '../types'\n\ntype FiscalDocument = NonNullable<Awaited<ReturnType<NonNullable<FinancialDataProvider['getNotaById']>>>>\n\nfunction fiscalReader(provider: FinancialDataProvider) {\n if (!provider.getNotaById) throw new Error('PlugNotas requires fiscal document reads from Financial.')\n return provider.getNotaById.bind(provider)\n}\n\nasync function committed(read: (id: string) => Promise<FiscalDocument | null>, id: string) {\n const document = await read(id)\n if (!document) throw new Error('Fiscal document was not found after the server operation.')\n return document\n}\n\nfunction resultError(result: { status: string; message?: string }, document: FiscalDocument, fallback: string) {\n if (result.status !== 'error') return\n throw new Error(result.message?.trim() || document.errorMessage?.trim() || fallback)\n}\n\nfunction cancellationPending(metadata: Record<string, unknown> | undefined): boolean {\n const provider = metadata?.plugnotas\n if (!provider || typeof provider !== 'object' || Array.isArray(provider)) return false\n return (provider as Record<string, unknown>).cancellationPending === true\n}\n\nfunction artifactUrl(value: unknown): string {\n if (typeof value !== 'string' || !value.trim()) throw new Error('PlugNotas returned an invalid artifact URL.')\n let parsed: URL\n try {\n parsed = new URL(value)\n } catch {\n throw new Error('PlugNotas returned an invalid artifact URL.')\n }\n if (!['http:', 'https:'].includes(parsed.protocol)) throw new Error('PlugNotas returned an invalid artifact URL.')\n return parsed.toString()\n}\n\n/**\n * Decorates Financial without creating another fiscal ledger. The gateway only\n * requests server operations; every result returned to the UI is read back\n * from the server-committed `plg_financial_nfe` row.\n */\nexport function createPlugNotasFinancialProvider(\n options: PlugNotasFinancialProviderOptions,\n): FinancialDataProvider {\n const { base, gateway } = options\n\n const issueNota: NonNullable<FinancialDataProvider['issueNota']> = async (id) => {\n const read = fiscalReader(base)\n const document = await read(id)\n if (!document) throw new Error('Fiscal document not found.')\n if (document.model !== 'nfce') throw new Error('PlugNotas currently authorizes NFC-e only.')\n if (!document.orderId?.trim()) throw new Error('PlugNotas NFC-e must be created from an order.')\n if (!['draft', 'error'].includes(document.status)) return document\n const result = await gateway.issue({ notaId: id, idempotencyKey: `nfce:${id}` })\n const saved = await committed(read, id)\n resultError(result, saved, 'PlugNotas could not issue this NFC-e.')\n return saved\n }\n\n const refreshNota: NonNullable<FinancialDataProvider['refreshNota']> = async (id) => {\n const read = fiscalReader(base)\n const document = await read(id)\n if (!document) throw new Error('Fiscal document not found.')\n if (!['processing', 'error'].includes(document.status) && !cancellationPending(document.metadata)) return document\n const result = await gateway.refresh({ notaId: id })\n const saved = await committed(read, id)\n resultError(result, saved, 'PlugNotas could not refresh this NFC-e.')\n return saved\n }\n\n const cancelNota: NonNullable<FinancialDataProvider['cancelNota']> = async (id, reason) => {\n const read = fiscalReader(base)\n const document = await read(id)\n if (!document) throw new Error('Fiscal document not found.')\n if (document.status !== 'issued') throw new Error('Only an issued NFC-e can be cancelled.')\n if (cancellationPending(document.metadata)) throw new Error('NFC-e cancellation is already in progress.')\n const normalized = reason?.trim() ?? ''\n if (normalized.length < 15 || normalized.length > 255) {\n throw new Error('Cancellation reason must contain between 15 and 255 characters.')\n }\n const result = await gateway.cancel({ notaId: id, reason: normalized })\n const saved = await committed(read, id)\n resultError(result, saved, 'PlugNotas could not cancel this NFC-e.')\n return saved\n }\n\n const getNotaArtifactUrl: NonNullable<FinancialDataProvider['getNotaArtifactUrl']> = async (id, kind) => {\n const read = fiscalReader(base)\n const document = await read(id)\n if (!document) throw new Error('Fiscal document not found.')\n if (!['issued', 'cancelled'].includes(document.status)) {\n throw new Error('Fiscal artifacts are available only after authorization.')\n }\n if (kind === 'cancellation_xml' && document.status !== 'cancelled') {\n throw new Error('Cancellation XML is available only after cancellation.')\n }\n return artifactUrl((await gateway.artifact({ notaId: id, kind })).url)\n }\n\n return new Proxy(base, {\n get(target, property, receiver) {\n if (property === 'issueNota') return issueNota\n if (property === 'refreshNota') return refreshNota\n if (property === 'cancelNota') return cancelNota\n if (property === 'getNotaArtifactUrl') return getNotaArtifactUrl\n return Reflect.get(target, property, receiver)\n },\n })\n}\n","import { definePlugin, type PluginManifest } from '@fayz-ai/core'\nimport { createPlugNotasConnector, type CreatePlugNotasConnectorOptions } from './connectors/plugnotas'\nimport { MIGRATIONS } from './migrations'\n\nexport * from './connectors/plugnotas'\nexport * from './lib/financial-provider'\nexport * from './types'\n\nexport interface CreatePlugNotasPluginOptions extends CreatePlugNotasConnectorOptions {}\n\n/**\n * Fiscal Brasil is an addon: Financial owns the fiscal document, while this\n * package owns the provider connection and its asynchronous delivery trail.\n */\nexport function createPlugNotasPlugin(options: CreatePlugNotasPluginOptions = {}): PluginManifest {\n return definePlugin({\n id: 'fiscal-br',\n name: 'Fiscal Brasil',\n icon: 'ReceiptText',\n version: '0.1.0',\n apiVersion: 1,\n scope: 'addon',\n defaultEnabled: true,\n dependencies: ['financial'],\n navigation: [],\n routes: [],\n connectors: [createPlugNotasConnector(options)],\n functions: [{\n slug: 'plugnotas-bridge',\n ingress: 'none',\n verifyJwt: false,\n secrets: [\n 'PLUGNOTAS_API_KEY',\n 'PLUGNOTAS_BASE_URL',\n 'PLUGNOTAS_ENVIRONMENT',\n 'PLUGNOTAS_WEBHOOK_SECRET',\n 'PLUGNOTAS_ARTIFACT_SIGNING_KEY',\n 'PLUGNOTAS_ALLOWED_ORIGINS',\n 'PLUGNOTAS_WORKER_SECRET',\n 'SUPABASE_URL',\n 'SUPABASE_ANON_KEY',\n 'SUPABASE_SERVICE_ROLE_KEY'\n ],\n grants: [\n 'plugnotas_register_emitter',\n 'plugnotas_disconnect_emitter',\n 'plugnotas_create_nfce_draft_for_order',\n 'plugnotas_prepare_nfce',\n 'plugnotas_prepare_nfce_cancel',\n 'plugnotas_apply_nfce_result',\n 'plugnotas_apply_nfce_webhook',\n 'plugnotas_get_artifact_locator',\n 'plugnotas_claim_nfce_recovery',\n 'plugnotas_record_nfce_attempt'\n ],\n }],\n migrations: MIGRATIONS.map((entry) => ({\n id: `fiscal-br-${entry.id}`,\n version: '1.0.0',\n sql: entry.sql,\n description: 'Durable PlugNotas delivery, recovery, webhook and emitter-registration boundary.',\n })),\n diagnostics: [{\n id: 'plugnotas-nfce-ready',\n description: 'NFC-e requires the shared connection, order, item and financial fiscal-document contracts.',\n requires: {\n tables: [\n 'connections',\n 'orders',\n 'items',\n 'plg_financial_nfe',\n 'plg_fiscalbr_nfce_jobs',\n 'plg_fiscalbr_nfce_events',\n 'plg_fiscalbr_webhook_receipts',\n ],\n rpcs: [\n 'plugnotas_create_nfce_draft_for_order',\n 'plugnotas_prepare_nfce',\n 'plugnotas_apply_nfce_result',\n 'plugnotas_apply_nfce_webhook',\n 'plugnotas_claim_nfce_recovery',\n ],\n env: [\n 'PLUGNOTAS_API_KEY',\n 'PLUGNOTAS_BASE_URL',\n 'PLUGNOTAS_ENVIRONMENT',\n 'PLUGNOTAS_WEBHOOK_SECRET',\n 'PLUGNOTAS_ARTIFACT_SIGNING_KEY',\n 'PLUGNOTAS_ALLOWED_ORIGINS',\n 'PLUGNOTAS_WORKER_SECRET',\n ],\n },\n level: 'error',\n }],\n })\n}\n"]}
|
|
1
|
+
{"version":3,"sources":["../src/connectors/plugnotas.ts","../src/migrations/index.ts","../src/data/registries.ts","../src/lib/financial-provider.ts","../src/index.ts"],"names":[],"mappings":"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;AAAA;AAAA;AAAA;AAAA;AAAA;;AAAA;AAAA;AAAA;;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;;AAAA;AAAA;;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;;AAAA;AAAA;AAAA;AAAA;;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;;AAAA;AAAA;AAAA;;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;;AAAA;AAAA;AAAA;AAAA;;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;;AAAA;AAAA;AAAA;AAAA;AAAA;;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA,CAAA;AA6jB/B,IAAM,0DAAA,GAA6D,CAAA;AAAA;AAAA;;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;;AAAA;AAAA;AAAA;AAAA;AAAA;;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA,CAAA;AAqMnE,IAAM,+CAAA,GAAkD,CAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;;AAAA;AAAA;AAAA;AAAA;;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;;AAAA;AAAA;;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;;AAAA;AAAA;;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;;AAAA;AAAA;;AAAA;AAAA;AAAA;;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;;AAAA;AAAA;AAAA;AAAA;AAAA,CAAA;AA2WxD,IAAM,0CAAA,GAA6C,CAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;;AAAA;;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;;AAAA;AAAA;AAAA;;AAAA;AAAA,CAAA;AAkPnD,IAAM,8CAAA,GAAiD,CAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;;AAAA;AAAA;AAAA;AAA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","file":"index.js","sourcesContent":["import type React from 'react'\nimport {\n connectionSettings,\n connectionStatus,\n createConnectionStore,\n settingsStatus,\n type ConnectionConfig,\n type ConnectorDefinition,\n type ConnectorField,\n type ConnectorSetting,\n} from '@fayz-ai/core'\nimport type { PlugNotasConnectorClient, PlugNotasEnvironment, PlugNotasSetupState } from '../types'\n\nexport const PLUGNOTAS_CONNECTOR_ID = 'plugnotas'\n\nconst PLUGNOTAS_MARK = \"data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' width='32' height='32' viewBox='0 0 32 32'%3E%3Crect width='32' height='32' rx='8' fill='%230F766E'/%3E%3Cpath d='M10 7h12v18l-2-1.5-2 1.5-2-1.5-2 1.5-2-1.5-2 1.5V7Z' fill='white'/%3E%3Cpath d='M13 12h6M13 16h6M13 20h4' stroke='%230F766E' stroke-width='1.8' stroke-linecap='round'/%3E%3C/svg%3E\"\n\nexport const PLUGNOTAS_FIELDS = [\n {\n key: 'emitterCnpj',\n label: 'CNPJ do emitente',\n type: 'text',\n placeholder: '00.000.000/0000-00',\n },\n] as const satisfies readonly ConnectorField[]\n\nexport const PLUGNOTAS_SETTINGS = [{\n key: 'environment',\n label: 'Ambiente fiscal',\n description: 'Sandbox simula o emissor. Homologação envia para a SEFAZ sem valor fiscal.',\n type: 'select',\n options: [\n { value: 'sandbox', label: 'Sandbox simulado' },\n { value: 'homologation', label: 'Homologação SEFAZ' },\n ],\n requiredToOperate: true,\n defaultValue: 'sandbox',\n}] as const satisfies readonly ConnectorSetting[]\n\nconst INTERNAL_SETTINGS = [\n 'certificateReady',\n 'cscReady',\n 'providerEmitterId',\n 'certificateExpiresAt',\n 'certificateFingerprint',\n // Non-secret operational data written by the host's server-side setup flow.\n // Declaring these keys keeps the shared connection store authoritative while\n // credentials (API key, A1 bytes/password, CSC) remain outside public JSON.\n 'stateRegistration',\n 'corporateName',\n 'tradeName',\n 'email',\n 'taxRegime',\n 'address',\n 'series',\n 'paymentMethod',\n]\nconst DAY_MS = 24 * 60 * 60 * 1000\n\nfunction digits(value: string): string {\n return value.replace(/\\D/g, '')\n}\n\nexport function isValidCnpj(value: string): boolean {\n const normalized = digits(value)\n if (!/^\\d{14}$/.test(normalized) || /^(\\d)\\1{13}$/.test(normalized)) return false\n const digit = (base: string, weights: number[]) => {\n const sum = weights.reduce((total, weight, index) => total + Number(base[index]) * weight, 0)\n const remainder = sum % 11\n return remainder < 2 ? 0 : 11 - remainder\n }\n const first = digit(normalized.slice(0, 12), [5, 4, 3, 2, 9, 8, 7, 6, 5, 4, 3, 2])\n const second = digit(`${normalized.slice(0, 12)}${first}`, [6, 5, 4, 3, 2, 9, 8, 7, 6, 5, 4, 3, 2])\n return normalized.endsWith(`${first}${second}`)\n}\n\nfunction environment(value: unknown): PlugNotasEnvironment {\n if (value === undefined || value === null || value === '' || value === 'sandbox') return 'sandbox'\n if (value === 'homologation') return value\n throw new Error('Produção não está disponível neste conector preview.')\n}\n\nfunction store() {\n return createConnectionStore(PLUGNOTAS_CONNECTOR_ID, {\n fields: [...PLUGNOTAS_FIELDS],\n settings: [...PLUGNOTAS_SETTINGS],\n settingsKeys: INTERNAL_SETTINGS,\n })\n}\n\nfunction deriveStatus(connection: ConnectionConfig | null) {\n if (!connection?.active) return { connected: false }\n const readiness = settingsStatus([...PLUGNOTAS_SETTINGS], connection)\n const certificateReady = connection.settings.certificateReady === true\n const cscReady = connection.settings.cscReady === true\n const expiry = typeof connection.settings.certificateExpiresAt === 'string'\n ? Date.parse(connection.settings.certificateExpiresAt)\n : Number.NaN\n const daysUntilExpiry = Number.isFinite(expiry) ? Math.ceil((expiry - Date.now()) / DAY_MS) : null\n const expired = daysUntilExpiry !== null && daysUntilExpiry <= 0\n const expiringSoon = daysUntilExpiry !== null && daysUntilExpiry > 0 && daysUntilExpiry <= 30\n const missingSettings = [...new Set([\n ...(readiness.missingSettings ?? []),\n ...(!certificateReady || expired ? ['certificate'] : []),\n ...(!cscReady ? ['csc'] : []),\n ])]\n\n return {\n connected: true,\n needsSetup: missingSettings.length > 0,\n ...(missingSettings.length ? { missingSettings } : {}),\n detail: expired\n ? 'Certificado A1 vencido'\n : !certificateReady\n ? 'Envie o certificado A1 do emitente.'\n : !cscReady\n ? 'Confirme o CSC e o ID Token do emitente.'\n : connection.settings.environment === 'homologation'\n ? 'Emitente pronto em homologação'\n : 'Emitente pronto no sandbox',\n ...(expiringSoon\n ? { issues: [`O certificado A1 vence em ${daysUntilExpiry} dia${daysUntilExpiry === 1 ? '' : 's'}.`] }\n : {}),\n plane: 'sync' as const,\n }\n}\n\nexport interface CreatePlugNotasConnectorOptions {\n client?: PlugNotasConnectorClient\n SetupPanel?: React.ComponentType\n}\n\nexport function createPlugNotasConnector(options: CreatePlugNotasConnectorOptions = {}): ConnectorDefinition {\n const connectionStore = store()\n return {\n id: PLUGNOTAS_CONNECTOR_ID,\n // PlugBank and PlugNotas are different products from the same vendor. The\n // directory key names the product, otherwise one silently replaces the\n // other in the generated connector map.\n catalogSlug: 'plugnotas',\n hostPluginId: 'financial',\n name: 'PlugNotas · NFC-e',\n description: 'Emita NFC-e pela TecnoSpeed sem expor certificado, senha ou API key no navegador.',\n icon: 'ReceiptText',\n logoUrl: PLUGNOTAS_MARK,\n authKind: 'api-key',\n category: 'erp',\n direction: 'outbound',\n planes: ['sync'],\n docsUrl: 'https://docs.plugnotas.com.br/',\n fields: [...PLUGNOTAS_FIELDS],\n settings: [...PLUGNOTAS_SETTINGS],\n permissions: [{\n kind: 'data',\n resource: 'fiscal-document',\n actions: ['read', 'write'],\n reason: 'Enviar a NFC-e e reconciliar situação, chave de acesso e artefatos no Financeiro.',\n sensitive: true,\n }],\n invokedAt: [{\n key: 'financial.nfce.issue',\n label: 'Quando um operador confirma a emissão da NFC-e de um pedido concluído',\n plane: 'sync',\n trigger: 'manual',\n touches: ['fiscal-document'],\n }],\n howItSyncs: [\n 'A emissão cria primeiro um comando durável e só depois chama o provedor.',\n 'Webhook e recuperação consultiva convergem o mesmo comando, sem reenviar no escuro.',\n ],\n ...connectionStatus(PLUGNOTAS_CONNECTOR_ID, deriveStatus, { store }),\n ...connectionSettings(connectionStore),\n async testConnection(values) {\n const emitterCnpj = digits(values.emitterCnpj ?? '')\n if (!isValidCnpj(emitterCnpj)) return { ok: false, message: 'Informe um CNPJ válido.' }\n let selected: PlugNotasEnvironment\n try {\n selected = environment(values.environment)\n } catch (error) {\n return { ok: false, message: error instanceof Error ? error.message : 'Ambiente inválido.' }\n }\n return options.client?.testEmitter\n ? options.client.testEmitter({ emitterCnpj, environment: selected })\n : { ok: true }\n },\n async saveConnection(values) {\n const emitterCnpj = digits(values.emitterCnpj ?? '')\n if (!isValidCnpj(emitterCnpj)) throw new Error('Informe um CNPJ válido.')\n await connectionStore.save({\n active: true,\n displayName: `Emitente ${emitterCnpj.slice(-6)}`,\n providerAccountKey: emitterCnpj,\n settings: {\n emitterCnpj,\n environment: environment(values.environment),\n certificateReady: false,\n cscReady: false,\n },\n })\n },\n ...(options.client?.disconnectEmitter\n ? { disconnect: () => options.client!.disconnectEmitter!() }\n : {}),\n disconnectNote: 'As notas existentes permanecem no Financeiro; somente novas emissões são interrompidas.',\n ExtraPanel: options.SetupPanel,\n }\n}\n\n/** Persist only server-confirmed readiness and non-secret certificate metadata. */\nexport async function markPlugNotasEmitterReady(input: PlugNotasSetupState): Promise<void> {\n const emitterCnpj = digits(input.emitterCnpj)\n if (!isValidCnpj(emitterCnpj)) throw new Error('Informe um CNPJ válido.')\n await store().save({\n active: true,\n providerAccountKey: emitterCnpj,\n settings: {\n emitterCnpj,\n environment: environment(input.environment),\n certificateReady: input.certificateReady,\n cscReady: input.cscReady,\n providerEmitterId: input.providerEmitterId,\n certificateExpiresAt: input.certificateExpiresAt,\n certificateFingerprint: input.certificateFingerprint,\n },\n })\n}\n","// AUTO-GENERATED from 000_baseline.sql, 001_preserve_terminal_results_and_recover_leases.sql, 002_a_nota_que_chega_tem_onde_esperar.sql, 003_a_caixa_de_entrada_atravessa.sql, 004_o_grupo_fiscal_do_v1_repete_nome.sql, 005_a_nota_emitida_e_um_documento.sql, 006_o_grupo_fiscal_ganha_o_rail.sql, 007_a_rls_do_documento_passa_a_ser_legivel.sql — regenerate with scripts/embed-migrations.mjs\n// SQL files are the source of truth; this inline copy lets the manifest declare\n// migrations as data. Do not edit by hand — run the embed script instead.\n\nexport const MIGRATION_000_BASELINE = `-- Fiscal Brasil / PlugNotas baseline.\n--\n-- This addon does not create a second fiscal ledger. \\`plg_financial_nfe\\`\n-- remains the authoritative document owned by plugin-financial. The tables\n-- below describe delivery to an external issuer: durable commands, attempts,\n-- webhooks and emitter ownership.\n--\n-- The file is additive and idempotent because the current restaurant pool is\n-- live. Applying it is deliberately outside this draft PR.\n\nCREATE TABLE IF NOT EXISTS public.plg_fiscalbr_nfce_jobs (\n id uuid PRIMARY KEY DEFAULT gen_random_uuid(),\n tenant_id uuid NOT NULL REFERENCES public.tenants(id) ON DELETE CASCADE,\n -- Logical reference to plugin-financial's fiscal ledger. Cross-plugin\n -- physical foreign keys are intentionally forbidden: installation and\n -- removal of this addon must never rewrite another plugin's schema.\n nota_id uuid NOT NULL,\n order_id uuid NOT NULL REFERENCES public.orders(id) ON DELETE RESTRICT,\n connection_id uuid NOT NULL REFERENCES public.connections(id) ON DELETE RESTRICT,\n operation text NOT NULL CHECK (operation IN ('issue', 'cancel')),\n status text NOT NULL DEFAULT 'queued'\n CHECK (status IN ('queued', 'running', 'waiting', 'succeeded', 'failed', 'manual_review')),\n idempotency_key text NOT NULL,\n provider_document_id text,\n integration_id text,\n request_snapshot jsonb NOT NULL DEFAULT '{}'::jsonb,\n provider_response jsonb NOT NULL DEFAULT '{}'::jsonb,\n attempts integer NOT NULL DEFAULT 0 CHECK (attempts >= 0),\n next_attempt_at timestamptz,\n locked_until timestamptz,\n last_error text,\n created_at timestamptz NOT NULL DEFAULT now(),\n updated_at timestamptz NOT NULL DEFAULT now(),\n UNIQUE (tenant_id, idempotency_key)\n);\n\nCREATE INDEX IF NOT EXISTS plg_fiscalbr_nfce_jobs_recovery_idx\n ON public.plg_fiscalbr_nfce_jobs (status, next_attempt_at, locked_until)\n WHERE status IN ('queued', 'waiting');\n\nCREATE TABLE IF NOT EXISTS public.plg_fiscalbr_nfce_events (\n id bigint GENERATED BY DEFAULT AS IDENTITY PRIMARY KEY,\n tenant_id uuid NOT NULL REFERENCES public.tenants(id) ON DELETE CASCADE,\n nota_id uuid NOT NULL,\n job_id uuid REFERENCES public.plg_fiscalbr_nfce_jobs(id) ON DELETE SET NULL,\n event_type text NOT NULL,\n payload jsonb NOT NULL DEFAULT '{}'::jsonb,\n created_at timestamptz NOT NULL DEFAULT now()\n);\n\nCREATE INDEX IF NOT EXISTS plg_fiscalbr_nfce_events_document_idx\n ON public.plg_fiscalbr_nfce_events (tenant_id, nota_id, created_at DESC);\n\nCREATE TABLE IF NOT EXISTS public.plg_fiscalbr_webhook_receipts (\n id bigint GENERATED BY DEFAULT AS IDENTITY PRIMARY KEY,\n provider text NOT NULL DEFAULT 'plugnotas',\n provider_event_id text NOT NULL,\n tenant_id uuid REFERENCES public.tenants(id) ON DELETE SET NULL,\n nota_id uuid,\n payload_sha256 text NOT NULL,\n received_at timestamptz NOT NULL DEFAULT now(),\n processed_at timestamptz,\n processing_error text,\n UNIQUE (provider, provider_event_id)\n);\n\nCREATE TABLE IF NOT EXISTS public.plg_fiscalbr_emitter_ownership (\n emitter_cnpj text PRIMARY KEY CHECK (emitter_cnpj ~ '^[0-9]{14}$'),\n tenant_id uuid NOT NULL REFERENCES public.tenants(id) ON DELETE CASCADE,\n connection_id uuid NOT NULL REFERENCES public.connections(id) ON DELETE CASCADE,\n environment text NOT NULL CHECK (environment IN ('sandbox', 'homologation')),\n provider_emitter_id text,\n certificate_expires_at timestamptz,\n certificate_fingerprint text,\n csc_ready boolean NOT NULL DEFAULT false,\n created_at timestamptz NOT NULL DEFAULT now(),\n updated_at timestamptz NOT NULL DEFAULT now(),\n UNIQUE (tenant_id, connection_id)\n);\n\nALTER TABLE public.plg_fiscalbr_nfce_jobs ENABLE ROW LEVEL SECURITY;\nALTER TABLE public.plg_fiscalbr_nfce_events ENABLE ROW LEVEL SECURITY;\nALTER TABLE public.plg_fiscalbr_webhook_receipts ENABLE ROW LEVEL SECURITY;\nALTER TABLE public.plg_fiscalbr_emitter_ownership ENABLE ROW LEVEL SECURITY;\n\nDROP POLICY IF EXISTS plg_fiscalbr_nfce_jobs_read ON public.plg_fiscalbr_nfce_jobs;\nCREATE POLICY plg_fiscalbr_nfce_jobs_read ON public.plg_fiscalbr_nfce_jobs\n FOR SELECT TO authenticated\n USING (\n tenant_id = (SELECT app.current_tenant_id())\n AND (SELECT app.has_permission('financial.read', NULL))\n );\n\nDROP POLICY IF EXISTS plg_fiscalbr_nfce_events_read ON public.plg_fiscalbr_nfce_events;\nCREATE POLICY plg_fiscalbr_nfce_events_read ON public.plg_fiscalbr_nfce_events\n FOR SELECT TO authenticated\n USING (\n tenant_id = (SELECT app.current_tenant_id())\n AND (SELECT app.has_permission('financial.read', NULL))\n );\n\nDROP POLICY IF EXISTS plg_fiscalbr_emitter_ownership_read ON public.plg_fiscalbr_emitter_ownership;\nCREATE POLICY plg_fiscalbr_emitter_ownership_read ON public.plg_fiscalbr_emitter_ownership\n FOR SELECT TO authenticated\n USING (\n tenant_id = (SELECT app.current_tenant_id())\n AND (SELECT app.has_permission('financial.read', NULL))\n );\n\nREVOKE ALL ON public.plg_fiscalbr_nfce_jobs FROM anon, authenticated;\nREVOKE ALL ON public.plg_fiscalbr_nfce_events FROM anon, authenticated;\nREVOKE ALL ON public.plg_fiscalbr_webhook_receipts FROM anon, authenticated;\nREVOKE ALL ON public.plg_fiscalbr_emitter_ownership FROM anon, authenticated;\nGRANT SELECT ON public.plg_fiscalbr_nfce_jobs TO authenticated;\nGRANT SELECT ON public.plg_fiscalbr_nfce_events TO authenticated;\nGRANT SELECT ON public.plg_fiscalbr_emitter_ownership TO authenticated;\nGRANT ALL ON public.plg_fiscalbr_nfce_jobs TO service_role;\nGRANT ALL ON public.plg_fiscalbr_nfce_events TO service_role;\nGRANT ALL ON public.plg_fiscalbr_webhook_receipts TO service_role;\nGRANT ALL ON public.plg_fiscalbr_emitter_ownership TO service_role;\n\nCREATE OR REPLACE FUNCTION public.plugnotas_register_emitter(\n p_tenant_id uuid,\n p_connection_id uuid,\n p_emitter_cnpj text,\n p_environment text,\n p_provider_emitter_id text DEFAULT NULL,\n p_certificate_expires_at timestamptz DEFAULT NULL,\n p_certificate_fingerprint text DEFAULT NULL,\n p_csc_ready boolean DEFAULT false\n) RETURNS jsonb\nLANGUAGE plpgsql SECURITY DEFINER SET search_path = '' AS $$\nDECLARE\n v_cnpj text := regexp_replace(coalesce(p_emitter_cnpj, ''), '[^0-9]', '', 'g');\nBEGIN\n IF v_cnpj !~ '^[0-9]{14}$' THEN\n RAISE EXCEPTION 'plugnotas_register_emitter: invalid CNPJ' USING ERRCODE = '22023';\n END IF;\n IF p_environment NOT IN ('sandbox', 'homologation') THEN\n RAISE EXCEPTION 'plugnotas_register_emitter: production is not enabled' USING ERRCODE = '22023';\n END IF;\n IF NOT EXISTS (\n SELECT 1 FROM public.connections c\n WHERE c.id = p_connection_id\n AND c.tenant_id = p_tenant_id\n AND c.connector_id = 'plugnotas'\n AND c.active\n ) THEN\n RAISE EXCEPTION 'plugnotas_register_emitter: active connection not found' USING ERRCODE = 'P0002';\n END IF;\n\n INSERT INTO public.plg_fiscalbr_emitter_ownership\n (emitter_cnpj, tenant_id, connection_id, environment, provider_emitter_id,\n certificate_expires_at, certificate_fingerprint, csc_ready, updated_at)\n VALUES\n (v_cnpj, p_tenant_id, p_connection_id, p_environment, p_provider_emitter_id,\n p_certificate_expires_at, p_certificate_fingerprint, p_csc_ready, now())\n ON CONFLICT (emitter_cnpj) DO UPDATE SET\n tenant_id = EXCLUDED.tenant_id,\n connection_id = EXCLUDED.connection_id,\n environment = EXCLUDED.environment,\n provider_emitter_id = EXCLUDED.provider_emitter_id,\n certificate_expires_at = EXCLUDED.certificate_expires_at,\n certificate_fingerprint = EXCLUDED.certificate_fingerprint,\n csc_ready = EXCLUDED.csc_ready,\n updated_at = now()\n WHERE public.plg_fiscalbr_emitter_ownership.tenant_id = EXCLUDED.tenant_id;\n\n IF NOT FOUND THEN\n RAISE EXCEPTION 'plugnotas_register_emitter: CNPJ belongs to another tenant' USING ERRCODE = '23505';\n END IF;\n\n UPDATE public.connections\n SET provider_account_key = v_cnpj,\n settings = settings || jsonb_build_object(\n 'environment', p_environment,\n 'certificateReady', p_certificate_expires_at IS NOT NULL,\n 'certificateExpiresAt', p_certificate_expires_at,\n 'certificateFingerprint', p_certificate_fingerprint,\n 'providerEmitterId', p_provider_emitter_id,\n 'cscReady', p_csc_ready\n ),\n status = CASE WHEN p_certificate_expires_at IS NOT NULL AND p_csc_ready THEN 'connected' ELSE 'pending' END,\n updated_at = now()\n WHERE id = p_connection_id AND tenant_id = p_tenant_id;\n\n RETURN jsonb_build_object('emitterCnpj', v_cnpj, 'environment', p_environment);\nEND $$;\n\nCREATE OR REPLACE FUNCTION public.plugnotas_disconnect_emitter(\n p_tenant_id uuid,\n p_connection_id uuid\n) RETURNS void\nLANGUAGE plpgsql SECURITY DEFINER SET search_path = '' AS $$\nBEGIN\n DELETE FROM public.plg_fiscalbr_emitter_ownership\n WHERE tenant_id = p_tenant_id AND connection_id = p_connection_id;\n UPDATE public.connections\n SET active = false, status = 'revoked', settings = '{}'::jsonb,\n provider_state = '{}'::jsonb, updated_at = now()\n WHERE tenant_id = p_tenant_id AND id = p_connection_id AND connector_id = 'plugnotas';\nEND $$;\n\nCREATE OR REPLACE FUNCTION public.plugnotas_create_nfce_draft_for_order(\n p_tenant_id uuid,\n p_order_id uuid\n) RETURNS uuid\nLANGUAGE plpgsql SECURITY DEFINER SET search_path = '' AS $$\nDECLARE\n v_order public.orders%ROWTYPE;\n v_nota_id uuid;\nBEGIN\n -- Serialize draft creation per tenant/order without adding a column or\n -- index to the financial plugin's table. The order itself remains the\n -- stable idempotency boundary for one NFC-e draft.\n PERFORM pg_advisory_xact_lock(hashtextextended(p_tenant_id::text || ':' || p_order_id::text, 0));\n\n SELECT * INTO v_order FROM public.orders\n WHERE tenant_id = p_tenant_id AND id = p_order_id;\n IF NOT FOUND THEN\n RAISE EXCEPTION 'plugnotas_create_nfce_draft_for_order: order not found' USING ERRCODE = 'P0002';\n END IF;\n IF v_order.status NOT IN ('completed', 'paid', 'invoiced') THEN\n RAISE EXCEPTION 'plugnotas_create_nfce_draft_for_order: order is not closed' USING ERRCODE = '23514';\n END IF;\n IF NOT EXISTS (SELECT 1 FROM public.items i WHERE i.tenant_id = p_tenant_id AND i.order_id = p_order_id) THEN\n RAISE EXCEPTION 'plugnotas_create_nfce_draft_for_order: order has no items' USING ERRCODE = '23514';\n END IF;\n\n SELECT n.id INTO v_nota_id FROM public.plg_financial_nfe n\n WHERE n.tenant_id = p_tenant_id AND n.order_id = p_order_id AND n.model = 'nfce'\n ORDER BY n.created_at DESC LIMIT 1;\n IF v_nota_id IS NOT NULL THEN RETURN v_nota_id; END IF;\n\n INSERT INTO public.plg_financial_nfe\n (tenant_id, unit_id, model, status, order_id, contact_id, total_amount,\n emissor, metadata)\n VALUES\n (p_tenant_id, v_order.unit_id, 'nfce', 'draft', p_order_id, v_order.party_id,\n coalesce(v_order.total, 0), 'plugnotas',\n jsonb_build_object('source', 'order', 'orderKind', v_order.kind, 'orderChannel', v_order.channel))\n RETURNING id INTO v_nota_id;\n RETURN v_nota_id;\nEND $$;\n\nCREATE OR REPLACE FUNCTION public.plugnotas_prepare_nfce(\n p_tenant_id uuid,\n p_nota_id uuid,\n p_connection_id uuid,\n p_idempotency_key text\n) RETURNS jsonb\nLANGUAGE plpgsql SECURITY DEFINER SET search_path = '' AS $$\nDECLARE\n v_nota public.plg_financial_nfe%ROWTYPE;\n v_job public.plg_fiscalbr_nfce_jobs%ROWTYPE;\n v_snapshot jsonb;\nBEGIN\n SELECT * INTO v_nota FROM public.plg_financial_nfe\n WHERE tenant_id = p_tenant_id AND id = p_nota_id FOR UPDATE;\n IF NOT FOUND OR v_nota.model <> 'nfce' OR v_nota.order_id IS NULL THEN\n RAISE EXCEPTION 'plugnotas_prepare_nfce: order-backed NFC-e not found' USING ERRCODE = 'P0002';\n END IF;\n IF v_nota.status NOT IN ('draft', 'error', 'processing') THEN\n RAISE EXCEPTION 'plugnotas_prepare_nfce: invalid document status %', v_nota.status USING ERRCODE = '23514';\n END IF;\n IF NOT EXISTS (\n SELECT 1 FROM public.plg_fiscalbr_emitter_ownership e\n WHERE e.tenant_id = p_tenant_id AND e.connection_id = p_connection_id\n AND e.csc_ready AND e.certificate_expires_at > now()\n ) THEN\n RAISE EXCEPTION 'plugnotas_prepare_nfce: emitter is not ready' USING ERRCODE = '23514';\n END IF;\n\n SELECT jsonb_build_object(\n 'nota', to_jsonb(v_nota),\n 'order', to_jsonb(o),\n 'items', coalesce((\n SELECT jsonb_agg(to_jsonb(i) ORDER BY i.sort_order, i.id)\n FROM public.items i\n WHERE i.tenant_id = p_tenant_id AND i.order_id = v_nota.order_id\n ), '[]'::jsonb),\n -- Product tax metadata must be frozen in the same transaction as the order\n -- lines. Looking products up in the Edge Function would let a retry emit\n -- with NCM/CFOP changed after the operator confirmed the document.\n 'products', coalesce((\n SELECT jsonb_agg(to_jsonb(p) ORDER BY p.id)\n FROM public.products p\n WHERE p.tenant_id = p_tenant_id\n AND p.id IN (\n SELECT i.product_id FROM public.items i\n WHERE i.tenant_id = p_tenant_id\n AND i.order_id = v_nota.order_id\n AND i.product_id IS NOT NULL\n )\n ), '[]'::jsonb)\n ) INTO v_snapshot\n FROM public.orders o\n WHERE o.tenant_id = p_tenant_id AND o.id = v_nota.order_id;\n\n IF jsonb_array_length(v_snapshot->'items') = 0 THEN\n RAISE EXCEPTION 'plugnotas_prepare_nfce: order has no items' USING ERRCODE = '23514';\n END IF;\n\n INSERT INTO public.plg_fiscalbr_nfce_jobs\n (tenant_id, nota_id, order_id, connection_id, operation, status,\n idempotency_key, request_snapshot, next_attempt_at, updated_at)\n VALUES\n (p_tenant_id, p_nota_id, v_nota.order_id, p_connection_id, 'issue', 'queued',\n p_idempotency_key, v_snapshot, now(), now())\n ON CONFLICT (tenant_id, idempotency_key) DO UPDATE\n SET updated_at = now()\n RETURNING * INTO v_job;\n\n UPDATE public.plg_financial_nfe\n SET status = 'processing', error_message = NULL,\n metadata = metadata || jsonb_build_object('plugnotas', jsonb_build_object(\n 'jobId', v_job.id, 'jobStatus', v_job.status, 'operation', 'issue'\n )), updated_at = now()\n WHERE id = p_nota_id AND tenant_id = p_tenant_id;\n\n INSERT INTO public.plg_fiscalbr_nfce_events (tenant_id, nota_id, job_id, event_type, payload)\n VALUES (p_tenant_id, p_nota_id, v_job.id, 'issue.queued', '{}'::jsonb);\n RETURN jsonb_build_object('jobId', v_job.id, 'snapshot', v_job.request_snapshot, 'replayed', v_job.attempts > 0);\nEND $$;\n\nCREATE OR REPLACE FUNCTION public.plugnotas_prepare_nfce_cancel(\n p_tenant_id uuid,\n p_nota_id uuid,\n p_connection_id uuid,\n p_reason text\n) RETURNS jsonb\nLANGUAGE plpgsql SECURITY DEFINER SET search_path = '' AS $$\nDECLARE\n v_nota public.plg_financial_nfe%ROWTYPE;\n v_job_id uuid;\n v_reason text := btrim(coalesce(p_reason, ''));\nBEGIN\n IF char_length(v_reason) NOT BETWEEN 15 AND 255 THEN\n RAISE EXCEPTION 'plugnotas_prepare_nfce_cancel: reason must contain 15 to 255 characters' USING ERRCODE = '22023';\n END IF;\n SELECT * INTO v_nota FROM public.plg_financial_nfe\n WHERE tenant_id = p_tenant_id AND id = p_nota_id FOR UPDATE;\n IF NOT FOUND OR v_nota.status <> 'issued' OR v_nota.access_key IS NULL THEN\n RAISE EXCEPTION 'plugnotas_prepare_nfce_cancel: authorized NFC-e not found' USING ERRCODE = '23514';\n END IF;\n\n INSERT INTO public.plg_fiscalbr_nfce_jobs\n (tenant_id, nota_id, order_id, connection_id, operation, status,\n idempotency_key, provider_document_id, integration_id,\n request_snapshot, next_attempt_at)\n VALUES\n (p_tenant_id, p_nota_id, v_nota.order_id, p_connection_id, 'cancel', 'queued',\n 'nfce-cancel:' || p_nota_id::text,\n v_nota.metadata #>> '{plugnotas,providerDocumentId}',\n v_nota.metadata #>> '{plugnotas,integrationId}',\n jsonb_build_object('reason', v_reason), now())\n ON CONFLICT (tenant_id, idempotency_key) DO UPDATE SET\n provider_document_id = coalesce(\n public.plg_fiscalbr_nfce_jobs.provider_document_id,\n EXCLUDED.provider_document_id\n ),\n integration_id = coalesce(\n public.plg_fiscalbr_nfce_jobs.integration_id,\n EXCLUDED.integration_id\n ),\n updated_at = now()\n RETURNING id INTO v_job_id;\n\n UPDATE public.plg_financial_nfe\n SET metadata = metadata || jsonb_build_object('plugnotas', jsonb_build_object(\n 'jobId', v_job_id, 'jobStatus', 'queued', 'operation', 'cancel', 'cancellationPending', true\n )), updated_at = now()\n WHERE id = p_nota_id AND tenant_id = p_tenant_id;\n RETURN jsonb_build_object('jobId', v_job_id, 'reason', v_reason);\nEND $$;\n\nCREATE OR REPLACE FUNCTION public.plugnotas_record_nfce_attempt(\n p_tenant_id uuid,\n p_job_id uuid,\n p_status text,\n p_provider_response jsonb DEFAULT '{}'::jsonb,\n p_error text DEFAULT NULL,\n p_next_attempt_at timestamptz DEFAULT NULL\n) RETURNS void\nLANGUAGE plpgsql SECURITY DEFINER SET search_path = '' AS $$\nBEGIN\n IF p_status NOT IN ('running', 'waiting', 'failed', 'manual_review') THEN\n RAISE EXCEPTION 'plugnotas_record_nfce_attempt: invalid status' USING ERRCODE = '22023';\n END IF;\n UPDATE public.plg_fiscalbr_nfce_jobs\n SET status = p_status,\n -- One provider dispatch is one attempt. Moving that same attempt from\n -- running to waiting/failed must not count it a second time.\n attempts = attempts + CASE WHEN p_status = 'running' THEN 1 ELSE 0 END,\n provider_response = coalesce(p_provider_response, '{}'::jsonb),\n last_error = p_error, next_attempt_at = p_next_attempt_at,\n locked_until = NULL, updated_at = now()\n WHERE tenant_id = p_tenant_id AND id = p_job_id;\nEND $$;\n\nCREATE OR REPLACE FUNCTION public.plugnotas_apply_nfce_result(\n p_tenant_id uuid,\n p_job_id uuid,\n p_status text,\n p_provider_document_id text DEFAULT NULL,\n p_integration_id text DEFAULT NULL,\n p_number text DEFAULT NULL,\n p_series text DEFAULT NULL,\n p_access_key text DEFAULT NULL,\n p_message text DEFAULT NULL,\n p_provider_response jsonb DEFAULT '{}'::jsonb\n) RETURNS uuid\nLANGUAGE plpgsql SECURITY DEFINER SET search_path = '' AS $$\nDECLARE\n v_job public.plg_fiscalbr_nfce_jobs%ROWTYPE;\n v_document_status text;\nBEGIN\n SELECT * INTO v_job FROM public.plg_fiscalbr_nfce_jobs\n WHERE tenant_id = p_tenant_id AND id = p_job_id FOR UPDATE;\n IF NOT FOUND THEN\n RAISE EXCEPTION 'plugnotas_apply_nfce_result: job not found' USING ERRCODE = 'P0002';\n END IF;\n IF p_status NOT IN ('processing', 'issued', 'cancelled', 'error') THEN\n RAISE EXCEPTION 'plugnotas_apply_nfce_result: invalid status' USING ERRCODE = '22023';\n END IF;\n\n v_document_status := CASE\n WHEN v_job.operation = 'cancel' AND p_status = 'processing' THEN 'issued'\n ELSE p_status\n END;\n UPDATE public.plg_fiscalbr_nfce_jobs\n SET status = CASE\n WHEN p_status IN ('issued', 'cancelled') THEN 'succeeded'\n WHEN p_status = 'error' THEN 'failed'\n ELSE 'waiting'\n END,\n provider_document_id = coalesce(p_provider_document_id, provider_document_id),\n integration_id = coalesce(p_integration_id, integration_id),\n provider_response = coalesce(p_provider_response, '{}'::jsonb),\n last_error = CASE WHEN p_status = 'error' THEN p_message ELSE NULL END,\n next_attempt_at = CASE WHEN p_status = 'processing' THEN now() + interval '2 minutes' ELSE NULL END,\n locked_until = NULL, updated_at = now()\n WHERE id = p_job_id;\n\n UPDATE public.plg_financial_nfe\n SET status = v_document_status,\n number = coalesce(p_number, number),\n series = coalesce(p_series, series),\n access_key = coalesce(p_access_key, access_key),\n issued_at = CASE WHEN p_status = 'issued' THEN coalesce(issued_at, now()) ELSE issued_at END,\n error_message = CASE WHEN p_status = 'error' THEN p_message ELSE NULL END,\n metadata = metadata || jsonb_build_object('plugnotas', jsonb_strip_nulls(jsonb_build_object(\n 'jobId', p_job_id,\n 'jobStatus', CASE WHEN p_status IN ('issued', 'cancelled') THEN 'succeeded' WHEN p_status = 'error' THEN 'failed' ELSE 'waiting' END,\n 'operation', v_job.operation,\n 'providerDocumentId', coalesce(p_provider_document_id, v_job.provider_document_id),\n 'integrationId', coalesce(p_integration_id, v_job.integration_id),\n 'cancellationPending', v_job.operation = 'cancel' AND p_status = 'processing'\n ))),\n updated_at = now()\n WHERE tenant_id = p_tenant_id AND id = v_job.nota_id;\n\n INSERT INTO public.plg_fiscalbr_nfce_events (tenant_id, nota_id, job_id, event_type, payload)\n VALUES (p_tenant_id, v_job.nota_id, p_job_id, v_job.operation || '.' || p_status,\n coalesce(p_provider_response, '{}'::jsonb));\n RETURN v_job.nota_id;\nEND $$;\n\nCREATE OR REPLACE FUNCTION public.plugnotas_apply_nfce_webhook(\n p_provider_event_id text,\n p_payload_sha256 text,\n p_provider_document_id text,\n p_status text,\n p_integration_id text DEFAULT NULL,\n p_number text DEFAULT NULL,\n p_series text DEFAULT NULL,\n p_access_key text DEFAULT NULL,\n p_message text DEFAULT NULL,\n p_payload jsonb DEFAULT '{}'::jsonb\n) RETURNS jsonb\nLANGUAGE plpgsql SECURITY DEFINER SET search_path = '' AS $$\nDECLARE\n v_job public.plg_fiscalbr_nfce_jobs%ROWTYPE;\n v_receipt_id bigint;\nBEGIN\n INSERT INTO public.plg_fiscalbr_webhook_receipts\n (provider_event_id, payload_sha256)\n VALUES (p_provider_event_id, p_payload_sha256)\n ON CONFLICT (provider, provider_event_id) DO NOTHING\n RETURNING id INTO v_receipt_id;\n IF v_receipt_id IS NULL THEN RETURN jsonb_build_object('duplicate', true); END IF;\n\n SELECT * INTO v_job FROM public.plg_fiscalbr_nfce_jobs\n WHERE provider_document_id = p_provider_document_id\n ORDER BY created_at DESC LIMIT 1;\n IF NOT FOUND THEN\n UPDATE public.plg_fiscalbr_webhook_receipts\n SET processed_at = now(), processing_error = 'job_not_found'\n WHERE id = v_receipt_id;\n RETURN jsonb_build_object('duplicate', false, 'matched', false);\n END IF;\n\n UPDATE public.plg_fiscalbr_webhook_receipts\n SET tenant_id = v_job.tenant_id, nota_id = v_job.nota_id, processed_at = now()\n WHERE id = v_receipt_id;\n PERFORM public.plugnotas_apply_nfce_result(\n v_job.tenant_id, v_job.id, p_status, p_provider_document_id,\n coalesce(p_integration_id, v_job.integration_id), p_number, p_series,\n p_access_key, p_message, p_payload\n );\n RETURN jsonb_build_object('duplicate', false, 'matched', true, 'jobId', v_job.id);\nEND $$;\n\nCREATE OR REPLACE FUNCTION public.plugnotas_get_artifact_locator(\n p_tenant_id uuid,\n p_nota_id uuid\n) RETURNS jsonb\nLANGUAGE sql SECURITY DEFINER SET search_path = '' STABLE AS $$\n SELECT jsonb_build_object(\n 'notaId', n.id,\n 'status', n.status,\n 'providerDocumentId', n.metadata #>> '{plugnotas,providerDocumentId}',\n 'integrationId', n.metadata #>> '{plugnotas,integrationId}'\n )\n FROM public.plg_financial_nfe n\n WHERE n.tenant_id = p_tenant_id AND n.id = p_nota_id\n AND n.status IN ('issued', 'cancelled');\n$$;\n\nCREATE OR REPLACE FUNCTION public.plugnotas_claim_nfce_recovery(\n p_limit integer DEFAULT 20\n) RETURNS SETOF public.plg_fiscalbr_nfce_jobs\nLANGUAGE plpgsql SECURITY DEFINER SET search_path = '' AS $$\nBEGIN\n RETURN QUERY\n WITH candidates AS (\n SELECT j.id FROM public.plg_fiscalbr_nfce_jobs j\n WHERE j.status IN ('queued', 'waiting')\n AND coalesce(j.next_attempt_at, j.created_at) <= now()\n AND coalesce(j.locked_until, '-infinity'::timestamptz) < now()\n ORDER BY coalesce(j.next_attempt_at, j.created_at), j.created_at\n FOR UPDATE SKIP LOCKED\n LIMIT greatest(1, least(coalesce(p_limit, 20), 100))\n )\n UPDATE public.plg_fiscalbr_nfce_jobs j\n SET status = 'running', locked_until = now() + interval '2 minutes', updated_at = now()\n FROM candidates c WHERE j.id = c.id\n RETURNING j.*;\nEND $$;\n\nREVOKE ALL ON FUNCTION public.plugnotas_register_emitter(uuid, uuid, text, text, text, timestamptz, text, boolean) FROM PUBLIC, anon, authenticated;\nREVOKE ALL ON FUNCTION public.plugnotas_disconnect_emitter(uuid, uuid) FROM PUBLIC, anon, authenticated;\nREVOKE ALL ON FUNCTION public.plugnotas_create_nfce_draft_for_order(uuid, uuid) FROM PUBLIC, anon, authenticated;\nREVOKE ALL ON FUNCTION public.plugnotas_prepare_nfce(uuid, uuid, uuid, text) FROM PUBLIC, anon, authenticated;\nREVOKE ALL ON FUNCTION public.plugnotas_prepare_nfce_cancel(uuid, uuid, uuid, text) FROM PUBLIC, anon, authenticated;\nREVOKE ALL ON FUNCTION public.plugnotas_record_nfce_attempt(uuid, uuid, text, jsonb, text, timestamptz) FROM PUBLIC, anon, authenticated;\nREVOKE ALL ON FUNCTION public.plugnotas_apply_nfce_result(uuid, uuid, text, text, text, text, text, text, text, jsonb) FROM PUBLIC, anon, authenticated;\nREVOKE ALL ON FUNCTION public.plugnotas_apply_nfce_webhook(text, text, text, text, text, text, text, text, text, jsonb) FROM PUBLIC, anon, authenticated;\nREVOKE ALL ON FUNCTION public.plugnotas_get_artifact_locator(uuid, uuid) FROM PUBLIC, anon, authenticated;\nREVOKE ALL ON FUNCTION public.plugnotas_claim_nfce_recovery(integer) FROM PUBLIC, anon, authenticated;\n\nGRANT EXECUTE ON FUNCTION public.plugnotas_register_emitter(uuid, uuid, text, text, text, timestamptz, text, boolean) TO service_role;\nGRANT EXECUTE ON FUNCTION public.plugnotas_disconnect_emitter(uuid, uuid) TO service_role;\nGRANT EXECUTE ON FUNCTION public.plugnotas_create_nfce_draft_for_order(uuid, uuid) TO service_role;\nGRANT EXECUTE ON FUNCTION public.plugnotas_prepare_nfce(uuid, uuid, uuid, text) TO service_role;\nGRANT EXECUTE ON FUNCTION public.plugnotas_prepare_nfce_cancel(uuid, uuid, uuid, text) TO service_role;\nGRANT EXECUTE ON FUNCTION public.plugnotas_record_nfce_attempt(uuid, uuid, text, jsonb, text, timestamptz) TO service_role;\nGRANT EXECUTE ON FUNCTION public.plugnotas_apply_nfce_result(uuid, uuid, text, text, text, text, text, text, text, jsonb) TO service_role;\nGRANT EXECUTE ON FUNCTION public.plugnotas_apply_nfce_webhook(text, text, text, text, text, text, text, text, text, jsonb) TO service_role;\nGRANT EXECUTE ON FUNCTION public.plugnotas_get_artifact_locator(uuid, uuid) TO service_role;\nGRANT EXECUTE ON FUNCTION public.plugnotas_claim_nfce_recovery(integer) TO service_role;\n`\n\nexport const MIGRATION_001_PRESERVE_TERMINAL_RESULTS_AND_RECOVER_LEASES = `-- Incremental recovery fix. Preserve 000_baseline.sql and its recorded checksum.\n-- Only existing fiscal functions change; no parallel ledger/schema or backfill.\n-- CREATE OR REPLACE retains the service-role-only ACL from the baseline.\n\nCREATE OR REPLACE FUNCTION public.plugnotas_record_nfce_attempt(\n p_tenant_id uuid,\n p_job_id uuid,\n p_status text,\n p_provider_response jsonb DEFAULT '{}'::jsonb,\n p_error text DEFAULT NULL,\n p_next_attempt_at timestamptz DEFAULT NULL\n) RETURNS void\nLANGUAGE plpgsql SECURITY DEFINER SET search_path = '' AS $$\nBEGIN\n IF p_status NOT IN ('running', 'waiting', 'failed', 'manual_review') THEN\n RAISE EXCEPTION 'plugnotas_record_nfce_attempt: invalid status' USING ERRCODE = '22023';\n END IF;\n UPDATE public.plg_fiscalbr_nfce_jobs\n SET status = p_status,\n -- One provider dispatch is one attempt. Moving that same attempt from\n -- running to waiting/failed must not count it a second time.\n attempts = attempts + CASE WHEN p_status = 'running' THEN 1 ELSE 0 END,\n provider_response = coalesce(p_provider_response, '{}'::jsonb),\n last_error = p_error, next_attempt_at = p_next_attempt_at,\n locked_until = CASE WHEN p_status = 'running'\n THEN greatest(coalesce(locked_until, now()), now() + interval '2 minutes') ELSE NULL END,\n updated_at = now()\n WHERE tenant_id = p_tenant_id AND id = p_job_id AND status <> 'succeeded';\nEND $$;\n\nCREATE OR REPLACE FUNCTION public.plugnotas_apply_nfce_result(\n p_tenant_id uuid,\n p_job_id uuid,\n p_status text,\n p_provider_document_id text DEFAULT NULL,\n p_integration_id text DEFAULT NULL,\n p_number text DEFAULT NULL,\n p_series text DEFAULT NULL,\n p_access_key text DEFAULT NULL,\n p_message text DEFAULT NULL,\n p_provider_response jsonb DEFAULT '{}'::jsonb\n) RETURNS uuid\nLANGUAGE plpgsql SECURITY DEFINER SET search_path = '' AS $$\nDECLARE\n v_job public.plg_fiscalbr_nfce_jobs%ROWTYPE;\n v_document_status text;\n v_existing_status text;\n v_job_status text;\nBEGIN\n SELECT * INTO v_job FROM public.plg_fiscalbr_nfce_jobs\n WHERE tenant_id = p_tenant_id AND id = p_job_id;\n IF NOT FOUND THEN\n RAISE EXCEPTION 'plugnotas_apply_nfce_result: job not found' USING ERRCODE = 'P0002';\n END IF;\n IF p_status NOT IN ('processing', 'issued', 'cancelled', 'error') THEN\n RAISE EXCEPTION 'plugnotas_apply_nfce_result: invalid status' USING ERRCODE = '22023';\n END IF;\n\n -- Match prepare/cancel's lock order: document, then job.\n SELECT status INTO v_existing_status FROM public.plg_financial_nfe\n WHERE tenant_id = p_tenant_id AND id = v_job.nota_id FOR UPDATE;\n IF NOT FOUND THEN\n RAISE EXCEPTION 'plugnotas_apply_nfce_result: document not found' USING ERRCODE = 'P0002';\n END IF;\n SELECT * INTO v_job FROM public.plg_fiscalbr_nfce_jobs\n WHERE tenant_id = p_tenant_id AND id = p_job_id FOR UPDATE;\n\n -- Late polling must not regress a concluded document or an active cancellation.\n IF v_existing_status = 'cancelled'\n OR (v_job.operation = 'issue' AND v_existing_status = 'issued' AND p_status <> 'cancelled')\n OR (v_job.status = 'succeeded' AND p_status IN ('processing', 'error')) THEN\n -- Old/interrupted jobs may still be running after the document concluded.\n -- Settle their lease too, otherwise recovery would claim them forever.\n UPDATE public.plg_fiscalbr_nfce_jobs\n SET status = 'succeeded', locked_until = NULL, next_attempt_at = NULL, updated_at = now()\n WHERE tenant_id = p_tenant_id AND id = p_job_id;\n RETURN v_job.nota_id;\n END IF;\n\n v_job_status := CASE\n WHEN p_status = 'cancelled' OR (v_job.operation = 'issue' AND p_status = 'issued') THEN 'succeeded'\n WHEN p_status = 'error' THEN 'failed'\n ELSE 'waiting' END;\n v_document_status := CASE\n WHEN v_job.operation = 'cancel' AND p_status <> 'cancelled' THEN v_existing_status\n ELSE p_status\n END;\n UPDATE public.plg_fiscalbr_nfce_jobs\n SET status = v_job_status,\n provider_document_id = coalesce(p_provider_document_id, provider_document_id),\n integration_id = coalesce(p_integration_id, integration_id),\n provider_response = coalesce(p_provider_response, '{}'::jsonb),\n last_error = CASE WHEN p_status = 'error' THEN p_message ELSE NULL END,\n next_attempt_at = CASE WHEN v_job_status = 'waiting' THEN now() + interval '2 minutes' ELSE NULL END,\n locked_until = NULL, updated_at = now()\n WHERE id = p_job_id;\n\n UPDATE public.plg_financial_nfe\n SET status = v_document_status,\n number = coalesce(p_number, number),\n series = coalesce(p_series, series),\n access_key = coalesce(p_access_key, access_key),\n issued_at = CASE WHEN p_status = 'issued' THEN coalesce(issued_at, now()) ELSE issued_at END,\n error_message = CASE WHEN v_document_status = 'error' THEN p_message ELSE NULL END,\n metadata = metadata || jsonb_build_object('plugnotas', jsonb_strip_nulls(jsonb_build_object(\n 'jobId', p_job_id,\n 'jobStatus', v_job_status,\n 'operation', v_job.operation,\n 'providerDocumentId', coalesce(p_provider_document_id, v_job.provider_document_id),\n 'integrationId', coalesce(p_integration_id, v_job.integration_id),\n 'cancellationPending', v_job.operation = 'cancel' AND v_job_status = 'waiting'\n ))),\n updated_at = now()\n WHERE tenant_id = p_tenant_id AND id = v_job.nota_id;\n\n INSERT INTO public.plg_fiscalbr_nfce_events (tenant_id, nota_id, job_id, event_type, payload)\n VALUES (p_tenant_id, v_job.nota_id, p_job_id, v_job.operation || '.' || p_status,\n coalesce(p_provider_response, '{}'::jsonb));\n RETURN v_job.nota_id;\nEND $$;\n\nCREATE OR REPLACE FUNCTION public.plugnotas_apply_nfce_webhook(\n p_provider_event_id text,\n p_payload_sha256 text,\n p_provider_document_id text,\n p_status text,\n p_integration_id text DEFAULT NULL,\n p_number text DEFAULT NULL,\n p_series text DEFAULT NULL,\n p_access_key text DEFAULT NULL,\n p_message text DEFAULT NULL,\n p_payload jsonb DEFAULT '{}'::jsonb\n) RETURNS jsonb\nLANGUAGE plpgsql SECURITY DEFINER SET search_path = '' AS $$\nDECLARE\n v_job public.plg_fiscalbr_nfce_jobs%ROWTYPE;\n v_receipt_id bigint;\n v_receipt public.plg_fiscalbr_webhook_receipts%ROWTYPE;\nBEGIN\n INSERT INTO public.plg_fiscalbr_webhook_receipts\n (provider_event_id, payload_sha256)\n VALUES (p_provider_event_id, p_payload_sha256)\n ON CONFLICT (provider, provider_event_id) DO NOTHING\n RETURNING id INTO v_receipt_id;\n SELECT * INTO v_receipt FROM public.plg_fiscalbr_webhook_receipts\n WHERE provider = 'plugnotas' AND provider_event_id = p_provider_event_id FOR UPDATE;\n v_receipt_id := v_receipt.id;\n IF v_receipt.payload_sha256 IS DISTINCT FROM p_payload_sha256 THEN\n RAISE EXCEPTION 'plugnotas_apply_nfce_webhook: conflicting event payload' USING ERRCODE = '22023';\n END IF;\n IF v_receipt.processed_at IS NOT NULL AND v_receipt.processing_error IS NULL THEN\n RETURN jsonb_build_object('duplicate', true, 'matched', true);\n END IF;\n\n SELECT * INTO v_job FROM public.plg_fiscalbr_nfce_jobs\n WHERE provider_document_id = p_provider_document_id\n ORDER BY created_at DESC LIMIT 1;\n IF NOT FOUND THEN\n UPDATE public.plg_fiscalbr_webhook_receipts\n SET processed_at = NULL, processing_error = 'job_not_found'\n WHERE id = v_receipt_id;\n RETURN jsonb_build_object('duplicate', false, 'matched', false);\n END IF;\n\n UPDATE public.plg_fiscalbr_webhook_receipts\n SET tenant_id = v_job.tenant_id, nota_id = v_job.nota_id, processed_at = now(), processing_error = NULL\n WHERE id = v_receipt_id;\n PERFORM public.plugnotas_apply_nfce_result(\n v_job.tenant_id, v_job.id, p_status, p_provider_document_id,\n coalesce(p_integration_id, v_job.integration_id), p_number, p_series,\n p_access_key, p_message, p_payload\n );\n RETURN jsonb_build_object('duplicate', false, 'matched', true, 'jobId', v_job.id);\nEND $$;\n\nCREATE OR REPLACE FUNCTION public.plugnotas_claim_nfce_recovery(\n p_limit integer DEFAULT 20\n) RETURNS SETOF public.plg_fiscalbr_nfce_jobs\nLANGUAGE plpgsql SECURITY DEFINER SET search_path = '' AS $$\nBEGIN\n RETURN QUERY\n WITH candidates AS (\n SELECT j.id FROM public.plg_fiscalbr_nfce_jobs j\n WHERE j.status IN ('queued', 'waiting', 'running')\n AND coalesce(j.next_attempt_at, j.created_at) <= now()\n AND coalesce(j.locked_until, '-infinity'::timestamptz) < now()\n ORDER BY coalesce(j.next_attempt_at, j.created_at), j.created_at\n FOR UPDATE SKIP LOCKED\n LIMIT greatest(1, least(coalesce(p_limit, 20), 100))\n )\n UPDATE public.plg_fiscalbr_nfce_jobs j\n SET status = 'running', locked_until = now() + interval '2 minutes', updated_at = now()\n FROM candidates c WHERE j.id = c.id\n RETURNING j.*;\nEND $$;\n`\n\nexport const MIGRATION_002_A_NOTA_QUE_CHEGA_TEM_ONDE_ESPERAR = `-- A nota que chega tem onde esperar.\n--\n-- O V1 recebe NF-e de fornecedor pela SEFAZ e guarda cada uma numa caixa de\n-- entrada antes de alguém decidir o que fazer com ela: manifestar, importar\n-- para o estoque, ou nada. Essa caixa não tinha destino no V2.\n--\n-- dfe_inbox 703 a nota recebida, com o XML inteiro\n-- dfe_inbox_items 3.215 o item da nota, com todo o tributo\n-- dfe_inbox_payments 739 a forma de pagamento declarada\n-- dfe_inbox_duplicatas 830 o vencimento da duplicata\n-- fiscal_groups 11 o grupo fiscal (CFOP, CST, alíquotas)\n--\n-- Os números somam os dois tenants: Espaço Facial e Pertinho do Céu. É o único\n-- domínio fiscal que os dois usam.\n--\n-- ── O XML é arquivo, e arquivo mora em public.documents ─────────────────────\n--\n-- \\`dfe_inbox.xml_content\\` é o XML inteiro numa coluna de texto, e\n-- \\`pdf_storage_path\\` aponta para o DANFE no storage do V1. Os dois são ARQUIVO,\n-- e o V2 já tem o arquétipo: \\`public.documents\\`, com kind, checksum, storage_*\n-- e a RLS de arquivo que uma coluna de texto nunca teria.\n--\n-- Então a extensão guarda um PONTEIRO — \\`document_id\\` — e os bytes vão para o\n-- arquétipo, pelo estágio de assets (\\`make mig-assets\\`), que é quem alcança\n-- object storage. Enquanto os bytes não chegam, \\`document_id\\` fica nulo e o\n-- caminho de origem fica em metadata: a linha é verdadeira nos dois momentos.\n--\n-- ── O tributo vem inteiro, e de propósito ───────────────────────────────────\n--\n-- 30 colunas de imposto por item (ICMS, ICMS-ST, PIS, COFINS, IPI, com base,\n-- alíquota e valor de cada). É muita coluna, e a tentação é jogar tudo num\n-- jsonb. Não: isto é documento fiscal recebido, e a pergunta que se faz dele é\n-- \"quanto de ICMS entrou neste mês\" — uma soma, sobre coluna, com índice. Um\n-- jsonb transformaria toda apuração numa varredura.\n--\n-- O que VAI para jsonb é o \\`raw_xml_item\\`, que já é jsonb na origem e existe\n-- para conferência, não para conta.\n--\n-- Idempotente.\n\nCREATE TABLE IF NOT EXISTS public.plg_fiscalbr_inbox_documents (\n id uuid DEFAULT gen_random_uuid() NOT NULL,\n tenant_id uuid NOT NULL,\n unit_id uuid,\n -- O ponteiro para o arquétipo. Nulo enquanto os bytes não subiram — o\n -- caminho de origem fica em metadata até lá.\n document_id uuid,\n access_key text,\n nfe_number text,\n nfe_series text,\n nfe_model text,\n issue_date timestamp with time zone,\n received_at timestamp with time zone,\n status text DEFAULT 'received'::text NOT NULL,\n -- A manifestação do destinatário é outro eixo que o status: uma nota pode\n -- estar recebida e não manifestada, e é a manifestação que a SEFAZ cobra.\n manifestation_status text,\n manifested_at timestamp with time zone,\n manifestation_attempts integer DEFAULT 0 NOT NULL,\n manifestation_response jsonb,\n supplier_party_id uuid,\n supplier_cnpj text,\n supplier_name text,\n supplier_ie text,\n total_value numeric(14,2),\n total_products numeric(14,2),\n total_discount numeric(14,2),\n total_freight numeric(14,2),\n total_insurance numeric(14,2),\n total_other numeric(14,2),\n total_icms_base numeric(14,2),\n total_icms_value numeric(14,2),\n total_icms_st_base numeric(14,2),\n total_icms_st_value numeric(14,2),\n total_pis_value numeric(14,2),\n total_cofins_value numeric(14,2),\n total_ipi_value numeric(14,2),\n nfe_protocol text,\n nfe_authorized_at timestamp with time zone,\n carrier_name text,\n carrier_cnpj text,\n freight_mode_code text,\n additional_info text,\n reviewed_at timestamp with time zone,\n -- O que essa nota virou no estoque, quando virou. Nulo é nota recebida e\n -- não importada, que é a maioria.\n inventory_operation_id uuid,\n owner_id uuid,\n metadata jsonb DEFAULT '{}'::jsonb NOT NULL,\n created_at timestamp with time zone DEFAULT now() NOT NULL,\n updated_at timestamp with time zone DEFAULT now() NOT NULL\n);\n\nCOMMENT ON TABLE public.plg_fiscalbr_inbox_documents IS\n 'A NF-e de fornecedor recebida da SEFAZ, esperando manifestação ou importação. V1: dfe_inbox.';\nCOMMENT ON COLUMN public.plg_fiscalbr_inbox_documents.document_id IS\n 'V1: xml_content + pdf_storage_path. O XML e o DANFE são arquivo; os bytes moram em public.documents e aqui fica o ponteiro.';\n\nCREATE TABLE IF NOT EXISTS public.plg_fiscalbr_inbox_items (\n id uuid DEFAULT gen_random_uuid() NOT NULL,\n tenant_id uuid NOT NULL,\n inbox_document_id uuid NOT NULL,\n item_number integer,\n supplier_product_code text,\n ean_barcode text,\n ean_taxable text,\n description text,\n ncm text,\n cest text,\n cfop text,\n unit_commercial text,\n unit_taxable text,\n quantity_commercial numeric(18,4),\n quantity_taxable numeric(18,4),\n unit_price_commercial numeric(18,6),\n unit_price_taxable numeric(18,6),\n total_price numeric(14,2),\n discount numeric(14,2),\n freight numeric(14,2),\n insurance numeric(14,2),\n other_expenses numeric(14,2),\n icms_cst text, icms_csosn text, icms_origin text,\n icms_base numeric(14,2), icms_rate numeric(9,4), icms_value numeric(14,2),\n icms_st_base numeric(14,2), icms_st_value numeric(14,2),\n pis_cst text, pis_base numeric(14,2), pis_rate numeric(9,4), pis_value numeric(14,2),\n cofins_cst text, cofins_base numeric(14,2), cofins_rate numeric(9,4), cofins_value numeric(14,2),\n ipi_cst text, ipi_rate numeric(9,4), ipi_value numeric(14,2),\n -- O produto do V2 que esta linha da nota representa. \\`reconciled\\` é a\n -- decisão de quem conferiu; o id é o resultado dela.\n product_id uuid,\n reconciled boolean DEFAULT false NOT NULL,\n raw_xml_item jsonb,\n metadata jsonb DEFAULT '{}'::jsonb NOT NULL,\n created_at timestamp with time zone DEFAULT now() NOT NULL,\n updated_at timestamp with time zone DEFAULT now() NOT NULL\n);\n\nCOMMENT ON TABLE public.plg_fiscalbr_inbox_items IS\n 'O item da NF-e recebida, com o tributo em coluna — a apuração soma coluna, não varre jsonb. V1: dfe_inbox_items.';\n\nCREATE TABLE IF NOT EXISTS public.plg_fiscalbr_inbox_payments (\n id uuid DEFAULT gen_random_uuid() NOT NULL,\n tenant_id uuid NOT NULL,\n inbox_document_id uuid NOT NULL,\n payment_index integer,\n payment_indicator text,\n payment_type_code text,\n payment_type_label text,\n value numeric(14,2),\n change_given numeric(14,2),\n card_brand text,\n card_acquirer_cnpj text,\n card_auth_code text,\n metadata jsonb DEFAULT '{}'::jsonb NOT NULL,\n created_at timestamp with time zone DEFAULT now() NOT NULL,\n updated_at timestamp with time zone DEFAULT now() NOT NULL\n);\n\nCREATE TABLE IF NOT EXISTS public.plg_fiscalbr_inbox_duplicates (\n id uuid DEFAULT gen_random_uuid() NOT NULL,\n tenant_id uuid NOT NULL,\n inbox_document_id uuid NOT NULL,\n duplicate_index integer,\n number text,\n due_date date,\n value numeric(14,2),\n -- O que essa duplicata virou no contas a pagar, se alguém a lançou.\n installment_id uuid,\n metadata jsonb DEFAULT '{}'::jsonb NOT NULL,\n created_at timestamp with time zone DEFAULT now() NOT NULL,\n updated_at timestamp with time zone DEFAULT now() NOT NULL\n);\n\nCOMMENT ON TABLE public.plg_fiscalbr_inbox_duplicates IS\n 'O vencimento declarado na NF-e recebida. Vira parcela do contas a pagar quando alguém lança a nota. V1: dfe_inbox_duplicatas.';\n\nCREATE TABLE IF NOT EXISTS public.plg_fiscalbr_tax_groups (\n id uuid DEFAULT gen_random_uuid() NOT NULL,\n tenant_id uuid NOT NULL,\n name text NOT NULL,\n cfop text,\n csosn text,\n cst text,\n icms_origin text,\n icms_rate numeric(9,4),\n icms_bc_mode text,\n pis_situation text,\n pis_rate numeric(9,4),\n cofins_situation text,\n cofins_rate numeric(9,4),\n -- A reforma tributária já está no cadastro do V1 e vem junto: deixar de\n -- fora obrigaria a recadastrar tudo no dia em que IBS/CBS valerem.\n ibs_cbs_situation text,\n ibs_cbs_classification text,\n base_calculation_mode text,\n benefit_code text,\n cest text,\n poverty_fund_rate numeric(9,4),\n is_active boolean DEFAULT true NOT NULL,\n metadata jsonb DEFAULT '{}'::jsonb NOT NULL,\n created_at timestamp with time zone DEFAULT now() NOT NULL,\n updated_at timestamp with time zone DEFAULT now() NOT NULL\n);\n\nCOMMENT ON TABLE public.plg_fiscalbr_tax_groups IS\n 'O grupo fiscal: CFOP, CST/CSOSN e alíquotas que o produto herda na emissão. V1: fiscal_groups. Traz IBS/CBS porque o cadastro do V1 já os tem.';\n\n-- ═══════════════════════════════════════════════════════════════════════════\n-- As chaves, os índices e a cerca\n-- ═══════════════════════════════════════════════════════════════════════════\n\nDO $$\nDECLARE t text;\nBEGIN\n FOREACH t IN ARRAY ARRAY['plg_fiscalbr_inbox_documents', 'plg_fiscalbr_inbox_items',\n 'plg_fiscalbr_inbox_payments', 'plg_fiscalbr_inbox_duplicates',\n 'plg_fiscalbr_tax_groups'] LOOP\n IF NOT EXISTS (SELECT 1 FROM pg_constraint WHERE conrelid = ('public.' || t)::regclass AND contype = 'p') THEN\n EXECUTE format('ALTER TABLE ONLY public.%I ADD CONSTRAINT %I PRIMARY KEY (id)', t, t || '_pkey');\n END IF;\n IF NOT EXISTS (SELECT 1 FROM pg_constraint WHERE conname = t || '_tenant_id_id_key') THEN\n EXECUTE format('ALTER TABLE ONLY public.%I ADD CONSTRAINT %I UNIQUE (tenant_id, id)', t, t || '_tenant_id_id_key');\n END IF;\n IF NOT EXISTS (SELECT 1 FROM pg_constraint WHERE conname = t || '_tenant_fkey') THEN\n EXECUTE format('ALTER TABLE ONLY public.%I ADD CONSTRAINT %I FOREIGN KEY (tenant_id) REFERENCES public.tenants(id) ON DELETE CASCADE', t, t || '_tenant_fkey');\n END IF;\n END LOOP;\nEND $$;\n\nDO $$ BEGIN\n IF NOT EXISTS (SELECT 1 FROM pg_constraint WHERE conname = 'plg_fiscalbr_inbox_documents_unit_fkey') THEN\n ALTER TABLE ONLY public.plg_fiscalbr_inbox_documents ADD CONSTRAINT plg_fiscalbr_inbox_documents_unit_fkey\n FOREIGN KEY (tenant_id, unit_id) REFERENCES app.units(tenant_id, id) ON DELETE SET NULL;\n END IF;\n -- O arquivo pode ser apagado sem levar a nota junto: a nota fiscal existe\n -- mesmo sem o XML à mão, e é ela que a SEFAZ conhece.\n IF NOT EXISTS (SELECT 1 FROM pg_constraint WHERE conname = 'plg_fiscalbr_inbox_documents_document_fkey') THEN\n ALTER TABLE ONLY public.plg_fiscalbr_inbox_documents ADD CONSTRAINT plg_fiscalbr_inbox_documents_document_fkey\n FOREIGN KEY (document_id) REFERENCES public.documents(id) ON DELETE SET NULL;\n END IF;\n IF NOT EXISTS (SELECT 1 FROM pg_constraint WHERE conname = 'plg_fiscalbr_inbox_documents_supplier_fkey') THEN\n ALTER TABLE ONLY public.plg_fiscalbr_inbox_documents ADD CONSTRAINT plg_fiscalbr_inbox_documents_supplier_fkey\n FOREIGN KEY (supplier_party_id) REFERENCES public.people(id) ON DELETE SET NULL;\n END IF;\n IF NOT EXISTS (SELECT 1 FROM pg_constraint WHERE conname = 'plg_fiscalbr_inbox_documents_operation_fkey') THEN\n ALTER TABLE ONLY public.plg_fiscalbr_inbox_documents ADD CONSTRAINT plg_fiscalbr_inbox_documents_operation_fkey\n FOREIGN KEY (inventory_operation_id) REFERENCES public.plg_inventory_operations(id) ON DELETE SET NULL;\n END IF;\n\n -- Item, pagamento e duplicata não existem fora da nota: CASCADE.\n IF NOT EXISTS (SELECT 1 FROM pg_constraint WHERE conname = 'plg_fiscalbr_inbox_items_document_fkey') THEN\n ALTER TABLE ONLY public.plg_fiscalbr_inbox_items ADD CONSTRAINT plg_fiscalbr_inbox_items_document_fkey\n FOREIGN KEY (tenant_id, inbox_document_id) REFERENCES public.plg_fiscalbr_inbox_documents(tenant_id, id) ON DELETE CASCADE;\n END IF;\n IF NOT EXISTS (SELECT 1 FROM pg_constraint WHERE conname = 'plg_fiscalbr_inbox_items_product_fkey') THEN\n ALTER TABLE ONLY public.plg_fiscalbr_inbox_items ADD CONSTRAINT plg_fiscalbr_inbox_items_product_fkey\n FOREIGN KEY (product_id) REFERENCES public.products(id) ON DELETE SET NULL;\n END IF;\n IF NOT EXISTS (SELECT 1 FROM pg_constraint WHERE conname = 'plg_fiscalbr_inbox_payments_document_fkey') THEN\n ALTER TABLE ONLY public.plg_fiscalbr_inbox_payments ADD CONSTRAINT plg_fiscalbr_inbox_payments_document_fkey\n FOREIGN KEY (tenant_id, inbox_document_id) REFERENCES public.plg_fiscalbr_inbox_documents(tenant_id, id) ON DELETE CASCADE;\n END IF;\n IF NOT EXISTS (SELECT 1 FROM pg_constraint WHERE conname = 'plg_fiscalbr_inbox_duplicates_document_fkey') THEN\n ALTER TABLE ONLY public.plg_fiscalbr_inbox_duplicates ADD CONSTRAINT plg_fiscalbr_inbox_duplicates_document_fkey\n FOREIGN KEY (tenant_id, inbox_document_id) REFERENCES public.plg_fiscalbr_inbox_documents(tenant_id, id) ON DELETE CASCADE;\n END IF;\n IF NOT EXISTS (SELECT 1 FROM pg_constraint WHERE conname = 'plg_fiscalbr_inbox_duplicates_installment_fkey') THEN\n ALTER TABLE ONLY public.plg_fiscalbr_inbox_duplicates ADD CONSTRAINT plg_fiscalbr_inbox_duplicates_installment_fkey\n FOREIGN KEY (installment_id) REFERENCES public.plg_financial_invoice_installments(id) ON DELETE SET NULL;\n END IF;\nEND $$;\n\n-- A chave de acesso é única por casa: a mesma nota chegando duas vezes da SEFAZ\n-- é a mesma nota, e é isso que impede a importação em dobro.\nCREATE UNIQUE INDEX IF NOT EXISTS plg_fiscalbr_inbox_documents_access_key_uidx\n ON public.plg_fiscalbr_inbox_documents USING btree (tenant_id, access_key)\n WHERE access_key IS NOT NULL;\nCREATE INDEX IF NOT EXISTS plg_fiscalbr_inbox_documents_pending_idx\n ON public.plg_fiscalbr_inbox_documents USING btree (tenant_id, unit_id, issue_date DESC)\n WHERE inventory_operation_id IS NULL;\nCREATE INDEX IF NOT EXISTS plg_fiscalbr_inbox_items_document_idx\n ON public.plg_fiscalbr_inbox_items USING btree (tenant_id, inbox_document_id);\nCREATE INDEX IF NOT EXISTS plg_fiscalbr_inbox_items_product_idx\n ON public.plg_fiscalbr_inbox_items USING btree (tenant_id, product_id) WHERE product_id IS NOT NULL;\nCREATE INDEX IF NOT EXISTS plg_fiscalbr_inbox_payments_document_idx\n ON public.plg_fiscalbr_inbox_payments USING btree (tenant_id, inbox_document_id);\nCREATE INDEX IF NOT EXISTS plg_fiscalbr_inbox_duplicates_document_idx\n ON public.plg_fiscalbr_inbox_duplicates USING btree (tenant_id, inbox_document_id);\nCREATE UNIQUE INDEX IF NOT EXISTS plg_fiscalbr_tax_groups_name_uidx\n ON public.plg_fiscalbr_tax_groups USING btree (tenant_id, lower(name));\n\nDO $$\nDECLARE t text;\nBEGIN\n FOREACH t IN ARRAY ARRAY['plg_fiscalbr_inbox_documents', 'plg_fiscalbr_inbox_items',\n 'plg_fiscalbr_inbox_payments', 'plg_fiscalbr_inbox_duplicates',\n 'plg_fiscalbr_tax_groups'] LOOP\n EXECUTE format('DROP TRIGGER IF EXISTS %I ON public.%I', t || '_updated_at', t);\n EXECUTE format('CREATE TRIGGER %I BEFORE UPDATE ON public.%I FOR EACH ROW EXECUTE FUNCTION public.handle_updated_at()', t || '_updated_at', t);\n END LOOP;\nEND $$;\n\n-- ═══════════════════════════════════════════════════════════════════════════\n-- Quem enxerga\n-- ═══════════════════════════════════════════════════════════════════════════\n--\n-- Nota fiscal é do FINANCEIRO, e a permissão é a que ele já usa. A caixa de\n-- entrada não tem unidade nos filhos — eles herdam a da nota —, então o escopo\n-- de unidade vale só no documento; o resto é do tenant, e é a CASCADE da FK que\n-- garante que ninguém veja filho de nota alheia.\n-- RLS, uma a uma e por extenso. O laço adiante já liga tudo isto — mas ele liga\n-- por \\`EXECUTE format(...)\\`, e o portão que revisa plugin lê o ARQUIVO, não o\n-- banco: para ele, tabela com tenant_id e sem um ENABLE literal é tabela\n-- aberta. Uma garantia que o revisor não consegue ler é uma garantia que\n-- ninguém confere.\nALTER TABLE public.plg_fiscalbr_inbox_documents ENABLE ROW LEVEL SECURITY;\nALTER TABLE public.plg_fiscalbr_inbox_items ENABLE ROW LEVEL SECURITY;\nALTER TABLE public.plg_fiscalbr_tax_groups ENABLE ROW LEVEL SECURITY;\nALTER TABLE public.plg_fiscalbr_inbox_payments ENABLE ROW LEVEL SECURITY;\nALTER TABLE public.plg_fiscalbr_inbox_duplicates ENABLE ROW LEVEL SECURITY;\n\nDO $$\nDECLARE\n t text;\n doc boolean;\nBEGIN\n FOREACH t IN ARRAY ARRAY['plg_fiscalbr_inbox_documents', 'plg_fiscalbr_inbox_items',\n 'plg_fiscalbr_inbox_payments', 'plg_fiscalbr_inbox_duplicates',\n 'plg_fiscalbr_tax_groups'] LOOP\n doc := t = 'plg_fiscalbr_inbox_documents';\n EXECUTE format('ALTER TABLE public.%I FORCE ROW LEVEL SECURITY', t);\n EXECUTE format('ALTER TABLE public.%I ENABLE ROW LEVEL SECURITY', t);\n EXECUTE format('GRANT SELECT, INSERT, UPDATE, DELETE ON TABLE public.%I TO authenticated', t);\n EXECUTE format('GRANT ALL ON TABLE public.%I TO service_role', t);\n\n EXECUTE format('DROP POLICY IF EXISTS %I ON public.%I', t || '_authz_select', t);\n EXECUTE format($f$CREATE POLICY %I ON public.%I FOR SELECT TO authenticated USING (\n tenant_id = (SELECT app.current_tenant_id())\n AND (%s)\n AND (SELECT app.has_permission('financial.read', %s)))$f$,\n t || '_authz_select', t,\n CASE WHEN doc THEN format('unit_id IS NULL OR (SELECT app.has_unit(%I.unit_id))', t) ELSE 'true' END,\n CASE WHEN doc THEN format('%I.unit_id', t) ELSE 'NULL::uuid' END);\n\n EXECUTE format('DROP POLICY IF EXISTS %I ON public.%I', t || '_authz_write', t);\n EXECUTE format($f$CREATE POLICY %I ON public.%I FOR ALL TO authenticated USING (\n tenant_id = (SELECT app.current_tenant_id())\n AND (SELECT app.has_permission('financial.edit', %s)))\n WITH CHECK (\n tenant_id = (SELECT app.current_tenant_id())\n AND (SELECT app.has_permission('financial.edit', %s)))$f$,\n t || '_authz_write', t,\n CASE WHEN doc THEN format('%I.unit_id', t) ELSE 'NULL::uuid' END,\n CASE WHEN doc THEN format('%I.unit_id', t) ELSE 'NULL::uuid' END);\n END LOOP;\nEND $$;\n\nINSERT INTO public.scoped_resources (resource_table, unit_column, owner_column, shareable, default_visibility, label)\nVALUES ('plg_fiscalbr_inbox_documents', 'unit_id', 'owner_id', false, 'unit', 'Notas recebidas')\nON CONFLICT (resource_table) DO UPDATE\n SET unit_column = EXCLUDED.unit_column, default_visibility = EXCLUDED.default_visibility, label = EXCLUDED.label;\n`\n\nexport const MIGRATION_003_A_CAIXA_DE_ENTRADA_ATRAVESSA = `-- A caixa de entrada atravessa.\n--\n-- A 002 fez as cinco tabelas. Esta escreve o de-para. As cinco usam o writer\n-- genérico \\`by_shape\\` (spine 123): as colunas do destino foram desenhadas\n-- olhando a origem, então o que falta é dizer qual nome vira qual — não uma\n-- função de upsert por tabela.\n--\n-- ── O arquivo não vem por aqui ──────────────────────────────────────────────\n--\n-- \\`xml_content\\` guarda o XML inteiro e \\`pdf_storage_path\\` aponta para o DANFE.\n-- Os dois saem do de-para com \\`-\\`: arquivo atravessa pelo estágio de assets,\n-- que é quem alcança object storage, e vira linha de \\`public.documents\\`. O\n-- caminho de origem fica em metadata para o estágio achar.\n--\n-- ── O fornecedor fica para a costura ────────────────────────────────────────\n--\n-- \\`supplier_party_id\\` não é preenchido aqui. A origem guarda CNPJ e nome, não\n-- um id de contato, e casar CNPJ com \\`public.people\\` é uma decisão de\n-- identidade — a mesma que já pôs 438 clientes em quarentena. Fica nulo, com o\n-- CNPJ na coluna ao lado, e uma passada de costura resolve quando alguém\n-- decidir a regra.\n--\n-- Idempotente.\n\nSET LOCAL migration.allowlist_unlock = 'on';\n\nSELECT migration._allow(\n 'fiscal_groups', 'public.plg_fiscalbr_tax_groups', 'fiscal_br', 'by_shape',\n jsonb_build_object('target', 'public.plg_fiscalbr_tax_groups'),\n NULL::text[],\n jsonb_build_object(\n 'uuid', 'id',\n 'name', 'name',\n 'cfop', 'cfop',\n 'csosn', 'csosn',\n 'cst', 'cst',\n 'icms_origin', 'icms_origin',\n 'icms_rate', 'icms_rate',\n 'icms_modalidade_bc', 'icms_bc_mode',\n 'pis_situation', 'pis_situation',\n 'pis_rate', 'pis_rate',\n 'cofins_situation', 'cofins_situation',\n 'cofins_rate', 'cofins_rate',\n 'ibs_cbs_situation', 'ibs_cbs_situation',\n 'ibs_cbs_classification', 'ibs_cbs_classification',\n 'base_calculation_mode', 'base_calculation_mode',\n 'cbenef', 'benefit_code',\n 'cest', 'cest',\n 'poverty_fund_rate', 'poverty_fund_rate',\n 'active', 'is_active',\n 'created_at', 'created_at',\n 'updated_at', 'updated_at',\n 'tenant_id', '-',\n 'id', 'jsonb:fiscalbr.v1_ref'),\n 'O grupo fiscal: CFOP, CST/CSOSN e alíquotas que o produto herda na emissão. '\n 'IBS/CBS vêm junto porque o cadastro do V1 já os tem — deixar de fora '\n 'obrigaria a recadastrar tudo no dia em que a reforma valer.'\n);\n\nSELECT migration._allow(\n 'dfe_inbox', 'public.plg_fiscalbr_inbox_documents', 'fiscal_br', 'by_shape',\n jsonb_build_object('target', 'public.plg_fiscalbr_inbox_documents'),\n NULL::text[],\n jsonb_build_object(\n 'uuid', 'id',\n 'company_id', 'unit_id@companies',\n 'access_key', 'access_key',\n 'nfe_number', 'nfe_number',\n 'nfe_series', 'nfe_series',\n 'nfe_model', 'nfe_model',\n 'issue_date', 'issue_date',\n 'received_at', 'received_at',\n 'status', 'status',\n 'manifestation_status', 'manifestation_status',\n 'manifested_at', 'manifested_at',\n 'manifestation_attempts', 'manifestation_attempts',\n 'manifestation_response', 'manifestation_response',\n 'supplier_cnpj', 'supplier_cnpj',\n 'supplier_name', 'supplier_name',\n 'supplier_ie', 'supplier_ie',\n 'total_value', 'total_value',\n 'total_products', 'total_products',\n 'total_discount', 'total_discount',\n 'total_freight', 'total_freight',\n 'total_insurance', 'total_insurance',\n 'total_other', 'total_other',\n 'total_icms_base', 'total_icms_base',\n 'total_icms_value', 'total_icms_value',\n 'total_icms_st_base', 'total_icms_st_base',\n 'total_icms_st_value', 'total_icms_st_value',\n 'total_pis_value', 'total_pis_value',\n 'total_cofins_value', 'total_cofins_value',\n 'total_ipi_value', 'total_ipi_value',\n 'nfe_protocol', 'nfe_protocol') ||\n -- \\`jsonb_build_object\\` aceita no máximo 100 argumentos, e este de-para tem\n -- 59 pares. Dois objetos concatenados, e não uma tabela a menos.\n jsonb_build_object(\n 'nfe_authorized_at', 'nfe_authorized_at',\n 'carrier_name', 'carrier_name',\n 'carrier_cnpj', 'carrier_cnpj',\n 'freight_mode_code', 'freight_mode_code',\n 'additional_info', 'additional_info',\n 'reviewed_at', 'reviewed_at',\n 'created_at', 'created_at',\n 'updated_at', 'updated_at',\n 'tenant_id', '-',\n 'reviewed_by', '-',\n 'xml_content', '-',\n 'pdf_storage_path', 'jsonb:fiscalbr.v1_pdf_path',\n 'pdf_fetched_at', 'jsonb:fiscalbr.v1_pdf_fetched_at',\n 'danfe_import_id', 'jsonb:fiscalbr.v1_danfe_import_ref',\n 'plugnotas_payload', 'jsonb:fiscalbr.v1_plugnotas_payload',\n 'plugnotas_received_at', 'jsonb:fiscalbr.v1_plugnotas_received_at',\n 'nfe_cnf', 'jsonb:fiscalbr.nfe.cnf',\n 'nfe_natureza_operacao', 'jsonb:fiscalbr.nfe.natureza_operacao',\n 'nfe_finalidade', 'jsonb:fiscalbr.nfe.finalidade',\n 'nfe_tipo_operacao', 'jsonb:fiscalbr.nfe.tipo_operacao',\n 'nfe_destination', 'jsonb:fiscalbr.nfe.destination',\n 'supplier_address', 'jsonb:fiscalbr.supplier.street',\n 'supplier_number', 'jsonb:fiscalbr.supplier.number',\n 'supplier_complement', 'jsonb:fiscalbr.supplier.complement',\n 'supplier_neighborhood', 'jsonb:fiscalbr.supplier.neighborhood',\n 'supplier_city', 'jsonb:fiscalbr.supplier.city',\n 'supplier_state', 'jsonb:fiscalbr.supplier.state',\n 'supplier_postal_code', 'jsonb:fiscalbr.supplier.postal_code',\n 'supplier_phone', 'jsonb:fiscalbr.supplier.phone',\n 'supplier_email', 'jsonb:fiscalbr.supplier.email',\n 'id', 'jsonb:fiscalbr.v1_ref'),\n 'A NF-e de fornecedor recebida da SEFAZ. \\`xml_content\\` sai com \\`-\\`: arquivo '\n 'atravessa pelo estágio de assets e vira public.documents, e o caminho do '\n 'DANFE fica em metadata para o estágio achar. O endereço do fornecedor vai '\n 'para metadata porque o fornecedor é \\`public.people\\`, e duplicá-lo em coluna '\n 'daria duas respostas para \"onde ele fica\".'\n);\n\nSELECT migration._allow(\n 'dfe_inbox_items', 'public.plg_fiscalbr_inbox_items', 'fiscal_br', 'by_shape',\n jsonb_build_object('target', 'public.plg_fiscalbr_inbox_items'),\n NULL::text[],\n jsonb_build_object(\n 'uuid', 'id',\n 'dfe_inbox_id', 'inbox_document_id@dfe_inbox',\n 'item_number', 'item_number',\n 'product_code_supplier', 'supplier_product_code',\n 'ean_barcode', 'ean_barcode',\n 'ean_trib', 'ean_taxable',\n 'description', 'description',\n 'ncm', 'ncm',\n 'cest', 'cest',\n 'cfop', 'cfop',\n 'unit_commercial', 'unit_commercial',\n 'unit_taxable', 'unit_taxable',\n 'quantity_commercial', 'quantity_commercial',\n 'quantity_taxable', 'quantity_taxable',\n 'unit_price_commercial', 'unit_price_commercial',\n 'unit_price_taxable', 'unit_price_taxable',\n 'total_price', 'total_price',\n 'discount', 'discount',\n 'freight', 'freight',\n 'insurance', 'insurance',\n 'other_expenses', 'other_expenses',\n 'icms_cst', 'icms_cst',\n 'icms_csosn', 'icms_csosn',\n 'icms_origin', 'icms_origin',\n 'icms_base', 'icms_base',\n 'icms_rate', 'icms_rate',\n 'icms_value', 'icms_value',\n 'icms_st_base', 'icms_st_base',\n 'icms_st_value', 'icms_st_value',\n 'pis_cst', 'pis_cst',\n 'pis_base', 'pis_base',\n 'pis_rate', 'pis_rate',\n 'pis_value', 'pis_value',\n 'cofins_cst', 'cofins_cst',\n 'cofins_base', 'cofins_base',\n 'cofins_rate', 'cofins_rate',\n 'cofins_value', 'cofins_value',\n 'ipi_cst', 'ipi_cst',\n 'ipi_rate', 'ipi_rate',\n 'ipi_value', 'ipi_value',\n 'linked_product_id', 'product_id@products',\n 'reconciled', 'reconciled',\n 'raw_xml_item', 'raw_xml_item',\n 'created_at', 'created_at',\n 'updated_at', 'updated_at',\n 'tenant_id', '-',\n 'id', 'jsonb:fiscalbr.v1_ref'),\n 'O item da nota, com o tributo em COLUNA — a apuração soma coluna, não varre '\n 'jsonb. \\`raw_xml_item\\` é a exceção: já é jsonb na origem e serve para '\n 'conferência, não para conta.'\n);\n\nSELECT migration._allow(\n 'dfe_inbox_payments', 'public.plg_fiscalbr_inbox_payments', 'fiscal_br', 'by_shape',\n jsonb_build_object('target', 'public.plg_fiscalbr_inbox_payments'),\n NULL::text[],\n jsonb_build_object(\n 'uuid', 'id',\n 'dfe_inbox_id', 'inbox_document_id@dfe_inbox',\n 'payment_index', 'payment_index',\n 'indicador_pagamento', 'payment_indicator',\n 'tipo_pagamento_code', 'payment_type_code',\n 'tipo_pagamento_label', 'payment_type_label',\n 'value', 'value',\n 'troco', 'change_given',\n 'card_flag', 'card_brand',\n 'card_acquirer_cnpj', 'card_acquirer_cnpj',\n 'card_auth_code', 'card_auth_code',\n 'created_at', 'created_at',\n 'updated_at', 'updated_at',\n 'tenant_id', '-',\n 'id', 'jsonb:fiscalbr.v1_ref'),\n 'A forma de pagamento declarada na nota recebida.'\n);\n\nSELECT migration._allow(\n 'dfe_inbox_duplicatas', 'public.plg_fiscalbr_inbox_duplicates', 'fiscal_br', 'by_shape',\n jsonb_build_object('target', 'public.plg_fiscalbr_inbox_duplicates'),\n NULL::text[],\n jsonb_build_object(\n 'uuid', 'id',\n 'dfe_inbox_id', 'inbox_document_id@dfe_inbox',\n 'duplicata_index', 'duplicate_index',\n 'numero', 'number',\n 'vencimento', 'due_date',\n 'valor', 'value',\n 'created_at', 'created_at',\n 'updated_at', 'updated_at',\n 'tenant_id', '-',\n 'id', 'jsonb:fiscalbr.v1_ref'),\n 'O vencimento declarado na nota. Vira parcela do contas a pagar quando alguém '\n 'lança a nota — \\`installment_id\\` fica nulo até lá, porque a parcela é '\n 'decisão de quem lança, não da SEFAZ.'\n);\n\nDELETE FROM migration.excluded_tables\n WHERE source_table IN ('dfe_inbox', 'dfe_inbox_items', 'dfe_inbox_payments',\n 'dfe_inbox_duplicatas', 'fiscal_groups');\n\nRESET migration.allowlist_unlock;\n`\n\nexport const MIGRATION_004_O_GRUPO_FISCAL_DO_V1_REPETE_NOME = `-- O grupo fiscal do V1 repete nome.\n--\n-- A 002 criou índice ÚNICO em \\`(tenant_id, lower(name))\\`. O V1 tem, no tenant\n-- 8, \"Bebidas Alcoólicas\", \"Bebidas Não Alcoólicas\" e \"Vinhos e Espumantes\"\n-- DUAS vezes cada — grupos fiscais diferentes com o mesmo rótulo, e o V1\n-- permite. Três das onze linhas foram para a quarentena por uma regra que eu\n-- inventei, não que a origem tenha.\n--\n-- O índice vira comum: serve para buscar, não para proibir. Quem quiser proibir\n-- decide isso na tela, com o dado já dentro — e sabendo quais três são.\n--\n-- Esta correção nasceu dentro de \\`financial/083\\` e mudou de arquivo: o\n-- financeiro roda ANTES do fiscal_br na corrente, e um arquivo do financeiro\n-- que mexe numa tabela do fiscal_br falha do zero com\n-- \\`relation \"public.plg_fiscalbr_tax_groups\" does not exist\\`. Cada plugin mexe\n-- na tabela que é dele.\n--\n-- Idempotente.\n\nDROP INDEX IF EXISTS public.plg_fiscalbr_tax_groups_name_uidx;\nCREATE INDEX IF NOT EXISTS plg_fiscalbr_tax_groups_name_idx\n ON public.plg_fiscalbr_tax_groups USING btree (tenant_id, lower(name));\n`\n\nexport const MIGRATION_005_A_NOTA_EMITIDA_E_UM_DOCUMENTO = `-- A nota emitida é um DOCUMENTO, e o documento já tem arquétipo.\n--\n-- O V1 do Pertinho do Céu emitiu 496 notas (todas NFC-e: 440 concluídas, 51\n-- rejeitadas, 5 com erro) e nenhuma delas tem onde morar no V2. O que existe\n-- hoje aqui é a caixa de ENTRADA (\\`plg_fiscalbr_inbox_*\\`, a nota que chega do\n-- fornecedor) e o JOB de saída (\\`plg_fiscalbr_nfce_jobs\\`, a tentativa de\n-- emitir). Falta o resultado: a nota que a casa emitiu, com chave, protocolo e\n-- XML.\n--\n-- ── Por que pendurar em \\`public.documents\\` e não numa tabela solta ─────────\n--\n-- Porque a pergunta que o operador faz não é \"quais notas fiscais existem\",\n-- é \"o que foi emitido sobre ESTE pedido\". Hoje a resposta está espalhada:\n-- o anexo do título está em \\`documents\\`, o formulário está em \\`documents\\`, a\n-- foto do ponto está em \\`documents\\` — e a nota fiscal ficaria de fora, sendo\n-- o documento mais documento de todos.\n--\n-- \\`documents\\` é o arquétipo: \\`kind\\` diz a espécie, \\`subject_type\\`/\\`subject_id\\`\n-- dizem sobre o quê, e o plugin pendura o que só ele entende numa extensão 1:1\n-- pela chave — exatamente como \\`plg_forms_documents\\` já faz. Esta migração\n-- segue esse padrão, não inventa outro.\n--\n-- ── O que entra e o que NÃO entra ─────────────────────────────────────────\n--\n-- ENTRA o artefato legal e a trilha:\n-- xml_content 113 de 496 preenchidos, 618 kB no total (5,6 kB cada).\n-- É a prova da emissão e não se regenera. Fica.\n-- os quatro payloads 108 kB para as 496 notas inteiras. É a conversa com a\n-- SEFAZ, e é ela que explica as 51 rejeições.\n-- pdf_storage_path 84 de 496. Vira \\`documents.storage_path\\` com\n-- \\`storage_provider = 'v1-pending'\\`, a mesma convenção\n-- que \\`upsert_document\\` usa: o REGISTRO chegou, os bytes\n-- ainda não.\n--\n-- NÃO entra nada que o V2 já saiba responder por conta própria, e a emissão\n-- nova do V2 continua nascendo por \\`plg_fiscalbr_nfce_jobs\\` — este arquivo é o\n-- destino do resultado, não um segundo caminho de emissão.\n--\n-- ── Os dois ponteiros da nota do V1, que não são o mesmo ──────────────────\n--\n-- fiscal_documents.order_id 496 de 496 — a COMANDA (\\`table_orders\\`)\n-- fiscal_documents.invoice_id 118 de 496 — a VENDA (\\`invoices\\`)\n--\n-- Os dois chegam em \\`public.orders\\` no V2, e às vezes na MESMA linha (a\n-- comanda que virou nota adota a venda). Guardar os dois na mesma coluna\n-- perderia justamente a distinção: \\`order_id\\` na extensão é a comanda; a venda\n-- vai para \\`plg_fiscalbr_document_orders\\`, que é N:N porque uma nota pode\n-- juntar várias (511 vínculos para 496 notas).\n--\n-- Idempotente.\n\n-- ═══════════════════════════════════════════════════════════════════════════\n-- 1. A espécie entra no vocabulário\n-- ═══════════════════════════════════════════════════════════════════════════\n-- \\`migration.upsert_document\\` RECUSA espécie que não esteja aqui, e faz certo:\n-- é o registro que impede \\`kind\\` de virar campo livre.\n\nINSERT INTO public.document_kinds (kind, plugin, description, statuses, status)\nVALUES ('fiscal_document', 'fiscal_br',\n 'A nota fiscal que a casa EMITIU (NFC-e, NF-e, NFS-e), com chave, protocolo e XML',\n ARRAY['draft','processing','authorized','rejected','error','cancelled','substituted'],\n 'declared')\nON CONFLICT (kind) DO UPDATE\n SET plugin = EXCLUDED.plugin,\n description = EXCLUDED.description,\n statuses = EXCLUDED.statuses,\n updated_at = now();\n\n-- ═══════════════════════════════════════════════════════════════════════════\n-- 2. A extensão: o que só o fiscal entende\n-- ═══════════════════════════════════════════════════════════════════════════\n\nCREATE TABLE IF NOT EXISTS public.plg_fiscalbr_documents (\n -- A chave É o documento. Não há id próprio: a nota e o documento são a mesma\n -- coisa vista de dois lugares, e um segundo id só criaria a chance de eles\n -- discordarem.\n document_id uuid PRIMARY KEY REFERENCES public.documents(id) ON DELETE CASCADE,\n tenant_id uuid NOT NULL REFERENCES public.tenants(id) ON DELETE CASCADE,\n unit_id uuid,\n -- A comanda/pedido que originou a emissão.\n order_id uuid REFERENCES public.orders(id) ON DELETE SET NULL,\n model text NOT NULL,\n environment text,\n status text NOT NULL,\n series text,\n number text,\n -- 44 dígitos. Sem CHECK de formato: uma nota REJEITADA não tem chave, e uma\n -- chave que a SEFAZ devolveu torta é informação, não erro de digitação.\n access_key text,\n verification_code text,\n protocol text,\n provider text,\n provider_document_id text,\n provider_integration_id text,\n total_value numeric(14,2),\n iss_value numeric(14,2),\n recipient_snapshot jsonb,\n request_payload jsonb,\n response_payload jsonb,\n status_payload jsonb,\n rejection_payload jsonb,\n source_snapshot jsonb,\n -- O artefato legal. 5,6 kB por nota nas 113 que o têm; não se regenera.\n xml text,\n authorized_at timestamptz,\n last_polled_at timestamptz,\n emitted_by_user_id uuid,\n cancelled_at timestamptz,\n cancel_reason text,\n cancelled_by_user_id uuid,\n -- A cadeia de substituição. Auto-referência, então SET NULL: apagar a nota\n -- substituída não pode arrastar a que a substituiu.\n substitutes_document_id uuid REFERENCES public.plg_fiscalbr_documents(document_id) ON DELETE SET NULL,\n substituted_by_document_id uuid REFERENCES public.plg_fiscalbr_documents(document_id) ON DELETE SET NULL,\n observations text,\n metadata jsonb NOT NULL DEFAULT '{}'::jsonb,\n created_at timestamptz NOT NULL DEFAULT now(),\n updated_at timestamptz NOT NULL DEFAULT now()\n);\n\nDO $$ BEGIN\n ALTER TABLE public.plg_fiscalbr_documents\n ADD CONSTRAINT plg_fiscalbr_documents_unit_fkey\n FOREIGN KEY (tenant_id, unit_id) REFERENCES app.units(tenant_id, id) ON DELETE SET NULL;\nEXCEPTION WHEN duplicate_object THEN NULL; END $$;\n\n-- A chave de acesso é única POR INQUILINO e só quando existe: 56 das 496 notas\n-- do Pertinho não têm chave nenhuma (rejeitadas e com erro), e um UNIQUE\n-- comum as empilharia todas num conflito só.\nCREATE UNIQUE INDEX IF NOT EXISTS plg_fiscalbr_documents_chave_idx\n ON public.plg_fiscalbr_documents (tenant_id, access_key) WHERE access_key IS NOT NULL;\nCREATE INDEX IF NOT EXISTS plg_fiscalbr_documents_order_idx\n ON public.plg_fiscalbr_documents (tenant_id, order_id);\nCREATE INDEX IF NOT EXISTS plg_fiscalbr_documents_status_idx\n ON public.plg_fiscalbr_documents (tenant_id, unit_id, status, authorized_at DESC);\n\n-- ═══════════════════════════════════════════════════════════════════════════\n-- 3. A trilha da emissão\n-- ═══════════════════════════════════════════════════════════════════════════\n-- 1.886 eventos para 496 notas — quase quatro por nota. É aqui que a rejeição\n-- diz o motivo, e é o único lugar em que ela diz.\n\nCREATE TABLE IF NOT EXISTS public.plg_fiscalbr_document_events (\n id uuid PRIMARY KEY DEFAULT gen_random_uuid(),\n tenant_id uuid NOT NULL REFERENCES public.tenants(id) ON DELETE CASCADE,\n unit_id uuid,\n document_id uuid NOT NULL REFERENCES public.plg_fiscalbr_documents(document_id) ON DELETE CASCADE,\n event_type text NOT NULL,\n status text,\n justification text,\n request_payload jsonb,\n response_payload jsonb,\n created_by_user_id uuid,\n metadata jsonb NOT NULL DEFAULT '{}'::jsonb,\n created_at timestamptz NOT NULL DEFAULT now()\n);\n\nCREATE INDEX IF NOT EXISTS plg_fiscalbr_document_events_doc_idx\n ON public.plg_fiscalbr_document_events (document_id, created_at DESC);\n\n-- ═══════════════════════════════════════════════════════════════════════════\n-- 4. Quais vendas a nota cobre\n-- ═══════════════════════════════════════════════════════════════════════════\n\nCREATE TABLE IF NOT EXISTS public.plg_fiscalbr_document_orders (\n id uuid PRIMARY KEY DEFAULT gen_random_uuid(),\n tenant_id uuid NOT NULL REFERENCES public.tenants(id) ON DELETE CASCADE,\n document_id uuid NOT NULL REFERENCES public.plg_fiscalbr_documents(document_id) ON DELETE CASCADE,\n order_id uuid NOT NULL REFERENCES public.orders(id) ON DELETE CASCADE,\n created_at timestamptz NOT NULL DEFAULT now(),\n UNIQUE (document_id, order_id)\n);\n\nCREATE INDEX IF NOT EXISTS plg_fiscalbr_document_orders_order_idx\n ON public.plg_fiscalbr_document_orders (tenant_id, order_id);\n\n-- ═══════════════════════════════════════════════════════════════════════════\n-- 5. updated_at e a cerca\n-- ═══════════════════════════════════════════════════════════════════════════\n\nDO $$ BEGIN\n CREATE TRIGGER plg_fiscalbr_documents_updated_at BEFORE UPDATE\n ON public.plg_fiscalbr_documents FOR EACH ROW EXECUTE FUNCTION public.handle_updated_at();\nEXCEPTION WHEN duplicate_object THEN NULL; END $$;\n\n-- \\`app.current_tenant_id()\\` e não \\`user_tenant_ids()\\`, pela razão que a 013 do\n-- plugin-tables teve de consertar em produção: a segunda devolve a UNIÃO das\n-- contas do chamador e mostraria a nota fiscal de um cliente para outro.\nDO $$\nDECLARE t text;\nBEGIN\n FOREACH t IN ARRAY ARRAY['plg_fiscalbr_documents','plg_fiscalbr_document_events',\n 'plg_fiscalbr_document_orders'] LOOP\n EXECUTE format('ALTER TABLE public.%I ENABLE ROW LEVEL SECURITY', t);\n EXECUTE format('DROP POLICY IF EXISTS %I ON public.%I', t || '_select', t);\n EXECUTE format('DROP POLICY IF EXISTS %I ON public.%I', t || '_insert', t);\n EXECUTE format('DROP POLICY IF EXISTS %I ON public.%I', t || '_update', t);\n EXECUTE format('DROP POLICY IF EXISTS %I ON public.%I', t || '_delete', t);\n EXECUTE format('CREATE POLICY %I ON public.%I FOR SELECT USING (tenant_id = (select app.current_tenant_id()))', t || '_select', t);\n EXECUTE format('CREATE POLICY %I ON public.%I FOR INSERT WITH CHECK (tenant_id = (select app.current_tenant_id()))', t || '_insert', t);\n EXECUTE format('CREATE POLICY %I ON public.%I FOR UPDATE USING (tenant_id = (select app.current_tenant_id()))', t || '_update', t);\n EXECUTE format('CREATE POLICY %I ON public.%I FOR DELETE USING (tenant_id = (select app.current_tenant_id()))', t || '_delete', t);\n EXECUTE format('GRANT SELECT, INSERT, UPDATE, DELETE ON public.%I TO authenticated', t);\n END LOOP;\nEND $$;\n`\n\nexport const MIGRATION_006_O_GRUPO_FISCAL_GANHA_O_RAIL = `-- O grupo fiscal ganha observação e etiqueta.\n--\n-- A tabela existe desde a 002 e nunca teve TELA: o grupo fiscal de toda conta\n-- migrada estava no banco sem porta. Estas duas colunas são o que falta para\n-- ele ter o mesmo rail de todo cadastro do produto — e a etiqueta tem uso\n-- imediato aqui, porque «Simples», «monofásico» e «substituição tributária»\n-- são recortes que nenhum campo do grupo responde e que decidem qual grupo\n-- alguém procura.\n--\n-- Idempotente.\n\nALTER TABLE public.plg_fiscalbr_tax_groups\n ADD COLUMN IF NOT EXISTS notes text,\n ADD COLUMN IF NOT EXISTS tags text[] DEFAULT '{}'::text[],\n ADD COLUMN IF NOT EXISTS unit_id uuid;\n\nCREATE INDEX IF NOT EXISTS plg_fiscalbr_tax_groups_tags_gin\n ON public.plg_fiscalbr_tax_groups USING gin (tags);\n`\n\nexport const MIGRATION_007_A_RLS_DO_DOCUMENTO_PASSA_A_SER_LEGIVEL = `-- ---------------------------------------------------------------------------\n-- 007_a_rls_do_documento_passa_a_ser_legivel.sql\n--\n-- MESMO caso da 036 do plugin-tables, e a mesma medição antes de escrever.\n--\n-- \\`check-plugin-review\\` reprovou estas tabelas com *«cria public.X com\n-- tenant_id e nunca liga row security»*. Fui ao banco: **todas têm RLS ligada,\n-- com políticas** — medido no cluster do Pertinho em 29-09:\n--\n-- plg_fiscalbr_documents ........ rls=t, 4 políticas\n-- plg_fiscalbr_document_events .. rls=t, 4\n-- plg_fiscalbr_document_orders .. rls=t, 4\n--\n-- Não há vazamento. O \\`ENABLE\\` dessas tabelas mora dentro de um\n-- \\`DO $$ … EXECUTE format(...)\\`, e o portão lê o ARQUIVO, não o banco — uma\n-- garantia que o revisor não consegue ler é uma garantia que ninguém confere.\n--\n-- As migrations que as criam JÁ FORAM APLICADAS; reescrevê-las trocaria o\n-- checksum, que é a deriva de ledger pela qual este repositório para o mundo.\n-- Então a legibilidade entra por arquivo NOVO — o portão pergunta ao SQL\n-- INTEIRO do plugin.\n--\n-- Ligar RLS já ligada é no-op: isto não muda comportamento, torna conferível o\n-- que já era verdade.\n--\n-- Idempotente e replay-safe.\n-- ---------------------------------------------------------------------------\n\nALTER TABLE public.plg_fiscalbr_documents ENABLE ROW LEVEL SECURITY;\nALTER TABLE public.plg_fiscalbr_document_events ENABLE ROW LEVEL SECURITY;\nALTER TABLE public.plg_fiscalbr_document_orders ENABLE ROW LEVEL SECURITY;\n`\n\nexport const MIGRATIONS: Array<{ id: string; sql: string }> = [\n { id: \"000_baseline\", sql: MIGRATION_000_BASELINE },\n { id: \"001_preserve_terminal_results_and_recover_leases\", sql: MIGRATION_001_PRESERVE_TERMINAL_RESULTS_AND_RECOVER_LEASES },\n { id: \"002_a_nota_que_chega_tem_onde_esperar\", sql: MIGRATION_002_A_NOTA_QUE_CHEGA_TEM_ONDE_ESPERAR },\n { id: \"003_a_caixa_de_entrada_atravessa\", sql: MIGRATION_003_A_CAIXA_DE_ENTRADA_ATRAVESSA },\n { id: \"004_o_grupo_fiscal_do_v1_repete_nome\", sql: MIGRATION_004_O_GRUPO_FISCAL_DO_V1_REPETE_NOME },\n { id: \"005_a_nota_emitida_e_um_documento\", sql: MIGRATION_005_A_NOTA_EMITIDA_E_UM_DOCUMENTO },\n { id: \"006_o_grupo_fiscal_ganha_o_rail\", sql: MIGRATION_006_O_GRUPO_FISCAL_GANHA_O_RAIL },\n { id: \"007_a_rls_do_documento_passa_a_ser_legivel\", sql: MIGRATION_007_A_RLS_DO_DOCUMENTO_PASSA_A_SER_LEGIVEL },\n]\n","import type { EntityDef, PluginRegistryDef } from '@fayz-ai/core'\nimport { RECORD_GROUP, recordRailFields, activeFacet } from '@fayz-ai/core'\n\n// ---------------------------------------------------------------------------\n// Grupos fiscais — a tributação que um produto herda.\n//\n// A tabela existe desde a 002 deste plugin e NUNCA teve tela: o grupo fiscal\n// de toda conta migrada estava no banco sem porta. Isto é a porta.\n//\n// O que ele responde é uma pergunta só, feita muitas vezes: com que imposto\n// este produto sai na nota. Agrupar é o ponto — sem grupo, CFOP, CST, origem e\n// três alíquotas seriam repetidos em cada produto, e mudar a regra de um\n// estado viraria uma edição em massa.\n//\n// A ordem dos campos segue a da nota: primeiro o que classifica a OPERAÇÃO\n// (CFOP), depois o ICMS, depois PIS/COFINS, e por último IBS/CBS — que é a\n// reforma tributária, e que já tem campo porque o V1 já tem.\n// ---------------------------------------------------------------------------\n\nconst taxGroupEntity: EntityDef = {\n name: 'Grupo fiscal',\n namePlural: 'Grupos fiscais',\n icon: 'ReceiptText',\n displayField: 'name',\n defaultSort: 'name',\n formColumns: 2,\n facets: [activeFacet({ active: 'Ativos', inactive: 'Inativos', all: 'Todos' })],\n fieldGroups: [\n {\n id: 'identity', label: 'Identificação', columns: 2,\n description: 'O nome é como a casa chama o grupo («Bebida alcoólica», «Serviço isento») — o resto é o que a nota exige.',\n },\n { id: 'icms', label: 'ICMS', columns: 3 },\n { id: 'pis_cofins', label: 'PIS e COFINS', columns: 2 },\n {\n id: 'ibs_cbs', label: 'IBS e CBS', columns: 2,\n description: 'A reforma tributária. Em branco enquanto não for exigido da sua operação.',\n },\n { ...RECORD_GROUP },\n ],\n fields: [\n { key: 'name', label: 'Nome', type: 'text', required: true, showInTable: true, searchable: true, group: 'identity', span: 2 },\n // O CFOP classifica a OPERAÇÃO (venda dentro do estado, fora, devolução) e\n // é o primeiro campo que o contador confere.\n { key: 'cfop', label: 'CFOP', type: 'text', showInTable: true, searchable: true, group: 'identity' },\n // CEST anda com a substituição tributária: só existe para quem está nela.\n { key: 'cest', label: 'CEST', type: 'text', showInTable: false, group: 'identity' },\n { key: 'benefitCode', label: 'Código de benefício', type: 'text', showInTable: false, group: 'identity' },\n\n // CST é do regime normal e CSOSN é do Simples — a casa usa UM dos dois, e\n // qual depende do regime dela, não deste grupo. Os dois ficam à vista de\n // propósito: esconder o que a conta não usa exigiria saber o regime aqui.\n { key: 'cst', label: 'CST', type: 'text', showInTable: true, group: 'icms' },\n { key: 'csosn', label: 'CSOSN (Simples)', type: 'text', showInTable: false, group: 'icms' },\n {\n key: 'icmsOrigin', label: 'Origem da mercadoria', type: 'select', showInTable: false, group: 'icms',\n options: [\n { value: '0', label: '0 — Nacional' },\n { value: '1', label: '1 — Estrangeira, importação direta' },\n { value: '2', label: '2 — Estrangeira, mercado interno' },\n { value: '3', label: '3 — Nacional, conteúdo de importação > 40%' },\n { value: '4', label: '4 — Nacional, processos produtivos básicos' },\n { value: '5', label: '5 — Nacional, conteúdo de importação ≤ 40%' },\n { value: '6', label: '6 — Estrangeira, sem similar nacional' },\n { value: '7', label: '7 — Estrangeira, mercado interno sem similar' },\n { value: '8', label: '8 — Nacional, conteúdo de importação > 70%' },\n ],\n },\n { key: 'icmsRate', label: 'Alíquota ICMS (%)', type: 'number', min: 0, max: 100, showInTable: true, group: 'icms' },\n { key: 'icmsBcMode', label: 'Modalidade da base', type: 'text', showInTable: false, group: 'icms' },\n { key: 'baseCalculationMode', label: 'Cálculo da base', type: 'text', showInTable: false, group: 'icms' },\n // O fundo de combate à pobreza é adicional ao ICMS em alguns estados.\n { key: 'povertyFundRate', label: 'Fundo de pobreza (%)', type: 'number', min: 0, max: 100, showInTable: false, group: 'icms' },\n\n { key: 'pisSituation', label: 'Situação PIS', type: 'text', showInTable: false, group: 'pis_cofins' },\n { key: 'pisRate', label: 'Alíquota PIS (%)', type: 'number', min: 0, max: 100, showInTable: false, group: 'pis_cofins' },\n { key: 'cofinsSituation', label: 'Situação COFINS', type: 'text', showInTable: false, group: 'pis_cofins' },\n { key: 'cofinsRate', label: 'Alíquota COFINS (%)', type: 'number', min: 0, max: 100, showInTable: false, group: 'pis_cofins' },\n\n { key: 'ibsCbsSituation', label: 'Situação IBS/CBS', type: 'text', showInTable: false, group: 'ibs_cbs' },\n { key: 'ibsCbsClassification', label: 'Classificação IBS/CBS', type: 'text', showInTable: false, group: 'ibs_cbs' },\n\n ...recordRailFields({ tagsTable: 'plg_fiscalbr_tax_groups', activeLabel: 'Ativo' }),\n ],\n data: {\n table: 'plg_fiscalbr_tax_groups',\n schema: 'public',\n tenantScoped: true,\n searchColumns: ['name', 'cfop'],\n },\n}\n\nexport const fiscalRegistries: PluginRegistryDef[] = [\n {\n id: 'taxGroups',\n icon: 'ReceiptText',\n entity: taxGroupEntity,\n description: 'A tributação que um produto herda: CFOP, ICMS, PIS/COFINS e, quando exigido, IBS/CBS.',\n agentHint: 'Regras de imposto que um produto herda — NÃO é a nota emitida nem o imposto de uma venda específica.',\n },\n]\n","import type { FinancialDataProvider } from '@fayz-ai/plugin-financial'\nimport type { PlugNotasFinancialProviderOptions } from '../types'\n\ntype FiscalDocument = NonNullable<Awaited<ReturnType<NonNullable<FinancialDataProvider['getNotaById']>>>>\n\nfunction fiscalReader(provider: FinancialDataProvider) {\n if (!provider.getNotaById) throw new Error('PlugNotas requires fiscal document reads from Financial.')\n return provider.getNotaById.bind(provider)\n}\n\nasync function committed(read: (id: string) => Promise<FiscalDocument | null>, id: string) {\n const document = await read(id)\n if (!document) throw new Error('Fiscal document was not found after the server operation.')\n return document\n}\n\nfunction resultError(result: { status: string; message?: string }, document: FiscalDocument, fallback: string) {\n if (result.status !== 'error') return\n throw new Error(result.message?.trim() || document.errorMessage?.trim() || fallback)\n}\n\nfunction cancellationPending(metadata: Record<string, unknown> | undefined): boolean {\n const provider = metadata?.plugnotas\n if (!provider || typeof provider !== 'object' || Array.isArray(provider)) return false\n return (provider as Record<string, unknown>).cancellationPending === true\n}\n\nfunction artifactUrl(value: unknown): string {\n if (typeof value !== 'string' || !value.trim()) throw new Error('PlugNotas returned an invalid artifact URL.')\n let parsed: URL\n try {\n parsed = new URL(value)\n } catch {\n throw new Error('PlugNotas returned an invalid artifact URL.')\n }\n if (!['http:', 'https:'].includes(parsed.protocol)) throw new Error('PlugNotas returned an invalid artifact URL.')\n return parsed.toString()\n}\n\n/**\n * Decorates Financial without creating another fiscal ledger. The gateway only\n * requests server operations; every result returned to the UI is read back\n * from the server-committed `plg_financial_nfe` row.\n */\nexport function createPlugNotasFinancialProvider(\n options: PlugNotasFinancialProviderOptions,\n): FinancialDataProvider {\n const { base, gateway } = options\n\n const issueNota: NonNullable<FinancialDataProvider['issueNota']> = async (id) => {\n const read = fiscalReader(base)\n const document = await read(id)\n if (!document) throw new Error('Fiscal document not found.')\n if (document.model !== 'nfce') throw new Error('PlugNotas currently authorizes NFC-e only.')\n if (!document.orderId?.trim()) throw new Error('PlugNotas NFC-e must be created from an order.')\n if (!['draft', 'error'].includes(document.status)) return document\n const result = await gateway.issue({ notaId: id, idempotencyKey: `nfce:${id}` })\n const saved = await committed(read, id)\n resultError(result, saved, 'PlugNotas could not issue this NFC-e.')\n return saved\n }\n\n const refreshNota: NonNullable<FinancialDataProvider['refreshNota']> = async (id) => {\n const read = fiscalReader(base)\n const document = await read(id)\n if (!document) throw new Error('Fiscal document not found.')\n if (!['processing', 'error'].includes(document.status) && !cancellationPending(document.metadata)) return document\n const result = await gateway.refresh({ notaId: id })\n const saved = await committed(read, id)\n resultError(result, saved, 'PlugNotas could not refresh this NFC-e.')\n return saved\n }\n\n const cancelNota: NonNullable<FinancialDataProvider['cancelNota']> = async (id, reason) => {\n const read = fiscalReader(base)\n const document = await read(id)\n if (!document) throw new Error('Fiscal document not found.')\n if (document.status !== 'issued') throw new Error('Only an issued NFC-e can be cancelled.')\n if (cancellationPending(document.metadata)) throw new Error('NFC-e cancellation is already in progress.')\n const normalized = reason?.trim() ?? ''\n if (normalized.length < 15 || normalized.length > 255) {\n throw new Error('Cancellation reason must contain between 15 and 255 characters.')\n }\n const result = await gateway.cancel({ notaId: id, reason: normalized })\n const saved = await committed(read, id)\n resultError(result, saved, 'PlugNotas could not cancel this NFC-e.')\n return saved\n }\n\n const getNotaArtifactUrl: NonNullable<FinancialDataProvider['getNotaArtifactUrl']> = async (id, kind) => {\n const read = fiscalReader(base)\n const document = await read(id)\n if (!document) throw new Error('Fiscal document not found.')\n if (!['issued', 'cancelled'].includes(document.status)) {\n throw new Error('Fiscal artifacts are available only after authorization.')\n }\n if (kind === 'cancellation_xml' && document.status !== 'cancelled') {\n throw new Error('Cancellation XML is available only after cancellation.')\n }\n return artifactUrl((await gateway.artifact({ notaId: id, kind })).url)\n }\n\n return new Proxy(base, {\n get(target, property, receiver) {\n if (property === 'issueNota') return issueNota\n if (property === 'refreshNota') return refreshNota\n if (property === 'cancelNota') return cancelNota\n if (property === 'getNotaArtifactUrl') return getNotaArtifactUrl\n return Reflect.get(target, property, receiver)\n },\n })\n}\n","import { definePlugin, type PluginManifest } from '@fayz-ai/core'\nimport { createPlugNotasConnector, type CreatePlugNotasConnectorOptions } from './connectors/plugnotas'\nimport { MIGRATIONS } from './migrations'\nimport { fiscalRegistries } from './data/registries'\n\nexport * from './connectors/plugnotas'\nexport * from './lib/financial-provider'\nexport * from './types'\n\nexport interface CreatePlugNotasPluginOptions extends CreatePlugNotasConnectorOptions {}\n\n/**\n * Fiscal Brasil is an addon: Financial owns the fiscal document, while this\n * package owns the provider connection and its asynchronous delivery trail.\n */\nexport function createPlugNotasPlugin(options: CreatePlugNotasPluginOptions = {}): PluginManifest {\n return definePlugin({\n id: 'fiscal-br',\n name: 'Fiscal Brasil',\n icon: 'ReceiptText',\n version: '0.1.0',\n apiVersion: 1,\n scope: 'addon',\n defaultEnabled: true,\n dependencies: ['financial'],\n navigation: [],\n routes: [],\n // O grupo fiscal é CADASTRO, e por isso entra como registry e não como\n // rota própria: ele aparece onde os outros cadastros do produto aparecem.\n registries: fiscalRegistries,\n connectors: [createPlugNotasConnector(options)],\n functions: [{\n slug: 'plugnotas-bridge',\n ingress: 'none',\n verifyJwt: false,\n secrets: [\n 'PLUGNOTAS_API_KEY',\n 'PLUGNOTAS_BASE_URL',\n 'PLUGNOTAS_ENVIRONMENT',\n 'PLUGNOTAS_WEBHOOK_SECRET',\n 'PLUGNOTAS_ARTIFACT_SIGNING_KEY',\n 'PLUGNOTAS_ALLOWED_ORIGINS',\n 'PLUGNOTAS_WORKER_SECRET',\n 'SUPABASE_URL',\n 'SUPABASE_ANON_KEY',\n 'SUPABASE_SERVICE_ROLE_KEY'\n ],\n grants: [\n 'plugnotas_register_emitter',\n 'plugnotas_disconnect_emitter',\n 'plugnotas_create_nfce_draft_for_order',\n 'plugnotas_prepare_nfce',\n 'plugnotas_prepare_nfce_cancel',\n 'plugnotas_apply_nfce_result',\n 'plugnotas_apply_nfce_webhook',\n 'plugnotas_get_artifact_locator',\n 'plugnotas_claim_nfce_recovery',\n 'plugnotas_record_nfce_attempt'\n ],\n }],\n migrations: MIGRATIONS.map((entry) => ({\n id: `fiscal-br-${entry.id}`,\n version: '1.0.0',\n sql: entry.sql,\n description: 'Durable PlugNotas delivery, recovery, webhook and emitter-registration boundary.',\n })),\n diagnostics: [{\n id: 'plugnotas-nfce-ready',\n description: 'NFC-e requires the shared connection, order, item and financial fiscal-document contracts.',\n requires: {\n tables: [\n 'connections',\n 'orders',\n 'items',\n 'plg_financial_nfe',\n 'plg_fiscalbr_nfce_jobs',\n 'plg_fiscalbr_nfce_events',\n 'plg_fiscalbr_webhook_receipts',\n ],\n rpcs: [\n 'plugnotas_create_nfce_draft_for_order',\n 'plugnotas_prepare_nfce',\n 'plugnotas_apply_nfce_result',\n 'plugnotas_apply_nfce_webhook',\n 'plugnotas_claim_nfce_recovery',\n ],\n env: [\n 'PLUGNOTAS_API_KEY',\n 'PLUGNOTAS_BASE_URL',\n 'PLUGNOTAS_ENVIRONMENT',\n 'PLUGNOTAS_WEBHOOK_SECRET',\n 'PLUGNOTAS_ARTIFACT_SIGNING_KEY',\n 'PLUGNOTAS_ALLOWED_ORIGINS',\n 'PLUGNOTAS_WORKER_SECRET',\n ],\n },\n level: 'error',\n }],\n })\n}\n"]}
|
|
@@ -3,6 +3,9 @@ export declare const MIGRATION_001_PRESERVE_TERMINAL_RESULTS_AND_RECOVER_LEASES
|
|
|
3
3
|
export declare const MIGRATION_002_A_NOTA_QUE_CHEGA_TEM_ONDE_ESPERAR = "-- A nota que chega tem onde esperar.\n--\n-- O V1 recebe NF-e de fornecedor pela SEFAZ e guarda cada uma numa caixa de\n-- entrada antes de algu\u00E9m decidir o que fazer com ela: manifestar, importar\n-- para o estoque, ou nada. Essa caixa n\u00E3o tinha destino no V2.\n--\n-- dfe_inbox 703 a nota recebida, com o XML inteiro\n-- dfe_inbox_items 3.215 o item da nota, com todo o tributo\n-- dfe_inbox_payments 739 a forma de pagamento declarada\n-- dfe_inbox_duplicatas 830 o vencimento da duplicata\n-- fiscal_groups 11 o grupo fiscal (CFOP, CST, al\u00EDquotas)\n--\n-- Os n\u00FAmeros somam os dois tenants: Espa\u00E7o Facial e Pertinho do C\u00E9u. \u00C9 o \u00FAnico\n-- dom\u00EDnio fiscal que os dois usam.\n--\n-- \u2500\u2500 O XML \u00E9 arquivo, e arquivo mora em public.documents \u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\n--\n-- `dfe_inbox.xml_content` \u00E9 o XML inteiro numa coluna de texto, e\n-- `pdf_storage_path` aponta para o DANFE no storage do V1. Os dois s\u00E3o ARQUIVO,\n-- e o V2 j\u00E1 tem o arqu\u00E9tipo: `public.documents`, com kind, checksum, storage_*\n-- e a RLS de arquivo que uma coluna de texto nunca teria.\n--\n-- Ent\u00E3o a extens\u00E3o guarda um PONTEIRO \u2014 `document_id` \u2014 e os bytes v\u00E3o para o\n-- arqu\u00E9tipo, pelo est\u00E1gio de assets (`make mig-assets`), que \u00E9 quem alcan\u00E7a\n-- object storage. Enquanto os bytes n\u00E3o chegam, `document_id` fica nulo e o\n-- caminho de origem fica em metadata: a linha \u00E9 verdadeira nos dois momentos.\n--\n-- \u2500\u2500 O tributo vem inteiro, e de prop\u00F3sito \u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\n--\n-- 30 colunas de imposto por item (ICMS, ICMS-ST, PIS, COFINS, IPI, com base,\n-- al\u00EDquota e valor de cada). \u00C9 muita coluna, e a tenta\u00E7\u00E3o \u00E9 jogar tudo num\n-- jsonb. N\u00E3o: isto \u00E9 documento fiscal recebido, e a pergunta que se faz dele \u00E9\n-- \"quanto de ICMS entrou neste m\u00EAs\" \u2014 uma soma, sobre coluna, com \u00EDndice. Um\n-- jsonb transformaria toda apura\u00E7\u00E3o numa varredura.\n--\n-- O que VAI para jsonb \u00E9 o `raw_xml_item`, que j\u00E1 \u00E9 jsonb na origem e existe\n-- para confer\u00EAncia, n\u00E3o para conta.\n--\n-- Idempotente.\n\nCREATE TABLE IF NOT EXISTS public.plg_fiscalbr_inbox_documents (\n id uuid DEFAULT gen_random_uuid() NOT NULL,\n tenant_id uuid NOT NULL,\n unit_id uuid,\n -- O ponteiro para o arqu\u00E9tipo. Nulo enquanto os bytes n\u00E3o subiram \u2014 o\n -- caminho de origem fica em metadata at\u00E9 l\u00E1.\n document_id uuid,\n access_key text,\n nfe_number text,\n nfe_series text,\n nfe_model text,\n issue_date timestamp with time zone,\n received_at timestamp with time zone,\n status text DEFAULT 'received'::text NOT NULL,\n -- A manifesta\u00E7\u00E3o do destinat\u00E1rio \u00E9 outro eixo que o status: uma nota pode\n -- estar recebida e n\u00E3o manifestada, e \u00E9 a manifesta\u00E7\u00E3o que a SEFAZ cobra.\n manifestation_status text,\n manifested_at timestamp with time zone,\n manifestation_attempts integer DEFAULT 0 NOT NULL,\n manifestation_response jsonb,\n supplier_party_id uuid,\n supplier_cnpj text,\n supplier_name text,\n supplier_ie text,\n total_value numeric(14,2),\n total_products numeric(14,2),\n total_discount numeric(14,2),\n total_freight numeric(14,2),\n total_insurance numeric(14,2),\n total_other numeric(14,2),\n total_icms_base numeric(14,2),\n total_icms_value numeric(14,2),\n total_icms_st_base numeric(14,2),\n total_icms_st_value numeric(14,2),\n total_pis_value numeric(14,2),\n total_cofins_value numeric(14,2),\n total_ipi_value numeric(14,2),\n nfe_protocol text,\n nfe_authorized_at timestamp with time zone,\n carrier_name text,\n carrier_cnpj text,\n freight_mode_code text,\n additional_info text,\n reviewed_at timestamp with time zone,\n -- O que essa nota virou no estoque, quando virou. Nulo \u00E9 nota recebida e\n -- n\u00E3o importada, que \u00E9 a maioria.\n inventory_operation_id uuid,\n owner_id uuid,\n metadata jsonb DEFAULT '{}'::jsonb NOT NULL,\n created_at timestamp with time zone DEFAULT now() NOT NULL,\n updated_at timestamp with time zone DEFAULT now() NOT NULL\n);\n\nCOMMENT ON TABLE public.plg_fiscalbr_inbox_documents IS\n 'A NF-e de fornecedor recebida da SEFAZ, esperando manifesta\u00E7\u00E3o ou importa\u00E7\u00E3o. V1: dfe_inbox.';\nCOMMENT ON COLUMN public.plg_fiscalbr_inbox_documents.document_id IS\n 'V1: xml_content + pdf_storage_path. O XML e o DANFE s\u00E3o arquivo; os bytes moram em public.documents e aqui fica o ponteiro.';\n\nCREATE TABLE IF NOT EXISTS public.plg_fiscalbr_inbox_items (\n id uuid DEFAULT gen_random_uuid() NOT NULL,\n tenant_id uuid NOT NULL,\n inbox_document_id uuid NOT NULL,\n item_number integer,\n supplier_product_code text,\n ean_barcode text,\n ean_taxable text,\n description text,\n ncm text,\n cest text,\n cfop text,\n unit_commercial text,\n unit_taxable text,\n quantity_commercial numeric(18,4),\n quantity_taxable numeric(18,4),\n unit_price_commercial numeric(18,6),\n unit_price_taxable numeric(18,6),\n total_price numeric(14,2),\n discount numeric(14,2),\n freight numeric(14,2),\n insurance numeric(14,2),\n other_expenses numeric(14,2),\n icms_cst text, icms_csosn text, icms_origin text,\n icms_base numeric(14,2), icms_rate numeric(9,4), icms_value numeric(14,2),\n icms_st_base numeric(14,2), icms_st_value numeric(14,2),\n pis_cst text, pis_base numeric(14,2), pis_rate numeric(9,4), pis_value numeric(14,2),\n cofins_cst text, cofins_base numeric(14,2), cofins_rate numeric(9,4), cofins_value numeric(14,2),\n ipi_cst text, ipi_rate numeric(9,4), ipi_value numeric(14,2),\n -- O produto do V2 que esta linha da nota representa. `reconciled` \u00E9 a\n -- decis\u00E3o de quem conferiu; o id \u00E9 o resultado dela.\n product_id uuid,\n reconciled boolean DEFAULT false NOT NULL,\n raw_xml_item jsonb,\n metadata jsonb DEFAULT '{}'::jsonb NOT NULL,\n created_at timestamp with time zone DEFAULT now() NOT NULL,\n updated_at timestamp with time zone DEFAULT now() NOT NULL\n);\n\nCOMMENT ON TABLE public.plg_fiscalbr_inbox_items IS\n 'O item da NF-e recebida, com o tributo em coluna \u2014 a apura\u00E7\u00E3o soma coluna, n\u00E3o varre jsonb. V1: dfe_inbox_items.';\n\nCREATE TABLE IF NOT EXISTS public.plg_fiscalbr_inbox_payments (\n id uuid DEFAULT gen_random_uuid() NOT NULL,\n tenant_id uuid NOT NULL,\n inbox_document_id uuid NOT NULL,\n payment_index integer,\n payment_indicator text,\n payment_type_code text,\n payment_type_label text,\n value numeric(14,2),\n change_given numeric(14,2),\n card_brand text,\n card_acquirer_cnpj text,\n card_auth_code text,\n metadata jsonb DEFAULT '{}'::jsonb NOT NULL,\n created_at timestamp with time zone DEFAULT now() NOT NULL,\n updated_at timestamp with time zone DEFAULT now() NOT NULL\n);\n\nCREATE TABLE IF NOT EXISTS public.plg_fiscalbr_inbox_duplicates (\n id uuid DEFAULT gen_random_uuid() NOT NULL,\n tenant_id uuid NOT NULL,\n inbox_document_id uuid NOT NULL,\n duplicate_index integer,\n number text,\n due_date date,\n value numeric(14,2),\n -- O que essa duplicata virou no contas a pagar, se algu\u00E9m a lan\u00E7ou.\n installment_id uuid,\n metadata jsonb DEFAULT '{}'::jsonb NOT NULL,\n created_at timestamp with time zone DEFAULT now() NOT NULL,\n updated_at timestamp with time zone DEFAULT now() NOT NULL\n);\n\nCOMMENT ON TABLE public.plg_fiscalbr_inbox_duplicates IS\n 'O vencimento declarado na NF-e recebida. Vira parcela do contas a pagar quando algu\u00E9m lan\u00E7a a nota. V1: dfe_inbox_duplicatas.';\n\nCREATE TABLE IF NOT EXISTS public.plg_fiscalbr_tax_groups (\n id uuid DEFAULT gen_random_uuid() NOT NULL,\n tenant_id uuid NOT NULL,\n name text NOT NULL,\n cfop text,\n csosn text,\n cst text,\n icms_origin text,\n icms_rate numeric(9,4),\n icms_bc_mode text,\n pis_situation text,\n pis_rate numeric(9,4),\n cofins_situation text,\n cofins_rate numeric(9,4),\n -- A reforma tribut\u00E1ria j\u00E1 est\u00E1 no cadastro do V1 e vem junto: deixar de\n -- fora obrigaria a recadastrar tudo no dia em que IBS/CBS valerem.\n ibs_cbs_situation text,\n ibs_cbs_classification text,\n base_calculation_mode text,\n benefit_code text,\n cest text,\n poverty_fund_rate numeric(9,4),\n is_active boolean DEFAULT true NOT NULL,\n metadata jsonb DEFAULT '{}'::jsonb NOT NULL,\n created_at timestamp with time zone DEFAULT now() NOT NULL,\n updated_at timestamp with time zone DEFAULT now() NOT NULL\n);\n\nCOMMENT ON TABLE public.plg_fiscalbr_tax_groups IS\n 'O grupo fiscal: CFOP, CST/CSOSN e al\u00EDquotas que o produto herda na emiss\u00E3o. V1: fiscal_groups. Traz IBS/CBS porque o cadastro do V1 j\u00E1 os tem.';\n\n-- \u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\n-- As chaves, os \u00EDndices e a cerca\n-- \u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\n\nDO $$\nDECLARE t text;\nBEGIN\n FOREACH t IN ARRAY ARRAY['plg_fiscalbr_inbox_documents', 'plg_fiscalbr_inbox_items',\n 'plg_fiscalbr_inbox_payments', 'plg_fiscalbr_inbox_duplicates',\n 'plg_fiscalbr_tax_groups'] LOOP\n IF NOT EXISTS (SELECT 1 FROM pg_constraint WHERE conrelid = ('public.' || t)::regclass AND contype = 'p') THEN\n EXECUTE format('ALTER TABLE ONLY public.%I ADD CONSTRAINT %I PRIMARY KEY (id)', t, t || '_pkey');\n END IF;\n IF NOT EXISTS (SELECT 1 FROM pg_constraint WHERE conname = t || '_tenant_id_id_key') THEN\n EXECUTE format('ALTER TABLE ONLY public.%I ADD CONSTRAINT %I UNIQUE (tenant_id, id)', t, t || '_tenant_id_id_key');\n END IF;\n IF NOT EXISTS (SELECT 1 FROM pg_constraint WHERE conname = t || '_tenant_fkey') THEN\n EXECUTE format('ALTER TABLE ONLY public.%I ADD CONSTRAINT %I FOREIGN KEY (tenant_id) REFERENCES public.tenants(id) ON DELETE CASCADE', t, t || '_tenant_fkey');\n END IF;\n END LOOP;\nEND $$;\n\nDO $$ BEGIN\n IF NOT EXISTS (SELECT 1 FROM pg_constraint WHERE conname = 'plg_fiscalbr_inbox_documents_unit_fkey') THEN\n ALTER TABLE ONLY public.plg_fiscalbr_inbox_documents ADD CONSTRAINT plg_fiscalbr_inbox_documents_unit_fkey\n FOREIGN KEY (tenant_id, unit_id) REFERENCES app.units(tenant_id, id) ON DELETE SET NULL;\n END IF;\n -- O arquivo pode ser apagado sem levar a nota junto: a nota fiscal existe\n -- mesmo sem o XML \u00E0 m\u00E3o, e \u00E9 ela que a SEFAZ conhece.\n IF NOT EXISTS (SELECT 1 FROM pg_constraint WHERE conname = 'plg_fiscalbr_inbox_documents_document_fkey') THEN\n ALTER TABLE ONLY public.plg_fiscalbr_inbox_documents ADD CONSTRAINT plg_fiscalbr_inbox_documents_document_fkey\n FOREIGN KEY (document_id) REFERENCES public.documents(id) ON DELETE SET NULL;\n END IF;\n IF NOT EXISTS (SELECT 1 FROM pg_constraint WHERE conname = 'plg_fiscalbr_inbox_documents_supplier_fkey') THEN\n ALTER TABLE ONLY public.plg_fiscalbr_inbox_documents ADD CONSTRAINT plg_fiscalbr_inbox_documents_supplier_fkey\n FOREIGN KEY (supplier_party_id) REFERENCES public.people(id) ON DELETE SET NULL;\n END IF;\n IF NOT EXISTS (SELECT 1 FROM pg_constraint WHERE conname = 'plg_fiscalbr_inbox_documents_operation_fkey') THEN\n ALTER TABLE ONLY public.plg_fiscalbr_inbox_documents ADD CONSTRAINT plg_fiscalbr_inbox_documents_operation_fkey\n FOREIGN KEY (inventory_operation_id) REFERENCES public.plg_inventory_operations(id) ON DELETE SET NULL;\n END IF;\n\n -- Item, pagamento e duplicata n\u00E3o existem fora da nota: CASCADE.\n IF NOT EXISTS (SELECT 1 FROM pg_constraint WHERE conname = 'plg_fiscalbr_inbox_items_document_fkey') THEN\n ALTER TABLE ONLY public.plg_fiscalbr_inbox_items ADD CONSTRAINT plg_fiscalbr_inbox_items_document_fkey\n FOREIGN KEY (tenant_id, inbox_document_id) REFERENCES public.plg_fiscalbr_inbox_documents(tenant_id, id) ON DELETE CASCADE;\n END IF;\n IF NOT EXISTS (SELECT 1 FROM pg_constraint WHERE conname = 'plg_fiscalbr_inbox_items_product_fkey') THEN\n ALTER TABLE ONLY public.plg_fiscalbr_inbox_items ADD CONSTRAINT plg_fiscalbr_inbox_items_product_fkey\n FOREIGN KEY (product_id) REFERENCES public.products(id) ON DELETE SET NULL;\n END IF;\n IF NOT EXISTS (SELECT 1 FROM pg_constraint WHERE conname = 'plg_fiscalbr_inbox_payments_document_fkey') THEN\n ALTER TABLE ONLY public.plg_fiscalbr_inbox_payments ADD CONSTRAINT plg_fiscalbr_inbox_payments_document_fkey\n FOREIGN KEY (tenant_id, inbox_document_id) REFERENCES public.plg_fiscalbr_inbox_documents(tenant_id, id) ON DELETE CASCADE;\n END IF;\n IF NOT EXISTS (SELECT 1 FROM pg_constraint WHERE conname = 'plg_fiscalbr_inbox_duplicates_document_fkey') THEN\n ALTER TABLE ONLY public.plg_fiscalbr_inbox_duplicates ADD CONSTRAINT plg_fiscalbr_inbox_duplicates_document_fkey\n FOREIGN KEY (tenant_id, inbox_document_id) REFERENCES public.plg_fiscalbr_inbox_documents(tenant_id, id) ON DELETE CASCADE;\n END IF;\n IF NOT EXISTS (SELECT 1 FROM pg_constraint WHERE conname = 'plg_fiscalbr_inbox_duplicates_installment_fkey') THEN\n ALTER TABLE ONLY public.plg_fiscalbr_inbox_duplicates ADD CONSTRAINT plg_fiscalbr_inbox_duplicates_installment_fkey\n FOREIGN KEY (installment_id) REFERENCES public.plg_financial_invoice_installments(id) ON DELETE SET NULL;\n END IF;\nEND $$;\n\n-- A chave de acesso \u00E9 \u00FAnica por casa: a mesma nota chegando duas vezes da SEFAZ\n-- \u00E9 a mesma nota, e \u00E9 isso que impede a importa\u00E7\u00E3o em dobro.\nCREATE UNIQUE INDEX IF NOT EXISTS plg_fiscalbr_inbox_documents_access_key_uidx\n ON public.plg_fiscalbr_inbox_documents USING btree (tenant_id, access_key)\n WHERE access_key IS NOT NULL;\nCREATE INDEX IF NOT EXISTS plg_fiscalbr_inbox_documents_pending_idx\n ON public.plg_fiscalbr_inbox_documents USING btree (tenant_id, unit_id, issue_date DESC)\n WHERE inventory_operation_id IS NULL;\nCREATE INDEX IF NOT EXISTS plg_fiscalbr_inbox_items_document_idx\n ON public.plg_fiscalbr_inbox_items USING btree (tenant_id, inbox_document_id);\nCREATE INDEX IF NOT EXISTS plg_fiscalbr_inbox_items_product_idx\n ON public.plg_fiscalbr_inbox_items USING btree (tenant_id, product_id) WHERE product_id IS NOT NULL;\nCREATE INDEX IF NOT EXISTS plg_fiscalbr_inbox_payments_document_idx\n ON public.plg_fiscalbr_inbox_payments USING btree (tenant_id, inbox_document_id);\nCREATE INDEX IF NOT EXISTS plg_fiscalbr_inbox_duplicates_document_idx\n ON public.plg_fiscalbr_inbox_duplicates USING btree (tenant_id, inbox_document_id);\nCREATE UNIQUE INDEX IF NOT EXISTS plg_fiscalbr_tax_groups_name_uidx\n ON public.plg_fiscalbr_tax_groups USING btree (tenant_id, lower(name));\n\nDO $$\nDECLARE t text;\nBEGIN\n FOREACH t IN ARRAY ARRAY['plg_fiscalbr_inbox_documents', 'plg_fiscalbr_inbox_items',\n 'plg_fiscalbr_inbox_payments', 'plg_fiscalbr_inbox_duplicates',\n 'plg_fiscalbr_tax_groups'] LOOP\n EXECUTE format('DROP TRIGGER IF EXISTS %I ON public.%I', t || '_updated_at', t);\n EXECUTE format('CREATE TRIGGER %I BEFORE UPDATE ON public.%I FOR EACH ROW EXECUTE FUNCTION public.handle_updated_at()', t || '_updated_at', t);\n END LOOP;\nEND $$;\n\n-- \u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\n-- Quem enxerga\n-- \u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\n--\n-- Nota fiscal \u00E9 do FINANCEIRO, e a permiss\u00E3o \u00E9 a que ele j\u00E1 usa. A caixa de\n-- entrada n\u00E3o tem unidade nos filhos \u2014 eles herdam a da nota \u2014, ent\u00E3o o escopo\n-- de unidade vale s\u00F3 no documento; o resto \u00E9 do tenant, e \u00E9 a CASCADE da FK que\n-- garante que ningu\u00E9m veja filho de nota alheia.\n-- RLS, uma a uma e por extenso. O la\u00E7o adiante j\u00E1 liga tudo isto \u2014 mas ele liga\n-- por `EXECUTE format(...)`, e o port\u00E3o que revisa plugin l\u00EA o ARQUIVO, n\u00E3o o\n-- banco: para ele, tabela com tenant_id e sem um ENABLE literal \u00E9 tabela\n-- aberta. Uma garantia que o revisor n\u00E3o consegue ler \u00E9 uma garantia que\n-- ningu\u00E9m confere.\nALTER TABLE public.plg_fiscalbr_inbox_documents ENABLE ROW LEVEL SECURITY;\nALTER TABLE public.plg_fiscalbr_inbox_items ENABLE ROW LEVEL SECURITY;\nALTER TABLE public.plg_fiscalbr_tax_groups ENABLE ROW LEVEL SECURITY;\nALTER TABLE public.plg_fiscalbr_inbox_payments ENABLE ROW LEVEL SECURITY;\nALTER TABLE public.plg_fiscalbr_inbox_duplicates ENABLE ROW LEVEL SECURITY;\n\nDO $$\nDECLARE\n t text;\n doc boolean;\nBEGIN\n FOREACH t IN ARRAY ARRAY['plg_fiscalbr_inbox_documents', 'plg_fiscalbr_inbox_items',\n 'plg_fiscalbr_inbox_payments', 'plg_fiscalbr_inbox_duplicates',\n 'plg_fiscalbr_tax_groups'] LOOP\n doc := t = 'plg_fiscalbr_inbox_documents';\n EXECUTE format('ALTER TABLE public.%I FORCE ROW LEVEL SECURITY', t);\n EXECUTE format('ALTER TABLE public.%I ENABLE ROW LEVEL SECURITY', t);\n EXECUTE format('GRANT SELECT, INSERT, UPDATE, DELETE ON TABLE public.%I TO authenticated', t);\n EXECUTE format('GRANT ALL ON TABLE public.%I TO service_role', t);\n\n EXECUTE format('DROP POLICY IF EXISTS %I ON public.%I', t || '_authz_select', t);\n EXECUTE format($f$CREATE POLICY %I ON public.%I FOR SELECT TO authenticated USING (\n tenant_id = (SELECT app.current_tenant_id())\n AND (%s)\n AND (SELECT app.has_permission('financial.read', %s)))$f$,\n t || '_authz_select', t,\n CASE WHEN doc THEN format('unit_id IS NULL OR (SELECT app.has_unit(%I.unit_id))', t) ELSE 'true' END,\n CASE WHEN doc THEN format('%I.unit_id', t) ELSE 'NULL::uuid' END);\n\n EXECUTE format('DROP POLICY IF EXISTS %I ON public.%I', t || '_authz_write', t);\n EXECUTE format($f$CREATE POLICY %I ON public.%I FOR ALL TO authenticated USING (\n tenant_id = (SELECT app.current_tenant_id())\n AND (SELECT app.has_permission('financial.edit', %s)))\n WITH CHECK (\n tenant_id = (SELECT app.current_tenant_id())\n AND (SELECT app.has_permission('financial.edit', %s)))$f$,\n t || '_authz_write', t,\n CASE WHEN doc THEN format('%I.unit_id', t) ELSE 'NULL::uuid' END,\n CASE WHEN doc THEN format('%I.unit_id', t) ELSE 'NULL::uuid' END);\n END LOOP;\nEND $$;\n\nINSERT INTO public.scoped_resources (resource_table, unit_column, owner_column, shareable, default_visibility, label)\nVALUES ('plg_fiscalbr_inbox_documents', 'unit_id', 'owner_id', false, 'unit', 'Notas recebidas')\nON CONFLICT (resource_table) DO UPDATE\n SET unit_column = EXCLUDED.unit_column, default_visibility = EXCLUDED.default_visibility, label = EXCLUDED.label;\n";
|
|
4
4
|
export declare const MIGRATION_003_A_CAIXA_DE_ENTRADA_ATRAVESSA = "-- A caixa de entrada atravessa.\n--\n-- A 002 fez as cinco tabelas. Esta escreve o de-para. As cinco usam o writer\n-- gen\u00E9rico `by_shape` (spine 123): as colunas do destino foram desenhadas\n-- olhando a origem, ent\u00E3o o que falta \u00E9 dizer qual nome vira qual \u2014 n\u00E3o uma\n-- fun\u00E7\u00E3o de upsert por tabela.\n--\n-- \u2500\u2500 O arquivo n\u00E3o vem por aqui \u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\n--\n-- `xml_content` guarda o XML inteiro e `pdf_storage_path` aponta para o DANFE.\n-- Os dois saem do de-para com `-`: arquivo atravessa pelo est\u00E1gio de assets,\n-- que \u00E9 quem alcan\u00E7a object storage, e vira linha de `public.documents`. O\n-- caminho de origem fica em metadata para o est\u00E1gio achar.\n--\n-- \u2500\u2500 O fornecedor fica para a costura \u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\n--\n-- `supplier_party_id` n\u00E3o \u00E9 preenchido aqui. A origem guarda CNPJ e nome, n\u00E3o\n-- um id de contato, e casar CNPJ com `public.people` \u00E9 uma decis\u00E3o de\n-- identidade \u2014 a mesma que j\u00E1 p\u00F4s 438 clientes em quarentena. Fica nulo, com o\n-- CNPJ na coluna ao lado, e uma passada de costura resolve quando algu\u00E9m\n-- decidir a regra.\n--\n-- Idempotente.\n\nSET LOCAL migration.allowlist_unlock = 'on';\n\nSELECT migration._allow(\n 'fiscal_groups', 'public.plg_fiscalbr_tax_groups', 'fiscal_br', 'by_shape',\n jsonb_build_object('target', 'public.plg_fiscalbr_tax_groups'),\n NULL::text[],\n jsonb_build_object(\n 'uuid', 'id',\n 'name', 'name',\n 'cfop', 'cfop',\n 'csosn', 'csosn',\n 'cst', 'cst',\n 'icms_origin', 'icms_origin',\n 'icms_rate', 'icms_rate',\n 'icms_modalidade_bc', 'icms_bc_mode',\n 'pis_situation', 'pis_situation',\n 'pis_rate', 'pis_rate',\n 'cofins_situation', 'cofins_situation',\n 'cofins_rate', 'cofins_rate',\n 'ibs_cbs_situation', 'ibs_cbs_situation',\n 'ibs_cbs_classification', 'ibs_cbs_classification',\n 'base_calculation_mode', 'base_calculation_mode',\n 'cbenef', 'benefit_code',\n 'cest', 'cest',\n 'poverty_fund_rate', 'poverty_fund_rate',\n 'active', 'is_active',\n 'created_at', 'created_at',\n 'updated_at', 'updated_at',\n 'tenant_id', '-',\n 'id', 'jsonb:fiscalbr.v1_ref'),\n 'O grupo fiscal: CFOP, CST/CSOSN e al\u00EDquotas que o produto herda na emiss\u00E3o. '\n 'IBS/CBS v\u00EAm junto porque o cadastro do V1 j\u00E1 os tem \u2014 deixar de fora '\n 'obrigaria a recadastrar tudo no dia em que a reforma valer.'\n);\n\nSELECT migration._allow(\n 'dfe_inbox', 'public.plg_fiscalbr_inbox_documents', 'fiscal_br', 'by_shape',\n jsonb_build_object('target', 'public.plg_fiscalbr_inbox_documents'),\n NULL::text[],\n jsonb_build_object(\n 'uuid', 'id',\n 'company_id', 'unit_id@companies',\n 'access_key', 'access_key',\n 'nfe_number', 'nfe_number',\n 'nfe_series', 'nfe_series',\n 'nfe_model', 'nfe_model',\n 'issue_date', 'issue_date',\n 'received_at', 'received_at',\n 'status', 'status',\n 'manifestation_status', 'manifestation_status',\n 'manifested_at', 'manifested_at',\n 'manifestation_attempts', 'manifestation_attempts',\n 'manifestation_response', 'manifestation_response',\n 'supplier_cnpj', 'supplier_cnpj',\n 'supplier_name', 'supplier_name',\n 'supplier_ie', 'supplier_ie',\n 'total_value', 'total_value',\n 'total_products', 'total_products',\n 'total_discount', 'total_discount',\n 'total_freight', 'total_freight',\n 'total_insurance', 'total_insurance',\n 'total_other', 'total_other',\n 'total_icms_base', 'total_icms_base',\n 'total_icms_value', 'total_icms_value',\n 'total_icms_st_base', 'total_icms_st_base',\n 'total_icms_st_value', 'total_icms_st_value',\n 'total_pis_value', 'total_pis_value',\n 'total_cofins_value', 'total_cofins_value',\n 'total_ipi_value', 'total_ipi_value',\n 'nfe_protocol', 'nfe_protocol') ||\n -- `jsonb_build_object` aceita no m\u00E1ximo 100 argumentos, e este de-para tem\n -- 59 pares. Dois objetos concatenados, e n\u00E3o uma tabela a menos.\n jsonb_build_object(\n 'nfe_authorized_at', 'nfe_authorized_at',\n 'carrier_name', 'carrier_name',\n 'carrier_cnpj', 'carrier_cnpj',\n 'freight_mode_code', 'freight_mode_code',\n 'additional_info', 'additional_info',\n 'reviewed_at', 'reviewed_at',\n 'created_at', 'created_at',\n 'updated_at', 'updated_at',\n 'tenant_id', '-',\n 'reviewed_by', '-',\n 'xml_content', '-',\n 'pdf_storage_path', 'jsonb:fiscalbr.v1_pdf_path',\n 'pdf_fetched_at', 'jsonb:fiscalbr.v1_pdf_fetched_at',\n 'danfe_import_id', 'jsonb:fiscalbr.v1_danfe_import_ref',\n 'plugnotas_payload', 'jsonb:fiscalbr.v1_plugnotas_payload',\n 'plugnotas_received_at', 'jsonb:fiscalbr.v1_plugnotas_received_at',\n 'nfe_cnf', 'jsonb:fiscalbr.nfe.cnf',\n 'nfe_natureza_operacao', 'jsonb:fiscalbr.nfe.natureza_operacao',\n 'nfe_finalidade', 'jsonb:fiscalbr.nfe.finalidade',\n 'nfe_tipo_operacao', 'jsonb:fiscalbr.nfe.tipo_operacao',\n 'nfe_destination', 'jsonb:fiscalbr.nfe.destination',\n 'supplier_address', 'jsonb:fiscalbr.supplier.street',\n 'supplier_number', 'jsonb:fiscalbr.supplier.number',\n 'supplier_complement', 'jsonb:fiscalbr.supplier.complement',\n 'supplier_neighborhood', 'jsonb:fiscalbr.supplier.neighborhood',\n 'supplier_city', 'jsonb:fiscalbr.supplier.city',\n 'supplier_state', 'jsonb:fiscalbr.supplier.state',\n 'supplier_postal_code', 'jsonb:fiscalbr.supplier.postal_code',\n 'supplier_phone', 'jsonb:fiscalbr.supplier.phone',\n 'supplier_email', 'jsonb:fiscalbr.supplier.email',\n 'id', 'jsonb:fiscalbr.v1_ref'),\n 'A NF-e de fornecedor recebida da SEFAZ. `xml_content` sai com `-`: arquivo '\n 'atravessa pelo est\u00E1gio de assets e vira public.documents, e o caminho do '\n 'DANFE fica em metadata para o est\u00E1gio achar. O endere\u00E7o do fornecedor vai '\n 'para metadata porque o fornecedor \u00E9 `public.people`, e duplic\u00E1-lo em coluna '\n 'daria duas respostas para \"onde ele fica\".'\n);\n\nSELECT migration._allow(\n 'dfe_inbox_items', 'public.plg_fiscalbr_inbox_items', 'fiscal_br', 'by_shape',\n jsonb_build_object('target', 'public.plg_fiscalbr_inbox_items'),\n NULL::text[],\n jsonb_build_object(\n 'uuid', 'id',\n 'dfe_inbox_id', 'inbox_document_id@dfe_inbox',\n 'item_number', 'item_number',\n 'product_code_supplier', 'supplier_product_code',\n 'ean_barcode', 'ean_barcode',\n 'ean_trib', 'ean_taxable',\n 'description', 'description',\n 'ncm', 'ncm',\n 'cest', 'cest',\n 'cfop', 'cfop',\n 'unit_commercial', 'unit_commercial',\n 'unit_taxable', 'unit_taxable',\n 'quantity_commercial', 'quantity_commercial',\n 'quantity_taxable', 'quantity_taxable',\n 'unit_price_commercial', 'unit_price_commercial',\n 'unit_price_taxable', 'unit_price_taxable',\n 'total_price', 'total_price',\n 'discount', 'discount',\n 'freight', 'freight',\n 'insurance', 'insurance',\n 'other_expenses', 'other_expenses',\n 'icms_cst', 'icms_cst',\n 'icms_csosn', 'icms_csosn',\n 'icms_origin', 'icms_origin',\n 'icms_base', 'icms_base',\n 'icms_rate', 'icms_rate',\n 'icms_value', 'icms_value',\n 'icms_st_base', 'icms_st_base',\n 'icms_st_value', 'icms_st_value',\n 'pis_cst', 'pis_cst',\n 'pis_base', 'pis_base',\n 'pis_rate', 'pis_rate',\n 'pis_value', 'pis_value',\n 'cofins_cst', 'cofins_cst',\n 'cofins_base', 'cofins_base',\n 'cofins_rate', 'cofins_rate',\n 'cofins_value', 'cofins_value',\n 'ipi_cst', 'ipi_cst',\n 'ipi_rate', 'ipi_rate',\n 'ipi_value', 'ipi_value',\n 'linked_product_id', 'product_id@products',\n 'reconciled', 'reconciled',\n 'raw_xml_item', 'raw_xml_item',\n 'created_at', 'created_at',\n 'updated_at', 'updated_at',\n 'tenant_id', '-',\n 'id', 'jsonb:fiscalbr.v1_ref'),\n 'O item da nota, com o tributo em COLUNA \u2014 a apura\u00E7\u00E3o soma coluna, n\u00E3o varre '\n 'jsonb. `raw_xml_item` \u00E9 a exce\u00E7\u00E3o: j\u00E1 \u00E9 jsonb na origem e serve para '\n 'confer\u00EAncia, n\u00E3o para conta.'\n);\n\nSELECT migration._allow(\n 'dfe_inbox_payments', 'public.plg_fiscalbr_inbox_payments', 'fiscal_br', 'by_shape',\n jsonb_build_object('target', 'public.plg_fiscalbr_inbox_payments'),\n NULL::text[],\n jsonb_build_object(\n 'uuid', 'id',\n 'dfe_inbox_id', 'inbox_document_id@dfe_inbox',\n 'payment_index', 'payment_index',\n 'indicador_pagamento', 'payment_indicator',\n 'tipo_pagamento_code', 'payment_type_code',\n 'tipo_pagamento_label', 'payment_type_label',\n 'value', 'value',\n 'troco', 'change_given',\n 'card_flag', 'card_brand',\n 'card_acquirer_cnpj', 'card_acquirer_cnpj',\n 'card_auth_code', 'card_auth_code',\n 'created_at', 'created_at',\n 'updated_at', 'updated_at',\n 'tenant_id', '-',\n 'id', 'jsonb:fiscalbr.v1_ref'),\n 'A forma de pagamento declarada na nota recebida.'\n);\n\nSELECT migration._allow(\n 'dfe_inbox_duplicatas', 'public.plg_fiscalbr_inbox_duplicates', 'fiscal_br', 'by_shape',\n jsonb_build_object('target', 'public.plg_fiscalbr_inbox_duplicates'),\n NULL::text[],\n jsonb_build_object(\n 'uuid', 'id',\n 'dfe_inbox_id', 'inbox_document_id@dfe_inbox',\n 'duplicata_index', 'duplicate_index',\n 'numero', 'number',\n 'vencimento', 'due_date',\n 'valor', 'value',\n 'created_at', 'created_at',\n 'updated_at', 'updated_at',\n 'tenant_id', '-',\n 'id', 'jsonb:fiscalbr.v1_ref'),\n 'O vencimento declarado na nota. Vira parcela do contas a pagar quando algu\u00E9m '\n 'lan\u00E7a a nota \u2014 `installment_id` fica nulo at\u00E9 l\u00E1, porque a parcela \u00E9 '\n 'decis\u00E3o de quem lan\u00E7a, n\u00E3o da SEFAZ.'\n);\n\nDELETE FROM migration.excluded_tables\n WHERE source_table IN ('dfe_inbox', 'dfe_inbox_items', 'dfe_inbox_payments',\n 'dfe_inbox_duplicatas', 'fiscal_groups');\n\nRESET migration.allowlist_unlock;\n";
|
|
5
5
|
export declare const MIGRATION_004_O_GRUPO_FISCAL_DO_V1_REPETE_NOME = "-- O grupo fiscal do V1 repete nome.\n--\n-- A 002 criou \u00EDndice \u00DANICO em `(tenant_id, lower(name))`. O V1 tem, no tenant\n-- 8, \"Bebidas Alco\u00F3licas\", \"Bebidas N\u00E3o Alco\u00F3licas\" e \"Vinhos e Espumantes\"\n-- DUAS vezes cada \u2014 grupos fiscais diferentes com o mesmo r\u00F3tulo, e o V1\n-- permite. Tr\u00EAs das onze linhas foram para a quarentena por uma regra que eu\n-- inventei, n\u00E3o que a origem tenha.\n--\n-- O \u00EDndice vira comum: serve para buscar, n\u00E3o para proibir. Quem quiser proibir\n-- decide isso na tela, com o dado j\u00E1 dentro \u2014 e sabendo quais tr\u00EAs s\u00E3o.\n--\n-- Esta corre\u00E7\u00E3o nasceu dentro de `financial/083` e mudou de arquivo: o\n-- financeiro roda ANTES do fiscal_br na corrente, e um arquivo do financeiro\n-- que mexe numa tabela do fiscal_br falha do zero com\n-- `relation \"public.plg_fiscalbr_tax_groups\" does not exist`. Cada plugin mexe\n-- na tabela que \u00E9 dele.\n--\n-- Idempotente.\n\nDROP INDEX IF EXISTS public.plg_fiscalbr_tax_groups_name_uidx;\nCREATE INDEX IF NOT EXISTS plg_fiscalbr_tax_groups_name_idx\n ON public.plg_fiscalbr_tax_groups USING btree (tenant_id, lower(name));\n";
|
|
6
|
+
export declare const MIGRATION_005_A_NOTA_EMITIDA_E_UM_DOCUMENTO = "-- A nota emitida \u00E9 um DOCUMENTO, e o documento j\u00E1 tem arqu\u00E9tipo.\n--\n-- O V1 do Pertinho do C\u00E9u emitiu 496 notas (todas NFC-e: 440 conclu\u00EDdas, 51\n-- rejeitadas, 5 com erro) e nenhuma delas tem onde morar no V2. O que existe\n-- hoje aqui \u00E9 a caixa de ENTRADA (`plg_fiscalbr_inbox_*`, a nota que chega do\n-- fornecedor) e o JOB de sa\u00EDda (`plg_fiscalbr_nfce_jobs`, a tentativa de\n-- emitir). Falta o resultado: a nota que a casa emitiu, com chave, protocolo e\n-- XML.\n--\n-- \u2500\u2500 Por que pendurar em `public.documents` e n\u00E3o numa tabela solta \u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\n--\n-- Porque a pergunta que o operador faz n\u00E3o \u00E9 \"quais notas fiscais existem\",\n-- \u00E9 \"o que foi emitido sobre ESTE pedido\". Hoje a resposta est\u00E1 espalhada:\n-- o anexo do t\u00EDtulo est\u00E1 em `documents`, o formul\u00E1rio est\u00E1 em `documents`, a\n-- foto do ponto est\u00E1 em `documents` \u2014 e a nota fiscal ficaria de fora, sendo\n-- o documento mais documento de todos.\n--\n-- `documents` \u00E9 o arqu\u00E9tipo: `kind` diz a esp\u00E9cie, `subject_type`/`subject_id`\n-- dizem sobre o qu\u00EA, e o plugin pendura o que s\u00F3 ele entende numa extens\u00E3o 1:1\n-- pela chave \u2014 exatamente como `plg_forms_documents` j\u00E1 faz. Esta migra\u00E7\u00E3o\n-- segue esse padr\u00E3o, n\u00E3o inventa outro.\n--\n-- \u2500\u2500 O que entra e o que N\u00C3O entra \u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\n--\n-- ENTRA o artefato legal e a trilha:\n-- xml_content 113 de 496 preenchidos, 618 kB no total (5,6 kB cada).\n-- \u00C9 a prova da emiss\u00E3o e n\u00E3o se regenera. Fica.\n-- os quatro payloads 108 kB para as 496 notas inteiras. \u00C9 a conversa com a\n-- SEFAZ, e \u00E9 ela que explica as 51 rejei\u00E7\u00F5es.\n-- pdf_storage_path 84 de 496. Vira `documents.storage_path` com\n-- `storage_provider = 'v1-pending'`, a mesma conven\u00E7\u00E3o\n-- que `upsert_document` usa: o REGISTRO chegou, os bytes\n-- ainda n\u00E3o.\n--\n-- N\u00C3O entra nada que o V2 j\u00E1 saiba responder por conta pr\u00F3pria, e a emiss\u00E3o\n-- nova do V2 continua nascendo por `plg_fiscalbr_nfce_jobs` \u2014 este arquivo \u00E9 o\n-- destino do resultado, n\u00E3o um segundo caminho de emiss\u00E3o.\n--\n-- \u2500\u2500 Os dois ponteiros da nota do V1, que n\u00E3o s\u00E3o o mesmo \u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\n--\n-- fiscal_documents.order_id 496 de 496 \u2014 a COMANDA (`table_orders`)\n-- fiscal_documents.invoice_id 118 de 496 \u2014 a VENDA (`invoices`)\n--\n-- Os dois chegam em `public.orders` no V2, e \u00E0s vezes na MESMA linha (a\n-- comanda que virou nota adota a venda). Guardar os dois na mesma coluna\n-- perderia justamente a distin\u00E7\u00E3o: `order_id` na extens\u00E3o \u00E9 a comanda; a venda\n-- vai para `plg_fiscalbr_document_orders`, que \u00E9 N:N porque uma nota pode\n-- juntar v\u00E1rias (511 v\u00EDnculos para 496 notas).\n--\n-- Idempotente.\n\n-- \u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\n-- 1. A esp\u00E9cie entra no vocabul\u00E1rio\n-- \u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\n-- `migration.upsert_document` RECUSA esp\u00E9cie que n\u00E3o esteja aqui, e faz certo:\n-- \u00E9 o registro que impede `kind` de virar campo livre.\n\nINSERT INTO public.document_kinds (kind, plugin, description, statuses, status)\nVALUES ('fiscal_document', 'fiscal_br',\n 'A nota fiscal que a casa EMITIU (NFC-e, NF-e, NFS-e), com chave, protocolo e XML',\n ARRAY['draft','processing','authorized','rejected','error','cancelled','substituted'],\n 'declared')\nON CONFLICT (kind) DO UPDATE\n SET plugin = EXCLUDED.plugin,\n description = EXCLUDED.description,\n statuses = EXCLUDED.statuses,\n updated_at = now();\n\n-- \u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\n-- 2. A extens\u00E3o: o que s\u00F3 o fiscal entende\n-- \u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\n\nCREATE TABLE IF NOT EXISTS public.plg_fiscalbr_documents (\n -- A chave \u00C9 o documento. N\u00E3o h\u00E1 id pr\u00F3prio: a nota e o documento s\u00E3o a mesma\n -- coisa vista de dois lugares, e um segundo id s\u00F3 criaria a chance de eles\n -- discordarem.\n document_id uuid PRIMARY KEY REFERENCES public.documents(id) ON DELETE CASCADE,\n tenant_id uuid NOT NULL REFERENCES public.tenants(id) ON DELETE CASCADE,\n unit_id uuid,\n -- A comanda/pedido que originou a emiss\u00E3o.\n order_id uuid REFERENCES public.orders(id) ON DELETE SET NULL,\n model text NOT NULL,\n environment text,\n status text NOT NULL,\n series text,\n number text,\n -- 44 d\u00EDgitos. Sem CHECK de formato: uma nota REJEITADA n\u00E3o tem chave, e uma\n -- chave que a SEFAZ devolveu torta \u00E9 informa\u00E7\u00E3o, n\u00E3o erro de digita\u00E7\u00E3o.\n access_key text,\n verification_code text,\n protocol text,\n provider text,\n provider_document_id text,\n provider_integration_id text,\n total_value numeric(14,2),\n iss_value numeric(14,2),\n recipient_snapshot jsonb,\n request_payload jsonb,\n response_payload jsonb,\n status_payload jsonb,\n rejection_payload jsonb,\n source_snapshot jsonb,\n -- O artefato legal. 5,6 kB por nota nas 113 que o t\u00EAm; n\u00E3o se regenera.\n xml text,\n authorized_at timestamptz,\n last_polled_at timestamptz,\n emitted_by_user_id uuid,\n cancelled_at timestamptz,\n cancel_reason text,\n cancelled_by_user_id uuid,\n -- A cadeia de substitui\u00E7\u00E3o. Auto-refer\u00EAncia, ent\u00E3o SET NULL: apagar a nota\n -- substitu\u00EDda n\u00E3o pode arrastar a que a substituiu.\n substitutes_document_id uuid REFERENCES public.plg_fiscalbr_documents(document_id) ON DELETE SET NULL,\n substituted_by_document_id uuid REFERENCES public.plg_fiscalbr_documents(document_id) ON DELETE SET NULL,\n observations text,\n metadata jsonb NOT NULL DEFAULT '{}'::jsonb,\n created_at timestamptz NOT NULL DEFAULT now(),\n updated_at timestamptz NOT NULL DEFAULT now()\n);\n\nDO $$ BEGIN\n ALTER TABLE public.plg_fiscalbr_documents\n ADD CONSTRAINT plg_fiscalbr_documents_unit_fkey\n FOREIGN KEY (tenant_id, unit_id) REFERENCES app.units(tenant_id, id) ON DELETE SET NULL;\nEXCEPTION WHEN duplicate_object THEN NULL; END $$;\n\n-- A chave de acesso \u00E9 \u00FAnica POR INQUILINO e s\u00F3 quando existe: 56 das 496 notas\n-- do Pertinho n\u00E3o t\u00EAm chave nenhuma (rejeitadas e com erro), e um UNIQUE\n-- comum as empilharia todas num conflito s\u00F3.\nCREATE UNIQUE INDEX IF NOT EXISTS plg_fiscalbr_documents_chave_idx\n ON public.plg_fiscalbr_documents (tenant_id, access_key) WHERE access_key IS NOT NULL;\nCREATE INDEX IF NOT EXISTS plg_fiscalbr_documents_order_idx\n ON public.plg_fiscalbr_documents (tenant_id, order_id);\nCREATE INDEX IF NOT EXISTS plg_fiscalbr_documents_status_idx\n ON public.plg_fiscalbr_documents (tenant_id, unit_id, status, authorized_at DESC);\n\n-- \u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\n-- 3. A trilha da emiss\u00E3o\n-- \u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\n-- 1.886 eventos para 496 notas \u2014 quase quatro por nota. \u00C9 aqui que a rejei\u00E7\u00E3o\n-- diz o motivo, e \u00E9 o \u00FAnico lugar em que ela diz.\n\nCREATE TABLE IF NOT EXISTS public.plg_fiscalbr_document_events (\n id uuid PRIMARY KEY DEFAULT gen_random_uuid(),\n tenant_id uuid NOT NULL REFERENCES public.tenants(id) ON DELETE CASCADE,\n unit_id uuid,\n document_id uuid NOT NULL REFERENCES public.plg_fiscalbr_documents(document_id) ON DELETE CASCADE,\n event_type text NOT NULL,\n status text,\n justification text,\n request_payload jsonb,\n response_payload jsonb,\n created_by_user_id uuid,\n metadata jsonb NOT NULL DEFAULT '{}'::jsonb,\n created_at timestamptz NOT NULL DEFAULT now()\n);\n\nCREATE INDEX IF NOT EXISTS plg_fiscalbr_document_events_doc_idx\n ON public.plg_fiscalbr_document_events (document_id, created_at DESC);\n\n-- \u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\n-- 4. Quais vendas a nota cobre\n-- \u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\n\nCREATE TABLE IF NOT EXISTS public.plg_fiscalbr_document_orders (\n id uuid PRIMARY KEY DEFAULT gen_random_uuid(),\n tenant_id uuid NOT NULL REFERENCES public.tenants(id) ON DELETE CASCADE,\n document_id uuid NOT NULL REFERENCES public.plg_fiscalbr_documents(document_id) ON DELETE CASCADE,\n order_id uuid NOT NULL REFERENCES public.orders(id) ON DELETE CASCADE,\n created_at timestamptz NOT NULL DEFAULT now(),\n UNIQUE (document_id, order_id)\n);\n\nCREATE INDEX IF NOT EXISTS plg_fiscalbr_document_orders_order_idx\n ON public.plg_fiscalbr_document_orders (tenant_id, order_id);\n\n-- \u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\n-- 5. updated_at e a cerca\n-- \u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\n\nDO $$ BEGIN\n CREATE TRIGGER plg_fiscalbr_documents_updated_at BEFORE UPDATE\n ON public.plg_fiscalbr_documents FOR EACH ROW EXECUTE FUNCTION public.handle_updated_at();\nEXCEPTION WHEN duplicate_object THEN NULL; END $$;\n\n-- `app.current_tenant_id()` e n\u00E3o `user_tenant_ids()`, pela raz\u00E3o que a 013 do\n-- plugin-tables teve de consertar em produ\u00E7\u00E3o: a segunda devolve a UNI\u00C3O das\n-- contas do chamador e mostraria a nota fiscal de um cliente para outro.\nDO $$\nDECLARE t text;\nBEGIN\n FOREACH t IN ARRAY ARRAY['plg_fiscalbr_documents','plg_fiscalbr_document_events',\n 'plg_fiscalbr_document_orders'] LOOP\n EXECUTE format('ALTER TABLE public.%I ENABLE ROW LEVEL SECURITY', t);\n EXECUTE format('DROP POLICY IF EXISTS %I ON public.%I', t || '_select', t);\n EXECUTE format('DROP POLICY IF EXISTS %I ON public.%I', t || '_insert', t);\n EXECUTE format('DROP POLICY IF EXISTS %I ON public.%I', t || '_update', t);\n EXECUTE format('DROP POLICY IF EXISTS %I ON public.%I', t || '_delete', t);\n EXECUTE format('CREATE POLICY %I ON public.%I FOR SELECT USING (tenant_id = (select app.current_tenant_id()))', t || '_select', t);\n EXECUTE format('CREATE POLICY %I ON public.%I FOR INSERT WITH CHECK (tenant_id = (select app.current_tenant_id()))', t || '_insert', t);\n EXECUTE format('CREATE POLICY %I ON public.%I FOR UPDATE USING (tenant_id = (select app.current_tenant_id()))', t || '_update', t);\n EXECUTE format('CREATE POLICY %I ON public.%I FOR DELETE USING (tenant_id = (select app.current_tenant_id()))', t || '_delete', t);\n EXECUTE format('GRANT SELECT, INSERT, UPDATE, DELETE ON public.%I TO authenticated', t);\n END LOOP;\nEND $$;\n";
|
|
7
|
+
export declare const MIGRATION_006_O_GRUPO_FISCAL_GANHA_O_RAIL = "-- O grupo fiscal ganha observa\u00E7\u00E3o e etiqueta.\n--\n-- A tabela existe desde a 002 e nunca teve TELA: o grupo fiscal de toda conta\n-- migrada estava no banco sem porta. Estas duas colunas s\u00E3o o que falta para\n-- ele ter o mesmo rail de todo cadastro do produto \u2014 e a etiqueta tem uso\n-- imediato aqui, porque \u00ABSimples\u00BB, \u00ABmonof\u00E1sico\u00BB e \u00ABsubstitui\u00E7\u00E3o tribut\u00E1ria\u00BB\n-- s\u00E3o recortes que nenhum campo do grupo responde e que decidem qual grupo\n-- algu\u00E9m procura.\n--\n-- Idempotente.\n\nALTER TABLE public.plg_fiscalbr_tax_groups\n ADD COLUMN IF NOT EXISTS notes text,\n ADD COLUMN IF NOT EXISTS tags text[] DEFAULT '{}'::text[],\n ADD COLUMN IF NOT EXISTS unit_id uuid;\n\nCREATE INDEX IF NOT EXISTS plg_fiscalbr_tax_groups_tags_gin\n ON public.plg_fiscalbr_tax_groups USING gin (tags);\n";
|
|
8
|
+
export declare const MIGRATION_007_A_RLS_DO_DOCUMENTO_PASSA_A_SER_LEGIVEL = "-- ---------------------------------------------------------------------------\n-- 007_a_rls_do_documento_passa_a_ser_legivel.sql\n--\n-- MESMO caso da 036 do plugin-tables, e a mesma medi\u00E7\u00E3o antes de escrever.\n--\n-- `check-plugin-review` reprovou estas tabelas com *\u00ABcria public.X com\n-- tenant_id e nunca liga row security\u00BB*. Fui ao banco: **todas t\u00EAm RLS ligada,\n-- com pol\u00EDticas** \u2014 medido no cluster do Pertinho em 29-09:\n--\n-- plg_fiscalbr_documents ........ rls=t, 4 pol\u00EDticas\n-- plg_fiscalbr_document_events .. rls=t, 4\n-- plg_fiscalbr_document_orders .. rls=t, 4\n--\n-- N\u00E3o h\u00E1 vazamento. O `ENABLE` dessas tabelas mora dentro de um\n-- `DO $$ \u2026 EXECUTE format(...)`, e o port\u00E3o l\u00EA o ARQUIVO, n\u00E3o o banco \u2014 uma\n-- garantia que o revisor n\u00E3o consegue ler \u00E9 uma garantia que ningu\u00E9m confere.\n--\n-- As migrations que as criam J\u00C1 FORAM APLICADAS; reescrev\u00EA-las trocaria o\n-- checksum, que \u00E9 a deriva de ledger pela qual este reposit\u00F3rio para o mundo.\n-- Ent\u00E3o a legibilidade entra por arquivo NOVO \u2014 o port\u00E3o pergunta ao SQL\n-- INTEIRO do plugin.\n--\n-- Ligar RLS j\u00E1 ligada \u00E9 no-op: isto n\u00E3o muda comportamento, torna confer\u00EDvel o\n-- que j\u00E1 era verdade.\n--\n-- Idempotente e replay-safe.\n-- ---------------------------------------------------------------------------\n\nALTER TABLE public.plg_fiscalbr_documents ENABLE ROW LEVEL SECURITY;\nALTER TABLE public.plg_fiscalbr_document_events ENABLE ROW LEVEL SECURITY;\nALTER TABLE public.plg_fiscalbr_document_orders ENABLE ROW LEVEL SECURITY;\n";
|
|
6
9
|
export declare const MIGRATIONS: Array<{
|
|
7
10
|
id: string;
|
|
8
11
|
sql: string;
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../../src/migrations/index.ts"],"names":[],"mappings":"AAIA,eAAO,MAAM,sBAAsB,2g0BA2jBlC,CAAA;AAED,eAAO,MAAM,0DAA0D,uzRAmMtE,CAAA;AAED,eAAO,MAAM,+CAA+C,m5oBAyW3D,CAAA;AAED,eAAO,MAAM,0CAA0C,o1XAgPtD,CAAA;AAED,eAAO,MAAM,8CAA8C,6qCAsB1D,CAAA;AAED,eAAO,MAAM,UAAU,EAAE,KAAK,CAAC;IAAE,EAAE,EAAE,MAAM,CAAC;IAAC,GAAG,EAAE,MAAM,CAAA;CAAE,
|
|
1
|
+
{"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../../src/migrations/index.ts"],"names":[],"mappings":"AAIA,eAAO,MAAM,sBAAsB,2g0BA2jBlC,CAAA;AAED,eAAO,MAAM,0DAA0D,uzRAmMtE,CAAA;AAED,eAAO,MAAM,+CAA+C,m5oBAyW3D,CAAA;AAED,eAAO,MAAM,0CAA0C,o1XAgPtD,CAAA;AAED,eAAO,MAAM,8CAA8C,6qCAsB1D,CAAA;AAED,eAAO,MAAM,2CAA2C,w8dA6MvD,CAAA;AAED,eAAO,MAAM,yCAAyC,42BAkBrD,CAAA;AAED,eAAO,MAAM,oDAAoD,4oDA+BhE,CAAA;AAED,eAAO,MAAM,UAAU,EAAE,KAAK,CAAC;IAAE,EAAE,EAAE,MAAM,CAAC;IAAC,GAAG,EAAE,MAAM,CAAA;CAAE,CASzD,CAAA"}
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@fayz-ai/plugin-fiscal-br",
|
|
3
|
-
"version": "0.3.
|
|
3
|
+
"version": "0.3.4",
|
|
4
4
|
"description": "[preview] Brazilian fiscal addon for asynchronous NFC-e emission through PlugNotas",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"sideEffects": false,
|
|
@@ -21,8 +21,8 @@
|
|
|
21
21
|
"react-dom": "^18.0.0 || ^19.0.0"
|
|
22
22
|
},
|
|
23
23
|
"dependencies": {
|
|
24
|
-
"@fayz-ai/core": "^0.
|
|
25
|
-
"@fayz-ai/plugin-financial": "^0.
|
|
24
|
+
"@fayz-ai/core": "^0.33.0",
|
|
25
|
+
"@fayz-ai/plugin-financial": "^0.18.0"
|
|
26
26
|
},
|
|
27
27
|
"devDependencies": {
|
|
28
28
|
"@types/react": "^18.3.0",
|