@fayz-ai/plugin-financial 0.21.2 → 0.22.2
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/components/CancelInvoiceDialog.d.ts +14 -0
- package/dist/components/CancelInvoiceDialog.d.ts.map +1 -0
- package/dist/data/supabase.d.ts.map +1 -1
- package/dist/data/types.d.ts +11 -1
- package/dist/data/types.d.ts.map +1 -1
- package/dist/index.js +1628 -457
- package/dist/index.js.map +1 -1
- package/dist/lib/reconcile.d.ts +8 -5
- package/dist/lib/reconcile.d.ts.map +1 -1
- package/dist/migrations/index.d.ts +4 -1
- package/dist/migrations/index.d.ts.map +1 -1
- package/dist/types.d.ts +47 -0
- package/dist/types.d.ts.map +1 -1
- package/dist/views/InvoiceDetailView.d.ts.map +1 -1
- package/dist/views/InvoiceFormView.d.ts.map +1 -1
- package/dist/views/InvoiceListView.d.ts.map +1 -1
- package/package.json +4 -4
package/dist/lib/reconcile.d.ts
CHANGED
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import type { CreateInvoiceInput, FinancialMovement, ReconcileAdjustment, ReconcileAsBillInput, ReconcileBillItemInput, ReconcilePaymentInput, ReconciliationCandidate, TransactionDirection } from '../types';
|
|
1
|
+
import type { CreateInvoiceInput, FinancialMovement, ReconcileAdjustment, ReconcileAsBillInput, ReconcileBillItemInput, ReconcilePaymentInput, ReconciliationCandidate, TransactionDirection, InstallmentChargeInput } from '../types';
|
|
2
2
|
/** A linha do extrato reduzida ao que a conciliação precisa saber dela. */
|
|
3
3
|
export interface ReconcileLine {
|
|
4
4
|
id: string;
|
|
@@ -116,6 +116,8 @@ export interface ReconcileCapableProvider {
|
|
|
116
116
|
bankMovementId: string;
|
|
117
117
|
matchedMovementId?: string;
|
|
118
118
|
}): Promise<void>;
|
|
119
|
+
/** When present, interest/fine/discount become a line of the bill being settled (V1). */
|
|
120
|
+
addInstallmentCharge?(input: InstallmentChargeInput): Promise<void>;
|
|
119
121
|
}
|
|
120
122
|
/**
|
|
121
123
|
* «Lançar como conta» — a conta nasce classificada, é quitada e a linha do
|
|
@@ -132,10 +134,11 @@ export declare function runReconcileAsBill(provider: ReconcileCapableProvider, l
|
|
|
132
134
|
* «Vincular» — a conta já existe. Em aberto, conciliar a QUITA; já paga,
|
|
133
135
|
* conciliar só amarra.
|
|
134
136
|
*
|
|
135
|
-
*
|
|
136
|
-
*
|
|
137
|
-
*
|
|
138
|
-
*
|
|
137
|
+
* Juros, multa e desconto entram NA PRÓPRIA conta, como no V1: a linha
|
|
138
|
+
* «Juros e multa» é acrescentada e a parcela passa a valer o que o banco
|
|
139
|
+
* cobrou; aí um pagamento só quita tudo. A conta avulsa de juros (sem
|
|
140
|
+
* fornecedor, sem caminho de volta) fica só para o provider que não sabe
|
|
141
|
+
* emendar.
|
|
139
142
|
*/
|
|
140
143
|
export declare function runReconcileLink(provider: ReconcileCapableProvider, line: ReconcileLine, target: {
|
|
141
144
|
movementId: string;
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"reconcile.d.ts","sourceRoot":"","sources":["../../src/lib/reconcile.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EACV,kBAAkB,EAAE,iBAAiB,EAAE,mBAAmB,EAAE,oBAAoB,EAChF,sBAAsB,EAAE,qBAAqB,EAAE,uBAAuB,EAAE,oBAAoB,
|
|
1
|
+
{"version":3,"file":"reconcile.d.ts","sourceRoot":"","sources":["../../src/lib/reconcile.ts"],"names":[],"mappings":"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"}
|
|
@@ -36,7 +36,7 @@ export declare const MIGRATION_034_A_UNIDADE_TEM_CONTA_E_A_PESSOA_TEM_UMA_SO = "
|
|
|
36
36
|
export declare const MIGRATION_035_A_CLASSIFICACAO_CONTABIL_CHEGA_INTEIRA = "-- A classifica\u00E7\u00E3o cont\u00E1bil chega inteira.\n--\n-- Sem esta migration n\u00E3o existe DRE no V2 \u2014 e o motivo n\u00E3o \u00E9 a tela. O DRE do V1\n-- \u00E9 montado sobre DOIS eixos, e os dois se perdiam no caminho:\n--\n-- 1. a NATUREZA da conta (receita \u00D7 despesa)\n-- 2. a CONTA CONT\u00C1BIL DO ITEM (qual linha do DRE aquele lan\u00E7amento \u00E9)\n--\n-- \u2500\u2500 Eixo 1: a natureza estava na coluna errada \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-- O de-para mapeava `chart_of_accounts.account_type \u2192 node_type`. S\u00E3o coisas\n-- diferentes: no V1 `account_type` \u00E9 a NATUREZA (asset|liability|equity|revenue|\n-- expense); no V2 `node_type` \u00E9 a FORMA DO N\u00D3 (group|leaf) e `account_type` \u00E9 a\n-- natureza. O resultado, medido no cluster em 18/09/2026:\n--\n-- node_type = 'expense' \u2192 302 linhas account_type = NULL\n-- node_type = 'revenue' \u2192 38 linhas account_type = NULL\n-- node_type = 'leaf' \u2192 244 linhas account_type preenchido\n--\n-- As 244 corretas s\u00E3o as que algu\u00E9m criou pelo app; as 340 erradas s\u00E3o as que o\n-- porte escreveu. E `upsert_fin_chart_account` **nunca gravou `account_type`** \u2014\n-- a coluna n\u00E3o aparece no INSERT nem no UPDATE dele.\n--\n-- Um DRE sem natureza n\u00E3o separa receita de despesa. N\u00E3o erra por pouco: n\u00E3o\n-- monta.\n--\n-- \u2500\u2500 Eixo 2: a conta do item virava texto opaco \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-- `invoice_items.account_id \u2192 jsonb:financial.v1_account_ref`. A classifica\u00E7\u00E3o\n-- cont\u00E1bil, que no V1 \u00E9 do ITEM e n\u00E3o do movimento, chegava ao V2 como o id\n-- num\u00E9rico cru do V1 dentro de `metadata` \u2014 sem FK, sem join poss\u00EDvel. E n\u00E3o\n-- havia para onde resolver: `public.items` n\u00E3o tinha coluna de conta cont\u00E1bil.\n--\n-- (`plg_financial_invoice_items.chart_account_id` existe e tem ZERO linhas: o\n-- item do V1 cai s\u00F3 em `public.items`; n\u00E3o h\u00E1 split financeiro para ele.)\n--\n-- \u2500\u2500 O terceiro eixo N\u00C3O precisou de nada \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-- A dire\u00E7\u00E3o (receita \u00D7 despesa do documento) parecia perdida tamb\u00E9m \u2014\n-- `invoices.type \u2192 jsonb:financial.v1_type`. N\u00E3o est\u00E1: o writer `order` devolve\n-- em `extra` o T\u00CDTULO em `plg_financial_invoices`, e l\u00E1 `direction` j\u00E1 chega\n-- certa. Medido: 8.175 `receivable` + 2.503 `payable`. O DRE l\u00EA o t\u00EDtulo, n\u00E3o o\n-- pedido, e o eixo j\u00E1 existe.\n--\n-- Idempotente.\n\n-- \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\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-- 1. O item ganha onde guardar a classifica\u00E7\u00E3o\n-- \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\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\nALTER TABLE public.items\n ADD COLUMN IF NOT EXISTS chart_account_id uuid REFERENCES public.plg_financial_chart_of_accounts(id),\n ADD COLUMN IF NOT EXISTS cost_center_id uuid REFERENCES public.plg_financial_cost_centers(id);\n\nCOMMENT ON COLUMN public.items.chart_account_id IS\n 'Conta cont\u00E1bil DESTE item. A classifica\u00E7\u00E3o \u00E9 da linha, n\u00E3o do documento: no '\n 'V1 dois itens da mesma venda podem cair em ramos diferentes do DRE, e \u00E9 essa '\n 'granularidade que o relat\u00F3rio precisa.';\nCOMMENT ON COLUMN public.items.cost_center_id IS\n 'Centro de custo do item. O DRE n\u00E3o usa \u2014 \u00E9 a segunda dimens\u00E3o, para an\u00E1lise '\n 'de custo \u2014 mas perder o dado na entrada seria irrevers\u00EDvel.';\n\n-- A pergunta do DRE \u00E9 \"some os itens por conta, no per\u00EDodo\". Sem \u00EDndice ela\n-- varre `items` inteira por linha do relat\u00F3rio.\nCREATE INDEX IF NOT EXISTS items_chart_account_idx\n ON public.items (tenant_id, chart_account_id) WHERE chart_account_id IS NOT NULL;\n\n-- \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\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-- 2. Conserto do dado j\u00E1 migrado\n-- \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\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-- Devolve a natureza para a coluna certa nas linhas que o porte escreveu. O\n-- teste \u00E9 o pr\u00F3prio vocabul\u00E1rio: `node_type` s\u00F3 pode ser group|leaf|account, e\n-- qualquer outra coisa ali \u00E9 natureza que foi parar no lugar errado.\nUPDATE public.plg_financial_chart_of_accounts\n SET account_type = node_type,\n node_type = 'leaf'\n WHERE node_type IN ('asset', 'liability', 'equity', 'revenue', 'expense')\n AND account_type IS NULL;\n\n-- `node_type` de verdade: \u00E9 grupo quem tem filho. O V1 n\u00E3o tem essa coluna, e\n-- deriv\u00E1-la da pr\u00F3pria \u00E1rvore \u00E9 mais honesto que herdar um default.\nUPDATE public.plg_financial_chart_of_accounts p\n SET node_type = 'group'\n WHERE node_type <> 'group'\n AND EXISTS (SELECT 1 FROM public.plg_financial_chart_of_accounts c WHERE c.parent_id = p.id);\n\n-- \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\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-- 3. Os writers\n-- \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\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\nCREATE OR REPLACE FUNCTION migration.upsert_fin_chart_account(\n p_tenant uuid, p_id uuid, p_row jsonb, p_args jsonb DEFAULT '{}'::jsonb\n) RETURNS jsonb\nLANGUAGE plpgsql SECURITY DEFINER SET search_path TO ''\nAS $function$\nDECLARE\n v_meta jsonb := coalesce(p_row -> 'metadata', '{}'::jsonb);\n v_type text := nullif(btrim(lower(coalesce(p_row ->> 'account_type', ''))), '');\n v_parent uuid := (p_row ->> 'parent_id')::uuid;\n v_existing_tenant uuid;\n v_created boolean := false;\n v_standalone boolean := migration._standalone();\nBEGIN\n IF v_standalone THEN PERFORM migration._assert_fence(p_tenant); PERFORM set_config('migration.writer', 'on', true); END IF;\n IF p_id IS NULL OR nullif(btrim(coalesce(p_row ->> 'name', '')), '') IS NULL THEN\n RETURN jsonb_build_object('status', 'quarantined', 'kind', 'state', 'reason', 'this row needs an id and a name');\n END IF;\n\n -- Sem corre\u00E7\u00E3o silenciosa: uma natureza fora do vocabul\u00E1rio do raz\u00E3o faria a\n -- conta sumir do DRE sem aparecer em lugar nenhum \u2014 nem na receita, nem na\n -- despesa, nem num aviso.\n IF v_type IS NOT NULL AND v_type NOT IN ('asset', 'liability', 'equity', 'revenue', 'expense') THEN\n RETURN jsonb_build_object('status', 'quarantined', 'kind', 'value',\n 'reason', format('natureza de conta desconhecida: %L (o raz\u00E3o conhece asset, liability, equity, revenue e expense)', v_type));\n END IF;\n\n SELECT tenant_id INTO v_existing_tenant FROM public.plg_financial_chart_of_accounts WHERE id = p_id;\n IF v_existing_tenant IS NOT NULL AND v_existing_tenant <> p_tenant THEN\n RETURN jsonb_build_object('status', 'quarantined', 'kind', 'tenant', 'reason', format('%s already exists in another tenant', p_id));\n END IF;\n IF v_existing_tenant IS NULL THEN\n INSERT INTO public.plg_financial_chart_of_accounts\n (id, tenant_id, code, name, node_type, account_type, parent_id, is_active)\n VALUES (p_id, p_tenant, p_row ->> 'code', p_row ->> 'name',\n -- Nasce folha; vira grupo abaixo, quando o primeiro filho chegar.\n coalesce(p_row ->> 'node_type', 'leaf'),\n v_type, v_parent,\n coalesce((p_row ->> 'is_active')::boolean, true));\n v_created := true;\n ELSE\n UPDATE public.plg_financial_chart_of_accounts SET\n code = CASE WHEN p_row ? 'code' THEN p_row ->> 'code' ELSE code END,\n name = CASE WHEN p_row ? 'name' THEN p_row ->> 'name' ELSE name END,\n node_type = CASE WHEN p_row ? 'node_type' THEN coalesce(p_row ->> 'node_type', node_type) ELSE node_type END,\n account_type = coalesce(v_type, account_type),\n parent_id = CASE WHEN p_row ? 'parent_id' THEN v_parent ELSE parent_id END,\n is_active = CASE WHEN p_row ? 'is_active' THEN coalesce((p_row ->> 'is_active')::boolean, true) ELSE is_active END\n WHERE id = p_id;\n END IF;\n\n -- Quem tem filho \u00E9 grupo. Marcar aqui, e n\u00E3o num passe no fim, faz a \u00E1rvore\n -- ficar correta mesmo se a onda parar no meio.\n IF v_parent IS NOT NULL THEN\n UPDATE public.plg_financial_chart_of_accounts\n SET node_type = 'group'\n WHERE id = v_parent AND tenant_id = p_tenant AND node_type <> 'group';\n END IF;\n\n IF v_standalone THEN PERFORM migration._audit(p_tenant, 'public.plg_financial_chart_of_accounts', p_id, jsonb_build_object('reason', CASE WHEN v_created THEN 'created' ELSE 'updated' END)); END IF;\n RETURN jsonb_build_object('status', 'migrated', 'target_id', p_id, 'reason', CASE WHEN v_created THEN 'created' ELSE 'updated' END);\nEND $function$;\n\n-- O writer do item passa a gravar as duas colunas novas. O resto do corpo \u00E9 o\n-- que j\u00E1 estava l\u00E1 \u2014 est\u00E1 reemitido inteiro porque \u00E9 assim que se troca uma\n-- fun\u00E7\u00E3o no Postgres, n\u00E3o porque mudou.\nCREATE OR REPLACE FUNCTION migration.upsert_order_item(\n p_tenant uuid, p_id uuid, p_row jsonb, p_args jsonb DEFAULT '{}'::jsonb\n) RETURNS jsonb\nLANGUAGE plpgsql SECURITY DEFINER SET search_path TO ''\nAS $function$\nDECLARE\n v_meta jsonb := coalesce(p_row -> 'metadata', '{}'::jsonb);\n v_order uuid := (p_row ->> 'order_id')::uuid;\n v_chart uuid := (p_row ->> 'chart_account_id')::uuid;\n v_center uuid := (p_row ->> 'cost_center_id')::uuid;\n v_order_tenant uuid;\n v_existing uuid;\n v_cancelled timestamptz;\n v_created boolean := false;\n v_standalone boolean := migration._standalone();\nBEGIN\n IF v_standalone THEN PERFORM migration._assert_fence(p_tenant); PERFORM set_config('migration.writer', 'on', true); END IF;\n IF p_id IS NULL OR v_order IS NULL THEN\n RETURN jsonb_build_object('status', 'quarantined', 'kind', 'state', 'reason', 'an order line needs an id and an order');\n END IF;\n SELECT tenant_id INTO v_order_tenant FROM public.orders WHERE id = v_order;\n IF v_order_tenant IS NULL THEN\n RETURN jsonb_build_object('status', 'quarantined', 'kind', 'fk', 'reason', format('order %s is not in the destination yet', v_order));\n END IF;\n IF v_order_tenant <> p_tenant THEN\n RETURN jsonb_build_object('status', 'quarantined', 'kind', 'tenant', 'reason', format('order %s belongs to another tenant', v_order));\n END IF;\n\n v_cancelled := nullif(v_meta #>> '{cancellation,at}', '')::timestamptz;\n\n SELECT tenant_id INTO v_existing FROM public.items WHERE id = p_id;\n IF v_existing IS NOT NULL AND v_existing <> p_tenant THEN\n RETURN jsonb_build_object('status', 'quarantined', 'kind', 'tenant', 'reason', format('order line %s already exists in another tenant', p_id));\n END IF;\n IF v_existing IS NULL THEN\n -- `order_id` e S\u00D3 ele: a fatura alcan\u00E7a a linha pelo pedido (items_one_parent).\n INSERT INTO public.items (id, tenant_id, order_id, product_id, name, description,\n quantity, unit_price, discount, total, sort_order,\n cancelled_at, cancel_reason, chart_account_id, cost_center_id, metadata)\n VALUES (p_id, p_tenant, v_order, (p_row ->> 'product_id')::uuid,\n coalesce(nullif(btrim(coalesce(p_row ->> 'name', '')), ''), 'Item'),\n coalesce(p_row ->> 'description', nullif(btrim(coalesce(p_row ->> 'name', '')), '')),\n greatest(coalesce((p_row ->> 'quantity')::numeric, 1), 0.0001),\n round(coalesce((p_row ->> 'unit_price')::numeric, 0), 2),\n round(coalesce((p_row ->> 'discount')::numeric, 0), 2),\n round(coalesce((p_row ->> 'total')::numeric, 0), 2),\n coalesce((p_row ->> 'sort_order')::integer, 0),\n v_cancelled, v_meta #>> '{cancellation,reason}',\n v_chart, v_center, v_meta);\n v_created := true;\n ELSE\n UPDATE public.items SET\n order_id = v_order,\n product_id = CASE WHEN p_row ? 'product_id' THEN (p_row ->> 'product_id')::uuid ELSE product_id END,\n name = CASE WHEN p_row ? 'name' THEN coalesce(nullif(btrim(p_row ->> 'name'), ''), name) ELSE name END,\n quantity = CASE WHEN p_row ? 'quantity' THEN greatest(coalesce((p_row ->> 'quantity')::numeric, 1), 0.0001) ELSE quantity END,\n unit_price = CASE WHEN p_row ? 'unit_price' THEN round(coalesce((p_row ->> 'unit_price')::numeric, 0), 2) ELSE unit_price END,\n discount = CASE WHEN p_row ? 'discount' THEN round(coalesce((p_row ->> 'discount')::numeric, 0), 2) ELSE discount END,\n total = CASE WHEN p_row ? 'total' THEN round(coalesce((p_row ->> 'total')::numeric, 0), 2) ELSE total END,\n cancelled_at = coalesce(v_cancelled, cancelled_at),\n cancel_reason = coalesce(v_meta #>> '{cancellation,reason}', cancel_reason),\n -- `coalesce` e n\u00E3o CASE: a classifica\u00E7\u00E3o \u00E9 enriquecimento. Uma reoferta\n -- sem a coluna n\u00E3o deve apagar a conta que j\u00E1 est\u00E1 l\u00E1.\n chart_account_id = coalesce(v_chart, chart_account_id),\n cost_center_id = coalesce(v_center, cost_center_id),\n metadata = migration._merge_meta(metadata, v_meta)\n WHERE id = p_id;\n END IF;\n\n IF v_standalone THEN PERFORM migration._audit(p_tenant, 'public.items', p_id, jsonb_build_object('reason', CASE WHEN v_created THEN 'created' ELSE 'updated' END)); END IF;\n RETURN jsonb_build_object('status', 'migrated', 'target_id', p_id, 'reason', CASE WHEN v_created THEN 'created' ELSE 'updated' END);\nEND $function$;\n\n-- \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\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-- 4. O de-para\n-- \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\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\nSET LOCAL migration.allowlist_unlock = 'on';\n\n-- `account_type` para `account_type`. O mapa anterior mandava a natureza para\n-- `node_type`, e \u00E9 a corre\u00E7\u00E3o que faz o DRE existir.\nSELECT migration._allow(\n 'chart_of_accounts', 'public.plg_financial_chart_of_accounts', 'financial',\n 'fin_chart_account', '{}'::jsonb, ARRAY[]::text[],\n jsonb_build_object(\n 'uuid', 'id',\n 'code', 'code',\n 'name', 'name',\n 'account_type', 'account_type',\n 'parent_id', 'parent_id@chart_of_accounts',\n 'active', 'is_active'\n ),\n 'Plano de contas. `account_type` \u00E9 a NATUREZA (o eixo do DRE); `node_type` \u00E9 '\n 'derivado da \u00E1rvore pelo writer \u2014 quem tem filho \u00E9 grupo.'\n);\n\n-- O item passa a resolver a conta cont\u00E1bil e o centro de custo em vez de\n-- carreg\u00E1-los como id cru do V1 em metadata.\nSELECT migration._allow(\n 'invoice_items', 'public.items', 'financial',\n 'order_item', '{}'::jsonb, ARRAY['unit_price', 'total_amount'],\n jsonb_build_object(\n 'uuid', 'id',\n 'invoice_id', 'order_id@invoices:public.orders',\n 'description', 'name',\n 'quantity', 'quantity',\n 'unit_price', 'unit_price',\n 'discount', 'discount',\n 'total_amount', 'total',\n 'additional', 'jsonb:financial.additional',\n 'product_id', 'product_id@products',\n 'service_id', 'product_id@services',\n 'account_id', 'chart_account_id@chart_of_accounts',\n 'cost_center_id', 'cost_center_id@cost_centers',\n 'item_type', 'jsonb:item_type',\n 'canceled_at', 'jsonb:cancellation.at',\n 'cancel_reason', 'jsonb:cancellation.reason',\n 'appointment_id', 'jsonb:agenda.v1_appointment_ref',\n 'execution_date', 'jsonb:execution.date',\n 'execution_start_time', 'jsonb:execution.start_time',\n 'execution_end_time', 'jsonb:execution.end_time',\n 'execution_photos', '-',\n 'executed_by_professional_id', 'jsonb:execution.v1_professional_ref',\n 'is_executed', 'jsonb:execution.is_executed',\n 'gift_card_id', 'jsonb:v1_gift_card_ref',\n 'service_package_id', 'jsonb:v1_package_ref'\n ),\n 'A linha da venda. `account_id` \u00E9 a classifica\u00E7\u00E3o cont\u00E1bil DO ITEM \u2014 o eixo '\n 'que o DRE agrupa; antes ela vinha como id cru do V1 em metadata.'\n);\n";
|
|
37
37
|
export declare const MIGRATION_036_A_FUSAO_PARA_DE_DOBRAR_A_PROPRIA_MEMORIA = "-- A fus\u00E3o para de dobrar a pr\u00F3pria mem\u00F3ria.\n--\n-- Corre\u00E7\u00E3o de um defeito que eu introduzi na 048 e que derrubou o cluster quatro\n-- vezes em 18/09/2026.\n--\n-- \u2500\u2500 O que eu escrevi \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\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\n--\n-- Na fus\u00E3o de conta de pessoa, para registrar de onde a conta veio:\n--\n-- metadata = migration._merge_meta(\n-- metadata,\n-- migration._deep_set(v_meta, ARRAY['migration','merged_from'],\n-- coalesce(metadata #> '{migration,merged_from}', '[]'::jsonb)\n-- || jsonb_build_array(...)))\n--\n-- Leio o array que j\u00E1 est\u00E1 na linha, acrescento um elemento \u2014 e entrego o\n-- resultado a `_merge_meta`, que **concatena com o array que j\u00E1 est\u00E1 na linha de\n-- novo**. Cada oferta n\u00E3o acrescenta um: ela DOBRA.\n--\n-- oferta 1 \u2192 1 elemento\n-- oferta 5 \u2192 16\n-- oferta 10 \u2192 512\n-- oferta 18 \u2192 262.143 \u2190 medido\n--\n-- \u2500\u2500 O que isso custou \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\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\n--\n-- Medido antes deste conserto, no tenant Pertinho do C\u00E9u:\n--\n-- maior merged_from 262.143 elementos (2\u00B9\u2078 \u2212 1)\n-- maior metadata 1.378 kB numa \u00FAnica linha\n--\n-- Montar e mesclar um jsonb desse tamanho, uma vez por linha, dentro de um\n-- statement, num cluster com `shared_buffers` de 224 MB, mata o backend. E o\n-- sintoma n\u00E3o apontava para c\u00E1: a conex\u00E3o ca\u00EDa no meio de `account_central`, o\n-- transporte reportava \"server closed the connection unexpectedly\", e eu passei\n-- duas rodadas reduzindo o tamanho do lote \u2014 de 2.000 para 250, depois para 100.\n-- Nenhuma ajudou, porque 417 linhas nunca foram o problema.\n--\n-- \u2500\u2500 O conserto \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\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\n--\n-- `merged_from` sai. N\u00E3o vira \"append idempotente\", n\u00E3o vira \"array com limite\":\n-- sai inteiro, porque ele nunca deveria ter existido \u2014 **o ledger j\u00E1 registra a\n-- fus\u00E3o**, e melhor: dois `source_numeric_id` distintos apontando para o mesmo\n-- `target_id` \u00C9 a defini\u00E7\u00E3o de fus\u00E3o, \u00E9 consult\u00E1vel, e \u00E9 o lugar onde o resto\n-- deste schema j\u00E1 procura essa resposta.\n--\n-- select source_numeric_id, target_id\n-- from migration.ledger\n-- where source_table = 'account_central' and target_table = 'public.plg_financial_accounts'\n-- order by target_id;\n--\n-- Guardar a mesma informa\u00E7\u00E3o num jsonb ao lado era a segunda c\u00F3pia da regra \u2014\n-- exatamente o que o cabe\u00E7alho do `sdkmig.py` diz para n\u00E3o fazer. A segunda\n-- c\u00F3pia divergiu no primeiro dia, e de forma exponencial.\n--\n-- Idempotente.\n\n-- \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\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-- 1. Desarmar o que j\u00E1 explodiu\n-- \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\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-- Primeiro o dado, depois a fun\u00E7\u00E3o: enquanto as linhas gordas estiverem l\u00E1,\n-- qualquer rodada que as toque derruba o banco de novo \u2014 inclusive a que\n-- aplicaria esta migration mais adiante.\nUPDATE public.plg_financial_accounts\n SET metadata = (coalesce(metadata, '{}'::jsonb) #- '{migration,merged_from}')\n WHERE metadata #> '{migration,merged_from}' IS NOT NULL;\n\n-- \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\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-- 2. O writer para de acumular\n-- \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\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\nCREATE OR REPLACE FUNCTION migration.upsert_fin_account(\n p_tenant uuid, p_id uuid, p_row jsonb, p_args jsonb DEFAULT '{}'::jsonb\n) RETURNS jsonb\nLANGUAGE plpgsql SECURITY DEFINER SET search_path TO ''\nAS $function$\nDECLARE\n v_meta jsonb := coalesce(p_row -> 'metadata', '{}'::jsonb);\n v_kind text := coalesce(p_row ->> 'kind', 'bank');\n v_person uuid;\n v_unit uuid := (p_row ->> 'unit_id')::uuid;\n v_merge_into uuid;\n v_existing_tenant uuid;\n v_created boolean := false;\n v_standalone boolean := migration._standalone();\nBEGIN\n IF v_standalone THEN PERFORM migration._assert_fence(p_tenant); PERFORM set_config('migration.writer', 'on', true); END IF;\n IF p_id IS NULL OR nullif(btrim(coalesce(p_row ->> 'name', '')), '') IS NULL THEN\n RETURN jsonb_build_object('status', 'quarantined', 'kind', 'state', 'reason', 'this row needs an id and a name');\n END IF;\n IF v_kind NOT IN ('bank', 'cash_physical', 'cash_session', 'card_receivable_credit',\n 'card_receivable_debit', 'card_payable', 'person', 'system', 'unit') THEN\n RETURN jsonb_build_object('status', 'quarantined', 'kind', 'value',\n 'reason', format('unknown account kind %L \u2014 V1 account_type does not map onto the ledger''s nine', v_kind));\n END IF;\n\n IF v_kind = 'person' THEN\n SELECT l.target_id INTO v_person\n FROM migration.ledger l\n WHERE l.tenant_id = p_tenant\n AND l.source_table = (v_meta #>> '{v1,reference_table}')\n AND l.source_numeric_id = nullif(v_meta #>> '{v1,reference_id}', '')::bigint\n AND l.target_table = 'public.people'\n AND l.status = 'migrated'\n LIMIT 1;\n IF v_person IS NULL THEN\n RETURN jsonb_build_object('status', 'quarantined', 'kind', 'fk',\n 'reason', format('conta de pessoa cuja pessoa ainda n\u00E3o migrou (%s #%s)',\n coalesce(v_meta #>> '{v1,reference_table}', '<sem tabela>'),\n coalesce(v_meta #>> '{v1,reference_id}', '<sem id>')));\n END IF;\n\n SELECT id INTO v_merge_into\n FROM public.plg_financial_accounts\n WHERE tenant_id = p_tenant AND person_id = v_person AND id <> p_id\n LIMIT 1;\n END IF;\n\n IF v_kind = 'unit' AND v_unit IS NULL THEN\n SELECT l.target_id INTO v_unit\n FROM migration.ledger l\n WHERE l.tenant_id = p_tenant\n AND l.source_table = coalesce(v_meta #>> '{v1,reference_table}', 'companies')\n AND l.source_numeric_id = nullif(v_meta #>> '{v1,reference_id}', '')::bigint\n AND l.target_table = 'app.units'\n AND l.status = 'migrated'\n LIMIT 1;\n IF v_unit IS NULL THEN\n RETURN jsonb_build_object('status', 'quarantined', 'kind', 'fk',\n 'reason', format('conta de unidade cuja unidade ainda n\u00E3o migrou (%s #%s)',\n coalesce(v_meta #>> '{v1,reference_table}', 'companies'),\n coalesce(v_meta #>> '{v1,reference_id}', '<sem id>')));\n END IF;\n END IF;\n\n IF v_merge_into IS NOT NULL THEN\n -- A fus\u00E3o N\u00C3O escreve hist\u00F3rico aqui. Quem registra que duas contas do V1\n -- viraram uma no V2 \u00E9 o ledger: dois `source_numeric_id` apontando para o\n -- mesmo `target_id`. Acumular a mesma informa\u00E7\u00E3o num array de metadata foi o\n -- defeito que dobrava de tamanho a cada oferta e derrubou o cluster.\n UPDATE public.plg_financial_accounts\n SET is_active = is_active OR coalesce((p_row ->> 'is_active')::boolean, true)\n WHERE id = v_merge_into;\n IF v_standalone THEN PERFORM migration._audit(p_tenant, 'public.plg_financial_accounts', v_merge_into, jsonb_build_object('reason', 'merged')); END IF;\n RETURN jsonb_build_object('status', 'migrated', 'target_id', v_merge_into, 'merged', true,\n 'reason', 'conta fundida na conta \u00FAnica da pessoa (o V2 admite uma por pessoa)');\n END IF;\n\n SELECT tenant_id INTO v_existing_tenant FROM public.plg_financial_accounts WHERE id = p_id;\n IF v_existing_tenant IS NOT NULL AND v_existing_tenant <> p_tenant THEN\n RETURN jsonb_build_object('status', 'quarantined', 'kind', 'tenant', 'reason', format('%s already exists in another tenant', p_id));\n END IF;\n IF v_existing_tenant IS NULL THEN\n INSERT INTO public.plg_financial_accounts (id, tenant_id, unit_id, kind, code, name, bank_name, settlement_days, is_active, person_id, metadata)\n VALUES (p_id, p_tenant, v_unit, v_kind,\n p_row ->> 'code', p_row ->> 'name', p_row ->> 'bank_name',\n (p_row ->> 'settlement_days')::integer,\n coalesce((p_row ->> 'is_active')::boolean, true), v_person, v_meta);\n v_created := true;\n ELSE\n UPDATE public.plg_financial_accounts SET\n unit_id = coalesce(v_unit, unit_id),\n kind = CASE WHEN p_row ? 'kind' THEN v_kind ELSE kind END,\n person_id = CASE WHEN p_row ? 'kind' THEN v_person ELSE person_id END,\n code = CASE WHEN p_row ? 'code' THEN p_row ->> 'code' ELSE code END,\n name = CASE WHEN p_row ? 'name' THEN p_row ->> 'name' ELSE name END,\n bank_name = CASE WHEN p_row ? 'bank_name' THEN p_row ->> 'bank_name' ELSE bank_name END,\n settlement_days = CASE WHEN p_row ? 'settlement_days' THEN (p_row ->> 'settlement_days')::integer ELSE settlement_days END,\n is_active = CASE WHEN p_row ? 'is_active' THEN coalesce((p_row ->> 'is_active')::boolean, true) ELSE is_active END,\n metadata = migration._merge_meta(metadata, v_meta)\n WHERE id = p_id;\n END IF;\n IF v_standalone THEN PERFORM migration._audit(p_tenant, 'public.plg_financial_accounts', p_id, jsonb_build_object('reason', CASE WHEN v_created THEN 'created' ELSE 'updated' END)); END IF;\n RETURN jsonb_build_object('status', 'migrated', 'target_id', p_id, 'reason', CASE WHEN v_created THEN 'created' ELSE 'updated' END);\nEND $function$;\n";
|
|
38
38
|
export declare const MIGRATION_037_A_VENDA_DE_BALCAO_TEM_CONTA = "-- A venda de balc\u00E3o tem conta.\n--\n-- Terceira esp\u00E9cie de conta que o V1 tem e o raz\u00E3o do V2 n\u00E3o tinha \u2014 depois de\n-- `unit` (a conta da casa, migration 048) vem `unidentified`: a conta da venda\n-- para quem n\u00E3o deixou cadastro.\n--\n-- No V1 ela \u00E9 uma linha de `account_central` com `account_type = 'client'` e\n-- `reference_id = 0`. O tipo diz \"pessoa\"; a refer\u00EAncia \u00E9 o sentinela de \"n\u00E3o\n-- h\u00E1 pessoa\". O V2 n\u00E3o aceita a combina\u00E7\u00E3o, e com raz\u00E3o:\n--\n-- CHECK ((kind = 'person') = (person_id IS NOT NULL))\n--\n-- Uma conta de pessoa sem pessoa \u00E9 dinheiro pendurado em ningu\u00E9m. O CHECK est\u00E1\n-- certo; o que faltava era a palavra para o caso em que **n\u00E3o h\u00E1 contraparte e\n-- isso \u00E9 o fato**, n\u00E3o uma falha de cadastro.\n--\n-- \u2500\u2500 Por que esp\u00E9cie pr\u00F3pria, e n\u00E3o `system` \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-- `system` j\u00E1 tem dono: \u00E9 a conta do plano de contas, identificada por c\u00F3digo\n-- (`CHECK (kind <> 'system' OR code IS NOT NULL)`). Jogar a venda de balc\u00E3o l\u00E1\n-- dentro faria dois conceitos dividirem um balde que nenhum relat\u00F3rio separa\n-- depois \u2014 e o de balc\u00E3o n\u00E3o \u00E9 detalhe: num restaurante ele \u00E9 a maior parte do\n-- movimento.\n--\n-- `unidentified` diz a verdade: existe contraparte, ela comprou, e ela n\u00E3o se\n-- identificou. \u00C9 diferente de `system` (n\u00E3o h\u00E1 contraparte) e de `person` (h\u00E1, e\n-- sabemos quem).\n--\n-- \u2500\u2500 O que estava preso \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\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\n--\n-- Medido em 18/09/2026 no tenant Pertinho do C\u00E9u, antes deste arquivo:\n--\n-- account_central em quarentena por `clients #0` 5\n-- financial_movements travados atr\u00E1s delas 110\n-- valor R$ 15.102,78\n--\n-- A asser\u00E7\u00E3o de paridade (`make parity`) apontava exatamente isso: contas a\n-- pagar com \u0394 \u221210.218,30 contra a origem, e a cadeia inteira levava a uma conta.\n--\n-- Idempotente.\n\n-- \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\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-- 1. A esp\u00E9cie\n-- \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\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\nDO $$\nBEGIN\n ALTER TABLE public.plg_financial_accounts DROP CONSTRAINT IF EXISTS plg_financial_accounts_kind_check;\n ALTER TABLE public.plg_financial_accounts ADD CONSTRAINT plg_financial_accounts_kind_check\n CHECK (kind = ANY (ARRAY['bank', 'cash_physical', 'cash_session',\n 'card_receivable_credit', 'card_receivable_debit',\n 'card_payable', 'person', 'system', 'unit',\n 'unidentified']));\nEND $$;\n\nCOMMENT ON COLUMN public.plg_financial_accounts.kind IS\n 'bank | cash_physical | cash_session | card_receivable_credit | '\n 'card_receivable_debit | card_payable | person | system | unit | unidentified. '\n '`unidentified` \u00E9 a contraparte que comprou e n\u00E3o se identificou (venda de '\n 'balc\u00E3o): diferente de `system`, onde n\u00E3o h\u00E1 contraparte, e de `person`, onde '\n 'h\u00E1 e sabemos quem.';\n\n-- \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\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-- 2. O writer reconhece o sentinela\n-- \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\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-- S\u00F3 a perna que muda: quando a conta \u00E9 de pessoa e a refer\u00EAncia da origem \u00E9 o\n-- sentinela (`reference_id = 0`) ou aponta para linha que n\u00E3o existe mais, ela\n-- deixa de ser `person` e passa a ser `unidentified` \u2014 em vez de recusar a conta\n-- e, com ela, todo movimento que a cita.\n--\n-- A distin\u00E7\u00E3o importa e est\u00E1 no c\u00F3digo: refer\u00EAncia AUSENTE (0) vira\n-- `unidentified`; refer\u00EAncia PRESENTE cuja pessoa ainda n\u00E3o migrou continua\n-- caindo em quarentena de `fk`, porque a\u00ED a pessoa existe e a ordem \u00E9 que est\u00E1\n-- errada \u2014 e a segunda passada resolve.\nCREATE OR REPLACE FUNCTION migration.upsert_fin_account(\n p_tenant uuid, p_id uuid, p_row jsonb, p_args jsonb DEFAULT '{}'::jsonb\n) RETURNS jsonb\nLANGUAGE plpgsql SECURITY DEFINER SET search_path TO ''\nAS $function$\nDECLARE\n v_meta jsonb := coalesce(p_row -> 'metadata', '{}'::jsonb);\n v_kind text := coalesce(p_row ->> 'kind', 'bank');\n v_ref_id bigint := nullif(v_meta #>> '{v1,reference_id}', '')::bigint;\n v_ref_table text := v_meta #>> '{v1,reference_table}';\n v_person uuid;\n v_unit uuid := (p_row ->> 'unit_id')::uuid;\n v_merge_into uuid;\n v_existing_tenant uuid;\n v_created boolean := false;\n v_standalone boolean := migration._standalone();\nBEGIN\n IF v_standalone THEN PERFORM migration._assert_fence(p_tenant); PERFORM set_config('migration.writer', 'on', true); END IF;\n IF p_id IS NULL OR nullif(btrim(coalesce(p_row ->> 'name', '')), '') IS NULL THEN\n RETURN jsonb_build_object('status', 'quarantined', 'kind', 'state', 'reason', 'this row needs an id and a name');\n END IF;\n\n -- O sentinela ANTES da checagem de vocabul\u00E1rio: `clients#0` n\u00E3o \u00E9 uma conta de\n -- pessoa mal formada, \u00E9 uma conta sem contraparte.\n IF v_kind = 'person' AND coalesce(v_ref_id, 0) = 0 THEN\n v_kind := 'unidentified';\n END IF;\n\n IF v_kind NOT IN ('bank', 'cash_physical', 'cash_session', 'card_receivable_credit',\n 'card_receivable_debit', 'card_payable', 'person', 'system', 'unit',\n 'unidentified') THEN\n RETURN jsonb_build_object('status', 'quarantined', 'kind', 'value',\n 'reason', format('unknown account kind %L \u2014 V1 account_type does not map onto the ledger''s ten', v_kind));\n END IF;\n\n IF v_kind = 'person' THEN\n SELECT l.target_id INTO v_person\n FROM migration.ledger l\n WHERE l.tenant_id = p_tenant\n AND l.source_table = v_ref_table\n AND l.source_numeric_id = v_ref_id\n AND l.target_table = 'public.people'\n AND l.status = 'migrated'\n LIMIT 1;\n IF v_person IS NULL THEN\n RETURN jsonb_build_object('status', 'quarantined', 'kind', 'fk',\n 'reason', format('conta de pessoa cuja pessoa ainda n\u00E3o migrou (%s #%s)',\n coalesce(v_ref_table, '<sem tabela>'), coalesce(v_ref_id::text, '<sem id>')));\n END IF;\n\n SELECT id INTO v_merge_into\n FROM public.plg_financial_accounts\n WHERE tenant_id = p_tenant AND person_id = v_person AND id <> p_id\n LIMIT 1;\n END IF;\n\n IF v_kind = 'unit' AND v_unit IS NULL THEN\n SELECT l.target_id INTO v_unit\n FROM migration.ledger l\n WHERE l.tenant_id = p_tenant\n AND l.source_table = coalesce(v_ref_table, 'companies')\n AND l.source_numeric_id = v_ref_id\n AND l.target_table = 'app.units'\n AND l.status = 'migrated'\n LIMIT 1;\n IF v_unit IS NULL THEN\n RETURN jsonb_build_object('status', 'quarantined', 'kind', 'fk',\n 'reason', format('conta de unidade cuja unidade ainda n\u00E3o migrou (%s #%s)',\n coalesce(v_ref_table, 'companies'), coalesce(v_ref_id::text, '<sem id>')));\n END IF;\n END IF;\n\n IF v_merge_into IS NOT NULL THEN\n UPDATE public.plg_financial_accounts\n SET is_active = is_active OR coalesce((p_row ->> 'is_active')::boolean, true)\n WHERE id = v_merge_into;\n IF v_standalone THEN PERFORM migration._audit(p_tenant, 'public.plg_financial_accounts', v_merge_into, jsonb_build_object('reason', 'merged')); END IF;\n RETURN jsonb_build_object('status', 'migrated', 'target_id', v_merge_into, 'merged', true,\n 'reason', 'conta fundida na conta \u00FAnica da pessoa (o V2 admite uma por pessoa)');\n END IF;\n\n SELECT tenant_id INTO v_existing_tenant FROM public.plg_financial_accounts WHERE id = p_id;\n IF v_existing_tenant IS NOT NULL AND v_existing_tenant <> p_tenant THEN\n RETURN jsonb_build_object('status', 'quarantined', 'kind', 'tenant', 'reason', format('%s already exists in another tenant', p_id));\n END IF;\n IF v_existing_tenant IS NULL THEN\n INSERT INTO public.plg_financial_accounts (id, tenant_id, unit_id, kind, code, name, bank_name, settlement_days, is_active, person_id, metadata)\n VALUES (p_id, p_tenant, v_unit, v_kind,\n p_row ->> 'code', p_row ->> 'name', p_row ->> 'bank_name',\n (p_row ->> 'settlement_days')::integer,\n coalesce((p_row ->> 'is_active')::boolean, true), v_person, v_meta);\n v_created := true;\n ELSE\n UPDATE public.plg_financial_accounts SET\n unit_id = coalesce(v_unit, unit_id),\n kind = CASE WHEN p_row ? 'kind' THEN v_kind ELSE kind END,\n person_id = CASE WHEN p_row ? 'kind' THEN v_person ELSE person_id END,\n code = CASE WHEN p_row ? 'code' THEN p_row ->> 'code' ELSE code END,\n name = CASE WHEN p_row ? 'name' THEN p_row ->> 'name' ELSE name END,\n bank_name = CASE WHEN p_row ? 'bank_name' THEN p_row ->> 'bank_name' ELSE bank_name END,\n settlement_days = CASE WHEN p_row ? 'settlement_days' THEN (p_row ->> 'settlement_days')::integer ELSE settlement_days END,\n is_active = CASE WHEN p_row ? 'is_active' THEN coalesce((p_row ->> 'is_active')::boolean, true) ELSE is_active END,\n metadata = migration._merge_meta(metadata, v_meta)\n WHERE id = p_id;\n END IF;\n IF v_standalone THEN PERFORM migration._audit(p_tenant, 'public.plg_financial_accounts', p_id, jsonb_build_object('reason', CASE WHEN v_created THEN 'created' ELSE 'updated' END)); END IF;\n RETURN jsonb_build_object('status', 'migrated', 'target_id', p_id, 'reason', CASE WHEN v_created THEN 'created' ELSE 'updated' END);\nEND $function$;\n";
|
|
39
|
-
export declare const MIGRATION_038_A_LINHA_DA_FATURA_ALCANCA_QUEM_VEIO_PELO_PEDIDO = "-- A linha da fatura alcan\u00E7a quem veio pelo pedido.\n--\n-- `v_financial_invoice_lines` liga o item ao t\u00EDtulo por `items.invoice_id`. S\u00F3\n-- que `items_one_parent` \u2014 `CHECK (num_nonnulls(order_id, invoice_id,\n-- fulfillment_id, quote_id) = 1)` \u2014 diz que um item tem EXATAMENTE UM pai. Um\n-- item que nasceu numa venda tem `order_id` preenchido e `invoice_id` **nulo**,\n-- por constru\u00E7\u00E3o, e a view nunca o enxerga.\n--\n-- Isso n\u00E3o \u00E9 um caso de borda: \u00E9 todo item que veio de venda, e \u00E9 100% do que o\n-- porte do V1 traz. Medido em 18/09/2026 no tenant Pertinho do C\u00E9u:\n--\n-- public.items do tenant 5.420\n-- \u2026com invoice_id preenchido 0\n-- linhas em v_financial_invoice_lines 0\n--\n-- Cinco mil e quatrocentas linhas de fatura, e a view do financeiro devolvia\n-- zero. Qualquer tela ou relat\u00F3rio constru\u00EDdo sobre ela \u2014 an\u00E1lise de custo,\n-- encargos por conta cont\u00E1bil, DRE por natureza de linha \u2014 nasce vazio e parece\n-- \"cliente sem dado\" em vez de \"consulta pelo caminho errado\".\n--\n-- \u2500\u2500 O conserto \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\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\n--\n-- O t\u00EDtulo alcan\u00E7a a linha por DOIS caminhos, e a view passa a cobrir os dois:\n--\n-- direto items.invoice_id = f.id (linha criada no t\u00EDtulo)\n-- pelo pedido items.order_id = f.order_id (linha criada na venda)\n--\n-- \u00C9 `UNION ALL` e n\u00E3o um `JOIN ... ON a OR b` de prop\u00F3sito: a forma com OR faz o\n-- planner cair em nested loop sobre as duas tabelas inteiras, e esta view \u00E9 lida\n-- por tela. Dois ramos disjuntos (o segundo exige `invoice_id IS NULL`) d\u00E3o dois\n-- index scans e nenhuma linha contada duas vezes.\n--\n-- `chart_account_id` e `cost_center_id` passam a vir com `coalesce`: a extens\u00E3o\n-- financeira (`plg_financial_invoice_items`) vence quando existe, e o item do\n-- n\u00FAcleo responde quando n\u00E3o \u2014 que \u00E9 onde o porte grava a classifica\u00E7\u00E3o desde a\n-- migration 049 do spine.\n--\n-- Idempotente. Mesma lista de colunas, mesma ordem \u2014 `CREATE OR REPLACE VIEW`\n-- exige, e manter a assinatura \u00E9 o que deixa esta corre\u00E7\u00E3o ser um replace e n\u00E3o\n-- um drop com depend\u00EAncia quebrada.\n\nCREATE OR REPLACE VIEW public.v_financial_invoice_lines AS\n-- \u2500\u2500 caminho 1: a linha nasceu no pr\u00F3prio t\u00EDtulo \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\nSELECT i.id AS item_id,\n i.tenant_id,\n i.unit_id,\n i.invoice_id,\n f.number AS invoice_number,\n f.direction,\n f.issue_date,\n f.cancelled_at,\n i.product_id,\n i.name,\n i.description,\n i.quantity,\n i.unit_price,\n i.discount,\n i.total,\n COALESCE(x.chart_account_id, i.chart_account_id) AS chart_account_id,\n COALESCE(x.cost_center_id, i.cost_center_id) AS cost_center_id,\n x.tax_amount,\n x.cfop,\n x.ncm,\n a.from_item_id AS billed_from_order_item_id,\n a.quantity AS billed_quantity\n FROM public.items i\n JOIN public.plg_financial_invoices f ON f.id = i.invoice_id\n LEFT JOIN public.plg_financial_invoice_items x ON x.item_id = i.id\n LEFT JOIN public.item_associations a ON a.to_item_id = i.id\n WHERE i.invoice_id IS NOT NULL\n\nUNION ALL\n\n-- \u2500\u2500 caminho 2: a linha nasceu na venda, e o t\u00EDtulo veio dela \u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\n-- `i.invoice_id IS NULL` mant\u00E9m os dois ramos disjuntos: sem isso, um item com\n-- os dois preenchidos apareceria duas vezes e dobraria o valor da conta. Hoje o\n-- CHECK impede que isso exista, e a condi\u00E7\u00E3o \u00E9 o cinto que sobrevive a um dia em\n-- que o CHECK mude.\nSELECT i.id AS item_id,\n i.tenant_id,\n i.unit_id,\n f.id AS invoice_id,\n f.number AS invoice_number,\n f.direction,\n f.issue_date,\n f.cancelled_at,\n i.product_id,\n i.name,\n i.description,\n i.quantity,\n i.unit_price,\n i.discount,\n i.total,\n COALESCE(x.chart_account_id, i.chart_account_id) AS chart_account_id,\n COALESCE(x.cost_center_id, i.cost_center_id) AS cost_center_id,\n x.tax_amount,\n x.cfop,\n x.ncm,\n a.from_item_id AS billed_from_order_item_id,\n a.quantity AS billed_quantity\n FROM public.items i\n JOIN public.plg_financial_invoices f ON f.order_id = i.order_id\n LEFT JOIN public.plg_financial_invoice_items x ON x.item_id = i.id\n LEFT JOIN public.item_associations a ON a.to_item_id = i.id\n WHERE i.invoice_id IS NULL AND i.order_id IS NOT NULL\n -- Only for an invoice with no lines of its own. An invoice billed from\n -- PART of an order (financial_invoice_from_lines) has its own lines, and\n -- without this guard it also listed every line of the order \u2014 the whole\n -- sale reported as billed, and summed twice.\n AND NOT EXISTS (SELECT 1 FROM public.items own WHERE own.invoice_id = f.id)
|
|
39
|
+
export declare const MIGRATION_038_A_LINHA_DA_FATURA_ALCANCA_QUEM_VEIO_PELO_PEDIDO = "-- A linha da fatura alcan\u00E7a quem veio pelo pedido.\n--\n-- `v_financial_invoice_lines` liga o item ao t\u00EDtulo por `items.invoice_id`. S\u00F3\n-- que `items_one_parent` \u2014 `CHECK (num_nonnulls(order_id, invoice_id,\n-- fulfillment_id, quote_id) = 1)` \u2014 diz que um item tem EXATAMENTE UM pai. Um\n-- item que nasceu numa venda tem `order_id` preenchido e `invoice_id` **nulo**,\n-- por constru\u00E7\u00E3o, e a view nunca o enxerga.\n--\n-- Isso n\u00E3o \u00E9 um caso de borda: \u00E9 todo item que veio de venda, e \u00E9 100% do que o\n-- porte do V1 traz. Medido em 18/09/2026 no tenant Pertinho do C\u00E9u:\n--\n-- public.items do tenant 5.420\n-- \u2026com invoice_id preenchido 0\n-- linhas em v_financial_invoice_lines 0\n--\n-- Cinco mil e quatrocentas linhas de fatura, e a view do financeiro devolvia\n-- zero. Qualquer tela ou relat\u00F3rio constru\u00EDdo sobre ela \u2014 an\u00E1lise de custo,\n-- encargos por conta cont\u00E1bil, DRE por natureza de linha \u2014 nasce vazio e parece\n-- \"cliente sem dado\" em vez de \"consulta pelo caminho errado\".\n--\n-- \u2500\u2500 O conserto \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\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\n--\n-- O t\u00EDtulo alcan\u00E7a a linha por DOIS caminhos, e a view passa a cobrir os dois:\n--\n-- direto items.invoice_id = f.id (linha criada no t\u00EDtulo)\n-- pelo pedido items.order_id = f.order_id (linha criada na venda)\n--\n-- \u00C9 `UNION ALL` e n\u00E3o um `JOIN ... ON a OR b` de prop\u00F3sito: a forma com OR faz o\n-- planner cair em nested loop sobre as duas tabelas inteiras, e esta view \u00E9 lida\n-- por tela. Dois ramos disjuntos (o segundo exige `invoice_id IS NULL`) d\u00E3o dois\n-- index scans e nenhuma linha contada duas vezes.\n--\n-- `chart_account_id` e `cost_center_id` passam a vir com `coalesce`: a extens\u00E3o\n-- financeira (`plg_financial_invoice_items`) vence quando existe, e o item do\n-- n\u00FAcleo responde quando n\u00E3o \u2014 que \u00E9 onde o porte grava a classifica\u00E7\u00E3o desde a\n-- migration 049 do spine.\n--\n-- Idempotente. Mesma lista de colunas, mesma ordem \u2014 `CREATE OR REPLACE VIEW`\n-- exige, e manter a assinatura \u00E9 o que deixa esta corre\u00E7\u00E3o ser um replace e n\u00E3o\n-- um drop com depend\u00EAncia quebrada.\n\n-- Replay guard. financial/120 appended item_cancelled_at, item_cancel_reason and\n-- item_metadata to this view; re-running this narrower list over that shape\n-- fails with \u00ABcannot drop columns from view\u00BB. Once the view has them it is left\n-- as 120 made it. The invoker setting and the grant below are idempotent.\nDO $guard$\nBEGIN\n IF EXISTS (SELECT 1 FROM pg_attribute\n WHERE attrelid = to_regclass('public.v_financial_invoice_lines')\n AND attname = 'item_cancelled_at' AND NOT attisdropped) THEN\n RAISE NOTICE 'financial 038: v_financial_invoice_lines already has the 120 shape; left as it is';\n RETURN;\n END IF;\n\n EXECUTE $view$\nCREATE OR REPLACE VIEW public.v_financial_invoice_lines AS\n-- \u2500\u2500 caminho 1: a linha nasceu no pr\u00F3prio t\u00EDtulo \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\nSELECT i.id AS item_id,\n i.tenant_id,\n i.unit_id,\n i.invoice_id,\n f.number AS invoice_number,\n f.direction,\n f.issue_date,\n f.cancelled_at,\n i.product_id,\n i.name,\n i.description,\n i.quantity,\n i.unit_price,\n i.discount,\n i.total,\n COALESCE(x.chart_account_id, i.chart_account_id) AS chart_account_id,\n COALESCE(x.cost_center_id, i.cost_center_id) AS cost_center_id,\n x.tax_amount,\n x.cfop,\n x.ncm,\n a.from_item_id AS billed_from_order_item_id,\n a.quantity AS billed_quantity\n FROM public.items i\n JOIN public.plg_financial_invoices f ON f.id = i.invoice_id\n LEFT JOIN public.plg_financial_invoice_items x ON x.item_id = i.id\n LEFT JOIN public.item_associations a ON a.to_item_id = i.id\n WHERE i.invoice_id IS NOT NULL\n\nUNION ALL\n\n-- \u2500\u2500 caminho 2: a linha nasceu na venda, e o t\u00EDtulo veio dela \u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\n-- `i.invoice_id IS NULL` mant\u00E9m os dois ramos disjuntos: sem isso, um item com\n-- os dois preenchidos apareceria duas vezes e dobraria o valor da conta. Hoje o\n-- CHECK impede que isso exista, e a condi\u00E7\u00E3o \u00E9 o cinto que sobrevive a um dia em\n-- que o CHECK mude.\nSELECT i.id AS item_id,\n i.tenant_id,\n i.unit_id,\n f.id AS invoice_id,\n f.number AS invoice_number,\n f.direction,\n f.issue_date,\n f.cancelled_at,\n i.product_id,\n i.name,\n i.description,\n i.quantity,\n i.unit_price,\n i.discount,\n i.total,\n COALESCE(x.chart_account_id, i.chart_account_id) AS chart_account_id,\n COALESCE(x.cost_center_id, i.cost_center_id) AS cost_center_id,\n x.tax_amount,\n x.cfop,\n x.ncm,\n a.from_item_id AS billed_from_order_item_id,\n a.quantity AS billed_quantity\n FROM public.items i\n JOIN public.plg_financial_invoices f ON f.order_id = i.order_id\n LEFT JOIN public.plg_financial_invoice_items x ON x.item_id = i.id\n LEFT JOIN public.item_associations a ON a.to_item_id = i.id\n WHERE i.invoice_id IS NULL AND i.order_id IS NOT NULL\n -- Only for an invoice with no lines of its own. An invoice billed from\n -- PART of an order (financial_invoice_from_lines) has its own lines, and\n -- without this guard it also listed every line of the order \u2014 the whole\n -- sale reported as billed, and summed twice.\n AND NOT EXISTS (SELECT 1 FROM public.items own WHERE own.invoice_id = f.id)\n $view$;\n\n EXECUTE $comment$\nCOMMENT ON VIEW public.v_financial_invoice_lines IS\n 'Linha de fatura com classifica\u00E7\u00E3o cont\u00E1bil, pelos DOIS caminhos: a linha '\n 'criada no t\u00EDtulo (items.invoice_id) e a criada na venda (items.order_id = '\n 'plg_financial_invoices.order_id). O segundo \u00E9 100% do que o porte do V1 traz '\n '\u2014 items_one_parent pro\u00EDbe um item de ter os dois pais.'\n $comment$;\nEND $guard$;\n\n-- `security_invoker` precisa sobreviver ao replace: sem ele a view passaria a\n-- rodar com os direitos do dono e furaria a RLS de todo mundo que a l\u00EA.\nALTER VIEW public.v_financial_invoice_lines SET (security_invoker = true);\n\nGRANT SELECT ON public.v_financial_invoice_lines TO authenticated, service_role;\n";
|
|
40
40
|
export declare const MIGRATION_039_O_TITULO_DE_UMA_VENDA_CANCELADA_NASCE_CANCELADO = "-- O t\u00EDtulo de uma venda cancelada nasce cancelado.\n--\n-- Uma venda do V1 tem DOIS destinos no V2: a Venda em `public.orders` e o T\u00CDTULO\n-- em `public.plg_financial_invoices`, criado pelo `extra` do writer `order`. O\n-- cancelamento atravessa para o primeiro e **n\u00E3o** para o segundo:\n--\n-- V1 invoices #177530 status = 'canceled'\n-- \u2192 public.orders status = 'cancelled' \u2705\n-- \u2192 plg_financial_invoices cancelled_at = NULL \u274C\n--\n-- O resultado \u00E9 pior que perder a linha: o t\u00EDtulo cancelado **conta como\n-- ativo**. Medido em 18/09/2026 no tenant Pertinho do C\u00E9u: 8 t\u00EDtulos de venda\n-- cancelada, **R$ 24.362,90** somando no faturamento e no DRE como se fossem\n-- receita viva. Uma linha ausente algu\u00E9m procura; uma linha a mais ningu\u00E9m\n-- confere.\n--\n-- Foi a asser\u00E7\u00E3o de paridade que achou: o DRE do V2 dava R$ 12.000 A MAIS que o\n-- do V1 na conta 8.2.5 de agosto, e o destino ter mais que a origem s\u00F3 pode ser\n-- m\u00EAs deslocado ou dado que n\u00E3o deveria estar l\u00E1.\n--\n-- \u2500\u2500 Por que um gatilho, e n\u00E3o um conserto no writer \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-- O caminho \u00F3bvio seria fazer `upsert_order` carimbar o t\u00EDtulo junto. N\u00E3o basta,\n-- e por uma raz\u00E3o que vale al\u00E9m da migra\u00E7\u00E3o: o cancelamento de uma venda tamb\u00E9m\n-- acontece DEPOIS, pelo app, e nesse caminho o writer da migra\u00E7\u00E3o nem roda. A\n-- regra \"t\u00EDtulo de venda cancelada est\u00E1 cancelado\" \u00E9 do modelo, n\u00E3o do\n-- transporte \u2014 ent\u00E3o ela mora onde o modelo mora.\n--\n-- O gatilho \u00E9 `AFTER UPDATE OF status`, n\u00E3o `AFTER UPDATE`: uma venda recebe\n-- escrita o tempo todo (total, notas, unidade) e disparar em todas faria dele um\n-- custo em cada uma delas.\n--\n-- Idempotente.\n\n-- \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\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-- 1. O que j\u00E1 entrou errado\n-- \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\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-- `updated_at` da venda, e n\u00E3o `now()`: o carimbo tem que dizer quando a venda\n-- foi cancelada, n\u00E3o quando esta migration rodou. Um `now()` aqui colocaria\n-- todos os cancelamentos hist\u00F3ricos na data do deploy, e qualquer relat\u00F3rio por\n-- per\u00EDodo passaria a mentir a partir de hoje.\nUPDATE public.plg_financial_invoices fi\n SET cancelled_at = coalesce(\n nullif(o.metadata #>> '{cancellation,at}', '')::timestamptz,\n o.updated_at),\n cancel_reason = coalesce(fi.cancel_reason, o.metadata #>> '{cancellation,reason}')\n FROM public.orders o\n WHERE o.id = fi.order_id\n AND o.status = 'cancelled'\n AND fi.cancelled_at IS NULL;\n\n-- \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\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-- 2. O que entrar daqui para a frente\n-- \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\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\nCREATE OR REPLACE FUNCTION public.trg_order_cancels_its_invoice()\nRETURNS trigger\nLANGUAGE plpgsql\nSECURITY DEFINER\nSET search_path TO ''\nAS $function$\nBEGIN\n IF NEW.status = 'cancelled' AND coalesce(OLD.status, '') <> 'cancelled' THEN\n UPDATE public.plg_financial_invoices\n SET cancelled_at = coalesce(\n nullif(NEW.metadata #>> '{cancellation,at}', '')::timestamptz, now()),\n cancel_reason = coalesce(cancel_reason, NEW.metadata #>> '{cancellation,reason}')\n WHERE order_id = NEW.id AND cancelled_at IS NULL;\n -- Descancelar \u00E9 reabrir: se a venda volta, o t\u00EDtulo volta com ela. Sem esta\n -- perna o estorno de um cancelamento deixaria o t\u00EDtulo morto para sempre, e o\n -- dinheiro sumiria do relat\u00F3rio sem ningu\u00E9m ter pedido.\n ELSIF coalesce(OLD.status, '') = 'cancelled' AND NEW.status <> 'cancelled' THEN\n UPDATE public.plg_financial_invoices\n SET cancelled_at = NULL, cancel_reason = NULL\n WHERE order_id = NEW.id;\n END IF;\n RETURN NULL;\nEND $function$;\n\nDROP TRIGGER IF EXISTS orders_cancel_reaches_the_invoice ON public.orders;\nCREATE TRIGGER orders_cancel_reaches_the_invoice\n AFTER UPDATE OF status ON public.orders\n FOR EACH ROW EXECUTE FUNCTION public.trg_order_cancels_its_invoice();\n\nCOMMENT ON FUNCTION public.trg_order_cancels_its_invoice() IS\n 'Cancelar a venda cancela o t\u00EDtulo dela, e descancelar reabre. O porte do V1 '\n 'levava o cancelamento para public.orders e n\u00E3o para plg_financial_invoices \u2014 '\n '8 t\u00EDtulos, R$ 24.362,90, contavam como receita viva.';\n";
|
|
41
41
|
export declare const MIGRATION_040_A_DRE_LE_A_LINHA_DA_VENDA = "-- A DRE l\u00EA a linha da venda.\n--\n-- O relat\u00F3rio que o V1 tem e o V2 n\u00E3o tinha. Ele existe agora porque o DADO\n-- passou a bater: `make parity CLIENT=pertinho-do-ceu DE=2026-01-01\n-- ATE=2026-12-31` fecha em 376 de 376 chaves (conta cont\u00E1bil \u00D7 m\u00EAs), com\n-- A = R$ 2.181.543,30 e B = R$ 2.181.543,29. Construir a DRE antes disso seria\n-- dar forma bonita a um n\u00FAmero errado.\n--\n-- \u2500\u2500 De onde cada n\u00FAmero sai, e por qu\u00EA \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-- A DRE do V1 \u00E9 montada sobre `invoices \u00D7 invoice_items`, classificando pelo\n-- **item** e n\u00E3o pelo movimento \u2014 dois itens da mesma venda podem cair em ramos\n-- diferentes. Ela NUNCA l\u00EA `financial_movements`: parcela e baixa n\u00E3o importam,\n-- o lan\u00E7amento conta inteiro no m\u00EAs da compet\u00EAncia.\n--\n-- O caminho equivalente no V2 \u00E9 este, e n\u00E3o outro:\n--\n-- public.items \u2192 public.orders \u2192 public.plg_financial_invoices\n--\n-- **N\u00E3o** \u00E9 `items.invoice_id`. O CHECK `items_one_parent` exige exatamente um\n-- pai (`order_id` XOR `invoice_id` XOR \u2026), e o item que nasce numa venda tem\n-- `order_id`. Ligar pelo `invoice_id` devolve zero linha para todo dado migrado\n-- \u2014 foi o que aconteceu com `v_financial_invoice_lines` antes da migration 032.\n--\n-- A compet\u00EAncia \u00E9 `plg_financial_invoices.competence_date`, que \u00E9 onde a\n-- `invoices.invoice_date` do V1 foi parar. A dire\u00E7\u00E3o \u00E9 `fi.direction`\n-- (receivable | payable), que o writer `order` j\u00E1 grava certo \u2014 n\u00E3o \u00E9 preciso\n-- deduzir nada de `orders.direction`, que vem quase sempre nula.\n--\n-- \u2500\u2500 O que esta view N\u00C3O reproduz \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-- Tr\u00EAs defeitos do DRE do V1, provados no c\u00F3digo dele:\n--\n-- 1. subtrai o desconto DUAS vezes \u2014 `invoice_items.total_amount` j\u00E1 \u00E9 l\u00EDquido\n-- e a linha \"(\u2212) Descontos\" desconta de novo;\n-- 2. deixa passar nota `'canceled'` (1 L) pelo filtro de `'cancelled'` (2 L);\n-- 3. soma item cancelado individualmente (n\u00E3o filtra `canceled_at`).\n--\n-- Aqui `cancelled_at` \u00E9 filtrado nos dois n\u00EDveis e o desconto n\u00E3o \u00E9 subtra\u00EDdo\n-- duas vezes. Ou seja: a view bate com o DADO do V1, e vai divergir da TELA do\n-- V1 exatamente na medida desses tr\u00EAs. \u00C9 de prop\u00F3sito, e \u00E9 o motivo de a\n-- paridade comparar dado e n\u00E3o tela.\n--\n-- Idempotente.\n\nDROP VIEW IF EXISTS public.plg_financial_rep_dre;\n\nCREATE VIEW public.plg_financial_rep_dre AS\nSELECT\n fi.tenant_id,\n fi.unit_id,\n -- Primeiro dia do m\u00EAs: \u00E9 assim que `v_financial_trial_balance` j\u00E1 expressa\n -- compet\u00EAncia, e duas conven\u00E7\u00F5es de m\u00EAs no mesmo plugin fariam um relat\u00F3rio\n -- n\u00E3o casar com o outro por um dia.\n date_trunc('month', fi.competence_date)::date AS competence_month,\n fi.direction,\n coa.id AS account_id,\n coa.code AS account_code,\n coa.name AS account_name,\n coa.account_type,\n coa.parent_id AS account_parent_id,\n -- A linha da DRE que n\u00E3o tem conta. O relat\u00F3rio do V1 chama de \"sem\n -- classifica\u00E7\u00E3o\" e ela domina a tela dele; mant\u00EA-la como uma linha de\n -- verdade \u2014 em vez de simplesmente n\u00E3o aparecer \u2014 \u00E9 o que deixa a soma da\n -- \u00E1rvore fechar com o cabe\u00E7alho.\n (coa.id IS NULL) AS unclassified,\n count(*) AS line_count,\n sum(it.total) AS amount,\n sum(it.discount) AS discount,\n sum(it.quantity) AS quantity\n FROM public.items it\n JOIN public.orders o ON o.id = it.order_id\n JOIN public.plg_financial_invoices fi ON fi.order_id = o.id\n LEFT JOIN public.plg_financial_chart_of_accounts coa\n ON coa.id = it.chart_account_id\n AND coa.tenant_id = fi.tenant_id\n WHERE fi.cancelled_at IS NULL\n AND it.cancelled_at IS NULL\n GROUP BY 1, 2, 3, 4, 5, 6, 7, 8, 9, 10;\n\nCOMMENT ON VIEW public.plg_financial_rep_dre IS\n 'DRE: soma das linhas de venda por conta cont\u00E1bil \u00D7 m\u00EAs de compet\u00EAncia \u00D7 '\n 'dire\u00E7\u00E3o. Liga o item ao t\u00EDtulo PELO PEDIDO (items_one_parent pro\u00EDbe o item '\n 'de ter os dois pais). Confere com o V1 em 376 de 376 chaves.';\n\nALTER VIEW public.plg_financial_rep_dre SET (security_invoker = true);\nGRANT SELECT ON public.plg_financial_rep_dre TO authenticated, service_role;\n\n-- \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\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-- O motor de analytics s\u00F3 alcan\u00E7a o que est\u00E1 na allowlist\n-- \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\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-- Uma view fora de `analytics_read_models` \u00E9 inalcan\u00E7\u00E1vel pelo `analytics_run`,\n-- por melhor que seja o card. Registrar aqui, junto com a view, \u00E9 o que impede\n-- as duas coisas de divergirem \u2014 a view sem registro \u00E9 um relat\u00F3rio que abre\n-- vazio e n\u00E3o diz por qu\u00EA.\nDO $$\nBEGIN\n IF to_regclass('public.analytics_read_models') IS NULL THEN\n RAISE NOTICE 'financial 034: motor de analytics n\u00E3o instalado \u2014 read model n\u00E3o registrado';\n RETURN;\n END IF;\n INSERT INTO public.analytics_read_models (name, date_column, tenant_column, description)\n VALUES ('plg_financial_rep_dre', 'competence_month', 'tenant_id',\n 'Receitas e despesas por plano de contas e m\u00EAs de compet\u00EAncia, da linha da venda.')\n ON CONFLICT (name) DO UPDATE\n SET date_column = EXCLUDED.date_column,\n tenant_column = EXCLUDED.tenant_column,\n description = EXCLUDED.description;\nEND $$;\n";
|
|
42
42
|
export declare const MIGRATION_041_O_CAIXA_DO_V1_NAO_SEGURA_O_MOVIMENTO = "-- O caixa do V1 n\u00E3o segura o movimento.\n--\n-- `financial_movements.cash_session_id` estava no de-para como REFER\u00CANCIA DURA:\n--\n-- 'cash_session_id' \u2192 'cash_session_id@cash_register_sessions'\n--\n-- `cash_register_sessions` nunca entrou na allowlist. Toda linha que cita uma\n-- sess\u00E3o cai em quarentena `fk` \u2014 e n\u00E3o \u00E9 a sess\u00E3o que fica de fora, \u00E9 o\n-- DINHEIRO. Medido em 19/09/2026 no tenant Pertinho do C\u00E9u:\n--\n-- movimentos presos por `cash_session_id` 24\n-- contas a pagar que sumiam da lista por isso \u2248 R$ 2.319,46\n-- sess\u00F5es que os prendiam 2\n--\n-- Duas sess\u00F5es de confer\u00EAncia de gaveta seguravam dois mil reais de contas a\n-- pagar fora do relat\u00F3rio. E, medido na origem, essas duas sess\u00F5es n\u00E3o s\u00E3o\n-- opera\u00E7\u00E3o: uma abriu em 27/05 e fechou em 06/08 com diferen\u00E7a de R$ 20, a\n-- outra est\u00E1 aberta desde 18/09. N\u00E3o h\u00E1 fechamento de caixa acontecendo nesse\n-- tenant \u2014 h\u00E1 uma tela que algu\u00E9m abriu duas vezes.\n--\n-- \u2500\u2500 Por que metadata, e n\u00E3o portar a sess\u00E3o \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-- Portar `cash_register_sessions` resolveria a quarentena e criaria dois\n-- registros de confer\u00EAncia de caixa que ningu\u00E9m conferiu, num plugin onde\n-- `v_financial_cash_session_expected` e `v_financial_cash_session_review` os\n-- mostrariam como opera\u00E7\u00E3o real. O V2 passaria a ter um caixa que o neg\u00F3cio n\u00E3o\n-- tem.\n--\n-- A sess\u00E3o vira o que ela \u00E9 aqui: um FATO da linha de origem, guardado onde os\n-- outros fatos sem destino j\u00E1 moram \u2014\n--\n-- 'cash_session_id' \u2192 'jsonb:v1_cash_session_ref'\n--\n-- O id do V1 continua na linha, consult\u00E1vel, e quando o fechamento de caixa for\n-- portar de verdade ele \u00E9 a chave que liga os dois lados. O que muda \u00E9 que ele\n-- deixa de ser uma condi\u00E7\u00E3o para o movimento existir.\n--\n-- Esta \u00E9 a mesma decis\u00E3o j\u00E1 tomada para `bank_account_id`\n-- (`jsonb:v1_bank_account_ref`) na mesma tabela: refer\u00EAncia que o V2 ainda n\u00E3o\n-- alcan\u00E7a \u00E9 registro, n\u00E3o trava.\n--\n-- Idempotente.\n\n-- The row is already frozen, and trg_allowlist_frozen refuses a plain write.\n-- A migration file is the one writer the hatch exists for (same idiom as\n-- 033/035); SET LOCAL so the unlock dies with this transaction.\nSET LOCAL migration.allowlist_unlock = 'on';\n\nSELECT migration._allow(\n 'financial_movements', 'public.plg_financial_movements', 'financial',\n 'fin_movement', '{}'::jsonb, ARRAY['amount', 'paid_amount'],\n jsonb_build_object(\n 'uuid', 'id',\n 'type', 'jsonb:v1.type',\n 'notes', 'notes',\n 'amount', 'amount',\n 'status', 'status',\n 'barcode', 'jsonb:barcode',\n 'due_date', 'due_date',\n 'direction', 'direction',\n 'card_brand', 'jsonb:card.brand',\n 'invoice_id', 'financial_invoice_id@invoices:public.plg_financial_invoices',\n 'external_id', 'external_id',\n 'paid_amount', 'paid_amount',\n 'confirmed_at', 'confirmed_at',\n 'is_confirmed', 'jsonb:confirmed',\n 'payment_date', 'paid_at',\n 'movement_type', 'movement_kind',\n 'order_guest_id', '-',\n 'bank_account_id', 'jsonb:v1_bank_account_ref',\n -- A mudan\u00E7a desta migration, e a \u00FAnica.\n 'cash_session_id', 'jsonb:v1_cash_session_ref',\n 'external_source', 'external_source',\n 'debit_account_id', 'debit_account_id@account_central',\n 'card_installments', 'jsonb:card.installments',\n 'credit_account_id', 'credit_account_id@account_central',\n 'payment_method_id', 'payment_method_id@payment_methods',\n 'installment_number', 'installment_number',\n 'parent_movement_id', 'parent_movement_id@financial_movements',\n 'payment_method_type_id','payment_method_type_id@payment_method_types'\n ),\n 'PRD 05 R7/R13: append-only two-leg movements (debit + credit account). Never '\n 'overwritten. Paid invoices without movements receive an identifiable synthetic '\n 'payment (Cr\u00E9dito Legado, R14) \u2014 the extractor emits it, the destination records '\n 'it as a normal row. `cash_session_id` \u00E9 REGISTRO (v1_cash_session_ref), n\u00E3o '\n 'refer\u00EAncia: o fechamento de caixa do V1 n\u00E3o \u00E9 opera\u00E7\u00E3o real neste porte e n\u00E3o '\n 'pode segurar o dinheiro fora do relat\u00F3rio.'\n);\n";
|
|
@@ -118,6 +118,9 @@ export declare const MIGRATION_116_UM_ESTORNO_NAO_SE_ESTORNA = "-- -------------
|
|
|
118
118
|
export declare const MIGRATION_117_COPYING_LINES_IS_AN_INTERNAL_STEP = "-- ---------------------------------------------------------------------------\n-- 117 \u2014 copying lines onto an invoice is an internal step, not a door.\n--\n-- Both copiers are SECURITY DEFINER with no caller check and were executable by\n-- PUBLIC (so anon too): anyone holding an invoice id could write public.items\n-- into it. Every real caller is another SECURITY DEFINER function owned by\n-- postgres (fn_invoice_from_order_internal, order_raise_receivable,\n-- financial_recurring_run, financial_consolidate_occurrence), which keeps its\n-- right to execute as owner. So the fix is the grant, not the body.\n--\n-- Idempotent: grants only.\n-- ---------------------------------------------------------------------------\n\nREVOKE ALL ON FUNCTION public.plg_financial_copy_bill_items(uuid, uuid, jsonb) FROM PUBLIC, anon, authenticated;\nGRANT EXECUTE ON FUNCTION public.plg_financial_copy_bill_items(uuid, uuid, jsonb) TO service_role;\n\nREVOKE ALL ON FUNCTION public.plg_financial_copy_order_lines_to_invoice(uuid, uuid) FROM PUBLIC, anon, authenticated;\nGRANT EXECUTE ON FUNCTION public.plg_financial_copy_order_lines_to_invoice(uuid, uuid) TO service_role;\n";
|
|
119
119
|
export declare const MIGRATION_118_GIFT_CARD_FOUND_BY_EXACT_CODE = "-- 118_gift_card_found_by_exact_code.sql\n--\n-- Selling a gift card on a receivable takes the code typed at the counter and\n-- must match exactly one card. financial_gift_cards (027) matches by substring,\n-- so a partial code answered with real codes of other cards. This lookup\n-- returns the one card whose code equals the input (spaces, dots, dashes and\n-- case ignored), or nothing.\n--\n-- The shop table is optional and applied after this chain, so it is resolved\n-- at run time, as in 027.\n\nCREATE OR REPLACE FUNCTION public.financial_gift_card_by_code(p_code text)\nRETURNS TABLE (\n id uuid,\n code text,\n face_value numeric,\n balance numeric,\n balance_known boolean,\n state text,\n currency text,\n issued_at timestamptz,\n expires_at timestamptz,\n activated_at timestamptz,\n note text,\n customer_email text\n)\n LANGUAGE plpgsql\n STABLE\n SECURITY DEFINER\n SET search_path TO ''\nAS $$\nDECLARE\n v_tenant uuid := app.current_tenant_id();\n v_code text := upper(regexp_replace(coalesce(p_code, ''), '[\\s.\\-]', '', 'g'));\n v_activated text;\nBEGIN\n IF v_tenant IS NULL THEN\n RAISE EXCEPTION 'financial_gift_card_by_code: sem tenant na sess\u00E3o' USING ERRCODE = '42501';\n END IF;\n -- Whoever can raise a receivable can sell a card on it.\n IF NOT app.has_permission('financial.create', NULL) THEN\n RAISE EXCEPTION 'financial_gift_card_by_code: exige financial.create' USING ERRCODE = '42501';\n END IF;\n IF v_code = '' OR to_regclass('public.plg_shop_gift_cards') IS NULL THEN\n RETURN;\n END IF;\n\n IF EXISTS (SELECT 1 FROM information_schema.columns c\n WHERE c.table_schema = 'public' AND c.table_name = 'plg_shop_gift_cards'\n AND c.column_name = 'activated_at') THEN\n v_activated := 'g.activated_at';\n ELSE\n v_activated :=\n 'CASE WHEN g.status = ''disabled'' AND g.balance = 0 THEN NULL'\n || ' WHEN g.updated_at > g.created_at THEN g.updated_at'\n || ' ELSE g.created_at END';\n END IF;\n\n -- Same state vocabulary as financial_gift_cards (027).\n RETURN QUERY EXECUTE\n 'SELECT r.id, r.code, r.initial_balance,'\n || 'CASE WHEN r.never THEN NULL ELSE r.balance END,'\n || '(NOT r.never),'\n || 'CASE WHEN r.never THEN ''issued'''\n || ' WHEN r.balance = 0 OR r.status = ''redeemed'' THEN ''redeemed'''\n || ' WHEN r.expires_at IS NOT NULL AND r.expires_at < now() THEN ''expired'''\n || ' WHEN r.status = ''disabled'' THEN ''revoked'''\n || ' ELSE ''active'' END,'\n || 'r.currency, r.created_at, r.expires_at, r.activated_at, r.note, r.customer_email '\n || 'FROM (SELECT g.id, g.code, g.initial_balance, g.balance, g.currency, g.status,'\n || 'g.created_at, g.expires_at, g.note, g.customer_email,'\n || '(g.status = ''disabled'' AND g.balance = 0) AS never,'\n || v_activated || ' AS activated_at '\n || 'FROM public.plg_shop_gift_cards g '\n || 'WHERE g.tenant_id = $1 AND upper(g.code) = $2) r '\n || 'LIMIT 1'\n USING v_tenant, v_code;\nEND $$;\n\nCOMMENT ON FUNCTION public.financial_gift_card_by_code(text) IS\n 'The one gift card whose code equals the input (spaces/case ignored), for selling it on a receivable. Never matches part of a code (118).';\n\nREVOKE ALL ON FUNCTION public.financial_gift_card_by_code(text) FROM PUBLIC;\nREVOKE ALL ON FUNCTION public.financial_gift_card_by_code(text) FROM anon;\nGRANT EXECUTE ON FUNCTION public.financial_gift_card_by_code(text) TO authenticated;\nGRANT EXECUTE ON FUNCTION public.financial_gift_card_by_code(text) TO service_role;\n";
|
|
120
120
|
export declare const MIGRATION_119_AN_UNPAID_LEGACY_BILL_CAN_BE_RESCHEDULED = "-- An unpaid bill migrated from V1 could not be rescheduled.\n--\n-- Editing the lines of such a bill saved the new lines and total, then\n-- `financial_reschedule_invoice` refused with \"carries legacy bill movements\":\n-- the installment kept the old amount and every later save failed the same way.\n--\n-- The `bill` mirror in plg_financial_movements has had no reader since 044 \u2014\n-- the installment is the obligation. Its amount is append-only, its status is\n-- not, so an unpaid mirror is cancelled (stamped in metadata) and the schedule\n-- goes through. A mirror that records a payment still blocks.\n--\n-- Idempotent: replaces the function.\n\nCREATE OR REPLACE FUNCTION public.financial_reschedule_invoice(\n p_invoice_id uuid,\n p_installments jsonb,\n p_idempotency_key text DEFAULT NULL::text\n)\nRETURNS jsonb\nLANGUAGE plpgsql\nSECURITY DEFINER\nSET search_path TO ''\nAS $function$\nDECLARE\n v_tenant uuid := public.plg_financial_require_tenant();\n v_invoice public.plg_financial_invoices%ROWTYPE;\n v_paid numeric;\n v_mirror int;\n v_sum numeric := 0;\n v_i jsonb;\n v_n int := 0;\n v_out jsonb := '[]'::jsonb;\n v_inst_id uuid;\n v_due date;\n v_amount numeric;\n v_meta jsonb;\n v_total int;\nBEGIN\n SELECT * INTO v_invoice FROM public.plg_financial_invoices WHERE id = p_invoice_id AND tenant_id = v_tenant;\n IF NOT FOUND THEN\n RAISE EXCEPTION 'financial_reschedule_invoice: invoice % not found', p_invoice_id USING ERRCODE = 'P0002';\n END IF;\n PERFORM public.plg_financial_require_permission('financial.edit', v_invoice.unit_id);\n PERFORM public.plg_financial_claim_idempotency(v_tenant, p_idempotency_key);\n\n IF v_invoice.cancelled_at IS NOT NULL THEN\n RAISE EXCEPTION 'financial_reschedule_invoice: invoice % is cancelled', v_invoice.number USING ERRCODE = '55000';\n END IF;\n\n SELECT coalesce(sum(b.paid), 0) INTO v_paid\n FROM public.v_financial_installment_balances b WHERE b.invoice_id = p_invoice_id;\n IF v_paid > 0 THEN\n RAISE EXCEPTION 'financial_reschedule_invoice: invoice % has % already received; its schedule is history now', v_invoice.number, v_paid\n USING ERRCODE = '55000',\n HINT = 'Move the due date of an unpaid installment instead, or cancel and reissue.';\n END IF;\n\n -- The legacy `bill` mirror can't change amount, but nothing reads it any more\n -- (the installment is the obligation since 044) and its status may move.\n -- Retire it unless it records a payment the installments don't know about.\n SELECT count(*) INTO v_mirror FROM public.plg_financial_movements m\n WHERE m.financial_invoice_id = p_invoice_id AND m.movement_kind = 'bill'\n AND m.status <> 'cancelled' AND coalesce(m.paid_amount, 0) > 0;\n IF v_mirror > 0 THEN\n RAISE EXCEPTION 'financial_reschedule_invoice: invoice % carries a paid legacy bill movement and cannot be rescheduled', v_invoice.number\n USING ERRCODE = '55000',\n HINT = 'Cancel and reissue this invoice.';\n END IF;\n UPDATE public.plg_financial_movements m\n SET status = 'cancelled', updated_at = now(),\n metadata = coalesce(m.metadata, '{}'::jsonb)\n || jsonb_build_object('retired_by', 'financial_reschedule_invoice', 'retired_at', now())\n WHERE m.financial_invoice_id = p_invoice_id AND m.movement_kind = 'bill' AND m.status <> 'cancelled';\n\n IF jsonb_typeof(p_installments) <> 'array' OR jsonb_array_length(p_installments) = 0 THEN\n RAISE EXCEPTION 'financial_reschedule_invoice: installments must be a non-empty array' USING ERRCODE = '22023';\n END IF;\n FOR v_i IN SELECT * FROM jsonb_array_elements(p_installments) LOOP\n v_sum := v_sum + round(coalesce((v_i ->> 'amount')::numeric, 0), 2);\n END LOOP;\n IF v_sum <> round(v_invoice.total, 2) THEN\n RAISE EXCEPTION 'financial_reschedule_invoice: installments sum to % but the invoice total is %', v_sum, v_invoice.total\n USING ERRCODE = '23514';\n END IF;\n\n v_total := jsonb_array_length(p_installments);\n\n -- 1. A sobra sai PRIMEIRO. Se ela tiver movimento, o banco recusa aqui \u2014 e \u00E9\n -- a recusa certa: encolher o cronograma apagaria a linha que um\n -- lan\u00E7amento aponta.\n DELETE FROM public.plg_financial_invoice_installments\n WHERE invoice_id = p_invoice_id AND number > v_total;\n\n -- 2. E a parcela de n\u00FAmero N continua sendo a de n\u00FAmero N.\n FOR v_i IN SELECT * FROM jsonb_array_elements(p_installments) LOOP\n v_n := v_n + 1;\n v_due := coalesce((v_i ->> 'due_date')::date, v_invoice.competence_date, current_date);\n v_amount := round(coalesce((v_i ->> 'amount')::numeric, 0), 2);\n v_meta := CASE WHEN jsonb_typeof(v_i -> 'metadata') = 'object' THEN v_i -> 'metadata' ELSE NULL END;\n\n UPDATE public.plg_financial_invoice_installments\n SET due_date = v_due,\n amount = v_amount,\n metadata = coalesce(v_meta, metadata),\n updated_at = now()\n WHERE invoice_id = p_invoice_id AND number = v_n\n RETURNING id INTO v_inst_id;\n\n IF v_inst_id IS NULL THEN\n INSERT INTO public.plg_financial_invoice_installments\n (tenant_id, unit_id, invoice_id, number, due_date, amount, metadata)\n VALUES (v_tenant, v_invoice.unit_id, p_invoice_id, v_n, v_due, v_amount, coalesce(v_meta, '{}'::jsonb))\n RETURNING id INTO v_inst_id;\n END IF;\n\n v_out := v_out || jsonb_build_object('id', v_inst_id, 'number', v_n, 'due_date', v_due, 'amount', v_amount);\n END LOOP;\n\n PERFORM public.plg_financial_audit(v_tenant, 'financial.invoice.reschedule', 'plg_financial_invoices', p_invoice_id::text,\n jsonb_build_object('number', v_invoice.number, 'installments', v_n, 'idempotency_key', p_idempotency_key));\n\n RETURN jsonb_build_object('invoice_id', p_invoice_id, 'installments', v_out);\nEND $function$;\n\nCOMMENT ON FUNCTION public.financial_reschedule_invoice(uuid, jsonb, text) IS\n 'Rewrites the schedule by UPDATING each numbered installment in place, and retires an unpaid legacy bill mirror instead of refusing (119).';\n";
|
|
121
|
+
export declare const MIGRATION_120_CANCELAR_E_POR_ITEM_COM_MOTIVO_E_DESTINO = "-- Cancelar uma conta \u00E9 escolher O QUE se cancela, POR QU\u00CA e o que acontece\n-- com a mercadoria \u2014 e o que j\u00E1 foi pago vira cr\u00E9dito, n\u00E3o some.\n--\n-- Contrato: V1, `financeiro/contas-a-pagar-cancelamento/FLOW.md` (09-10-2026).\n-- At\u00E9 aqui o V2 cancelava s\u00F3 a conta inteira, com motivo em texto livre, e\n-- RECUSAVA conta com pagamento. O V1 faz quatro coisas que este arquivo traz:\n--\n-- 1. cancela por ITEM \u2014 corta a parte n\u00E3o paga das parcelas, da \u00FAltima para\n-- tr\u00E1s; s\u00F3 depois libera o pago, do pagamento mais recente para tr\u00E1s;\n-- 2. o motivo \u00E9 uma LISTA da casa (Outros, Solicitado pelo cliente,\n-- Solicitado pelo profissional) + um detalhe;\n-- 3. o que j\u00E1 foi pago vira CR\u00C9DITO da contraparte (fornecedor ou cliente);\n-- 4. mercadoria que entrou no estoque por aquela nota pede destino:\n-- devolver ao fornecedor / baixar como perda / manter.\n--\n-- E um defeito que estava esperando por isto: pagar uma conta a pagar com\n-- \u00ABcr\u00E9dito\u00BB conferia o saldo com o sinal do CLIENTE. Cr\u00E9dito de fornecedor \u00E9 o\n-- fornecedor nos devendo \u2014 d\u00E9bito na conta da pessoa \u2014 e s\u00F3 conta o que veio\n-- de um cancelamento (ver \u00A78).\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. Os motivos\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_financial_cancellation_reasons (\n id uuid PRIMARY KEY DEFAULT gen_random_uuid(),\n tenant_id uuid NOT NULL REFERENCES public.tenants(id) ON DELETE CASCADE,\n name text NOT NULL CHECK (btrim(name) <> ''),\n is_active boolean NOT NULL DEFAULT true,\n created_at timestamptz NOT NULL DEFAULT now(),\n updated_at timestamptz NOT NULL DEFAULT now()\n);\nCREATE UNIQUE INDEX IF NOT EXISTS plg_financial_cancellation_reasons_name_uq\n ON public.plg_financial_cancellation_reasons (tenant_id, lower(btrim(name)));\n\nDO $$\nBEGIN\n CREATE TRIGGER plg_financial_cancellation_reasons_updated_at\n BEFORE UPDATE ON public.plg_financial_cancellation_reasons\n FOR EACH ROW EXECUTE FUNCTION public.handle_updated_at();\nEXCEPTION WHEN duplicate_object THEN NULL;\nEND $$;\n\nSELECT app.scaffold_table('public.plg_financial_cancellation_reasons'::regclass,\n 'financial.invoice', 'financial', false, 'enforce');\nSELECT app.scaffold_scope('public.plg_financial_cancellation_reasons'::regclass, 'tenant', true);\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. O registro de cada cancelamento\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_financial_cancellations (\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 invoice_id uuid NOT NULL REFERENCES public.plg_financial_invoices(id) ON DELETE CASCADE,\n whole boolean NOT NULL,\n item_ids uuid[] NOT NULL DEFAULT '{}',\n reason_id uuid REFERENCES public.plg_financial_cancellation_reasons(id) ON DELETE SET NULL,\n reason text NOT NULL,\n amount numeric(14,2) NOT NULL,\n credit_amount numeric(14,2) NOT NULL DEFAULT 0,\n credit_movement_id uuid REFERENCES public.plg_financial_movements(id) ON DELETE SET NULL,\n -- [{item_id, product_id, destination, quantity, note, operation_ids}]\n stock jsonb NOT NULL DEFAULT '[]'::jsonb,\n idempotency_key text,\n created_by uuid,\n created_at timestamptz NOT NULL DEFAULT now(),\n updated_at timestamptz NOT NULL DEFAULT now()\n);\nCREATE INDEX IF NOT EXISTS plg_financial_cancellations_invoice_idx\n ON public.plg_financial_cancellations (invoice_id);\nCREATE UNIQUE INDEX IF NOT EXISTS plg_financial_cancellations_key_uq\n ON public.plg_financial_cancellations (tenant_id, idempotency_key) WHERE idempotency_key IS NOT NULL;\n\nDO $$\nBEGIN\n ALTER TABLE public.plg_financial_cancellations\n ADD CONSTRAINT plg_financial_cancellations_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;\nEND $$;\n\nDO $$\nBEGIN\n CREATE TRIGGER plg_financial_cancellations_updated_at\n BEFORE UPDATE ON public.plg_financial_cancellations\n FOR EACH ROW EXECUTE FUNCTION public.handle_updated_at();\nEXCEPTION WHEN duplicate_object THEN NULL;\nEND $$;\n\nSELECT app.scaffold_table('public.plg_financial_cancellations'::regclass,\n 'financial.invoice', 'financial', false, 'enforce');\nSELECT app.scaffold_scope('public.plg_financial_cancellations'::regclass, 'unit_optional', true);\n\nALTER TABLE public.plg_financial_invoices\n ADD COLUMN IF NOT EXISTS cancel_reason_id uuid REFERENCES public.plg_financial_cancellation_reasons(id) ON DELETE SET NULL;\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 linha diz que foi cancelada\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-- Colunas novas s\u00F3 no FIM: `CREATE OR REPLACE VIEW` n\u00E3o aceita outra ordem.\n\nCREATE OR REPLACE VIEW public.v_financial_invoice_lines AS\nSELECT i.id AS item_id, i.tenant_id, i.unit_id, i.invoice_id, f.number AS invoice_number,\n f.direction, f.issue_date, f.cancelled_at, i.product_id, i.name, i.description,\n i.quantity, i.unit_price, i.discount, i.total,\n COALESCE(x.chart_account_id, i.chart_account_id) AS chart_account_id,\n COALESCE(x.cost_center_id, i.cost_center_id) AS cost_center_id,\n x.tax_amount, x.cfop, x.ncm,\n a.from_item_id AS billed_from_order_item_id, a.quantity AS billed_quantity,\n i.cancelled_at AS item_cancelled_at, i.cancel_reason AS item_cancel_reason, i.metadata AS item_metadata\n FROM public.items i\n JOIN public.plg_financial_invoices f ON f.id = i.invoice_id\n LEFT JOIN public.plg_financial_invoice_items x ON x.item_id = i.id\n LEFT JOIN public.item_associations a ON a.to_item_id = i.id\n WHERE i.invoice_id IS NOT NULL\nUNION ALL\nSELECT i.id AS item_id, i.tenant_id, i.unit_id, f.id AS invoice_id, f.number AS invoice_number,\n f.direction, f.issue_date, f.cancelled_at, i.product_id, i.name, i.description,\n i.quantity, i.unit_price, i.discount, i.total,\n COALESCE(x.chart_account_id, i.chart_account_id) AS chart_account_id,\n COALESCE(x.cost_center_id, i.cost_center_id) AS cost_center_id,\n x.tax_amount, x.cfop, x.ncm,\n a.from_item_id AS billed_from_order_item_id, a.quantity AS billed_quantity,\n i.cancelled_at AS item_cancelled_at, i.cancel_reason AS item_cancel_reason, i.metadata AS item_metadata\n FROM public.items i\n JOIN public.plg_financial_invoices f ON f.order_id = i.order_id\n LEFT JOIN public.plg_financial_invoice_items x ON x.item_id = i.id\n LEFT JOIN public.item_associations a ON a.to_item_id = i.id\n WHERE i.invoice_id IS NULL AND i.order_id IS NOT NULL\n AND NOT EXISTS (SELECT 1 FROM public.items own WHERE own.invoice_id = f.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-- 4. A licen\u00E7a de emendar uma conta paga\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-- Conta com pagamento \u00E9 imut\u00E1vel (099/110) \u2014 certo para quem edita a conta,\n-- errado para quem a CANCELA ou lan\u00E7a o juros que o banco cobrou. As duas RPCs\n-- deste arquivo ligam `financial.amend` na pr\u00F3pria transa\u00E7\u00E3o; nenhum outro\n-- caminho liga.\n\nCREATE OR REPLACE FUNCTION public.trg_invoice_line_immutable() RETURNS trigger\n LANGUAGE plpgsql SECURITY DEFINER\n SET search_path TO ''\n AS $$\nDECLARE v_issued date; v_cancelled timestamptz; v_invoice uuid; v_paid numeric;\nBEGIN\n IF coalesce(current_setting('financial.amend', true), '') = 'on' THEN RETURN coalesce(NEW, OLD); END IF;\n v_invoice := coalesce(NEW.invoice_id, OLD.invoice_id);\n IF v_invoice IS NULL THEN RETURN coalesce(NEW, OLD); END IF;\n SELECT f.issue_date, f.cancelled_at INTO v_issued, v_cancelled\n FROM public.plg_financial_invoices f WHERE f.id = v_invoice;\n IF v_issued IS NOT NULL AND v_cancelled IS NULL THEN\n SELECT coalesce(sum(b.paid), 0) INTO v_paid\n FROM public.v_financial_installment_balances b WHERE b.invoice_id = v_invoice;\n IF v_paid > 0 THEN\n RAISE EXCEPTION 'items: the lines of invoice % cannot be altered \u2014 % already received', v_invoice, v_paid\n USING ERRCODE = '23514',\n HINT = 'A paid invoice is immutable (PRD 04). Reverse the payments, or raise a new invoice.';\n END IF;\n END IF;\n RETURN coalesce(NEW, OLD);\nEND $$;\n\nCREATE OR REPLACE FUNCTION public.trg_plg_financial_invoices_immutable()\nRETURNS trigger\nLANGUAGE plpgsql\nSECURITY DEFINER\nSET search_path TO ''\nAS $function$\nDECLARE v_paid numeric;\nBEGIN\n IF OLD.issue_date IS NULL OR OLD.cancelled_at IS NOT NULL THEN RETURN NEW; END IF;\n IF NEW.cancelled_at IS NOT NULL AND OLD.cancelled_at IS NULL THEN RETURN NEW; END IF;\n\n IF NEW.direction IS DISTINCT FROM OLD.direction\n OR NEW.order_id IS DISTINCT FROM OLD.order_id\n OR NEW.number IS DISTINCT FROM OLD.number\n OR NEW.issue_date IS DISTINCT FROM OLD.issue_date\n OR NEW.currency IS DISTINCT FROM OLD.currency THEN\n RAISE EXCEPTION 'plg_financial_invoices: invoice % is issued and cannot be altered', OLD.number\n USING ERRCODE = '23514',\n HINT = 'A closed invoice is immutable (PRD 04). Correct it with a new invoice, or cancel this one through financial_cancel_invoice.';\n END IF;\n\n -- S\u00F3 o TOTAL ganha a licen\u00E7a: cancelar itens e lan\u00E7ar juros mudam quanto a\n -- conta vale; compet\u00EAncia e contraparte continuam travadas.\n IF NEW.competence_date IS DISTINCT FROM OLD.competence_date\n OR (NEW.total IS DISTINCT FROM OLD.total AND coalesce(current_setting('financial.amend', true), '') <> 'on')\n OR NEW.counterparty_person_id IS DISTINCT FROM OLD.counterparty_person_id THEN\n SELECT coalesce(sum(b.paid), 0) INTO v_paid\n FROM public.v_financial_installment_balances b WHERE b.invoice_id = OLD.id;\n IF v_paid > 0 THEN\n RAISE EXCEPTION 'plg_financial_invoices: invoice % has % already received and cannot be rewritten', OLD.number, v_paid\n USING ERRCODE = '23514',\n HINT = 'Reverse the payments first, or correct it with a new invoice.';\n END IF;\n END IF;\n\n RETURN NEW;\nEND $function$;\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. Os motivos da casa, semeados na primeira pergunta\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 OR REPLACE FUNCTION public.financial_cancellation_reasons()\nRETURNS SETOF public.plg_financial_cancellation_reasons\nLANGUAGE plpgsql SECURITY DEFINER SET search_path TO '' AS $$\nDECLARE v_tenant uuid := public.plg_financial_require_tenant();\nBEGIN\n PERFORM public.plg_financial_require_permission('financial.read');\n IF NOT EXISTS (SELECT 1 FROM public.plg_financial_cancellation_reasons r WHERE r.tenant_id = v_tenant) THEN\n INSERT INTO public.plg_financial_cancellation_reasons (tenant_id, name)\n SELECT v_tenant, n FROM unnest(ARRAY['Outros', 'Solicitado pelo cliente', 'Solicitado pelo profissional']) n\n ON CONFLICT DO NOTHING;\n END IF;\n RETURN QUERY SELECT * FROM public.plg_financial_cancellation_reasons r\n WHERE r.tenant_id = v_tenant AND r.is_active ORDER BY r.name;\nEND $$;\nREVOKE ALL ON FUNCTION public.financial_cancellation_reasons() FROM PUBLIC;\nGRANT EXECUTE ON FUNCTION public.financial_cancellation_reasons() TO authenticated, service_role;\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-- 6. De onde veio a mercadoria de cada linha\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-- A nota \u00E9 a ponte: a entrada no estoque diz \u00ABEntrada via DANFE NF-e <n\u00BA>\u00BB\n-- (fiscal-br/015, e as linhas migradas do V1 j\u00E1 diziam). O n\u00FAmero sai da nota\n-- recebida pelo V2 ou, na conta que veio do V1, do pedido de compra. O produto\n-- casa pelo id e, se o V1 tiver duplicado o cadastro, pelo nome.\n--\n-- Leitura din\u00E2mica: o financeiro n\u00E3o depende fisicamente do estoque; sem o\n-- plugin de estoque, nenhuma linha est\u00E1 \u00ABno estoque\u00BB.\n\nCREATE OR REPLACE FUNCTION public.financial_invoice_stock_entries(p_invoice_id uuid)\nRETURNS TABLE (item_id uuid, product_id uuid, location_id uuid, entered numeric, available numeric, entered_on date)\nLANGUAGE plpgsql STABLE SECURITY DEFINER SET search_path TO '' AS $$\nDECLARE\n v_tenant uuid := public.plg_financial_require_tenant();\n v_inv public.plg_financial_invoices%ROWTYPE;\n v_nf text;\nBEGIN\n SELECT * INTO v_inv FROM public.plg_financial_invoices WHERE id = p_invoice_id AND tenant_id = v_tenant;\n IF NOT FOUND OR v_inv.direction <> 'payable' THEN RETURN; END IF;\n PERFORM public.plg_financial_require_permission('financial.read', v_inv.unit_id);\n IF to_regclass('public.plg_inventory_stock_movements') IS NULL THEN RETURN; END IF;\n\n IF to_regclass('public.plg_fiscalbr_inbox_documents') IS NOT NULL THEN\n EXECUTE 'SELECT d.nfe_number FROM public.plg_fiscalbr_inbox_documents d WHERE d.tenant_id = $1 AND d.invoice_id = $2 LIMIT 1'\n INTO v_nf USING v_tenant, p_invoice_id;\n END IF;\n IF v_nf IS NULL AND v_inv.order_id IS NOT NULL THEN\n SELECT nullif(btrim(o.reference_number), '') INTO v_nf FROM public.orders o WHERE o.id = v_inv.order_id;\n END IF;\n v_nf := coalesce(v_nf, nullif(btrim(v_inv.metadata ->> 'fiscalNumber'), ''));\n IF v_nf IS NULL THEN RETURN; END IF;\n\n RETURN QUERY EXECUTE $q$\n WITH lines AS (\n SELECT l.item_id, l.product_id, lower(btrim(coalesce(p.name, l.name))) AS pname\n FROM public.v_financial_invoice_lines l\n LEFT JOIN public.products p ON p.id = l.product_id\n WHERE l.invoice_id = $2 AND l.item_cancelled_at IS NULL\n ), entries AS (\n SELECT m.product_id, m.source_location_id AS location_id, sum(m.quantity) AS qty,\n min(m.movement_date)::date AS entered_on, lower(btrim(p.name)) AS pname\n FROM public.plg_inventory_stock_movements m\n LEFT JOIN public.products p ON p.id = m.product_id\n WHERE m.tenant_id = $1 AND m.kind = 'in'\n AND (m.reason = 'Entrada via DANFE NF-e ' || $3 OR m.document_number = 'NF-e ' || $3)\n GROUP BY m.product_id, m.source_location_id, p.name\n ), by_product AS (\n SELECT l.item_id, e.* FROM lines l JOIN entries e ON e.product_id = l.product_id\n ), by_name AS (\n SELECT l.item_id, e.* FROM lines l JOIN entries e ON e.pname = l.pname\n WHERE l.item_id NOT IN (SELECT item_id FROM by_product)\n AND e.product_id NOT IN (SELECT product_id FROM by_product)\n ), rest_l AS (\n SELECT l.item_id FROM lines l\n WHERE l.item_id NOT IN (SELECT item_id FROM by_product UNION SELECT item_id FROM by_name)\n ), rest_e AS (\n SELECT e.* FROM entries e\n WHERE e.product_id NOT IN (SELECT product_id FROM by_product UNION SELECT product_id FROM by_name)\n ), by_elimination AS (\n -- The V1 sometimes booked a note's entry under another product record.\n -- One line and one product left over on the same note: they are the pair.\n SELECT l.item_id, e.* FROM rest_l l CROSS JOIN rest_e e\n WHERE (SELECT count(*) FROM rest_l) = 1 AND (SELECT count(DISTINCT product_id) FROM rest_e) = 1\n ), matched AS (\n SELECT * FROM by_product UNION ALL SELECT * FROM by_name UNION ALL SELECT * FROM by_elimination\n )\n SELECT m.item_id, m.product_id, m.location_id, m.qty,\n greatest(least(m.qty, coalesce((SELECT sum(sp.quantity) FROM public.plg_inventory_stock_positions sp\n WHERE sp.tenant_id = $1 AND sp.product_id = m.product_id\n AND sp.stock_location_id = m.location_id), 0)), 0),\n m.entered_on\n FROM matched m\n $q$ USING v_tenant, p_invoice_id, v_nf;\nEND $$;\nREVOKE ALL ON FUNCTION public.financial_invoice_stock_entries(uuid) FROM PUBLIC;\nGRANT EXECUTE ON FUNCTION public.financial_invoice_stock_entries(uuid) TO authenticated, service_role;\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-- 7. Cancelar \u2014 itens ou a conta inteira\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-- p_item_ids vazio/nulo = todos. p_stock = [{item_id, destination: 'devolver'\n-- | 'perda' | 'manter', note}] \u2014 obrigat\u00F3rio para cada linha que entrou no\n-- estoque; NULL s\u00F3 pelo caminho antigo (financial_cancel_invoice), que nunca\n-- perguntou e continua sem mexer no estoque.\n\nCREATE OR REPLACE FUNCTION public.financial_cancel_invoice_items(\n p_invoice_id uuid,\n p_item_ids uuid[] DEFAULT NULL,\n p_reason_id uuid DEFAULT NULL,\n p_detail text DEFAULT NULL,\n p_stock jsonb DEFAULT '[]'::jsonb,\n p_idempotency_key text DEFAULT NULL\n) RETURNS jsonb\nLANGUAGE plpgsql SECURITY DEFINER SET search_path TO '' AS $$\nDECLARE\n v_tenant uuid := public.plg_financial_require_tenant();\n v_inv public.plg_financial_invoices%ROWTYPE;\n v_reason text;\n v_rname text;\n v_live uuid[];\n v_sel uuid[];\n v_whole boolean;\n v_amount numeric := 0;\n v_cut numeric;\n v_take numeric;\n v_credit numeric := 0;\n v_paid numeric;\n v_inst record;\n v_alloc record;\n v_entry record;\n v_dest jsonb;\n v_dest_k text;\n v_stock jsonb := '[]'::jsonb;\n v_op jsonb;\n v_ops jsonb;\n v_person uuid;\n v_cancel_acc uuid;\n v_mov uuid;\n v_log uuid;\n v_nf text;\nBEGIN\n SELECT * INTO v_inv FROM public.plg_financial_invoices WHERE id = p_invoice_id AND tenant_id = v_tenant FOR UPDATE;\n IF NOT FOUND THEN\n RAISE EXCEPTION 'financial_cancel_invoice_items: invoice % not found', p_invoice_id USING ERRCODE = 'P0002';\n END IF;\n PERFORM public.plg_financial_require_permission('financial.edit', v_inv.unit_id);\n\n IF p_idempotency_key IS NOT NULL THEN\n SELECT c.id INTO v_log FROM public.plg_financial_cancellations c\n WHERE c.tenant_id = v_tenant AND c.idempotency_key = p_idempotency_key;\n IF v_log IS NOT NULL THEN\n RETURN (SELECT jsonb_build_object('cancellation_id', c.id, 'invoice_id', c.invoice_id, 'whole', c.whole,\n 'amount', c.amount, 'credit_amount', c.credit_amount, 'replayed', true)\n FROM public.plg_financial_cancellations c WHERE c.id = v_log);\n END IF;\n END IF;\n IF v_inv.cancelled_at IS NOT NULL THEN\n RAISE EXCEPTION 'financial_cancel_invoice_items: invoice % is already cancelled', v_inv.number USING ERRCODE = '55000';\n END IF;\n\n -- O motivo: da lista, com o detalhe ao lado \u2014 \u00ABOutros \u2014 nota devolvida\u00BB.\n IF p_reason_id IS NOT NULL THEN\n SELECT r.name INTO v_rname FROM public.plg_financial_cancellation_reasons r\n WHERE r.id = p_reason_id AND r.tenant_id = v_tenant;\n IF v_rname IS NULL THEN\n RAISE EXCEPTION 'financial_cancel_invoice_items: reason % not found', p_reason_id USING ERRCODE = 'P0002';\n END IF;\n END IF;\n v_reason := nullif(concat_ws(' \u2014 ', v_rname, nullif(btrim(coalesce(p_detail, '')), '')), '');\n IF v_reason IS NULL THEN\n RAISE EXCEPTION 'financial_cancel_invoice_items: a reason is required' USING ERRCODE = '22023';\n END IF;\n\n SELECT coalesce(array_agg(l.item_id), '{}') INTO v_live\n FROM public.v_financial_invoice_lines l WHERE l.invoice_id = p_invoice_id AND l.item_cancelled_at IS NULL;\n IF p_item_ids IS NULL OR cardinality(p_item_ids) = 0 THEN\n v_sel := v_live;\n ELSE\n IF EXISTS (SELECT 1 FROM unnest(p_item_ids) x WHERE x <> ALL (v_live)) THEN\n RAISE EXCEPTION 'financial_cancel_invoice_items: an item is not a live line of invoice %', v_inv.number USING ERRCODE = '22023';\n END IF;\n SELECT array_agg(DISTINCT x) INTO v_sel FROM unnest(p_item_ids) x;\n END IF;\n v_whole := NOT EXISTS (SELECT 1 FROM unnest(v_live) x WHERE x <> ALL (v_sel));\n\n -- Quanto: a soma das linhas; a conta inteira cancela o que a conta vale (o\n -- V1 tem conta cujas linhas n\u00E3o somam o total, e o total \u00E9 o que se deve).\n IF v_whole THEN\n v_amount := v_inv.total;\n ELSE\n SELECT round(coalesce(sum(l.total), 0), 2) INTO v_amount\n FROM public.v_financial_invoice_lines l WHERE l.invoice_id = p_invoice_id AND l.item_id = ANY (v_sel);\n v_amount := least(v_amount, v_inv.total);\n END IF;\n\n PERFORM set_config('financial.amend', 'on', true);\n\n -- \u2500\u2500 a mercadoria \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\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\n IF p_stock IS NOT NULL THEN\n FOR v_entry IN\n SELECT e.* FROM public.financial_invoice_stock_entries(p_invoice_id) e\n WHERE e.item_id = ANY (v_sel) ORDER BY e.item_id, e.entered_on DESC\n LOOP\n SELECT d INTO v_dest FROM jsonb_array_elements(p_stock) d WHERE d ->> 'item_id' = v_entry.item_id::text LIMIT 1;\n v_dest_k := v_dest ->> 'destination';\n IF v_dest_k IS NULL OR v_dest_k NOT IN ('devolver', 'perda', 'manter') THEN\n RAISE EXCEPTION 'financial_cancel_invoice_items: choose what happens to the goods of item % (devolver | perda | manter)', v_entry.item_id\n USING ERRCODE = '22023', HINT = 'ESCOLHA_DESTINO';\n END IF;\n v_op := NULL;\n IF v_dest_k <> 'manter' AND v_entry.available > 0 THEN\n EXECUTE 'SELECT public.inventory_adjust($1, $2, $3, $4)' INTO v_op\n USING v_entry.location_id,\n jsonb_build_array(jsonb_strip_nulls(jsonb_build_object(\n 'product_id', v_entry.product_id, 'quantity', -v_entry.available,\n 'notes', nullif(btrim(coalesce(v_dest ->> 'note', '')), '')))),\n CASE v_dest_k WHEN 'devolver' THEN 'Devolu\u00E7\u00E3o ao fornecedor' ELSE 'Perda de mercadoria da compra' END,\n 'cancel:' || p_invoice_id || ':' || v_entry.item_id || ':' || v_entry.location_id\n || coalesce(':' || p_idempotency_key, '');\n END IF;\n v_stock := v_stock || jsonb_build_object(\n 'item_id', v_entry.item_id, 'product_id', v_entry.product_id, 'location_id', v_entry.location_id,\n 'destination', v_dest_k, 'entered', v_entry.entered,\n 'moved', CASE WHEN v_op IS NULL THEN 0 ELSE v_entry.available END,\n 'note', nullif(btrim(coalesce(v_dest ->> 'note', '')), ''),\n 'operation_id', v_op ->> 'operation_id');\n END LOOP;\n END IF;\n\n -- \u2500\u2500 as linhas \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\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\n UPDATE public.items SET cancelled_at = now(), cancel_reason = v_reason\n WHERE id = ANY (v_sel) AND tenant_id = v_tenant AND cancelled_at IS NULL;\n\n -- \u2500\u2500 o dinheiro \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\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\n IF v_whole THEN\n SELECT coalesce(sum(b.paid), 0) INTO v_paid FROM public.v_financial_installment_balances b WHERE b.invoice_id = p_invoice_id;\n v_credit := v_paid;\n UPDATE public.plg_financial_invoices\n SET cancelled_at = now(), cancel_reason = v_reason, cancel_reason_id = p_reason_id,\n metadata = metadata || jsonb_build_object('v2_decision', jsonb_build_object('cancelled', true, 'at', now()))\n WHERE id = p_invoice_id;\n UPDATE public.plg_financial_invoice_installments\n SET cancelled_at = now(),\n metadata = metadata || jsonb_build_object('v2_decision', jsonb_build_object('cancelled', true))\n WHERE invoice_id = p_invoice_id AND cancelled_at IS NULL;\n UPDATE public.plg_financial_movements SET status = 'cancelled', updated_at = now()\n WHERE financial_invoice_id = p_invoice_id AND movement_kind = 'bill' AND status <> 'cancelled';\n ELSE\n v_cut := v_amount;\n -- 1\u00BA a parte N\u00C3O paga, da \u00FAltima parcela para tr\u00E1s.\n FOR v_inst IN\n SELECT i.id, i.amount, public.plg_financial_installment_paid(i.id) AS paid\n FROM public.plg_financial_invoice_installments i\n WHERE i.invoice_id = p_invoice_id AND i.cancelled_at IS NULL\n ORDER BY i.number DESC\n LOOP\n EXIT WHEN v_cut <= 0;\n v_take := least(greatest(v_inst.amount - v_inst.paid, 0), v_cut);\n CONTINUE WHEN v_take <= 0;\n UPDATE public.plg_financial_invoice_installments\n SET amount = amount - v_take,\n cancelled_at = CASE WHEN amount - v_take <= 0 AND v_inst.paid = 0 THEN now() END,\n metadata = metadata || jsonb_build_object('v2_decision', jsonb_build_object('amended', true))\n WHERE id = v_inst.id;\n v_cut := v_cut - v_take;\n END LOOP;\n -- 2\u00BA o que sobrou sai do pago: o pagamento mais recente primeiro, e o que\n -- ele deixa de quitar vira cr\u00E9dito.\n FOR v_inst IN\n SELECT i.id, i.amount FROM public.plg_financial_invoice_installments i\n WHERE i.invoice_id = p_invoice_id AND i.cancelled_at IS NULL\n ORDER BY i.number DESC\n LOOP\n EXIT WHEN v_cut <= 0;\n FOR v_alloc IN\n SELECT a.id, a.amount FROM public.plg_financial_allocations a\n JOIN public.plg_financial_movements m ON m.id = a.payment_movement_id\n WHERE a.installment_id = v_inst.id AND m.status <> 'cancelled' AND m.reversed_by_id IS NULL\n ORDER BY m.payment_date DESC NULLS LAST, a.created_at DESC\n LOOP\n EXIT WHEN v_cut <= 0;\n v_take := least(v_alloc.amount, v_cut);\n IF v_take >= v_alloc.amount THEN\n DELETE FROM public.plg_financial_allocations WHERE id = v_alloc.id;\n ELSE\n UPDATE public.plg_financial_allocations SET amount = amount - v_take,\n metadata = metadata || jsonb_build_object('reduced_by_cancellation', v_take)\n WHERE id = v_alloc.id;\n END IF;\n UPDATE public.plg_financial_invoice_installments\n SET amount = amount - v_take,\n metadata = metadata || jsonb_build_object('v2_decision', jsonb_build_object('amended', true))\n WHERE id = v_inst.id;\n v_credit := v_credit + v_take;\n v_cut := v_cut - v_take;\n END LOOP;\n END LOOP;\n UPDATE public.plg_financial_invoices\n SET total = greatest(total - v_amount, 0),\n metadata = metadata || jsonb_build_object('v2_decision', jsonb_build_object('amended', true, 'at', now()))\n WHERE id = p_invoice_id;\n PERFORM public.plg_financial_sync_mirror(i.id)\n FROM public.plg_financial_invoice_installments i WHERE i.invoice_id = p_invoice_id;\n END IF;\n\n -- \u2500\u2500 o cr\u00E9dito \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\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\n -- Fornecedor: ele passa a nos dever \u2014 d\u00E9bito na conta dele. Cliente: n\u00F3s\n -- passamos a dever a ele \u2014 cr\u00E9dito na conta dele (como financial_cancel_paid_item).\n v_credit := round(v_credit, 2);\n IF v_credit > 0 THEN\n IF v_inv.counterparty_person_id IS NULL THEN\n RAISE EXCEPTION 'financial_cancel_invoice_items: % was paid and the invoice has no counterparty to hold the credit', v_credit\n USING ERRCODE = '55000';\n END IF;\n v_person := public.plg_financial_person_account(v_tenant, v_inv.counterparty_person_id);\n v_cancel_acc := public.plg_financial_system_account(v_tenant, 'cancellations');\n INSERT INTO public.plg_financial_movements\n (tenant_id, unit_id, financial_invoice_id, direction, movement_kind, amount, paid_amount, status, due_date, payment_date,\n method, reason, metadata, idempotency_key, created_by, debit_account_id, credit_account_id)\n VALUES\n (v_tenant, v_inv.unit_id, v_inv.id, CASE WHEN v_inv.direction = 'payable' THEN 'credit' ELSE 'debit' END,\n 'transfer', v_credit, v_credit, 'paid', current_date, current_date, 'credit', v_reason,\n jsonb_build_object('source', 'financial_cancel_invoice_items', 'credit_source', 'cancellation',\n 'cancelled_invoice_id', v_inv.id, 'cancelled_item_ids', to_jsonb(v_sel)),\n CASE WHEN p_idempotency_key IS NOT NULL THEN 'cancel-credit:' || p_idempotency_key END, auth.uid(),\n CASE WHEN v_inv.direction = 'payable' THEN v_person ELSE v_cancel_acc END,\n CASE WHEN v_inv.direction = 'payable' THEN v_cancel_acc ELSE v_person END)\n RETURNING id INTO v_mov;\n END IF;\n\n INSERT INTO public.plg_financial_cancellations\n (tenant_id, unit_id, invoice_id, whole, item_ids, reason_id, reason, amount, credit_amount, credit_movement_id,\n stock, idempotency_key, created_by)\n VALUES (v_tenant, v_inv.unit_id, v_inv.id, v_whole, v_sel, p_reason_id, v_reason, v_amount, v_credit, v_mov,\n v_stock, p_idempotency_key, auth.uid())\n RETURNING id INTO v_log;\n\n PERFORM public.plg_financial_audit(v_tenant,\n CASE WHEN v_whole THEN 'financial.invoice.cancel' ELSE 'financial.invoice.cancel_items' END,\n 'plg_financial_invoices', v_inv.id::text,\n jsonb_build_object('reason', v_reason, 'reason_id', p_reason_id, 'item_ids', to_jsonb(v_sel), 'amount', v_amount,\n 'credit_amount', v_credit, 'stock', v_stock, 'cancellation_id', v_log));\n\n RETURN jsonb_build_object('cancellation_id', v_log, 'invoice_id', v_inv.id, 'whole', v_whole, 'amount', v_amount,\n 'credit_amount', v_credit, 'credit_movement_id', v_mov, 'stock', v_stock, 'replayed', false);\nEND $$;\nREVOKE ALL ON FUNCTION public.financial_cancel_invoice_items(uuid, uuid[], uuid, text, jsonb, text) FROM PUBLIC;\nGRANT EXECUTE ON FUNCTION public.financial_cancel_invoice_items(uuid, uuid[], uuid, text, jsonb, text) TO authenticated, service_role;\n\n-- O caminho antigo (cancelar em lote na lista) vira a conta inteira do novo,\n-- com o texto como detalhe e sem tocar no estoque.\nCREATE OR REPLACE FUNCTION public.financial_cancel_invoice(p_invoice_id uuid, p_reason text, p_idempotency_key text DEFAULT NULL::text) RETURNS jsonb\n LANGUAGE plpgsql SECURITY DEFINER\n SET search_path TO ''\n AS $$\nDECLARE\n v_tenant uuid := public.plg_financial_require_tenant();\n v_res jsonb;\nBEGIN\n IF nullif(btrim(coalesce(p_reason, '')), '') IS NULL THEN\n RAISE EXCEPTION 'financial_cancel_invoice: a reason is required' USING ERRCODE = '22023';\n END IF;\n IF EXISTS (SELECT 1 FROM public.plg_financial_invoices WHERE id = p_invoice_id AND tenant_id = v_tenant AND cancelled_at IS NOT NULL) THEN\n RETURN jsonb_build_object('invoice_id', p_invoice_id, 'status', 'cancelled', 'replayed', true);\n END IF;\n v_res := public.financial_cancel_invoice_items(p_invoice_id, NULL, NULL, p_reason, NULL, p_idempotency_key);\n RETURN v_res || jsonb_build_object('status', 'cancelled');\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-- 8. Pagar com o cr\u00E9dito do fornecedor\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-- O saldo da conta da pessoa N\u00C3O serve: o porte do V1 lan\u00E7a os pagamentos a\n-- fornecedor contra essa mesma conta (R$ 102 mil na Artorius em 09-10-2026), e\n-- lido como cr\u00E9dito ele inventaria dinheiro. Cr\u00E9dito de fornecedor \u00E9 s\u00F3 o que\n-- um cancelamento criou, menos o que j\u00E1 se pagou com ele. As pernas do\n-- pagamento j\u00E1 estavam certas; muda s\u00F3 a confer\u00EAncia. Remendo no corpo vivo,\n-- como a fiscal-br/015: reescrever 134 linhas de mem\u00F3ria \u00E9 como se perde regra.\n\nCREATE OR REPLACE FUNCTION public.plg_financial_supplier_credit(p_tenant uuid, p_person uuid)\nRETURNS numeric LANGUAGE sql SECURITY DEFINER SET search_path TO '' AS $$\n WITH acc AS (SELECT public.plg_financial_person_account(p_tenant, p_person) AS id)\n SELECT round(\n coalesce((SELECT sum(m.amount) FROM public.plg_financial_movements m, acc\n WHERE m.tenant_id = p_tenant AND m.debit_account_id = acc.id AND m.status <> 'cancelled'\n AND m.reversed_by_id IS NULL AND m.metadata ->> 'credit_source' = 'cancellation'), 0)\n - coalesce((SELECT sum(m.amount) FROM public.plg_financial_movements m, acc\n WHERE m.tenant_id = p_tenant AND m.credit_account_id = acc.id AND m.status <> 'cancelled'\n AND m.reversed_by_id IS NULL AND m.movement_kind = 'payment' AND m.method = 'credit'\n AND m.direction = 'debit'), 0), 2)\n$$;\nREVOKE ALL ON FUNCTION public.plg_financial_supplier_credit(uuid, uuid) FROM PUBLIC;\nGRANT EXECUTE ON FUNCTION public.plg_financial_supplier_credit(uuid, uuid) TO service_role;\n\nCREATE OR REPLACE FUNCTION public.financial_supplier_credit(p_person_id uuid)\nRETURNS numeric LANGUAGE plpgsql SECURITY DEFINER SET search_path TO '' AS $$\nDECLARE v_tenant uuid := public.plg_financial_require_tenant();\nBEGIN\n PERFORM public.plg_financial_require_permission('financial.read');\n RETURN public.plg_financial_supplier_credit(v_tenant, p_person_id);\nEND $$;\nREVOKE ALL ON FUNCTION public.financial_supplier_credit(uuid) FROM PUBLIC;\nGRANT EXECUTE ON FUNCTION public.financial_supplier_credit(uuid) TO authenticated, service_role;\n\nDO $m$\nDECLARE\n v_def text;\n v_antes text := 'SELECT balance INTO v_credit FROM public.v_financial_account_balances WHERE account_id = v_holder;';\n v_depois text := 'SELECT CASE WHEN v_invoice.direction = ''payable'' THEN public.plg_financial_supplier_credit(v_tenant, v_invoice.counterparty_person_id) ELSE balance END INTO v_credit FROM public.v_financial_account_balances WHERE account_id = v_holder;';\nBEGIN\n SELECT pg_get_functiondef(p.oid) INTO v_def\n FROM pg_proc p JOIN pg_namespace n ON n.oid = p.pronamespace\n WHERE n.nspname = 'public' AND p.proname = 'financial_receive_payment';\n IF v_def IS NULL OR position('plg_financial_supplier_credit' in v_def) > 0 THEN RETURN; END IF;\n IF position(v_antes in v_def) = 0 THEN\n RAISE EXCEPTION 'financial/120: financial_receive_payment n\u00E3o \u00E9 o esperado \u2014 confira \u00E0 m\u00E3o';\n END IF;\n EXECUTE replace(v_def, v_antes, v_depois);\nEND\n$m$;\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-- 9. Juros, multa e desconto entram NA conta\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-- No V1 o boleto pago com atraso ganha a linha \u00ABMulta/Juros\u00BB (plano 5.1.3) e\n-- a parcela passa a valer o que o banco cobrou. A concilia\u00E7\u00E3o do V2 criava\n-- uma conta avulsa s\u00F3 com os juros, sem fornecedor e sem caminho de volta.\n\nCREATE OR REPLACE FUNCTION public.financial_add_installment_charge(\n p_installment_id uuid,\n p_kind text,\n p_amount numeric,\n p_chart_account_id uuid DEFAULT NULL,\n p_description text DEFAULT NULL,\n p_idempotency_key text DEFAULT NULL\n) RETURNS jsonb\nLANGUAGE plpgsql SECURITY DEFINER SET search_path TO '' AS $$\nDECLARE\n v_tenant uuid := public.plg_financial_require_tenant();\n v_inst public.plg_financial_invoice_installments%ROWTYPE;\n v_inv public.plg_financial_invoices%ROWTYPE;\n v_amount numeric := round(coalesce(p_amount, 0), 2);\n v_signed numeric;\n v_paid numeric;\n v_item uuid;\n v_own boolean;\n v_label text;\nBEGIN\n IF p_kind NOT IN ('interest', 'fine', 'discount') THEN\n RAISE EXCEPTION 'financial_add_installment_charge: kind must be interest | fine | discount' USING ERRCODE = '22023';\n END IF;\n IF v_amount <= 0 THEN\n RAISE EXCEPTION 'financial_add_installment_charge: amount must be positive' USING ERRCODE = '22023';\n END IF;\n SELECT * INTO v_inst FROM public.plg_financial_invoice_installments WHERE id = p_installment_id AND tenant_id = v_tenant FOR UPDATE;\n IF NOT FOUND THEN\n RAISE EXCEPTION 'financial_add_installment_charge: installment % not found', p_installment_id USING ERRCODE = 'P0002';\n END IF;\n SELECT * INTO v_inv FROM public.plg_financial_invoices WHERE id = v_inst.invoice_id FOR UPDATE;\n PERFORM public.plg_financial_require_permission('financial.edit', v_inv.unit_id);\n IF v_inst.cancelled_at IS NOT NULL OR v_inv.cancelled_at IS NOT NULL THEN\n RAISE EXCEPTION 'financial_add_installment_charge: installment is cancelled' USING ERRCODE = '55000';\n END IF;\n IF p_idempotency_key IS NOT NULL THEN\n SELECT i.id INTO v_item FROM public.items i\n WHERE i.tenant_id = v_tenant AND i.metadata ->> 'charge_idempotency_key' = p_idempotency_key;\n IF v_item IS NOT NULL THEN\n RETURN jsonb_build_object('item_id', v_item, 'installment_id', v_inst.id, 'invoice_id', v_inv.id, 'replayed', true);\n END IF;\n END IF;\n\n v_signed := CASE WHEN p_kind = 'discount' THEN -v_amount ELSE v_amount END;\n v_paid := public.plg_financial_installment_paid(v_inst.id);\n IF v_inst.amount + v_signed < v_paid THEN\n RAISE EXCEPTION 'financial_add_installment_charge: the discount takes installment % below what was already paid (%)', v_inst.number, v_paid\n USING ERRCODE = '23514';\n END IF;\n v_label := coalesce(nullif(btrim(coalesce(p_description, '')), ''),\n CASE p_kind WHEN 'interest' THEN 'Juros por atraso' WHEN 'fine' THEN 'Multa por atraso' ELSE 'Desconto obtido' END);\n\n PERFORM set_config('financial.amend', 'on', true);\n\n -- A linha mora onde as outras moram: conta que veio de um pedido (o V1 todo)\n -- lista as linhas DO PEDIDO enquanto n\u00E3o tiver nenhuma pr\u00F3pria \u2014 p\u00F4r a\n -- primeira na conta faria as outras sumirem da tela (v_financial_invoice_lines).\n v_own := EXISTS (SELECT 1 FROM public.items WHERE invoice_id = v_inv.id) OR v_inv.order_id IS NULL;\n INSERT INTO public.items (tenant_id, unit_id, invoice_id, order_id, name, description, quantity, unit_price, total,\n sort_order, chart_account_id, metadata)\n VALUES (v_tenant, v_inv.unit_id,\n CASE WHEN v_own THEN v_inv.id END, CASE WHEN v_own THEN NULL ELSE v_inv.order_id END,\n v_label, v_label, 1, v_signed, v_signed,\n 1000 + coalesce((SELECT count(*) FROM public.v_financial_invoice_lines l WHERE l.invoice_id = v_inv.id), 0),\n p_chart_account_id,\n jsonb_strip_nulls(jsonb_build_object('itemKind', 'other', 'charge', p_kind, 'installment_id', v_inst.id,\n 'source', 'financial_add_installment_charge',\n 'charge_idempotency_key', p_idempotency_key)))\n RETURNING id INTO v_item;\n\n UPDATE public.plg_financial_invoice_installments\n SET amount = amount + v_signed,\n metadata = metadata || jsonb_build_object('v2_decision', jsonb_build_object('amended', true))\n WHERE id = v_inst.id;\n UPDATE public.plg_financial_invoices\n SET total = total + v_signed,\n metadata = metadata || jsonb_build_object('v2_decision', jsonb_build_object('amended', true, 'at', now()))\n WHERE id = v_inv.id;\n PERFORM public.plg_financial_sync_mirror(v_inst.id);\n\n PERFORM public.plg_financial_audit(v_tenant, 'financial.invoice.charge', 'plg_financial_invoices', v_inv.id::text,\n jsonb_build_object('installment_id', v_inst.id, 'kind', p_kind, 'amount', v_signed, 'item_id', v_item,\n 'chart_account_id', p_chart_account_id));\n RETURN jsonb_build_object('item_id', v_item, 'installment_id', v_inst.id, 'invoice_id', v_inv.id,\n 'amount', v_signed, 'installment_amount', v_inst.amount + v_signed, 'replayed', false);\nEND $$;\nREVOKE ALL ON FUNCTION public.financial_add_installment_charge(uuid, text, numeric, uuid, text, text) FROM PUBLIC;\nGRANT EXECUTE ON FUNCTION public.financial_add_installment_charge(uuid, text, numeric, uuid, text, text) TO authenticated, service_role;\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-- 10. A sincronia do V1 respeita o V2 \u2014 e para de dobrar parcela\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-- Duas mudan\u00E7as no corpo vivo (que difere do arquivo 071/079):\n--\n-- a) parcela que o V2 cancelou ou emendou (`v2_decision`) n\u00E3o volta a valer\n-- nem a ter o valor antigo quando o V1 regrava a linha;\n-- b) o V1 apaga e recria a parcela ao salvar a conta. Em conviv\u00EAncia a\n-- exclus\u00E3o n\u00E3o atravessa, e a recriada ca\u00EDa no `unique_violation` e\n-- ENTRAVA COMO N\u00BA+1 \u2014 PAG-000330 com 5.656,40 + 5.656,48 para um aluguel\n-- de 5.656,48. Agora, se somar a nova estouraria o total da conta, ela \u00E9\n-- a substituta: a antiga (migrada, sem baixa) sai cancelada e a nova\n-- herda o n\u00FAmero. Duas parcelas vivas de mesmo n\u00FAmero que CABEM no total\n-- (a fatura #1869) continuam entrando como antes.\n\nCREATE OR REPLACE FUNCTION migration._fin_installment_from_bill(p_tenant uuid, p_id uuid, p_invoice uuid, p_unit uuid, p_number integer, p_due date, p_amount numeric, p_cancelled boolean, p_sync boolean)\n RETURNS void\n LANGUAGE plpgsql\n SECURITY DEFINER\n SET search_path TO ''\nAS $function$\nDECLARE\n v_number integer := greatest(coalesce(p_number, 1), 1);\n v_cancel timestamptz := CASE WHEN coalesce(p_cancelled, false)\n THEN coalesce(p_due, current_date)::timestamptz END;\n v_old record;\n v_total numeric;\n v_live numeric;\nBEGIN\n IF p_invoice IS NULL THEN RETURN; END IF;\n\n DELETE FROM public.plg_financial_invoice_installments i\n WHERE i.invoice_id = p_invoice\n AND i.id <> p_id\n AND NOT (i.metadata ? 'migration')\n AND NOT EXISTS (SELECT 1 FROM public.plg_financial_allocations a WHERE a.installment_id = i.id)\n AND NOT EXISTS (SELECT 1 FROM public.plg_financial_movements m\n WHERE m.financial_invoice_installment_id = i.id);\n\n BEGIN\n INSERT INTO public.plg_financial_invoice_installments\n (id, tenant_id, unit_id, invoice_id, number, due_date, amount, cancelled_at, metadata)\n VALUES (p_id, p_tenant, p_unit, p_invoice, v_number, p_due,\n greatest(coalesce(p_amount, 0), 0), v_cancel,\n jsonb_build_object('migration', jsonb_build_object('from', 'financial_movements')))\n ON CONFLICT (id) DO UPDATE SET\n number = EXCLUDED.number,\n due_date = EXCLUDED.due_date,\n amount = CASE WHEN public.plg_financial_invoice_installments.metadata ? 'v2_decision'\n THEN public.plg_financial_invoice_installments.amount ELSE EXCLUDED.amount END,\n cancelled_at = CASE WHEN public.plg_financial_invoice_installments.metadata ? 'v2_decision'\n THEN public.plg_financial_invoice_installments.cancelled_at ELSE EXCLUDED.cancelled_at END,\n unit_id = coalesce(public.plg_financial_invoice_installments.unit_id, EXCLUDED.unit_id),\n metadata = public.plg_financial_invoice_installments.metadata || EXCLUDED.metadata;\n EXCEPTION\n WHEN unique_violation THEN\n SELECT i.id, i.amount INTO v_old\n FROM public.plg_financial_invoice_installments i\n WHERE i.invoice_id = p_invoice AND i.number = v_number AND i.id <> p_id\n AND i.cancelled_at IS NULL\n AND i.metadata ? 'migration' AND NOT (i.metadata ? 'v2_decision')\n AND NOT EXISTS (SELECT 1 FROM public.plg_financial_allocations a WHERE a.installment_id = i.id);\n SELECT f.total INTO v_total FROM public.plg_financial_invoices f WHERE f.id = p_invoice;\n SELECT coalesce(sum(i.amount), 0) INTO v_live FROM public.plg_financial_invoice_installments i\n WHERE i.invoice_id = p_invoice AND i.cancelled_at IS NULL;\n\n IF v_old.id IS NOT NULL AND v_cancel IS NULL\n AND v_live + greatest(coalesce(p_amount, 0), 0) > coalesce(v_total, 0) + 0.01 THEN\n -- A substituta: a antiga sai do n\u00FAmero (cancelada) e a nova entra nele.\n UPDATE public.plg_financial_invoice_installments\n SET cancelled_at = now(),\n number = (SELECT coalesce(max(x.number), 0) + 1 FROM public.plg_financial_invoice_installments x WHERE x.invoice_id = p_invoice),\n metadata = metadata || jsonb_build_object('superseded_by', p_id)\n WHERE id = v_old.id;\n INSERT INTO public.plg_financial_invoice_installments\n (id, tenant_id, unit_id, invoice_id, number, due_date, amount, cancelled_at, metadata)\n VALUES (p_id, p_tenant, p_unit, p_invoice, v_number, p_due, greatest(coalesce(p_amount, 0), 0), NULL,\n jsonb_build_object('migration', jsonb_build_object('from', 'financial_movements', 'supersedes', v_old.id)))\n ON CONFLICT (id) DO NOTHING;\n PERFORM public.plg_financial_sync_mirror(v_old.id);\n ELSE\n INSERT INTO public.plg_financial_invoice_installments\n (id, tenant_id, unit_id, invoice_id, number, due_date, amount, cancelled_at, metadata)\n SELECT p_id, p_tenant, p_unit, p_invoice, coalesce(max(i.number), 0) + 1, p_due,\n greatest(coalesce(p_amount, 0), 0), v_cancel,\n jsonb_build_object('migration', jsonb_build_object('from', 'financial_movements', 'renumbered', true))\n FROM public.plg_financial_invoice_installments i\n WHERE i.invoice_id = p_invoice\n ON CONFLICT (id) DO NOTHING;\n END IF;\n END;\n\n IF p_sync THEN\n PERFORM public.plg_financial_sync_mirror(p_id);\n END IF;\nEND $function$;\n";
|
|
122
|
+
export declare const MIGRATION_121_O_MOTIVO_E_DO_ESTABELECIMENTO = "-- O terceiro motivo padr\u00E3o \u00E9 \u00ABSolicitado pelo estabelecimento\u00BB, n\u00E3o \u00ABpelo\n-- profissional\u00BB (decis\u00E3o do founder, 09-10-2026): numa conta a pagar quem pede\n-- o cancelamento \u00E9 a casa, e o V1 herdou o r\u00F3tulo da agenda de servi\u00E7os.\n--\n-- Renomeia o semeado pela 120 s\u00F3 onde ningu\u00E9m o usou \u2014 um motivo j\u00E1 gravado num\n-- cancelamento continua dizendo o que disse. Idempotente.\n\nUPDATE public.plg_financial_cancellation_reasons r\n SET name = 'Solicitado pelo estabelecimento'\n WHERE r.name = 'Solicitado pelo profissional'\n AND NOT EXISTS (SELECT 1 FROM public.plg_financial_cancellations c WHERE c.reason_id = r.id)\n AND NOT EXISTS (SELECT 1 FROM public.plg_financial_cancellation_reasons x\n WHERE x.tenant_id = r.tenant_id AND lower(x.name) = 'solicitado pelo estabelecimento');\n\nCREATE OR REPLACE FUNCTION public.financial_cancellation_reasons()\nRETURNS SETOF public.plg_financial_cancellation_reasons\nLANGUAGE plpgsql SECURITY DEFINER SET search_path TO '' AS $$\nDECLARE v_tenant uuid := public.plg_financial_require_tenant();\nBEGIN\n PERFORM public.plg_financial_require_permission('financial.read');\n IF NOT EXISTS (SELECT 1 FROM public.plg_financial_cancellation_reasons r WHERE r.tenant_id = v_tenant) THEN\n INSERT INTO public.plg_financial_cancellation_reasons (tenant_id, name)\n SELECT v_tenant, n FROM unnest(ARRAY['Outros', 'Solicitado pelo cliente', 'Solicitado pelo estabelecimento']) n\n ON CONFLICT DO NOTHING;\n END IF;\n RETURN QUERY SELECT * FROM public.plg_financial_cancellation_reasons r\n WHERE r.tenant_id = v_tenant AND r.is_active ORDER BY r.name;\nEND $$;\n";
|
|
123
|
+
export declare const MIGRATION_122_O_CANCELAMENTO_FECHA_AS_PORTAS_QUE_ABRIU = "-- ============================================================================\n-- 122 \u2014 cancelling closes the doors 120 left open\n--\n-- 120 (cancel by item, with reason, stock and supplier credit) shipped three\n-- regressions the gates caught on main and that were live on the cluster:\n--\n-- 1. `v_financial_invoice_lines` was recreated WITHOUT `security_invoker`. A\n-- view without it runs as its owner and skips RLS, so every signed-in user\n-- could read every tenant's invoice lines. Every earlier definition of this\n-- view (010 \u2026 114) was security_invoker; this puts it back.\n--\n-- 2. `plg_financial_supplier_credit(p_tenant, p_person)` takes the tenant as an\n-- argument and was revoked only FROM PUBLIC. The pools' default privileges\n-- grant EXECUTE to `authenticated` on every new function, so any user could\n-- ask for any tenant's supplier credit. Only the definer wrappers\n-- (`financial_supplier_credit`, the payment check) call it, and they run as\n-- the owner, so closing it to clients changes nothing for them.\n--\n-- 3. Three money RPCs accept `p_idempotency_key` but did not claim it, so two\n-- concurrent calls with the same key could both pass the replay lookup and\n-- both write (#142). They now take the same advisory lock every other money\n-- RPC takes, right before that lookup. The bodies are patched in place, as\n-- 120 itself patched the payment check: copying 130 lines by hand is how a\n-- rule gets lost.\n--\n-- 1 and 2 were hot-fixed on klvfxzreepavcpyjiwla on 2026-10-09 with the same\n-- statements; this file is the record, and it is idempotent.\n-- ============================================================================\n\n-- \u2500\u2500 1. the view reads with the caller's rights again \u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\nALTER VIEW public.v_financial_invoice_lines SET (security_invoker = true);\n\n-- \u2500\u2500 2. the tenant-taking helper is not a client door \u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\nREVOKE EXECUTE ON FUNCTION public.plg_financial_supplier_credit(uuid, uuid) FROM PUBLIC, anon, authenticated;\nGRANT EXECUTE ON FUNCTION public.plg_financial_supplier_credit(uuid, uuid) TO service_role;\n\n-- \u2500\u2500 3. an idempotency key is claimed before it is looked up \u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\nDO $patch$\nDECLARE\n v_fn regprocedure;\n v_def text;\n v_anchor text;\n v_claim constant text := 'PERFORM public.plg_financial_claim_idempotency(v_tenant, p_idempotency_key);';\nBEGIN\n FOR v_fn, v_anchor IN\n SELECT f::regprocedure, a FROM (VALUES\n -- the replay lookup is the first thing that reads the key\n ('public.financial_cancel_invoice_items(uuid, uuid[], uuid, text, jsonb, text)', ' IF p_idempotency_key IS NOT NULL THEN'),\n ('public.financial_add_installment_charge(uuid, text, numeric, uuid, text, text)', ' IF p_idempotency_key IS NOT NULL THEN'),\n -- this one replays on \"already cancelled\" before it delegates, so the\n -- claim goes ahead of that check; the delegate re-takes the same lock,\n -- which a transaction-scoped advisory lock allows\n ('public.financial_cancel_invoice(uuid, text, text)', ' IF EXISTS (SELECT 1 FROM public.plg_financial_invoices WHERE id = p_invoice_id')\n ) AS t(f, a)\n LOOP\n v_def := pg_get_functiondef(v_fn);\n IF position('plg_financial_claim_idempotency' in v_def) > 0 THEN\n CONTINUE; -- already claims (a replay, or a later rewrite)\n END IF;\n IF position(v_anchor in v_def) = 0 THEN\n RAISE EXCEPTION '122: % no longer has the line the claim goes before (%); patch it by hand', v_fn, v_anchor;\n END IF;\n -- first occurrence only: the anchor is the replay lookup, not every later use of the key\n EXECUTE overlay(v_def PLACING ' ' || v_claim || E'\\n' || v_anchor\n FROM position(v_anchor in v_def) FOR length(v_anchor));\n END LOOP;\nEND $patch$;\n";
|
|
121
124
|
export declare const MIGRATIONS: Array<{
|
|
122
125
|
id: string;
|
|
123
126
|
sql: string;
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../../src/migrations/index.ts"],"names":[],"mappings":"AAIA,eAAO,MAAM,sBAAsB,gu5QAm/HlC,CAAA;AAED,eAAO,MAAM,6DAA6D,4/NA2IzE,CAAA;AAED,eAAO,MAAM,0DAA0D,s8MA6GtE,CAAA;AAED,eAAO,MAAM,8CAA8C,2kSAiJ1D,CAAA;AAED,eAAO,MAAM,wCAAwC,6mRAmLpD,CAAA;AAED,eAAO,MAAM,qDAAqD,0tIAiFjE,CAAA;AAED,eAAO,MAAM,gEAAgE,0xjBAiU5E,CAAA;AAED,eAAO,MAAM,8DAA8D,mmgBA2Q1E,CAAA;AAED,eAAO,MAAM,+DAA+D,shwBAqX3E,CAAA;AAED,eAAO,MAAM,uDAAuD,ik2BAydnE,CAAA;AAED,eAAO,MAAM,+DAA+D,8iXAiR3E,CAAA;AAED,eAAO,MAAM,0DAA0D,qvRA6JtE,CAAA;AAED,eAAO,MAAM,wDAAwD,q3QAuKpE,CAAA;AAED,eAAO,MAAM,gEAAgE,0juBA2Y5E,CAAA;AAED,eAAO,MAAM,kDAAkD,60WAyN9D,CAAA;AAED,eAAO,MAAM,yCAAyC,u6OAgJrD,CAAA;AAED,eAAO,MAAM,4CAA4C,87PAwJxD,CAAA;AAED,eAAO,MAAM,iDAAiD,+3LAwH7D,CAAA;AAED,eAAO,MAAM,iDAAiD,o3GAmE7D,CAAA;AAED,eAAO,MAAM,yCAAyC,k0JAqGrD,CAAA;AAED,eAAO,MAAM,yDAAyD,s1CAsBrE,CAAA;AAED,eAAO,MAAM,yCAAyC,ymOAoIrD,CAAA;AAED,eAAO,MAAM,iDAAiD,g+MA0H7D,CAAA;AAED,eAAO,MAAM,6CAA6C,m1MAgIzD,CAAA;AAED,eAAO,MAAM,2CAA2C,+oMA+HvD,CAAA;AAED,eAAO,MAAM,4CAA4C,8rVA+LxD,CAAA;AAED,eAAO,MAAM,mDAAmD,qpMA4H/D,CAAA;AAED,eAAO,MAAM,0DAA0D,mshBAuStE,CAAA;AAED,eAAO,MAAM,8BAA8B,k4CAqB1C,CAAA;AAED,eAAO,MAAM,0CAA0C,smLA+GtD,CAAA;AAED,eAAO,MAAM,0CAA0C,sqGAiDtD,CAAA;AAED,eAAO,MAAM,4DAA4D,ipHA+ExE,CAAA;AAED,eAAO,MAAM,8CAA8C,+65BAke1D,CAAA;AAED,eAAO,MAAM,+CAA+C,q2XAmN3D,CAAA;AAED,eAAO,MAAM,uDAAuD,67bAsNnE,CAAA;AAED,eAAO,MAAM,oDAAoD,y+mBA8RhE,CAAA;AAED,eAAO,MAAM,sDAAsD,suXAgLlE,CAAA;AAED,eAAO,MAAM,yCAAyC,4jXAuLrD,CAAA;AAED,eAAO,MAAM,6DAA6D,
|
|
1
|
+
{"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../../src/migrations/index.ts"],"names":[],"mappings":"AAIA,eAAO,MAAM,sBAAsB,gu5QAm/HlC,CAAA;AAED,eAAO,MAAM,6DAA6D,4/NA2IzE,CAAA;AAED,eAAO,MAAM,0DAA0D,s8MA6GtE,CAAA;AAED,eAAO,MAAM,8CAA8C,2kSAiJ1D,CAAA;AAED,eAAO,MAAM,wCAAwC,6mRAmLpD,CAAA;AAED,eAAO,MAAM,qDAAqD,0tIAiFjE,CAAA;AAED,eAAO,MAAM,gEAAgE,0xjBAiU5E,CAAA;AAED,eAAO,MAAM,8DAA8D,mmgBA2Q1E,CAAA;AAED,eAAO,MAAM,+DAA+D,shwBAqX3E,CAAA;AAED,eAAO,MAAM,uDAAuD,ik2BAydnE,CAAA;AAED,eAAO,MAAM,+DAA+D,8iXAiR3E,CAAA;AAED,eAAO,MAAM,0DAA0D,qvRA6JtE,CAAA;AAED,eAAO,MAAM,wDAAwD,q3QAuKpE,CAAA;AAED,eAAO,MAAM,gEAAgE,0juBA2Y5E,CAAA;AAED,eAAO,MAAM,kDAAkD,60WAyN9D,CAAA;AAED,eAAO,MAAM,yCAAyC,u6OAgJrD,CAAA;AAED,eAAO,MAAM,4CAA4C,87PAwJxD,CAAA;AAED,eAAO,MAAM,iDAAiD,+3LAwH7D,CAAA;AAED,eAAO,MAAM,iDAAiD,o3GAmE7D,CAAA;AAED,eAAO,MAAM,yCAAyC,k0JAqGrD,CAAA;AAED,eAAO,MAAM,yDAAyD,s1CAsBrE,CAAA;AAED,eAAO,MAAM,yCAAyC,ymOAoIrD,CAAA;AAED,eAAO,MAAM,iDAAiD,g+MA0H7D,CAAA;AAED,eAAO,MAAM,6CAA6C,m1MAgIzD,CAAA;AAED,eAAO,MAAM,2CAA2C,+oMA+HvD,CAAA;AAED,eAAO,MAAM,4CAA4C,8rVA+LxD,CAAA;AAED,eAAO,MAAM,mDAAmD,qpMA4H/D,CAAA;AAED,eAAO,MAAM,0DAA0D,mshBAuStE,CAAA;AAED,eAAO,MAAM,8BAA8B,k4CAqB1C,CAAA;AAED,eAAO,MAAM,0CAA0C,smLA+GtD,CAAA;AAED,eAAO,MAAM,0CAA0C,sqGAiDtD,CAAA;AAED,eAAO,MAAM,4DAA4D,ipHA+ExE,CAAA;AAED,eAAO,MAAM,8CAA8C,+65BAke1D,CAAA;AAED,eAAO,MAAM,+CAA+C,q2XAmN3D,CAAA;AAED,eAAO,MAAM,uDAAuD,67bAsNnE,CAAA;AAED,eAAO,MAAM,oDAAoD,y+mBA8RhE,CAAA;AAED,eAAO,MAAM,sDAAsD,suXAgLlE,CAAA;AAED,eAAO,MAAM,yCAAyC,4jXAuLrD,CAAA;AAED,eAAO,MAAM,6DAA6D,k3NA4IzE,CAAA;AAED,eAAO,MAAM,6DAA6D,opMAyFzE,CAAA;AAED,eAAO,MAAM,uCAAuC,s1NA8GnD,CAAA;AAED,eAAO,MAAM,kDAAkD,kzJAwF9D,CAAA;AAED,eAAO,MAAM,mDAAmD,41bA0M/D,CAAA;AAED,eAAO,MAAM,mDAAmD,2mYAgN/D,CAAA;AAED,eAAO,MAAM,6CAA6C,goWAuLzD,CAAA;AAED,eAAO,MAAM,sDAAsD,y+MAsHlE,CAAA;AAED,eAAO,MAAM,oDAAoD,oxPAgJhE,CAAA;AAED,eAAO,MAAM,+DAA+D,++VA8K3E,CAAA;AAED,eAAO,MAAM,8DAA8D,69oBAyU1E,CAAA;AAED,eAAO,MAAM,8CAA8C,8vaAgO1D,CAAA;AAED,eAAO,MAAM,kDAAkD,w/SA+L9D,CAAA;AAED,eAAO,MAAM,8DAA8D,g6SAqL1E,CAAA;AAED,eAAO,MAAM,+CAA+C,snRAwK3D,CAAA;AAED,eAAO,MAAM,oDAAoD,ktRA0KhE,CAAA;AAED,eAAO,MAAM,4CAA4C,y+RA8KxD,CAAA;AAED,eAAO,MAAM,8CAA8C,w8KAiH1D,CAAA;AAED,eAAO,MAAM,qCAAqC,6iOA0IjD,CAAA;AAED,eAAO,MAAM,wCAAwC,+rIAgGpD,CAAA;AAED,eAAO,MAAM,kCAAkC,27IAkG9C,CAAA;AAED,eAAO,MAAM,iCAAiC,o9HA2F7C,CAAA;AAED,eAAO,MAAM,mDAAmD,0iEAsD/D,CAAA;AAED,eAAO,MAAM,8CAA8C,u48BA4e1D,CAAA;AAED,eAAO,MAAM,+CAA+C,+oaAiO3D,CAAA;AAED,eAAO,MAAM,uDAAuD,m/dAgOnE,CAAA;AAED,eAAO,MAAM,oDAAoD,inpBA0ShE,CAAA;AAED,eAAO,MAAM,sDAAsD,gpZA0LlE,CAAA;AAED,eAAO,MAAM,yCAAyC,s+YAiMrD,CAAA;AAED,eAAO,MAAM,yCAAyC,6gaA+LrD,CAAA;AAED,eAAO,MAAM,yDAAyD,ijbA0MrE,CAAA;AAED,eAAO,MAAM,iDAAiD,osUAwK7D,CAAA;AAED,eAAO,MAAM,gDAAgD,6jWAmM5D,CAAA;AAED,eAAO,MAAM,6CAA6C,kviBA0RzD,CAAA;AAED,eAAO,MAAM,+CAA+C,2pjBAuQ3D,CAAA;AAED,eAAO,MAAM,uDAAuD,8iPA8InE,CAAA;AAED,eAAO,MAAM,sDAAsD,y9GA2DlE,CAAA;AAED,eAAO,MAAM,qDAAqD,g9lBA2TjE,CAAA;AAED,eAAO,MAAM,4DAA4D,skHA2DxE,CAAA;AAED,eAAO,MAAM,kDAAkD,uiLA0G9D,CAAA;AAED,eAAO,MAAM,4DAA4D,i9LAuHxE,CAAA;AAED,eAAO,MAAM,yDAAyD,+9BAiBrE,CAAA;AAED,eAAO,MAAM,2DAA2D,47kBAsSvE,CAAA;AAED,eAAO,MAAM,+CAA+C,mxQAgK3D,CAAA;AAED,eAAO,MAAM,oDAAoD,m4IAkFhE,CAAA;AAED,eAAO,MAAM,uEAAuE,yzGA0DnF,CAAA;AAED,eAAO,MAAM,qDAAqD,+mgBAsPjE,CAAA;AAED,eAAO,MAAM,uEAAuE,k/HAqEnF,CAAA;AAED,eAAO,MAAM,iEAAiE,+5VA2M7E,CAAA;AAED,eAAO,MAAM,4DAA4D,40KAgGxE,CAAA;AAED,eAAO,MAAM,4DAA4D,knPAqHxE,CAAA;AAED,eAAO,MAAM,sDAAsD,m4EAsDlE,CAAA;AAED,eAAO,MAAM,0CAA0C,2/JAyHtD,CAAA;AAED,eAAO,MAAM,2CAA2C,k4HAmFvD,CAAA;AAED,eAAO,MAAM,kDAAkD,onTAiK9D,CAAA;AAED,eAAO,MAAM,mDAAmD,q1tBAmV/D,CAAA;AAED,eAAO,MAAM,4DAA4D,stlBAiSxE,CAAA;AAED,eAAO,MAAM,qDAAqD,krIAiEjE,CAAA;AAED,eAAO,MAAM,qDAAqD,u8JAiFjE,CAAA;AAED,eAAO,MAAM,2CAA2C,g5bAyMvD,CAAA;AAED,eAAO,MAAM,wEAAwE,yuSA0KpF,CAAA;AAED,eAAO,MAAM,qEAAqE,g+JAqFjF,CAAA;AAED,eAAO,MAAM,gDAAgD,q0KAqF5D,CAAA;AAED,eAAO,MAAM,wDAAwD,+lGA+CpE,CAAA;AAED,eAAO,MAAM,2DAA2D,03HAuFvE,CAAA;AAED,eAAO,MAAM,sDAAsD,ipDAmClE,CAAA;AAED,eAAO,MAAM,0CAA0C,k5HAuEtD,CAAA;AAED,eAAO,MAAM,sEAAsE,uoRAgLlF,CAAA;AAED,eAAO,MAAM,kDAAkD,urKAkG9D,CAAA;AAED,eAAO,MAAM,mDAAmD,2mNAuI/D,CAAA;AAED,eAAO,MAAM,2CAA2C,m+MA2IvD,CAAA;AAED,eAAO,MAAM,uDAAuD,m1fAmRnE,CAAA;AAED,eAAO,MAAM,yDAAyD,knIAyErE,CAAA;AAED,eAAO,MAAM,6CAA6C,ovNA0IzD,CAAA;AAED,eAAO,MAAM,0DAA0D,qgaAuLtE,CAAA;AAED,eAAO,MAAM,yDAAyD,qvPAgIrE,CAAA;AAED,eAAO,MAAM,uDAAuD,k4IAgGnE,CAAA;AAED,eAAO,MAAM,sDAAsD,woNA0IlE,CAAA;AAED,eAAO,MAAM,uCAAuC,4sIAqEnD,CAAA;AAED,eAAO,MAAM,+CAA+C,6pCAkB3D,CAAA;AAED,eAAO,MAAM,2CAA2C,8sHAsFvD,CAAA;AAED,eAAO,MAAM,sDAAsD,k/LA8HlE,CAAA;AAED,eAAO,MAAM,sDAAsD,k2qDAozBlE,CAAA;AAED,eAAO,MAAM,2CAA2C,osDA4BvD,CAAA;AAED,eAAO,MAAM,sDAAsD,qoIAmElE,CAAA;AAED,eAAO,MAAM,UAAU,EAAE,KAAK,CAAC;IAAE,EAAE,EAAE,MAAM,CAAC;IAAC,GAAG,EAAE,MAAM,CAAA;CAAE,CA4HzD,CAAA"}
|
package/dist/types.d.ts
CHANGED
|
@@ -106,6 +106,9 @@ export interface InvoiceItem {
|
|
|
106
106
|
executionDate?: string;
|
|
107
107
|
executedByProfessionalId?: string;
|
|
108
108
|
metadata?: Record<string, unknown>;
|
|
109
|
+
/** Set when the line was cancelled on its own (cancel by item). */
|
|
110
|
+
cancelledAt?: string;
|
|
111
|
+
cancelReason?: string;
|
|
109
112
|
createdAt: string;
|
|
110
113
|
}
|
|
111
114
|
export interface FinancialMovement {
|
|
@@ -328,6 +331,50 @@ export interface PayMovementInput {
|
|
|
328
331
|
/** Stable for every retry of the same payment attempt. */
|
|
329
332
|
idempotencyKey?: string;
|
|
330
333
|
}
|
|
334
|
+
export interface CancellationReason {
|
|
335
|
+
id: string;
|
|
336
|
+
name: string;
|
|
337
|
+
}
|
|
338
|
+
/** Goods a payable line brought into stock, still traceable to its note. */
|
|
339
|
+
export interface InvoiceStockEntry {
|
|
340
|
+
itemId: string;
|
|
341
|
+
productId: string;
|
|
342
|
+
locationId: string;
|
|
343
|
+
/** Quantity that entered, in the product's stock unit. */
|
|
344
|
+
entered: number;
|
|
345
|
+
/** What is still at that location — the most a return or loss can move. */
|
|
346
|
+
available: number;
|
|
347
|
+
enteredOn: string;
|
|
348
|
+
}
|
|
349
|
+
export type StockDestination = 'devolver' | 'perda' | 'manter';
|
|
350
|
+
export interface CancelInvoiceItemsInput {
|
|
351
|
+
invoiceId: string;
|
|
352
|
+
/** Empty = every live line (the whole invoice). */
|
|
353
|
+
itemIds: string[];
|
|
354
|
+
reasonId: string;
|
|
355
|
+
detail?: string;
|
|
356
|
+
stock: Array<{
|
|
357
|
+
itemId: string;
|
|
358
|
+
destination: StockDestination;
|
|
359
|
+
note?: string;
|
|
360
|
+
}>;
|
|
361
|
+
idempotencyKey?: string;
|
|
362
|
+
}
|
|
363
|
+
export interface CancelInvoiceItemsResult {
|
|
364
|
+
whole: boolean;
|
|
365
|
+
amount: number;
|
|
366
|
+
/** What had been paid on the cancelled part and became counterparty credit. */
|
|
367
|
+
creditAmount: number;
|
|
368
|
+
}
|
|
369
|
+
export interface InstallmentChargeInput {
|
|
370
|
+
installmentId: string;
|
|
371
|
+
kind: 'interest' | 'fine' | 'discount';
|
|
372
|
+
/** Always positive — `kind` carries the sign. */
|
|
373
|
+
amount: number;
|
|
374
|
+
accountId?: string;
|
|
375
|
+
description?: string;
|
|
376
|
+
idempotencyKey?: string;
|
|
377
|
+
}
|
|
331
378
|
export interface CancelInvoiceInput {
|
|
332
379
|
invoiceId: string;
|
|
333
380
|
reason: string;
|