@fayz-ai/plugin-financial 0.22.2 → 0.23.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/components/CancelInvoiceDialog.d.ts +15 -0
- package/dist/components/CancelInvoiceDialog.d.ts.map +1 -1
- package/dist/components/CashSessionHeaderControl.d.ts +2 -1
- package/dist/components/CashSessionHeaderControl.d.ts.map +1 -1
- package/dist/components/FinancialGeneralSettings.d.ts +2 -0
- package/dist/components/FinancialGeneralSettings.d.ts.map +1 -1
- package/dist/components/InstallmentRow.d.ts +4 -1
- package/dist/components/InstallmentRow.d.ts.map +1 -1
- package/dist/components/PaymentReceiptModal.d.ts.map +1 -1
- package/dist/components/PersonStatementWidget.d.ts.map +1 -1
- package/dist/components/ReceiveStockDialog.d.ts +22 -0
- package/dist/components/ReceiveStockDialog.d.ts.map +1 -0
- package/dist/components/StockEntryNote.d.ts +29 -0
- package/dist/components/StockEntryNote.d.ts.map +1 -0
- package/dist/data/supabase.d.ts.map +1 -1
- package/dist/data/types.d.ts +56 -0
- package/dist/data/types.d.ts.map +1 -1
- package/dist/index.js +2691 -698
- package/dist/index.js.map +1 -1
- package/dist/lib/fx-auto.d.ts +7 -0
- package/dist/lib/fx-auto.d.ts.map +1 -0
- package/dist/lib/installment-amounts.d.ts +18 -0
- package/dist/lib/installment-amounts.d.ts.map +1 -0
- package/dist/lib/item-icons.d.ts +6 -0
- package/dist/lib/item-icons.d.ts.map +1 -0
- package/dist/locales/en.d.ts.map +1 -1
- package/dist/locales/pt-BR.d.ts.map +1 -1
- package/dist/migrations/index.d.ts +9 -1
- package/dist/migrations/index.d.ts.map +1 -1
- package/dist/types.d.ts +21 -0
- package/dist/types.d.ts.map +1 -1
- package/dist/views/FinancialPage.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/dist/views/dashboardWidgets.d.ts.map +1 -1
- package/package.json +4 -4
|
@@ -0,0 +1,7 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Writes today's reference rate for `house → each currency` that has none.
|
|
3
|
+
* Returns true when at least one rate was written. Never throws: without a
|
|
4
|
+
* rate the amounts simply stay outside the totals, which the screen says.
|
|
5
|
+
*/
|
|
6
|
+
export declare function ensureReferenceRates(pub: any, tenantId: string, house: string, currencies: string[]): Promise<boolean>;
|
|
7
|
+
//# sourceMappingURL=fx-auto.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"fx-auto.d.ts","sourceRoot":"","sources":["../../src/lib/fx-auto.ts"],"names":[],"mappings":"AAaA;;;;GAIG;AACH,wBAAsB,oBAAoB,CACxC,GAAG,EAAE,GAAG,EAAE,QAAQ,EAAE,MAAM,EAAE,KAAK,EAAE,MAAM,EAAE,UAAU,EAAE,MAAM,EAAE,GAC9D,OAAO,CAAC,OAAO,CAAC,CAwBlB"}
|
|
@@ -0,0 +1,18 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* One installment takes a new amount and the rest of the open ones absorb the
|
|
3
|
+
* difference, so the schedule still sums to the invoice total.
|
|
4
|
+
*
|
|
5
|
+
* `locked` lines (paid, partly paid, cancelled) never move. Lines the person
|
|
6
|
+
* already set by hand (`pinned`) are spared while another open line can take
|
|
7
|
+
* the difference; when none can, they give way rather than leave the sum open.
|
|
8
|
+
* Returns null when no other line can absorb it.
|
|
9
|
+
*/
|
|
10
|
+
export declare function rebalanceInstallments(input: {
|
|
11
|
+
amounts: number[];
|
|
12
|
+
index: number;
|
|
13
|
+
value: number;
|
|
14
|
+
total: number;
|
|
15
|
+
locked?: boolean[];
|
|
16
|
+
pinned?: boolean[];
|
|
17
|
+
}): number[] | null;
|
|
18
|
+
//# sourceMappingURL=installment-amounts.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"installment-amounts.d.ts","sourceRoot":"","sources":["../../src/lib/installment-amounts.ts"],"names":[],"mappings":"AAEA;;;;;;;;GAQG;AACH,wBAAgB,qBAAqB,CAAC,KAAK,EAAE;IAC3C,OAAO,EAAE,MAAM,EAAE,CAAA;IACjB,KAAK,EAAE,MAAM,CAAA;IACb,KAAK,EAAE,MAAM,CAAA;IACb,KAAK,EAAE,MAAM,CAAA;IACb,MAAM,CAAC,EAAE,OAAO,EAAE,CAAA;IAClB,MAAM,CAAC,EAAE,OAAO,EAAE,CAAA;CACnB,GAAG,MAAM,EAAE,GAAG,IAAI,CAsBlB"}
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"item-icons.d.ts","sourceRoot":"","sources":["../../src/lib/item-icons.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,KAAK,MAAM,OAAO,CAAA;AAG9B,eAAO,MAAM,UAAU,EAAE,MAAM,CAAC,MAAM,EAAE,KAAK,CAAC,aAAa,CAAC;IAAE,SAAS,CAAC,EAAE,MAAM,CAAC;IAAC,WAAW,CAAC,EAAE,MAAM,GAAG,MAAM,CAAA;CAAE,CAAC,CAKjH,CAAA"}
|
package/dist/locales/en.d.ts.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"en.d.ts","sourceRoot":"","sources":["../../src/locales/en.ts"],"names":[],"mappings":"AACA,eAAO,MAAM,EAAE,EAAE,MAAM,CAAC,MAAM,EAAE,MAAM,
|
|
1
|
+
{"version":3,"file":"en.d.ts","sourceRoot":"","sources":["../../src/locales/en.ts"],"names":[],"mappings":"AACA,eAAO,MAAM,EAAE,EAAE,MAAM,CAAC,MAAM,EAAE,MAAM,CAs7BrC,CAAA"}
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"pt-BR.d.ts","sourceRoot":"","sources":["../../src/locales/pt-BR.ts"],"names":[],"mappings":"AACA,eAAO,MAAM,IAAI,EAAE,MAAM,CAAC,MAAM,EAAE,MAAM,
|
|
1
|
+
{"version":3,"file":"pt-BR.d.ts","sourceRoot":"","sources":["../../src/locales/pt-BR.ts"],"names":[],"mappings":"AACA,eAAO,MAAM,IAAI,EAAE,MAAM,CAAC,MAAM,EAAE,MAAM,CAy7BvC,CAAA"}
|
|
@@ -111,7 +111,7 @@ export declare const MIGRATION_109_A_REGUA_GRAVA_CAMELCASE_E_O_SQL_LIA_SNAKE = "
|
|
|
111
111
|
export declare const MIGRATION_110_TROCAR_DE_FORNECEDOR_ENQUANTO_NINGUEM_PAGOU = "-- Trocar de fornecedor enquanto ningu\u00E9m pagou.\n--\n-- A trava \u00E9 do BANCO: `trg_plg_financial_invoices_immutable` recusa qualquer\n-- mudan\u00E7a em `counterparty_person_id` a partir do instante em que a conta \u00E9\n-- emitida \u2014 mesmo que ela n\u00E3o tenha recebido um centavo.\n--\n-- Isso j\u00E1 foi revisto duas vezes para os vizinhos dela. A 098 abriu a\n-- compet\u00EAncia e a 099 abriu o total, as duas com o MESMO limite: vale enquanto\n-- `sum(paid) = 0`. A contraparte ficou de fora \u2014 n\u00E3o por decis\u00E3o, ao que tudo\n-- indica, mas porque ningu\u00E9m tinha esbarrado nela.\n--\n-- E ela \u00E9 o campo mais errado de todos na hora de lan\u00E7ar: quem digita seis\n-- contas seguidas escolhe o fornecedor da linha de cima na quarta. Hoje a\n-- corre\u00E7\u00E3o \u00E9 cancelar a conta e lan\u00E7ar outra \u2014 um documento cancelado no\n-- hist\u00F3rico para consertar um clique.\n--\n-- \u2500\u2500 O que continua imut\u00E1vel, 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\n--\n-- `number` \u00C9 a identidade do documento. Troc\u00E1-lo \u00E9 fazer o hist\u00F3rico\n-- falar de outra conta.\n-- `direction` Pagar e receber s\u00E3o lados opostos do raz\u00E3o; virar o sinal\n-- n\u00E3o \u00E9 corrigir, \u00E9 inverter o fato.\n-- `issue_date` A emiss\u00E3o \u00E9 quando o documento passou a existir.\n-- `currency` O valor gravado s\u00F3 significa alguma coisa com ela.\n-- `order_id` O v\u00EDnculo com a venda que a originou.\n--\n-- A contraparte entra no grupo que a 098 e a 099 j\u00E1 definiram: enquanto nada\n-- foi recebido, \u00E9 rascunho; depois do primeiro centavo, \u00E9 hist\u00F3ria.\n--\n-- Idempotente: substitui a fun\u00E7\u00E3o do gatilho.\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 -- O que \u00E9 identidade do documento n\u00E3o muda nunca.\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 -- Compet\u00EAncia (098), total (099) e CONTRAPARTE (110) t\u00EAm a mesma licen\u00E7a e o\n -- mesmo limite. A pergunta s\u00F3 se faz quando um dos tr\u00EAs mudou: ela varre as\n -- parcelas, e n\u00E3o vale pag\u00E1-la em toda grava\u00E7\u00E3o de observa\u00E7\u00E3o.\n IF NEW.competence_date IS DISTINCT FROM OLD.competence_date\n OR NEW.total IS DISTINCT FROM OLD.total\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\nCOMMENT ON FUNCTION public.trg_plg_financial_invoices_immutable() IS\n 'Uma conta emitida \u00E9 imut\u00E1vel na IDENTIDADE (n\u00FAmero, dire\u00E7\u00E3o, emiss\u00E3o, moeda, pedido) sempre; e na compet\u00EAncia, no total e na contraparte s\u00F3 depois do primeiro centavo recebido.';\n";
|
|
112
112
|
export declare const MIGRATION_111_REPARCELAR_SEM_APAGAR_A_PARCELA = "-- Reparcelar sem APAGAR a parcela.\n--\n-- Trocar o valor de uma conta cujo pagamento foi estornado falhava com:\n--\n-- plg_financial_movements is append-only: amount, kind, accounts and LINKS\n-- of movement \u2026 cannot change \u2014 post a reversal instead\n--\n-- S\u00E3o duas regras certas se atropelando:\n--\n-- `financial_reschedule_invoice` APAGA as parcelas e as reescreve \u2014 \u00E9 como\n-- ele transforma 2x em 3x sem passar por um estado que `(invoice_id, number)`\n-- recusa.\n--\n-- `plg_financial_movements.financial_invoice_installment_id` \u00E9 FK com ON\n-- DELETE SET NULL, e o movimento \u00E9 append-only justamente nos LINKS.\n--\n-- O pagamento estornado continua existindo \u2014 \u00E9 isso que um estorno significa:\n-- o par fica no livro, l\u00EDquido em zero. Ele aponta para a parcela. Apagar a\n-- parcela manda o banco anular esse ponteiro, e o gatilho recusa \u2014 com raz\u00E3o,\n-- porque um lan\u00E7amento que muda de dono depois de escrito deixa de ser\n-- hist\u00F3rico.\n--\n-- \u2500\u2500 A corre\u00E7\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\u2500\u2500\u2500\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 parcela de n\u00FAmero N continua sendo a parcela de n\u00FAmero N: ela \u00E9 ATUALIZADA\n-- no lugar, e o id n\u00E3o muda. S\u00F3 some a que sobrou quando o cronograma encolheu\n-- \u2014 e essa, se tiver movimento pendurado, o banco recusa, que \u00E9 o certo.\n--\n-- De quebra, o que estava anexado \u00E0 parcela deixa de precisar do resgate por\n-- n\u00FAmero: n\u00E3o h\u00E1 mais o que resgatar, porque a linha nunca deixou de existir.\n--\n-- Idempotente: substitui a fun\u00E7\u00E3o.\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 -- O espelho legado (`bill`) \u00E9 append-only: nem apaga nem muda de valor.\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' AND m.status <> 'cancelled';\n IF v_mirror > 0 THEN\n RAISE EXCEPTION 'financial_reschedule_invoice: invoice % carries legacy bill movements and cannot be rescheduled', v_invoice.number\n USING ERRCODE = '55000',\n HINT = 'Cancel and reissue this invoice.';\n END IF;\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 'Reescreve o cronograma ATUALIZANDO a parcela de cada n\u00FAmero, sem apag\u00E1-la: um pagamento estornado continua apontando para ela, e o movimento \u00E9 append-only nos links.';\n";
|
|
113
113
|
export declare const MIGRATION_112_A_LINHA_DO_EXTRATO_NAO_E_UM_LANCAMENTO_AINDA = "-- A linha do extrato n\u00E3o \u00E9 um lan\u00E7amento ainda.\n--\n-- \u2500\u2500 O defeito, primeiro, porque \u00E9 ele que decide o desenho \u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\n--\n-- `importBankTransactions` (data/supabase.ts) insere a linha importada como\n-- MOVIMENTO, assim:\n--\n-- movement_kind: 'payment', amount, status: 'paid',\n-- external_id, external_source -- e nenhuma perna\n--\n-- e o baseline n\u00E3o deixa:\n--\n-- CONSTRAINT plg_financial_movements_legs_check CHECK (\n-- movement_kind IN ('bill','commission')\n-- OR metadata ? 'legacy_mirror'\n-- OR (debit_account_id IS NOT NULL AND credit_account_id IS NOT NULL\n-- AND debit_account_id <> credit_account_id))\n--\n-- Todo INSERT daquele caminho \u00E9 recusado. Os testes do plugin passam porque\n-- exercitam o provider MOCK, que n\u00E3o tem constraint nenhuma \u2014 verde que n\u00E3o \u00E9\n-- verdade em lugar nenhum. \u00C9 por isso que a Concilia\u00E7\u00E3o do V2 nunca listou uma\n-- transa\u00E7\u00E3o: n\u00E3o havia como gravar uma.\n--\n-- \u2500\u2500 E a constraint est\u00E1 CERTA \u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\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 linha do extrato rec\u00E9m-chegada n\u00E3o tem duas pernas porque ainda n\u00E3o se\n-- sabe contra o qu\u00EA ela lan\u00E7a. \"RSHOP POSTO STA C-001016, R$ 100,00, d\u00E9bito\"\n-- \u00E9 evid\u00EAncia de que o dinheiro saiu do Ita\u00FA; a outra ponta \u2014 combust\u00EDvel?\n-- adiantamento? transfer\u00EAncia para a conta da outra loja? \u2014 \u00E9 justamente o que\n-- a concilia\u00E7\u00E3o existe para descobrir. For\u00E7\u00E1-la a nascer com uma conta-ponte\n-- inventada \u00E9 gravar um palpite com cara de fato, e \u00E9 o palpite que depois\n-- ningu\u00E9m consegue distinguir do lan\u00E7amento de verdade.\n--\n-- Ent\u00E3o a evid\u00EAncia ganha onde morar, e o movimento continua sendo o evento\n-- econ\u00F4mico. A view da 031 j\u00E1 trabalha exatamente nessa distin\u00E7\u00E3o:\n--\n-- \u00ABEconomic cash events. Native movements always count; imported statement\n-- lines count only after standalone acceptance, never when matched.\u00BB\n--\n-- \u2500\u2500 O que o V1 guarda, e por que quase tudo atravessa \u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\n--\n-- `ob_statement_transactions` tem 25 colunas e a tela do V1 mostra ou busca em\n-- quase todas. `payer_cpf_cnpj` \u00E9 o que casa um PIX com o cliente; `category` e\n-- `transaction_type` s\u00E3o a classifica\u00E7\u00E3o que o provedor j\u00E1 fez; `code` e\n-- `document` s\u00E3o o que o operador digita no campo de busca quando tem o\n-- comprovante na m\u00E3o; `balance` \u00E9 o saldo do banco NAQUELA linha, que \u00E9 como se\n-- confere se o extrato bate. `raw_payload` fica porque o provedor muda de\n-- contrato sem avisar, e quem tem o cru consegue reprocessar.\n--\n-- `provider_deleted` n\u00E3o \u00E9 enfeite: o Open Finance REMOVE lan\u00E7amento (um PIX\n-- devolvido, um estorno do adquirente). A linha n\u00E3o pode sumir da tabela, ou o\n-- que j\u00E1 foi conciliado contra ela vira \u00F3rf\u00E3o sem explica\u00E7\u00E3o \u2014 ela \u00E9 marcada, e\n-- a tela deixa de mostr\u00E1-la.\n--\n-- Idempotente.\n\n-- \u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\n-- 1. A evid\u00EAncia\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_bank_statement_lines (\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 -- A conta do extrato. `kind = 'bank'` na pr\u00E1tica, mas sem CHECK: o V1 tamb\u00E9m\n -- traz fatura de cart\u00E3o por este caminho (`statement_type = 'CREDIT_CARD'`).\n bank_account_id uuid NOT NULL REFERENCES public.plg_financial_accounts(id) ON DELETE CASCADE,\n\n -- Identidade do provedor. \u00C9 o par que torna a importa\u00E7\u00E3o idempotente, e \u00E9 o\n -- mesmo contrato que `plg_financial_movements (external_source, external_id)`\n -- j\u00E1 usa \u2014 reimportar o mesmo per\u00EDodo n\u00E3o duplica, que \u00E9 o comportamento\n -- que o sync do V1 d\u00E1 e sem o qual \u00ABSincronizar agora\u00BB vira uma armadilha.\n external_source text NOT NULL,\n external_id text NOT NULL,\n\n posted_on date NOT NULL,\n -- Quando o provedor diz que a transa\u00E7\u00E3o foi efetivada, se disser. Difere de\n -- `posted_on` em cart\u00E3o e em TED de fim de semana.\n effective_at timestamptz,\n direction text NOT NULL CHECK (direction IN ('credit', 'debit')),\n -- Sempre POSITIVO. O sinal est\u00E1 em `direction`, e guardar o valor com sinal\n -- faria toda soma da tela depender de lembrar disso.\n amount numeric(14,2) NOT NULL CHECK (amount >= 0),\n -- O saldo do banco NESTA linha. \u00C9 com ele que se confere se o extrato bate.\n balance_after numeric(14,2),\n\n description text,\n -- O nome que o provedor d\u00E1 \u00E0 contraparte ('itaubba', o nome do lojista).\n counterparty_name text,\n counterparty_document text,\n document text,\n code text,\n category text,\n transaction_type text,\n\n -- O provedor tirou a linha do extrato. Ela N\u00C3O \u00E9 apagada: o que j\u00E1 foi\n -- conciliado contra ela precisa continuar explic\u00E1vel.\n provider_deleted boolean NOT NULL DEFAULT false,\n -- O cru, porque o contrato do provedor muda sem avisar.\n raw_payload jsonb,\n\n -- A concilia\u00E7\u00E3o. `matched_movement_id` nulo com `reconciled_at` preenchido \u00E9\n -- a linha ACEITA COMO AVULSA \u2014 e \u00E9 a\u00ED que ela vira o evento econ\u00F4mico.\n reconciled_at timestamptz,\n matched_movement_id uuid REFERENCES public.plg_financial_movements(id) ON DELETE SET NULL,\n\n metadata jsonb NOT NULL DEFAULT '{}'::jsonb,\n created_at timestamptz NOT NULL DEFAULT now(),\n updated_at timestamptz NOT NULL DEFAULT now(),\n\n UNIQUE (tenant_id, external_source, external_id)\n);\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. Unidade, \u00EDndices, updated_at\n-- \u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\n\nDO $$\nBEGIN\n ALTER TABLE public.plg_financial_bank_statement_lines\n ADD CONSTRAINT plg_financial_bank_statement_lines_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\n-- A consulta da tela, na ordem em que ela filtra: conta, per\u00EDodo, e a data\n-- descendente que \u00E9 como o extrato se l\u00EA.\nCREATE INDEX IF NOT EXISTS plg_financial_bank_statement_lines_conta_periodo_idx\n ON public.plg_financial_bank_statement_lines (bank_account_id, posted_on DESC)\n WHERE provider_deleted = false;\n\n-- \u00ABStatus: Pendente\u00BB \u00E9 o filtro mais usado da tela, e \u00E9 o que sobra depois de\n-- um m\u00EAs de concilia\u00E7\u00E3o \u2014 o \u00EDndice parcial cobre exatamente essa fila.\nCREATE INDEX IF NOT EXISTS plg_financial_bank_statement_lines_pendentes_idx\n ON public.plg_financial_bank_statement_lines (tenant_id, posted_on DESC)\n WHERE reconciled_at IS NULL AND provider_deleted = false;\n\nCREATE INDEX IF NOT EXISTS plg_financial_bank_statement_lines_movimento_idx\n ON public.plg_financial_bank_statement_lines (matched_movement_id)\n WHERE matched_movement_id IS NOT NULL;\n\nDO $$\nBEGIN\n CREATE TRIGGER plg_financial_bank_statement_lines_updated_at\n BEFORE UPDATE ON public.plg_financial_bank_statement_lines\n FOR EACH ROW EXECUTE FUNCTION public.handle_updated_at();\nEXCEPTION WHEN duplicate_object THEN NULL;\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-- 3. A cerca, pelo template da casa\n-- \u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\n--\n-- Recurso pr\u00F3prio (`financial.bank_statement`) e n\u00E3o o do movimento: quem\n-- concilia n\u00E3o \u00E9 necessariamente quem lan\u00E7a. No V1 a tela \u00E9 do financeiro e do\n-- dono, e a recep\u00E7\u00E3o n\u00E3o a v\u00EA \u2014 um recurso separado \u00E9 o que permite dizer isso\n-- sem abrir o raz\u00E3o inteiro junto.\n--\n-- `unit_optional`: a linha carrega a unidade da conta quando a conta tem uma, e\n-- uma linha sem unidade n\u00E3o pode sumir da concilia\u00E7\u00E3o por causa de campo vazio.\nSELECT app.scaffold_table('public.plg_financial_bank_statement_lines'::regclass,\n 'financial.bank_statement', 'financial', false, 'enforce');\nSELECT app.scaffold_scope('public.plg_financial_bank_statement_lines'::regclass, 'unit_optional', 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-- 4. O extrato como evento econ\u00F4mico\n-- \u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\n--\n-- A 031 decidiu quando uma linha importada CONTA. Agora que a evid\u00EAncia saiu de\n-- `plg_financial_movements`, aquela regra vale aqui: a linha conta quando foi\n-- aceita como avulsa (`reconciled_at` preenchido, `matched_movement_id` nulo).\n-- Casada, quem conta \u00E9 o movimento \u2014 a linha \u00E9 s\u00F3 a prova de que ele aconteceu.\nCREATE OR REPLACE VIEW public.v_financial_bank_statement_unreconciled\nWITH (security_invoker = true) AS\nSELECT line.*\n FROM public.plg_financial_bank_statement_lines line\n WHERE line.reconciled_at IS NULL\n AND line.provider_deleted = false;\n\nCOMMENT ON VIEW public.v_financial_bank_statement_unreconciled IS\n 'Linhas do extrato que ainda n\u00E3o casaram com nada. A fila da Concilia\u00E7\u00E3o Banc\u00E1ria.';\n\nGRANT SELECT ON public.v_financial_bank_statement_unreconciled TO authenticated, service_role;\n";
|
|
114
|
-
export declare const MIGRATION_113_SO_A_CONTA_INTEGRADA_APARECE_NA_CONCILIACAO = "-- S\u00F3 a conta INTEGRADA aparece na Concilia\u00E7\u00E3o.\n--\n-- \u2500\u2500 O filtro que faltava \u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\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 V1 (`BankReconciliation.tsx:263`) monta o seletor assim:\n--\n-- .eq('active', true)\n-- .eq('openfinance_configured', true) \u2190 este\n-- account_type IN ('conta_bancaria','cartao_credito_pagamento')\n-- + cart\u00E3o exige openfinance_card_last4 com 4 d\u00EDgitos\n--\n-- \u00C9 por isso que l\u00E1 aparece UM Ita\u00FA: \u00E9 o \u00FAnico com Open Banking ligado. O V2\n-- listava toda conta banc\u00E1ria, cart\u00E3o e caixa do plano \u2014 inclusive as que nunca\n-- v\u00E3o trazer uma linha. Quem escolhe uma delas v\u00EA a lista vazia e conclui que a\n-- tela est\u00E1 quebrada; o defeito \u00E9 o convite, n\u00E3o a lista.\n--\n-- \u2500\u2500 Por que uma tabela, e n\u00E3o uma coluna \u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\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-- `openfinance_configured` \u00E9 um booleano porque o V1 tem UM provedor. Aqui a\n-- conta pode ser servida por mais de um (o banco por Open Finance, o cart\u00E3o\n-- pelo adquirente), e o v\u00EDnculo carrega coisas que n\u00E3o s\u00E3o da conta nem do\n-- provedor sozinhos: como o provedor CHAMA aquela conta (`external_account_ref`\n-- \u2014 o hash da conta, os quatro \u00FAltimos do cart\u00E3o) e se a origem ainda est\u00E1\n-- viva. Numa coluna, ligar o segundo provedor obriga a escolher qual dos dois\n-- fica.\n--\n-- O `connection_id` aponta para `connections` SEM `REFERENCES`: aquela tabela \u00E9\n-- de packages/db e o v\u00EDnculo \u00E9 opcional (uma origem pode ser semeada antes de\n-- existir conex\u00E3o, e \u00E9 assim que o porte prova a tela). A aus\u00EAncia da FK est\u00E1\n-- anotada aqui porque ela \u00E9 uma escolha, n\u00E3o um esquecimento.\n--\n-- Idempotente.\n\nCREATE TABLE IF NOT EXISTS public.plg_financial_account_statement_sources (\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 account_id uuid NOT NULL REFERENCES public.plg_financial_accounts(id) ON DELETE CASCADE,\n\n -- Quem traz o extrato desta conta: 'plugbank', 'inter', 'stone'\u2026\n -- Sem CHECK: a lista de provedores \u00E9 do mercado, n\u00E3o do schema, e uma\n -- constraint aqui derruba a concilia\u00E7\u00E3o no dia em que entrar o pr\u00F3ximo.\n provider text NOT NULL,\n -- A conex\u00E3o do conector, quando existe. Sem FK: ver o cabe\u00E7alho.\n connection_id uuid,\n -- Como o PROVEDOR chama esta conta. \u00C9 o que casa a linha que ele devolve com\n -- a conta daqui \u2014 no V1 s\u00E3o `openfinance_account_hash` e\n -- `openfinance_card_last4`.\n external_account_ref text,\n\n statement_kind text NOT NULL DEFAULT 'bank'\n CHECK (statement_kind IN ('bank', 'credit_card')),\n card_last4 text,\n -- A regra do V1, escrita: extrato de cart\u00E3o sem os quatro \u00FAltimos n\u00E3o casa\n -- com nada, ent\u00E3o ele n\u00E3o pode nem ser cadastrado.\n CONSTRAINT plg_financial_account_statement_sources_cartao_check\n CHECK (statement_kind <> 'credit_card' OR card_last4 ~ '^[0-9]{4}$'),\n\n -- Desligar a origem N\u00C3O apaga a linha: o extrato que j\u00E1 entrou por ela\n -- continua precisando dizer de onde veio.\n is_active boolean NOT NULL DEFAULT true,\n last_synced_at timestamptz,\n last_sync_status text,\n last_sync_error text,\n\n metadata jsonb NOT NULL DEFAULT '{}'::jsonb,\n created_at timestamptz NOT NULL DEFAULT now(),\n updated_at timestamptz NOT NULL DEFAULT now()\n);\n\nDO $$\nBEGIN\n ALTER TABLE public.plg_financial_account_statement_sources\n ADD CONSTRAINT plg_financial_account_statement_sources_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\n-- Uma conta n\u00E3o se liga duas vezes ao MESMO provedor pela mesma refer\u00EAncia\n-- externa. \u00CDndice e n\u00E3o constraint: `external_account_ref` \u00E9 nulo enquanto a\n-- conex\u00E3o n\u00E3o disse o nome dela, e `UNIQUE` deixaria passar N nulos.\nCREATE UNIQUE INDEX IF NOT EXISTS plg_financial_account_statement_sources_unica_idx\n ON public.plg_financial_account_statement_sources\n (tenant_id, account_id, provider, coalesce(external_account_ref, ''));\n\n-- A pergunta que a tela faz: quais contas deste inquilino t\u00EAm origem viva?\nCREATE INDEX IF NOT EXISTS plg_financial_account_statement_sources_ativas_idx\n ON public.plg_financial_account_statement_sources (tenant_id, account_id)\n WHERE is_active = true;\n\nDO $$\nBEGIN\n CREATE TRIGGER plg_financial_account_statement_sources_updated_at\n BEFORE UPDATE ON public.plg_financial_account_statement_sources\n FOR EACH ROW EXECUTE FUNCTION public.handle_updated_at();\nEXCEPTION WHEN duplicate_object THEN NULL;\nEND $$;\n\n-- Mesma cerca do extrato: quem pode ver a concilia\u00E7\u00E3o pode ver de onde ela vem.\nSELECT app.scaffold_table('public.plg_financial_account_statement_sources'::regclass,\n 'financial.bank_statement', 'financial', false, 'enforce');\nSELECT app.scaffold_scope('public.plg_financial_account_statement_sources'::regclass, 'unit_optional', 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-- O seletor da Concilia\u00E7\u00E3o, como uma pergunta s\u00F3\n-- \u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\n--\n-- Equivale ao `openfinance_configured = true` do V1, e resolve de quebra o que\n-- fazia a lista nascer vazia: `getBankAccounts` varria o plano INTEIRO ordenado\n-- por nome, e num inquilino com milhares de contas de pessoa o teto da resposta\n-- se enchia antes de chegar em \u00ABIta\u00FA\u00BB.\
|
|
114
|
+
export declare const MIGRATION_113_SO_A_CONTA_INTEGRADA_APARECE_NA_CONCILIACAO = "-- S\u00F3 a conta INTEGRADA aparece na Concilia\u00E7\u00E3o.\n--\n-- \u2500\u2500 O filtro que faltava \u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\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 V1 (`BankReconciliation.tsx:263`) monta o seletor assim:\n--\n-- .eq('active', true)\n-- .eq('openfinance_configured', true) \u2190 este\n-- account_type IN ('conta_bancaria','cartao_credito_pagamento')\n-- + cart\u00E3o exige openfinance_card_last4 com 4 d\u00EDgitos\n--\n-- \u00C9 por isso que l\u00E1 aparece UM Ita\u00FA: \u00E9 o \u00FAnico com Open Banking ligado. O V2\n-- listava toda conta banc\u00E1ria, cart\u00E3o e caixa do plano \u2014 inclusive as que nunca\n-- v\u00E3o trazer uma linha. Quem escolhe uma delas v\u00EA a lista vazia e conclui que a\n-- tela est\u00E1 quebrada; o defeito \u00E9 o convite, n\u00E3o a lista.\n--\n-- \u2500\u2500 Por que uma tabela, e n\u00E3o uma coluna \u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\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-- `openfinance_configured` \u00E9 um booleano porque o V1 tem UM provedor. Aqui a\n-- conta pode ser servida por mais de um (o banco por Open Finance, o cart\u00E3o\n-- pelo adquirente), e o v\u00EDnculo carrega coisas que n\u00E3o s\u00E3o da conta nem do\n-- provedor sozinhos: como o provedor CHAMA aquela conta (`external_account_ref`\n-- \u2014 o hash da conta, os quatro \u00FAltimos do cart\u00E3o) e se a origem ainda est\u00E1\n-- viva. Numa coluna, ligar o segundo provedor obriga a escolher qual dos dois\n-- fica.\n--\n-- O `connection_id` aponta para `connections` SEM `REFERENCES`: aquela tabela \u00E9\n-- de packages/db e o v\u00EDnculo \u00E9 opcional (uma origem pode ser semeada antes de\n-- existir conex\u00E3o, e \u00E9 assim que o porte prova a tela). A aus\u00EAncia da FK est\u00E1\n-- anotada aqui porque ela \u00E9 uma escolha, n\u00E3o um esquecimento.\n--\n-- Idempotente.\n\nCREATE TABLE IF NOT EXISTS public.plg_financial_account_statement_sources (\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 account_id uuid NOT NULL REFERENCES public.plg_financial_accounts(id) ON DELETE CASCADE,\n\n -- Quem traz o extrato desta conta: 'plugbank', 'inter', 'stone'\u2026\n -- Sem CHECK: a lista de provedores \u00E9 do mercado, n\u00E3o do schema, e uma\n -- constraint aqui derruba a concilia\u00E7\u00E3o no dia em que entrar o pr\u00F3ximo.\n provider text NOT NULL,\n -- A conex\u00E3o do conector, quando existe. Sem FK: ver o cabe\u00E7alho.\n connection_id uuid,\n -- Como o PROVEDOR chama esta conta. \u00C9 o que casa a linha que ele devolve com\n -- a conta daqui \u2014 no V1 s\u00E3o `openfinance_account_hash` e\n -- `openfinance_card_last4`.\n external_account_ref text,\n\n statement_kind text NOT NULL DEFAULT 'bank'\n CHECK (statement_kind IN ('bank', 'credit_card')),\n card_last4 text,\n -- A regra do V1, escrita: extrato de cart\u00E3o sem os quatro \u00FAltimos n\u00E3o casa\n -- com nada, ent\u00E3o ele n\u00E3o pode nem ser cadastrado.\n CONSTRAINT plg_financial_account_statement_sources_cartao_check\n CHECK (statement_kind <> 'credit_card' OR card_last4 ~ '^[0-9]{4}$'),\n\n -- Desligar a origem N\u00C3O apaga a linha: o extrato que j\u00E1 entrou por ela\n -- continua precisando dizer de onde veio.\n is_active boolean NOT NULL DEFAULT true,\n last_synced_at timestamptz,\n last_sync_status text,\n last_sync_error text,\n\n metadata jsonb NOT NULL DEFAULT '{}'::jsonb,\n created_at timestamptz NOT NULL DEFAULT now(),\n updated_at timestamptz NOT NULL DEFAULT now()\n);\n\nDO $$\nBEGIN\n ALTER TABLE public.plg_financial_account_statement_sources\n ADD CONSTRAINT plg_financial_account_statement_sources_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\n-- Uma conta n\u00E3o se liga duas vezes ao MESMO provedor pela mesma refer\u00EAncia\n-- externa. \u00CDndice e n\u00E3o constraint: `external_account_ref` \u00E9 nulo enquanto a\n-- conex\u00E3o n\u00E3o disse o nome dela, e `UNIQUE` deixaria passar N nulos.\nCREATE UNIQUE INDEX IF NOT EXISTS plg_financial_account_statement_sources_unica_idx\n ON public.plg_financial_account_statement_sources\n (tenant_id, account_id, provider, coalesce(external_account_ref, ''));\n\n-- A pergunta que a tela faz: quais contas deste inquilino t\u00EAm origem viva?\nCREATE INDEX IF NOT EXISTS plg_financial_account_statement_sources_ativas_idx\n ON public.plg_financial_account_statement_sources (tenant_id, account_id)\n WHERE is_active = true;\n\nDO $$\nBEGIN\n CREATE TRIGGER plg_financial_account_statement_sources_updated_at\n BEFORE UPDATE ON public.plg_financial_account_statement_sources\n FOR EACH ROW EXECUTE FUNCTION public.handle_updated_at();\nEXCEPTION WHEN duplicate_object THEN NULL;\nEND $$;\n\n-- Mesma cerca do extrato: quem pode ver a concilia\u00E7\u00E3o pode ver de onde ela vem.\nSELECT app.scaffold_table('public.plg_financial_account_statement_sources'::regclass,\n 'financial.bank_statement', 'financial', false, 'enforce');\nSELECT app.scaffold_scope('public.plg_financial_account_statement_sources'::regclass, 'unit_optional', 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-- O seletor da Concilia\u00E7\u00E3o, como uma pergunta s\u00F3\n-- \u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\n--\n-- Equivale ao `openfinance_configured = true` do V1, e resolve de quebra o que\n-- fazia a lista nascer vazia: `getBankAccounts` varria o plano INTEIRO ordenado\n-- por nome, e num inquilino com milhares de contas de pessoa o teto da resposta\n-- se enchia antes de chegar em \u00ABIta\u00FA\u00BB.\n-- DROP, then CREATE: the view selects `account.*`, and OR REPLACE refuses to\n-- shift columns when the table gains one (124 added `currency`), so a replay\n-- of this file failed on any pool that had moved on.\nDROP VIEW IF EXISTS public.v_financial_statement_accounts;\nCREATE VIEW public.v_financial_statement_accounts\nWITH (security_invoker = true) AS\nSELECT account.*,\n source.provider,\n source.statement_kind,\n source.card_last4,\n source.last_synced_at\n FROM public.plg_financial_accounts account\n JOIN public.plg_financial_account_statement_sources source\n ON source.account_id = account.id\n AND source.is_active = true\n WHERE account.is_active = true;\n\nCOMMENT ON VIEW public.v_financial_statement_accounts IS\n 'As contas que T\u00CAM extrato: as que carregam uma origem ativa. \u00C9 o seletor da Concilia\u00E7\u00E3o Banc\u00E1ria, e o equivalente ao openfinance_configured do V1.';\n\nGRANT SELECT ON public.v_financial_statement_accounts TO authenticated, service_role;\n";
|
|
115
115
|
export declare const MIGRATION_114_A_PARCELA_DIZ_POR_QUAL_CONTA_ELA_FOI_PAGA = "-- ============================================================================\n-- 114 \u2014 a parcela diz POR QUAL CONTA ela foi paga.\n--\n-- A lista do V1 recorta por \"Conta de pagamento\" / \"Conta de recebimento\", e\n-- aqui nao havia por onde: o vinculo parcela -> conta bancaria mora duas tabelas\n-- adiante (alocacao -> movimento), e pergunt\u00E1-lo de fora custaria uma lista de\n-- ids do tamanho do historico da conta antes da primeira linha aparecer.\n--\n-- O LATERAL que soma as baixas ja visita exatamente essas duas tabelas, pelo\n-- indice (installment_id). A conta de pagamento sai da MESMA visita, como um\n-- agregado a mais \u2014 o plano da consulta nao muda, e o recorte vira um `cs`\n-- sobre um array em vez de um `in` sobre dez mil ids.\n--\n-- ARRAY e nao coluna: uma parcela quitada em partes pode ter saido de duas\n-- contas, e escolher uma delas para caber numa coluna e escolher esconder a\n-- outra de quem filtra por ela.\n--\n-- Idempotente.\n-- ============================================================================\n\nDROP VIEW IF EXISTS public.v_financial_invoice_lines_due;\n\nCREATE VIEW public.v_financial_invoice_lines_due AS\nSELECT\n i.id AS installment_id,\n i.tenant_id,\n i.unit_id,\n i.invoice_id,\n t.direction,\n t.number,\n t.competence_date,\n t.issue_date,\n t.description,\n t.notes,\n t.metadata,\n t.order_id,\n t.counterparty_person_id,\n p.name AS counterparty_name,\n i.number AS installment_number,\n it.total AS installment_count,\n i.due_date,\n i.amount,\n COALESCE(pa.paid, 0::numeric)::numeric(14,2) AS paid,\n -- De quais contas bancarias saiu (ou entrou) o dinheiro desta parcela.\n -- Vazio enquanto ninguem pagou \u2014 e vazio nunca satisfaz um `cs`, que e\n -- exatamente o que o filtro quer dizer.\n COALESCE(pa.payment_account_ids, ARRAY[]::uuid[]) AS payment_account_ids,\n CASE\n WHEN i.cancelled_at IS NOT NULL OR t.cancelled_at IS NOT NULL THEN 0::numeric\n ELSE GREATEST(i.amount - COALESCE(pa.paid, 0::numeric), 0::numeric)\n END::numeric(14,2) AS balance,\n -- Mesmas palavras de `v_financial_installment_balances`. Se as duas\n -- divergirem, a lista e o detalhe passam a dizer coisas diferentes sobre a\n -- mesma parcela.\n CASE\n WHEN i.cancelled_at IS NOT NULL OR t.cancelled_at IS NOT NULL THEN 'cancelled'\n WHEN COALESCE(pa.paid, 0::numeric) >= i.amount THEN 'paid'\n WHEN COALESCE(pa.paid, 0::numeric) > 0::numeric THEN 'partial'\n WHEN i.due_date < CURRENT_DATE THEN 'overdue'\n ELSE 'pending'\n END AS status,\n i.cancelled_at,\n t.cancel_reason,\n t.created_by,\n i.created_at,\n i.updated_at\n FROM public.plg_financial_invoice_installments i\n JOIN public.plg_financial_invoices t ON t.id = i.invoice_id\n -- Correlacionado: so as baixas DESTA parcela, pelo indice (installment_id).\n LEFT JOIN LATERAL (\n SELECT sum(a.amount) AS paid,\n array_agg(DISTINCT m.bank_account_id)\n FILTER (WHERE m.bank_account_id IS NOT NULL) AS payment_account_ids\n 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 = i.id\n AND m.status <> 'cancelled'\n AND m.reversed_by_id IS NULL\n ) pa ON true\n -- \"3 de 4\": o denominador e do titulo, e sem ele a coluna de parcela mente.\n LEFT JOIN LATERAL (\n SELECT count(*) AS total\n FROM public.plg_financial_invoice_installments x\n WHERE x.invoice_id = i.invoice_id AND x.cancelled_at IS NULL\n ) it ON true\n LEFT JOIN public.people p ON p.id = t.counterparty_person_id;\n\nCOMMENT ON VIEW public.v_financial_invoice_lines_due IS\n 'Uma linha por PARCELA \u2014 a granularidade da lista de contas a pagar do V1, e '\n 'a unica em que \"o que vence no dia 11\" tem resposta. Filtra pelo indice '\n '(tenant_id, due_date) das parcelas: 0,15 s contra 7,5 s da forma por titulo. '\n 'payment_account_ids responde ao filtro \"conta de pagamento\" do V1 sem uma '\n 'segunda consulta.';\n\nALTER VIEW public.v_financial_invoice_lines_due SET (security_invoker = true);\nGRANT SELECT ON public.v_financial_invoice_lines_due TO authenticated, service_role;\n";
|
|
116
116
|
export declare const MIGRATION_115_A_REMESSA_APONTA_PARA_A_CONTA_QUE_EXISTE = "-- ---------------------------------------------------------------------------\n-- 115 \u2014 a remessa aponta para a conta que EXISTE.\n--\n-- `plg_financial_cnab_batches.bank_account_id` carrega DUAS chaves estrangeiras\n-- para a mesma coluna:\n--\n-- plg_financial_cnab_batches_account_fkey \u2192 plg_financial_bank_accounts\n-- plg_financial_cnab_batches_ledger_account_fkey \u2192 plg_financial_accounts\n--\n-- A segunda \u00E9 a certa. A primeira aponta para a tabela LEGADA \u2014 aquela que a\n-- 021 aposentou e que hoje \u00E9 s\u00F3 o encaminhamento por gatilho. Neste cluster ela\n-- tem 5 linhas contra 63 contas banc\u00E1rias no raz\u00E3o, e o efeito \u00E9 exato: gerar\n-- uma remessa por qualquer conta criada depois do porte morre com\n--\n-- violates foreign key constraint \"plg_financial_cnab_batches_account_fkey\"\n-- Key is not present in table \"plg_financial_bank_accounts\"\n--\n-- Uma coluna com duas FKs exige que o id esteja nas DUAS tabelas, e \u00E9 por isso\n-- que a tela nunca conseguiu gravar: ela escreve onde `getBankAccounts` l\u00EA, que\n-- \u00E9 o raz\u00E3o, e o legado nunca soube daquela conta.\n--\n-- A 080 trouxe a tabela do V1 com a FK que o V1 tinha. O que faltou foi\n-- aposentar a chave junto com a tabela a que ela se referia.\n--\n-- Derrubar a FK legada n\u00E3o perde nada: a do raz\u00E3o continua guardando a coluna, e\n-- \u00E9 ela que diz a verdade. As 60 remessas j\u00E1 migradas apontam para contas que\n-- est\u00E3o nas duas tabelas e seguem v\u00E1lidas.\n--\n-- Idempotente.\n-- ---------------------------------------------------------------------------\n\nALTER TABLE public.plg_financial_cnab_batches\n DROP CONSTRAINT IF EXISTS plg_financial_cnab_batches_account_fkey;\n\n-- A do raz\u00E3o pode n\u00E3o existir num pool que recebeu a 080 e n\u00E3o a corre\u00E7\u00E3o; \u00E9\n-- ela que fica, ent\u00E3o se faltar, nasce aqui.\nDO $$\nBEGIN\n IF NOT EXISTS (\n SELECT 1 FROM pg_constraint\n WHERE conrelid = 'public.plg_financial_cnab_batches'::regclass\n AND conname = 'plg_financial_cnab_batches_ledger_account_fkey'\n ) THEN\n ALTER TABLE public.plg_financial_cnab_batches\n ADD CONSTRAINT plg_financial_cnab_batches_ledger_account_fkey\n FOREIGN KEY (bank_account_id)\n REFERENCES public.plg_financial_accounts(id) ON DELETE SET NULL;\n END IF;\nEND $$;\n\nCOMMENT ON COLUMN public.plg_financial_cnab_batches.bank_account_id IS\n 'A conta do RAZ\u00C3O que o arquivo debita \u2014 plg_financial_accounts, a mesma que getBankAccounts l\u00EA. N\u00E3o \u00E9 a tabela legada plg_financial_bank_accounts: uma conta criada depois do porte s\u00F3 existe no raz\u00E3o.';\n\n-- \u2500\u2500 a parcela que n\u00E3o tem movimento g\u00EAmeo \u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\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-- `plg_financial_cnab_items.movement_id` aponta para `plg_financial_movements`.\n-- A tela manda a PARCELA, e o porte deixou parcela e movimento com o mesmo id\n-- na maioria das linhas \u2014 mas n\u00E3o em todas: aqui s\u00E3o 171.978 parcelas contra\n-- 143.175 com movimento de mesmo id. As outras 28.803 fariam a remessa morrer\n-- na hora de arquivar um arquivo que J\u00C1 foi baixado e j\u00E1 foi para o banco.\n--\n-- O item passa a aceitar a parcela em coluna pr\u00F3pria. `movement_id` continua\n-- sendo o elo com o raz\u00E3o onde ele existe; `installment_id` \u00E9 o que a tela\n-- sempre teve na m\u00E3o, e \u00E9 por ele que \u00ABesta conta j\u00E1 foi numa remessa\u00BB tem\n-- resposta.\nALTER TABLE public.plg_financial_cnab_items\n ADD COLUMN IF NOT EXISTS installment_id uuid;\n\nDO $$\nBEGIN\n IF NOT EXISTS (\n SELECT 1 FROM pg_constraint\n WHERE conrelid = 'public.plg_financial_cnab_items'::regclass\n AND conname = 'plg_financial_cnab_items_installment_fkey'\n ) THEN\n ALTER TABLE public.plg_financial_cnab_items\n ADD CONSTRAINT plg_financial_cnab_items_installment_fkey\n FOREIGN KEY (installment_id)\n REFERENCES public.plg_financial_invoice_installments(id) ON DELETE SET NULL;\n END IF;\nEND $$;\n\n-- As linhas j\u00E1 migradas: onde o movimento tem parcela de mesmo id, o elo novo\n-- se preenche sozinho. O que n\u00E3o casar fica nulo, que \u00E9 a verdade.\nUPDATE public.plg_financial_cnab_items ci\n SET installment_id = ci.movement_id\n WHERE ci.installment_id IS NULL\n AND ci.movement_id IS NOT NULL\n AND EXISTS (\n SELECT 1 FROM public.plg_financial_invoice_installments i WHERE i.id = ci.movement_id\n );\n\nCREATE INDEX IF NOT EXISTS plg_financial_cnab_items_installment_idx\n ON public.plg_financial_cnab_items (tenant_id, installment_id)\n WHERE installment_id IS NOT NULL;\n\nCOMMENT ON COLUMN public.plg_financial_cnab_items.installment_id IS\n 'A PARCELA que esta linha da remessa paga \u2014 \u00E9 o que a tela de contas tem na m\u00E3o. movement_id continua apontando para o raz\u00E3o onde o movimento g\u00EAmeo existe.';\n\n-- \u2500\u2500 uma conta padr\u00E3o por favorecido, sem derrubar a remessa \u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\n--\n-- `payee_accounts_one_default_uidx` \u00E9 \u00FAnico em (tenant_id, owner_type, owner_id)\n-- WHERE is_default, e est\u00E1 certo: duas contas padr\u00E3o fazem a tela mostrar uma e\n-- a remessa pagar a outra.\n--\n-- O que estava errado era quem escrevia: a remessa inseria sempre com\n-- `is_default = true`, ent\u00E3o a SEGUNDA tentativa de gerar o mesmo arquivo \u2014\n-- depois de a primeira morrer na FK acima \u2014 batia em\n--\n-- duplicate key value violates unique constraint\n--\n-- O conserto \u00E9 do lado de quem escreve (`saveCnabPayees` procura antes de\n-- inserir), e esta fun\u00E7\u00E3o \u00E9 a rede: um `insert` que j\u00E1 tem padr\u00E3o vira o padr\u00E3o\n-- da pessoa sem precisar que o chamador saiba disso.\nCREATE OR REPLACE FUNCTION public.trg_payee_accounts_one_default()\nRETURNS trigger\nLANGUAGE plpgsql\nSECURITY INVOKER\nSET search_path = public, pg_catalog\nAS $$\nBEGIN\n IF NEW.is_default THEN\n UPDATE public.payee_accounts\n SET is_default = false\n WHERE tenant_id = NEW.tenant_id\n AND owner_type = NEW.owner_type\n AND owner_id = NEW.owner_id\n AND is_default\n AND id <> NEW.id;\n END IF;\n RETURN NEW;\nEND\n$$;\n\nDROP TRIGGER IF EXISTS payee_accounts_one_default ON public.payee_accounts;\nCREATE TRIGGER payee_accounts_one_default\n BEFORE INSERT OR UPDATE OF is_default ON public.payee_accounts\n FOR EACH ROW EXECUTE FUNCTION public.trg_payee_accounts_one_default();\n";
|
|
117
117
|
export declare const MIGRATION_116_UM_ESTORNO_NAO_SE_ESTORNA = "-- ---------------------------------------------------------------------------\n-- 116 \u2014 um estorno n\u00E3o se estorna.\n--\n-- `financial_reverse_movement` aceitava qualquer `payment`, inclusive a linha\n-- que ela mesma lan\u00E7a como contrapartida. A tela mirava \u00ABo \u00FAltimo pagamento da\n-- parcela\u00BB, que depois do primeiro estorno \u00C9 o estorno: cada clique estornava o\n-- estorno anterior, o dinheiro ia e voltava, e o pagamento vivo continuava l\u00E1 \u2014\n-- a conta seguia \u00ABpaga\u00BB para sempre. Agora o banco recusa com uma mensagem que\n-- a tela mostra.\n--\n-- Idempotente: s\u00F3 rep\u00F5e a fun\u00E7\u00E3o.\n-- ---------------------------------------------------------------------------\n\nCREATE OR REPLACE FUNCTION public.financial_reverse_movement(p_movement_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_m public.plg_financial_movements%ROWTYPE;\n v_rev uuid;\nBEGIN\n IF nullif(btrim(coalesce(p_reason, '')), '') IS NULL THEN\n RAISE EXCEPTION 'financial_reverse_movement: a reason is required' USING ERRCODE = '22023';\n END IF;\n SELECT * INTO v_m FROM public.plg_financial_movements WHERE id = p_movement_id AND tenant_id = v_tenant;\n IF NOT FOUND THEN\n RAISE EXCEPTION 'financial_reverse_movement: movement % not found', p_movement_id USING ERRCODE = 'P0002';\n END IF;\n PERFORM public.plg_financial_require_permission('financial.edit', v_m.unit_id);\n IF v_m.movement_kind NOT IN ('payment', 'transfer') OR v_m.debit_account_id IS NULL THEN\n RAISE EXCEPTION 'financial_reverse_movement: % is not a money event', p_movement_id USING ERRCODE = '55000';\n END IF;\n IF v_m.reverses_id IS NOT NULL THEN\n RAISE EXCEPTION 'Este lan\u00E7amento j\u00E1 \u00E9 um estorno e n\u00E3o pode ser estornado.' USING ERRCODE = '55000';\n END IF;\n IF v_m.reversed_by_id IS NOT NULL THEN\n RETURN jsonb_build_object('reversal_id', v_m.reversed_by_id, 'movement_id', v_m.id, 'replayed', true);\n END IF;\n IF p_idempotency_key IS NOT NULL THEN\n PERFORM public.plg_financial_claim_idempotency(v_tenant, p_idempotency_key);\n SELECT m.id INTO v_rev FROM public.plg_financial_movements m WHERE m.tenant_id = v_tenant AND m.idempotency_key = p_idempotency_key;\n IF v_rev IS NOT NULL THEN\n RETURN jsonb_build_object('reversal_id', v_rev, 'movement_id', v_m.id, 'replayed', true);\n END IF;\n END IF;\n\n INSERT INTO public.plg_financial_movements\n (tenant_id, unit_id, invoice_id, financial_invoice_id, financial_invoice_installment_id, direction, movement_kind, amount, paid_amount, status,\n due_date, payment_date, installment_number, method, cash_session_id, bank_account_id, reason, notes, metadata,\n idempotency_key, created_by, reverses_id, debit_account_id, credit_account_id)\n VALUES\n (v_m.tenant_id, v_m.unit_id, v_m.invoice_id, v_m.financial_invoice_id, v_m.financial_invoice_installment_id,\n CASE WHEN v_m.direction = 'credit' THEN 'debit' ELSE 'credit' END, v_m.movement_kind, v_m.amount, v_m.amount, 'paid',\n current_date, current_date, v_m.installment_number, v_m.method, v_m.cash_session_id, v_m.bank_account_id, btrim(p_reason),\n 'Estorno de ' || v_m.id::text,\n jsonb_build_object('source', 'financial_reverse_movement', 'reversal_of', v_m.id, 'reason', btrim(p_reason)),\n p_idempotency_key, auth.uid(), v_m.id, v_m.credit_account_id, v_m.debit_account_id)\n RETURNING id INTO v_rev;\n\n UPDATE public.plg_financial_movements SET reversed_by_id = v_rev, reason = coalesce(reason, btrim(p_reason)), updated_at = now()\n WHERE id = v_m.id;\n IF v_m.financial_invoice_installment_id IS NOT NULL THEN PERFORM public.plg_financial_sync_mirror(v_m.financial_invoice_installment_id); END IF;\n\n PERFORM public.plg_financial_audit(v_tenant, 'financial.movement.reverse', 'plg_financial_movements', v_m.id::text,\n jsonb_build_object('reversal_id', v_rev, 'amount', v_m.amount, 'reason', btrim(p_reason), 'idempotency_key', p_idempotency_key));\n RETURN jsonb_build_object('reversal_id', v_rev, 'movement_id', v_m.id, 'replayed', false);\nEND $$;\n\n";
|
|
@@ -121,6 +121,14 @@ export declare const MIGRATION_119_AN_UNPAID_LEGACY_BILL_CAN_BE_RESCHEDULED = "-
|
|
|
121
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
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
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";
|
|
124
|
+
export declare const MIGRATION_123_THE_MIRROR_DOES_NOT_BILL_A_RECURRENCE_TWICE = "-- ============================================================================\n-- 123 \u2014 the mirror does not bill a recurrence twice\n--\n-- A tenant in coexistence has every recurring bill on both sides. When someone\n-- issues October in V2 and V1 then generates its own October bill, the mirror\n-- brought the V1 bill in as a second payable: same supplier, same amount, same\n-- due date, nothing tying the two together (Artorius, 2026-10-11: four pairs,\n-- R$ 16.599,50 owed twice on screen).\n--\n-- The tie exists, it was just never read: the occurrence is the SAME row on\n-- both sides, V2 stamps `invoice_id` on it when it issues, and V1 reports the\n-- bill it generated as `metadata.v1.invoice_id`. An occurrence carrying both\n-- names two bills for one month. The V2 one stays \u2014 it is the one somebody\n-- chose to issue \u2014 and the unpaid mirrored copy is cancelled as a V2 decision,\n-- so later passes of the mirror do not bring it back. A copy that already has\n-- a payment is left alone: which of two paid bills is wrong is not a sync's\n-- call.\n--\n-- Both writers ask, because either row can arrive last: the bill usually lands\n-- seconds before its occurrence is updated, but a bill held in quarantine\n-- lands batches later, when the occurrence no longer changes.\n-- ============================================================================\n\nCREATE INDEX IF NOT EXISTS plg_financial_recurring_bill_occurrences_v1_invoice_idx\n ON public.plg_financial_recurring_bill_occurrences (tenant_id, ((metadata -> 'v1' ->> 'invoice_id')))\n WHERE invoice_id IS NOT NULL;\n\nCREATE OR REPLACE FUNCTION migration._fin_cancel_recurring_duplicate(p_tenant uuid, p_v1_invoice text)\n RETURNS uuid\n LANGUAGE plpgsql\n SECURITY DEFINER\n SET search_path TO ''\nAS $function$\nDECLARE\n v_kept uuid;\n v_number text;\n v_dup uuid;\nBEGIN\n IF nullif(btrim(coalesce(p_v1_invoice, '')), '') IS NULL THEN\n RETURN NULL;\n END IF;\n\n SELECT f.id, f.number INTO v_kept, v_number\n FROM public.plg_financial_recurring_bill_occurrences o\n JOIN public.plg_financial_invoices f ON f.id = o.invoice_id AND f.cancelled_at IS NULL\n WHERE o.tenant_id = p_tenant\n AND o.invoice_id IS NOT NULL\n AND o.metadata -> 'v1' ->> 'invoice_id' = p_v1_invoice\n LIMIT 1;\n IF v_kept IS NULL THEN\n RETURN NULL;\n END IF;\n\n SELECT f.id INTO v_dup\n FROM public.plg_financial_invoices f\n WHERE f.tenant_id = p_tenant\n AND f.id <> v_kept\n AND f.cancelled_at IS NULL\n AND f.metadata -> 'migration' ->> 'source_table' = 'invoices'\n AND f.metadata -> 'migration' ->> 'source_numeric_id' = p_v1_invoice\n AND NOT EXISTS (SELECT 1\n FROM public.plg_financial_invoice_installments i\n JOIN public.plg_financial_allocations a ON a.installment_id = i.id\n WHERE i.invoice_id = f.id)\n LIMIT 1;\n IF v_dup IS NULL THEN\n RETURN NULL;\n END IF;\n\n -- Same three writes as the unpaid branch of financial_cancel_invoice_items,\n -- which cannot be called here: it reads the tenant from a session.\n UPDATE public.plg_financial_invoices\n SET cancelled_at = now(),\n cancel_reason = 'Duplicata: a mesma ocorr\u00EAncia da recorr\u00EAncia j\u00E1 foi lan\u00E7ada como ' || v_number,\n metadata = metadata || jsonb_build_object('v2_decision',\n jsonb_build_object('cancelled', true, 'at', now(), 'duplicate_of', v_kept))\n WHERE id = v_dup;\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 = v_dup AND cancelled_at IS NULL;\n UPDATE public.plg_financial_movements SET status = 'cancelled', updated_at = now()\n WHERE financial_invoice_id = v_dup AND movement_kind = 'bill' AND status <> 'cancelled';\n\n RETURN v_dup;\nEND $function$;\nREVOKE ALL ON FUNCTION migration._fin_cancel_recurring_duplicate(uuid, text) FROM PUBLIC, anon, authenticated;\nGRANT EXECUTE ON FUNCTION migration._fin_cancel_recurring_duplicate(uuid, text) TO service_role;\n\n-- The two writers are patched in place, as 122 patched the money RPCs: their\n-- live bodies differ from the files that created them.\nDO $patch$\nDECLARE\n v_fn regprocedure;\n v_def text;\n v_anchor text;\n v_call text;\nBEGIN\n FOR v_fn, v_anchor, v_call IN\n SELECT f::regprocedure, a, c FROM (VALUES\n ('migration.upsert_fin_recurring_bill_occurrence(uuid, uuid, jsonb, jsonb)',\n ' IF v_standalone THEN PERFORM migration._audit(p_tenant, ''public.plg_financial_recurring_bill_occurrences'', p_id, ''{}''::jsonb); END IF;',\n ' PERFORM migration._fin_cancel_recurring_duplicate(p_tenant, v_meta -> ''v1'' ->> ''invoice_id'');'),\n ('migration.upsert_invoice(uuid, uuid, jsonb, jsonb)',\n ' IF v_standalone THEN PERFORM migration._audit(p_tenant, v_table, p_id, jsonb_build_object(''reason'', ''created'', ''installments'', v_n)); END IF;',\n ' PERFORM migration._fin_cancel_recurring_duplicate(p_tenant, p_row -> ''metadata'' -> ''migration'' ->> ''source_numeric_id'');')\n ) AS t(f, a, c)\n LOOP\n v_def := pg_get_functiondef(v_fn);\n IF position('_fin_cancel_recurring_duplicate' in v_def) > 0 THEN\n CONTINUE; -- already asks (a replay, or a later rewrite)\n END IF;\n IF position(v_anchor in v_def) = 0 THEN\n RAISE EXCEPTION '123: % no longer has the line the duplicate check goes before (%); patch it by hand', v_fn, v_anchor;\n END IF;\n EXECUTE overlay(v_def PLACING v_call || E'\\n' || v_anchor\n FROM position(v_anchor in v_def) FOR length(v_anchor));\n END LOOP;\nEND $patch$;\n";
|
|
125
|
+
export declare const MIGRATION_124_MONEY_KNOWS_ITS_CURRENCY = "-- ============================================================================\n-- 124 \u2014 money knows its currency\n--\n-- Invoices carried a `currency` nobody set (always the 'BRL' default) and the\n-- ledger had none at all: an account, a payment and a balance were bare\n-- numbers. One tenant with a shop in Rio and one in Barcelona would have had\n-- 30 EUR and 30 BRL add up to 60.\n--\n-- \u00B7 an invoice takes the currency of the order it bills, else its unit's;\n-- \u00B7 a bank or cash account has ONE currency (its unit's, unless said);\n-- \u00B7 a money movement is in the currency of the account the money went\n-- through, and may not settle an invoice written in another one;\n-- \u00B7 every movement and invoice is stamped with the day's rate to the\n-- tenant's own currency, when a rate is on file, so a report converted\n-- today still says tomorrow what it said today.\n--\n-- Rates are data the tenant keeps (`plg_financial_fx_rates`). Nothing here\n-- fetches one, and nothing here converts a report: the reading side comes\n-- after the data is right.\n--\n-- Additive. Every existing row reads as BRL, which is what it was.\n-- ============================================================================\n\n-- \u2500\u2500 1. rates \u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\nCREATE TABLE IF NOT EXISTS public.plg_financial_fx_rates (\n id uuid PRIMARY KEY DEFAULT gen_random_uuid(),\n tenant_id uuid NOT NULL REFERENCES public.tenants(id) ON DELETE CASCADE,\n rate_date date NOT NULL,\n base_currency text NOT NULL CHECK (base_currency ~ '^[A-Z]{3}$'),\n quote_currency text NOT NULL CHECK (quote_currency ~ '^[A-Z]{3}$'),\n rate numeric NOT NULL CHECK (rate > 0),\n source text,\n created_at timestamptz NOT NULL DEFAULT now(),\n updated_at timestamptz NOT NULL DEFAULT now(),\n CHECK (base_currency <> quote_currency)\n);\nCOMMENT ON TABLE public.plg_financial_fx_rates IS '1 base_currency = rate quote_currency, on rate_date.';\nCREATE UNIQUE INDEX IF NOT EXISTS plg_financial_fx_rates_day_uq\n ON public.plg_financial_fx_rates (tenant_id, base_currency, quote_currency, rate_date);\n\nDO $$\nBEGIN\n CREATE TRIGGER plg_financial_fx_rates_updated_at\n BEFORE UPDATE ON public.plg_financial_fx_rates\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_fx_rates'::regclass,\n 'financial.invoice', 'financial', false, 'enforce');\nSELECT app.scaffold_scope('public.plg_financial_fx_rates'::regclass, 'tenant', true);\n\n-- The latest rate on or before the day, in either direction. NULL when the\n-- tenant has none on file: a missing rate is not 1.\nCREATE OR REPLACE FUNCTION public.plg_financial_fx_rate(p_tenant uuid, p_from text, p_to text, p_on date DEFAULT NULL)\n RETURNS numeric\n LANGUAGE sql\n STABLE SECURITY DEFINER\n SET search_path TO ''\nAS $function$\n SELECT CASE\n WHEN p_from IS NULL OR p_to IS NULL THEN NULL\n WHEN upper(p_from) = upper(p_to) THEN 1::numeric\n ELSE (\n SELECT CASE WHEN r.base_currency = upper(p_from) THEN r.rate ELSE round(1 / r.rate, 8) END\n FROM public.plg_financial_fx_rates r\n WHERE r.tenant_id = p_tenant\n AND r.rate_date <= coalesce(p_on, current_date)\n AND ((r.base_currency = upper(p_from) AND r.quote_currency = upper(p_to))\n OR (r.base_currency = upper(p_to) AND r.quote_currency = upper(p_from)))\n ORDER BY r.rate_date DESC, (r.base_currency = upper(p_from)) DESC\n LIMIT 1)\n END;\n$function$;\nREVOKE ALL ON FUNCTION public.plg_financial_fx_rate(uuid, text, text, date) FROM PUBLIC, anon, authenticated;\nGRANT EXECUTE ON FUNCTION public.plg_financial_fx_rate(uuid, text, text, date) TO service_role;\n\n-- \u2500\u2500 2. columns \u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\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-- Added with the default so every existing row becomes BRL without a rewrite,\n-- then the default goes: a new row is filled by its trigger, from its unit.\nDO $$\nBEGIN\n IF NOT EXISTS (SELECT 1 FROM information_schema.columns\n WHERE table_schema = 'public' AND table_name = 'plg_financial_accounts' AND column_name = 'currency') THEN\n ALTER TABLE public.plg_financial_accounts ADD COLUMN currency text NOT NULL DEFAULT 'BRL';\n ALTER TABLE public.plg_financial_accounts ALTER COLUMN currency DROP DEFAULT;\n END IF;\n IF NOT EXISTS (SELECT 1 FROM information_schema.columns\n WHERE table_schema = 'public' AND table_name = 'plg_financial_movements' AND column_name = 'currency') THEN\n ALTER TABLE public.plg_financial_movements ADD COLUMN currency text NOT NULL DEFAULT 'BRL';\n ALTER TABLE public.plg_financial_movements ALTER COLUMN currency DROP DEFAULT;\n END IF;\nEND $$;\nALTER TABLE public.plg_financial_movements ADD COLUMN IF NOT EXISTS fx_rate numeric;\nALTER TABLE public.plg_financial_movements ADD COLUMN IF NOT EXISTS base_amount numeric(14,2);\n-- A pool that took the first cut of this file has it bare; money declares its scale.\nDO $scale$\nBEGIN\n IF (SELECT format_type(a.atttypid, a.atttypmod) FROM pg_attribute a\n WHERE a.attrelid = 'public.plg_financial_movements'::regclass AND a.attname = 'base_amount') = 'numeric' THEN\n ALTER TABLE public.plg_financial_movements ALTER COLUMN base_amount TYPE numeric(14,2);\n END IF;\nEND $scale$;\nALTER TABLE public.plg_financial_invoices ADD COLUMN IF NOT EXISTS fx_rate numeric;\nALTER TABLE public.plg_financial_invoices ALTER COLUMN currency DROP DEFAULT;\n\nCOMMENT ON COLUMN public.plg_financial_accounts.currency IS 'The one currency this account holds. Money in another currency arrives by a transfer with a rate.';\nCOMMENT ON COLUMN public.plg_financial_movements.currency IS 'Currency of `amount`: the account the money went through.';\nCOMMENT ON COLUMN public.plg_financial_movements.fx_rate IS 'Rate to the tenant''s own currency on the day, when one was on file.';\nCOMMENT ON COLUMN public.plg_financial_movements.base_amount IS 'amount \u00D7 fx_rate, stored: what a consolidated report sums.';\nCOMMENT ON COLUMN public.plg_financial_invoices.fx_rate IS 'Rate to the tenant''s own currency on the issue date, when one was on file.';\n\n-- \u2500\u2500 3. an account is born in its unit's currency \u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\nCREATE OR REPLACE FUNCTION public.trg_plg_financial_accounts_currency()\n RETURNS trigger\n LANGUAGE plpgsql\n SECURITY DEFINER\n SET search_path TO ''\nAS $function$\nBEGIN\n IF nullif(btrim(coalesce(NEW.currency, '')), '') IS NULL THEN\n NEW.currency := app.unit_currency(NEW.tenant_id, NEW.unit_id);\n ELSE\n NEW.currency := upper(btrim(NEW.currency));\n END IF;\n RETURN NEW;\nEND $function$;\nDROP TRIGGER IF EXISTS plg_financial_accounts_currency ON public.plg_financial_accounts;\nCREATE TRIGGER plg_financial_accounts_currency\n BEFORE INSERT ON public.plg_financial_accounts\n FOR EACH ROW EXECUTE FUNCTION public.trg_plg_financial_accounts_currency();\n\n-- \u2500\u2500 4. an invoice is in the currency of what it bills \u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\nCREATE OR REPLACE FUNCTION public.trg_plg_financial_invoices_currency()\n RETURNS trigger\n LANGUAGE plpgsql\n SECURITY DEFINER\n SET search_path TO ''\nAS $function$\nBEGIN\n IF nullif(btrim(coalesce(NEW.currency, '')), '') IS NULL THEN\n IF NEW.order_id IS NOT NULL THEN\n SELECT nullif(btrim(o.currency), '') INTO NEW.currency FROM public.orders o WHERE o.id = NEW.order_id;\n END IF;\n NEW.currency := coalesce(NEW.currency, app.unit_currency(NEW.tenant_id, NEW.unit_id));\n END IF;\n NEW.currency := upper(btrim(NEW.currency));\n IF NEW.fx_rate IS NULL THEN\n NEW.fx_rate := public.plg_financial_fx_rate(\n NEW.tenant_id, NEW.currency, app.unit_currency(NEW.tenant_id, NULL), coalesce(NEW.issue_date, current_date));\n END IF;\n RETURN NEW;\nEND $function$;\n-- \"zz\": after `plg_financial_invoices_default_issuer`, which may set the unit.\nDROP TRIGGER IF EXISTS plg_financial_invoices_zz_currency ON public.plg_financial_invoices;\nCREATE TRIGGER plg_financial_invoices_zz_currency\n BEFORE INSERT ON public.plg_financial_invoices\n FOR EACH ROW EXECUTE FUNCTION public.trg_plg_financial_invoices_currency();\n\n-- \u2500\u2500 5. a movement is in the currency of the account it went through \u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\nCREATE OR REPLACE FUNCTION public.trg_plg_financial_movements_currency()\n RETURNS trigger\n LANGUAGE plpgsql\n SECURITY DEFINER\n SET search_path TO ''\nAS $function$\nDECLARE\n v_holder text;\n v_invoice text;\nBEGIN\n IF NEW.financial_invoice_id IS NOT NULL THEN\n SELECT i.currency INTO v_invoice FROM public.plg_financial_invoices i WHERE i.id = NEW.financial_invoice_id;\n END IF;\n\n -- The holder is the leg that actually keeps money. System and person\n -- accounts are bookkeeping counterparts and have no currency of their own.\n SELECT a.currency INTO v_holder\n FROM public.plg_financial_accounts a\n WHERE a.id IN (NEW.debit_account_id, NEW.credit_account_id)\n AND a.kind NOT IN ('system', 'person')\n ORDER BY (a.id = NEW.bank_account_id) DESC\n LIMIT 1;\n\n IF nullif(btrim(coalesce(NEW.currency, '')), '') IS NULL THEN\n NEW.currency := coalesce(v_holder, v_invoice, app.unit_currency(NEW.tenant_id, NEW.unit_id));\n END IF;\n NEW.currency := upper(btrim(NEW.currency));\n\n -- A reversal mirrors the movement it undoes and is never the one refused.\n IF NEW.movement_kind = 'payment' AND NEW.reverses_id IS NULL\n AND v_holder IS NOT NULL AND v_invoice IS NOT NULL AND v_holder <> v_invoice THEN\n RAISE EXCEPTION 'Esta conta \u00E9 em % e a conta banc\u00E1ria escolhida \u00E9 em %. Pague por uma conta em % ou transfira com c\u00E2mbio antes.',\n v_invoice, v_holder, v_invoice\n USING ERRCODE = '23514';\n END IF;\n\n IF NEW.fx_rate IS NULL THEN\n NEW.fx_rate := public.plg_financial_fx_rate(\n NEW.tenant_id, NEW.currency, app.unit_currency(NEW.tenant_id, NULL), coalesce(NEW.payment_date, NEW.due_date, current_date));\n END IF;\n IF NEW.base_amount IS NULL AND NEW.fx_rate IS NOT NULL THEN\n NEW.base_amount := round(NEW.amount * NEW.fx_rate, 2);\n END IF;\n RETURN NEW;\nEND $function$;\n-- Named to fire after `plg_financial_movements_before_insert`, which is the\n-- one that resolves the two legs this reads.\nDROP TRIGGER IF EXISTS plg_financial_movements_currency ON public.plg_financial_movements;\nCREATE TRIGGER plg_financial_movements_currency\n BEFORE INSERT ON public.plg_financial_movements\n FOR EACH ROW EXECUTE FUNCTION public.trg_plg_financial_movements_currency();\n";
|
|
126
|
+
export declare const MIGRATION_125_THE_SUMMARY_ADDS_ONE_CURRENCY_AT_A_TIME = "-- ============================================================================\n-- 125 \u2014 the summary adds one currency at a time\n--\n-- `financial_summary` summed every amount it could see. With a purchase in\n-- reais and a sale in euros in the same tenant, the dashboard showed their sum\n-- as one number in whichever currency the screen printed.\n--\n-- The arithmetic now runs per currency (`financial_summary_by_currency`: one\n-- row each, with today's rate to the tenant's own currency), and\n-- `financial_summary` \u2014 same name, same nine columns, so every screen that\n-- reads it keeps working \u2014 returns the converted total.\n--\n-- An amount in a currency with no rate on file is LEFT OUT of that total, not\n-- counted at 1:1: the per-currency rows still carry it, and the screen says so.\n-- A tenant with one currency gets the row it always got, at rate 1.\n--\n-- The fence (who sees which movement and account) is the body of 077/085/086,\n-- unchanged.\n-- ============================================================================\n\nCREATE OR REPLACE FUNCTION public.financial_summary_by_currency(p_from date DEFAULT NULL::date, p_to date DEFAULT NULL::date)\n RETURNS TABLE(currency text, house_currency text, fx_rate numeric,\n total_balance numeric, total_receivable numeric, total_payable numeric,\n monthly_inflow numeric, monthly_outflow numeric,\n overdue_receivable_count integer, overdue_receivable_amount numeric,\n overdue_payable_count integer, overdue_payable_amount numeric)\n LANGUAGE sql\n STABLE SECURITY DEFINER\n SET search_path TO ''\nAS $function$\n WITH ctx AS (\n SELECT app.current_tenant_id() AS tenant,\n auth.uid() AS me,\n app.visible_units() AS units,\n app.has_permission('financial.read', NULL::uuid) AS read_null,\n app.has_permission('financial.read_all', NULL::uuid) AS read_all_null,\n app.owner_scoped('financial.movement') AS own_mov,\n app.owner_scoped('financial.account') AS own_acc,\n app.grants_exist('financial.movement', ARRAY['shared_with']) AS grants_mov,\n app.grants_exist('financial.account', ARRAY['shared_with']) AS grants_acc,\n coalesce(p_from, (current_date - 30)) AS de,\n coalesce(p_to, current_date) AS ate,\n app.unit_currency(app.current_tenant_id(), NULL::uuid) AS house\n ),\n u_read AS (\n SELECT u FROM ctx, unnest(ctx.units) u\n WHERE app.has_permission('financial.read', u)\n ),\n u_read_all AS (\n SELECT u FROM ctx, unnest(ctx.units) u\n WHERE app.has_permission('financial.read_all', u)\n ),\n mov AS NOT MATERIALIZED (\n SELECT m.*,\n CASE\n WHEN m.reverses_id IS NULL THEN\n (m.external_source IS NULL\n OR (m.reconciled_at IS NOT NULL AND m.matched_movement_id IS NULL))\n ELSE coalesce(\n (SELECT r.external_source IS NULL\n OR (r.reconciled_at IS NOT NULL AND r.matched_movement_id IS NULL)\n FROM public.plg_financial_movements r\n WHERE r.id = m.reverses_id),\n (m.external_source IS NULL\n OR (m.reconciled_at IS NOT NULL AND m.matched_movement_id IS NULL)))\n END AS caixa_visivel\n FROM public.plg_financial_movements m, ctx\n WHERE m.tenant_id = ctx.tenant\n AND (CASE WHEN m.unit_id IS NULL THEN ctx.read_null\n ELSE m.unit_id IN (SELECT u FROM u_read) END)\n AND (NOT ctx.own_mov\n OR m.owner_id IS NULL\n OR m.owner_id = ctx.me\n OR (ctx.grants_mov\n AND app.has_grant('financial.movement', m.id, ARRAY['shared_with']))\n OR (CASE WHEN m.unit_id IS NULL THEN ctx.read_all_null\n ELSE m.unit_id IN (SELECT u FROM u_read_all) END))\n ),\n acc AS (\n SELECT a.id, a.kind, a.currency\n FROM public.plg_financial_accounts a, ctx\n WHERE a.tenant_id = ctx.tenant\n AND a.is_active\n AND a.kind IN ('bank', 'cash_physical', 'cash_session',\n 'card_receivable_credit', 'card_receivable_debit')\n AND (CASE WHEN a.unit_id IS NULL THEN ctx.read_null\n ELSE a.unit_id IN (SELECT u FROM u_read) END)\n AND (NOT ctx.own_acc\n OR a.owner_id IS NULL\n OR a.owner_id = ctx.me\n OR (ctx.grants_acc\n AND app.has_grant('financial.account', a.id, ARRAY['shared_with']))\n OR (CASE WHEN a.unit_id IS NULL THEN ctx.read_all_null\n ELSE a.unit_id IN (SELECT u FROM u_read_all) END))\n ),\n -- A balance is in the currency of the ACCOUNT, whatever its movements say.\n saldo AS (\n SELECT acc.currency,\n coalesce(sum(\n coalesce((SELECT sum(d.amount) FROM mov d\n WHERE d.debit_account_id = acc.id\n AND d.status <> 'cancelled'\n AND d.movement_kind IN ('payment', 'transfer')\n AND d.caixa_visivel), 0)\n - coalesce((SELECT sum(c.amount) FROM mov c\n WHERE c.credit_account_id = acc.id\n AND c.status <> 'cancelled'\n AND c.movement_kind IN ('payment', 'transfer')\n AND c.caixa_visivel), 0)\n ), 0) AS v\n FROM acc\n GROUP BY acc.currency\n ),\n aberto AS (\n SELECT m.currency,\n coalesce(sum(m.amount - m.paid_amount) FILTER (WHERE m.direction = 'credit'), 0) AS receber,\n coalesce(sum(m.amount - m.paid_amount) FILTER (WHERE m.direction = 'debit'), 0) AS pagar,\n count(*) FILTER (WHERE m.direction = 'credit'\n AND (m.status = 'overdue' OR m.due_date < current_date)) AS venc_receber_n,\n coalesce(sum(m.amount - m.paid_amount) FILTER (WHERE m.direction = 'credit'\n AND (m.status = 'overdue' OR m.due_date < current_date)), 0) AS venc_receber_v,\n count(*) FILTER (WHERE m.direction = 'debit'\n AND (m.status = 'overdue' OR m.due_date < current_date)) AS venc_pagar_n,\n coalesce(sum(m.amount - m.paid_amount) FILTER (WHERE m.direction = 'debit'\n AND (m.status = 'overdue' OR m.due_date < current_date)), 0) AS venc_pagar_v\n FROM mov m\n WHERE m.status IN ('pending', 'partial', 'overdue')\n GROUP BY m.currency\n ),\n caixa AS (\n SELECT m.currency,\n coalesce(sum(m.paid_amount) FILTER (WHERE m.direction = 'credit'), 0) AS entrada,\n coalesce(sum(m.paid_amount) FILTER (WHERE m.direction = 'debit'), 0) AS saida\n FROM mov m, ctx\n WHERE m.movement_kind = 'payment'\n AND m.payment_date >= ctx.de AND m.payment_date <= ctx.ate\n AND m.caixa_visivel\n GROUP BY m.currency\n ),\n cur AS (\n SELECT s.currency FROM saldo s\n UNION SELECT a.currency FROM aberto a\n UNION SELECT c.currency FROM caixa c\n UNION SELECT ctx.house FROM ctx\n )\n SELECT cur.currency, ctx.house,\n public.plg_financial_fx_rate(ctx.tenant, cur.currency, ctx.house, current_date),\n coalesce(saldo.v, 0), coalesce(aberto.receber, 0), coalesce(aberto.pagar, 0),\n coalesce(caixa.entrada, 0), coalesce(caixa.saida, 0),\n coalesce(aberto.venc_receber_n, 0)::integer, coalesce(aberto.venc_receber_v, 0),\n coalesce(aberto.venc_pagar_n, 0)::integer, coalesce(aberto.venc_pagar_v, 0)\n FROM cur\n CROSS JOIN ctx\n LEFT JOIN saldo ON saldo.currency = cur.currency\n LEFT JOIN aberto ON aberto.currency = cur.currency\n LEFT JOIN caixa ON caixa.currency = cur.currency\n ORDER BY (cur.currency = ctx.house) DESC, cur.currency;\n$function$;\nREVOKE ALL ON FUNCTION public.financial_summary_by_currency(date, date) FROM PUBLIC, anon;\nGRANT EXECUTE ON FUNCTION public.financial_summary_by_currency(date, date) TO authenticated, service_role;\n\n-- Same door, same nine columns: the total in the tenant's own currency.\nCREATE OR REPLACE FUNCTION public.financial_summary(p_from date DEFAULT NULL::date, p_to date DEFAULT NULL::date)\n RETURNS TABLE(total_balance numeric, total_receivable numeric, total_payable numeric, monthly_inflow numeric, monthly_outflow numeric, overdue_receivable_count integer, overdue_receivable_amount numeric, overdue_payable_count integer, overdue_payable_amount numeric)\n LANGUAGE sql\n STABLE SECURITY DEFINER\n SET search_path TO ''\nAS $function$\n SELECT coalesce(round(sum(s.total_balance * s.fx_rate), 2), 0),\n coalesce(round(sum(s.total_receivable * s.fx_rate), 2), 0),\n coalesce(round(sum(s.total_payable * s.fx_rate), 2), 0),\n coalesce(round(sum(s.monthly_inflow * s.fx_rate), 2), 0),\n coalesce(round(sum(s.monthly_outflow * s.fx_rate), 2), 0),\n coalesce(sum(s.overdue_receivable_count), 0)::integer,\n coalesce(round(sum(s.overdue_receivable_amount * s.fx_rate), 2), 0),\n coalesce(sum(s.overdue_payable_count), 0)::integer,\n coalesce(round(sum(s.overdue_payable_amount * s.fx_rate), 2), 0)\n FROM public.financial_summary_by_currency(p_from, p_to) s;\n$function$;\n";
|
|
127
|
+
export declare const MIGRATION_126_A_BILL_LINE_CAN_ENTER_STOCK = "-- ============================================================================\n-- 126 \u2014 a bill line can enter stock\n--\n-- A purchase was booked as a bill and the goods never reached stock unless\n-- someone typed them again on the stock screen. V1 had the gesture on the\n-- line; this is that gesture.\n--\n-- `financial_invoice_item_receive_stock` takes ONE line of a payable and\n-- receives it at a location, at the cost the line was billed at and in the\n-- bill's currency (the stock side converts to its unit's). A line that names\n-- no product is linked to one on the way \u2014 linking changes no value, so it is\n-- allowed on a bill that has already been paid.\n--\n-- The receipt is tied to the line, so the same line cannot enter twice, and\n-- `financial_invoice_stock_links` answers \"which lines entered\" without\n-- guessing from the invoice number (which `financial_invoice_stock_entries`\n-- still does for notes imported before this).\n-- ============================================================================\n\nCREATE OR REPLACE FUNCTION public.financial_invoice_item_receive_stock(\n p_item_id uuid, p_location_id uuid, p_product_id uuid DEFAULT NULL::uuid,\n p_quantity numeric DEFAULT NULL::numeric, p_fx_rate numeric DEFAULT NULL::numeric,\n p_idempotency_key text DEFAULT NULL::text)\n RETURNS jsonb\n LANGUAGE plpgsql\n SECURITY DEFINER\n SET search_path TO ''\nAS $function$\nDECLARE\n v_tenant uuid := public.plg_financial_require_tenant();\n v_item public.items%ROWTYPE;\n v_inv public.plg_financial_invoices%ROWTYPE;\n v_product uuid;\n v_qty numeric;\n v_cost numeric;\n v_amend text := coalesce(current_setting('financial.amend', true), '');\n v_res jsonb;\nBEGIN\n SELECT * INTO v_item FROM public.items WHERE id = p_item_id AND tenant_id = v_tenant;\n IF NOT FOUND THEN\n RAISE EXCEPTION 'financial_invoice_item_receive_stock: line % not found', p_item_id USING ERRCODE = 'P0002';\n END IF;\n SELECT f.* INTO v_inv FROM public.plg_financial_invoices f\n WHERE f.tenant_id = v_tenant\n AND (f.id = v_item.invoice_id OR (v_item.invoice_id IS NULL AND f.order_id = v_item.order_id))\n ORDER BY f.cancelled_at NULLS FIRST LIMIT 1;\n IF NOT FOUND OR v_inv.direction <> 'payable' THEN\n RAISE EXCEPTION 'S\u00F3 a linha de uma conta a pagar d\u00E1 entrada no estoque.' USING ERRCODE = '55000';\n END IF;\n PERFORM public.plg_financial_require_permission('financial.edit', v_inv.unit_id);\n IF v_inv.cancelled_at IS NOT NULL OR v_item.cancelled_at IS NOT NULL THEN\n RAISE EXCEPTION 'Esta linha est\u00E1 cancelada.' USING ERRCODE = '55000';\n END IF;\n IF to_regprocedure('public.inventory_receive(uuid,jsonb,jsonb,text)') IS NULL THEN\n RAISE EXCEPTION 'O estoque n\u00E3o est\u00E1 instalado nesta conta.' USING ERRCODE = '0A000';\n END IF;\n IF EXISTS (SELECT 1 FROM public.plg_inventory_stock_movements m\n WHERE m.tenant_id = v_tenant AND m.source_item_id = v_item.id AND m.kind = 'in'\n AND NOT EXISTS (SELECT 1 FROM public.plg_inventory_stock_movements r WHERE r.reverses_movement_id = m.id)) THEN\n RAISE EXCEPTION 'Esta linha j\u00E1 deu entrada no estoque.' USING ERRCODE = '23505';\n END IF;\n\n v_product := coalesce(v_item.product_id, p_product_id);\n IF v_product IS NULL THEN\n RAISE EXCEPTION 'Escolha o produto desta linha antes de dar entrada.' USING ERRCODE = '22023';\n END IF;\n IF NOT EXISTS (SELECT 1 FROM public.products p WHERE p.id = v_product AND p.tenant_id = v_tenant) THEN\n RAISE EXCEPTION 'Produto n\u00E3o encontrado nesta conta.' USING ERRCODE = '22023';\n END IF;\n IF v_item.product_id IS NULL THEN\n -- Naming the product changes no amount, so it passes the paid-bill lock.\n PERFORM set_config('financial.amend', 'on', true);\n UPDATE public.items SET product_id = v_product WHERE id = v_item.id;\n PERFORM set_config('financial.amend', v_amend, true);\n END IF;\n\n v_qty := coalesce(p_quantity, v_item.quantity);\n IF v_qty IS NULL OR v_qty <= 0 THEN\n RAISE EXCEPTION 'A quantidade precisa ser maior que zero.' USING ERRCODE = '22023';\n END IF;\n -- What the line cost per unit, discount included.\n v_cost := round(coalesce(v_item.total, v_item.unit_price * v_item.quantity, 0) / nullif(v_item.quantity, 0), 4);\n\n EXECUTE 'SELECT public.inventory_receive($1, $2, $3, $4)' INTO v_res USING\n p_location_id,\n jsonb_build_array(jsonb_strip_nulls(jsonb_build_object(\n 'product_id', v_product, 'variant_id', v_item.variant_id, 'quantity', v_qty, 'unit_cost', coalesce(v_cost, 0),\n 'source_item_type', 'invoice_item', 'source_item_id', v_item.id))),\n jsonb_strip_nulls(jsonb_build_object(\n 'type', 'manual', 'ref', v_inv.number, 'supplier_id', v_inv.counterparty_person_id,\n 'reason', 'Entrada pela conta ' || coalesce(v_inv.number, ''),\n 'currency', v_inv.currency, 'fx_rate', p_fx_rate)),\n coalesce(nullif(btrim(p_idempotency_key), ''), 'invoice-item:' || v_item.id::text);\n\n PERFORM public.plg_financial_audit(v_tenant, 'financial.invoice.item.receive_stock', 'items', v_item.id::text,\n jsonb_build_object('invoice_id', v_inv.id, 'invoice_number', v_inv.number, 'product_id', v_product,\n 'location_id', p_location_id, 'quantity', v_qty, 'unit_cost', v_cost, 'currency', v_inv.currency));\n RETURN jsonb_build_object('item_id', v_item.id, 'product_id', v_product, 'quantity', v_qty, 'receipt', v_res);\nEND $function$;\nREVOKE ALL ON FUNCTION public.financial_invoice_item_receive_stock(uuid, uuid, uuid, numeric, numeric, text) FROM PUBLIC, anon;\nGRANT EXECUTE ON FUNCTION public.financial_invoice_item_receive_stock(uuid, uuid, uuid, numeric, numeric, text) TO authenticated, service_role;\n\nCREATE OR REPLACE FUNCTION public.financial_invoice_stock_links(p_invoice_id uuid)\n RETURNS TABLE(item_id uuid, product_id uuid, location_id uuid, entered numeric, available numeric, entered_on date)\n LANGUAGE plpgsql\n STABLE SECURITY DEFINER\n SET search_path TO ''\nAS $function$\nDECLARE\n v_tenant uuid := public.plg_financial_require_tenant();\n v_inv public.plg_financial_invoices%ROWTYPE;\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 RETURN QUERY EXECUTE $q$\n SELECT m.source_item_id, m.product_id, m.source_location_id, sum(m.quantity),\n greatest(least(sum(m.quantity), 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.source_location_id), 0)), 0),\n min(m.movement_date)::date\n FROM public.plg_inventory_stock_movements m\n WHERE m.tenant_id = $1 AND m.kind = 'in'\n AND m.source_item_id IN (SELECT i.id FROM public.items i\n WHERE i.tenant_id = $1 AND (i.invoice_id = $2 OR ($3::uuid IS NOT NULL AND i.order_id = $3)))\n AND NOT EXISTS (SELECT 1 FROM public.plg_inventory_stock_movements r WHERE r.reverses_movement_id = m.id)\n GROUP BY m.source_item_id, m.product_id, m.source_location_id\n $q$ USING v_tenant, p_invoice_id, v_inv.order_id;\nEND $function$;\nREVOKE ALL ON FUNCTION public.financial_invoice_stock_links(uuid) FROM PUBLIC, anon;\nGRANT EXECUTE ON FUNCTION public.financial_invoice_stock_links(uuid) TO authenticated, service_role;\n";
|
|
128
|
+
export declare const MIGRATION_127_A_BILL_LINE_ENTERS_STOCK_AS_IT_REALLY_ARRIVED = "-- ============================================================================\n-- 127 \u2014 a bill line enters stock as it really arrived\n--\n-- 126 received one line once, in the billed quantity, on the default variant.\n-- Goods do not arrive like the bill reads: 24 shirts billed, 27 delivered,\n-- some M, some G, some GG, and not always on the same day.\n--\n-- `financial_invoice_item_receive_stock_lines` takes any number of entries\n-- (variant + quantity) for one line, and may be called again later for what\n-- arrives next; each call is one receipt tied to the line. The quantity is\n-- what was counted, not what was billed. The unit cost is the billed one\n-- unless the caller says otherwise \u2014 when the supplier sent more for the same\n-- money, the real cost is lower, and only a person knows that.\n--\n-- The single-entry door of 126 stays, as a call to this one.\n-- ============================================================================\n\nCREATE OR REPLACE FUNCTION public.financial_invoice_item_receive_stock_lines(\n p_item_id uuid, p_location_id uuid, p_lines jsonb,\n p_product_id uuid DEFAULT NULL::uuid, p_unit_cost numeric DEFAULT NULL::numeric,\n p_fx_rate numeric DEFAULT NULL::numeric, p_idempotency_key text DEFAULT NULL::text)\n RETURNS jsonb\n LANGUAGE plpgsql\n SECURITY DEFINER\n SET search_path TO ''\nAS $function$\nDECLARE\n v_tenant uuid := public.plg_financial_require_tenant();\n v_item public.items%ROWTYPE;\n v_inv public.plg_financial_invoices%ROWTYPE;\n v_product uuid;\n v_cost numeric;\n v_amend text := coalesce(current_setting('financial.amend', true), '');\n v_line jsonb;\n v_qty numeric;\n v_total numeric := 0;\n v_variant uuid;\n v_out jsonb := '[]'::jsonb;\n v_res jsonb;\nBEGIN\n PERFORM public.plg_financial_claim_idempotency(v_tenant, p_idempotency_key);\n IF p_lines IS NULL OR jsonb_typeof(p_lines) <> 'array' OR jsonb_array_length(p_lines) = 0 THEN\n RAISE EXCEPTION 'Informe ao menos uma quantidade para dar entrada.' USING ERRCODE = '22023';\n END IF;\n SELECT * INTO v_item FROM public.items WHERE id = p_item_id AND tenant_id = v_tenant;\n IF NOT FOUND THEN\n RAISE EXCEPTION 'financial_invoice_item_receive_stock_lines: line % not found', p_item_id USING ERRCODE = 'P0002';\n END IF;\n SELECT f.* INTO v_inv FROM public.plg_financial_invoices f\n WHERE f.tenant_id = v_tenant\n AND (f.id = v_item.invoice_id OR (v_item.invoice_id IS NULL AND f.order_id = v_item.order_id))\n ORDER BY f.cancelled_at NULLS FIRST LIMIT 1;\n IF NOT FOUND OR v_inv.direction <> 'payable' THEN\n RAISE EXCEPTION 'S\u00F3 a linha de uma conta a pagar d\u00E1 entrada no estoque.' USING ERRCODE = '55000';\n END IF;\n PERFORM public.plg_financial_require_permission('financial.edit', v_inv.unit_id);\n IF v_inv.cancelled_at IS NOT NULL OR v_item.cancelled_at IS NOT NULL THEN\n RAISE EXCEPTION 'Esta linha est\u00E1 cancelada.' USING ERRCODE = '55000';\n END IF;\n IF to_regprocedure('public.inventory_receive(uuid,jsonb,jsonb,text)') IS NULL THEN\n RAISE EXCEPTION 'O estoque n\u00E3o est\u00E1 instalado nesta conta.' USING ERRCODE = '0A000';\n END IF;\n\n v_product := coalesce(v_item.product_id, p_product_id);\n IF v_product IS NULL THEN\n RAISE EXCEPTION 'Escolha o produto desta linha antes de dar entrada.' USING ERRCODE = '22023';\n END IF;\n IF NOT EXISTS (SELECT 1 FROM public.products p WHERE p.id = v_product AND p.tenant_id = v_tenant) THEN\n RAISE EXCEPTION 'Produto n\u00E3o encontrado nesta conta.' USING ERRCODE = '22023';\n END IF;\n IF v_item.product_id IS NULL THEN\n -- Naming the product changes no amount, so it passes the paid-bill lock.\n PERFORM set_config('financial.amend', 'on', true);\n UPDATE public.items SET product_id = v_product WHERE id = v_item.id;\n PERFORM set_config('financial.amend', v_amend, true);\n END IF;\n\n -- The billed unit cost, discount included, unless the caller knows better.\n v_cost := coalesce(p_unit_cost,\n round(coalesce(v_item.total, v_item.unit_price * v_item.quantity, 0) / nullif(v_item.quantity, 0), 4), 0);\n IF v_cost < 0 THEN\n RAISE EXCEPTION 'O custo n\u00E3o pode ser negativo.' USING ERRCODE = '22023';\n END IF;\n\n FOR v_line IN SELECT * FROM jsonb_array_elements(p_lines) LOOP\n BEGIN\n v_qty := (v_line ->> 'quantity')::numeric;\n v_variant := nullif(v_line ->> 'variant_id', '')::uuid;\n EXCEPTION WHEN OTHERS THEN\n RAISE EXCEPTION 'Entrada inv\u00E1lida: %', v_line USING ERRCODE = '22023';\n END;\n CONTINUE WHEN v_qty IS NULL OR v_qty = 0;\n IF v_qty < 0 THEN\n RAISE EXCEPTION 'A quantidade precisa ser maior que zero.' USING ERRCODE = '22023';\n END IF;\n v_total := v_total + v_qty;\n v_out := v_out || jsonb_strip_nulls(jsonb_build_object(\n 'product_id', v_product, 'variant_id', v_variant, 'quantity', v_qty, 'unit_cost', v_cost,\n 'source_item_type', 'invoice_item', 'source_item_id', v_item.id));\n END LOOP;\n IF v_total <= 0 THEN\n RAISE EXCEPTION 'Informe ao menos uma quantidade para dar entrada.' USING ERRCODE = '22023';\n END IF;\n\n EXECUTE 'SELECT public.inventory_receive($1, $2, $3, $4)' INTO v_res USING\n p_location_id, v_out,\n jsonb_strip_nulls(jsonb_build_object(\n 'type', 'manual', 'ref', v_inv.number, 'supplier_id', v_inv.counterparty_person_id,\n 'reason', 'Entrada pela conta ' || coalesce(v_inv.number, ''),\n 'currency', v_inv.currency, 'fx_rate', p_fx_rate)),\n -- No key from the caller means a new receipt each time: a line can be\n -- received more than once.\n coalesce(nullif(btrim(p_idempotency_key), ''), 'invoice-item:' || v_item.id::text || ':' || gen_random_uuid()::text);\n\n PERFORM public.plg_financial_audit(v_tenant, 'financial.invoice.item.receive_stock', 'items', v_item.id::text,\n jsonb_build_object('invoice_id', v_inv.id, 'invoice_number', v_inv.number, 'product_id', v_product,\n 'location_id', p_location_id, 'quantity', v_total, 'unit_cost', v_cost,\n 'currency', v_inv.currency, 'entries', jsonb_array_length(v_out)));\n RETURN jsonb_build_object('item_id', v_item.id, 'product_id', v_product, 'quantity', v_total, 'unit_cost', v_cost, 'receipt', v_res);\nEND $function$;\nREVOKE ALL ON FUNCTION public.financial_invoice_item_receive_stock_lines(uuid, uuid, jsonb, uuid, numeric, numeric, text) FROM PUBLIC, anon;\nGRANT EXECUTE ON FUNCTION public.financial_invoice_item_receive_stock_lines(uuid, uuid, jsonb, uuid, numeric, numeric, text) TO authenticated, service_role;\n\n-- 126's door: one entry, in the quantity given or the billed one.\nCREATE OR REPLACE FUNCTION public.financial_invoice_item_receive_stock(\n p_item_id uuid, p_location_id uuid, p_product_id uuid DEFAULT NULL::uuid,\n p_quantity numeric DEFAULT NULL::numeric, p_fx_rate numeric DEFAULT NULL::numeric,\n p_idempotency_key text DEFAULT NULL::text)\n RETURNS jsonb\n LANGUAGE plpgsql\n SECURITY DEFINER\n SET search_path TO ''\nAS $function$\nBEGIN\n -- Same key, same lock as the door it delegates to (re-entrant in one transaction).\n PERFORM public.plg_financial_claim_idempotency(public.plg_financial_require_tenant(), p_idempotency_key);\n RETURN public.financial_invoice_item_receive_stock_lines(\n p_item_id, p_location_id,\n jsonb_build_array(jsonb_build_object('quantity', coalesce(p_quantity,\n (SELECT i.quantity FROM public.items i WHERE i.id = p_item_id)))),\n p_product_id, NULL::numeric, p_fx_rate, p_idempotency_key);\nEND $function$;\n\n-- Which variant each receipt was for, so the screen can say \"10 M, 9 G\".\n-- Dropped first: 128 widens what this returns, and OR REPLACE cannot narrow it back on a replay.\nDROP FUNCTION IF EXISTS public.financial_invoice_stock_link_details(uuid);\nCREATE OR REPLACE FUNCTION public.financial_invoice_stock_link_details(p_invoice_id uuid)\n RETURNS TABLE(item_id uuid, product_id uuid, variant_id uuid, variant_name text, location_id uuid,\n quantity numeric, unit_cost numeric, currency text, entered_on date)\n LANGUAGE plpgsql\n STABLE SECURITY DEFINER\n SET search_path TO ''\nAS $function$\nDECLARE\n v_tenant uuid := public.plg_financial_require_tenant();\n v_inv public.plg_financial_invoices%ROWTYPE;\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 RETURN QUERY EXECUTE $q$\n SELECT m.source_item_id, m.product_id, m.variant_id, v.name, m.source_location_id,\n sum(m.quantity), max(m.unit_cost), max(m.currency), min(m.movement_date)::date\n FROM public.plg_inventory_stock_movements m\n LEFT JOIN public.product_variants v ON v.id = m.variant_id\n WHERE m.tenant_id = $1 AND m.kind = 'in'\n AND m.source_item_id IN (SELECT i.id FROM public.items i\n WHERE i.tenant_id = $1 AND (i.invoice_id = $2 OR ($3::uuid IS NOT NULL AND i.order_id = $3)))\n AND NOT EXISTS (SELECT 1 FROM public.plg_inventory_stock_movements r WHERE r.reverses_movement_id = m.id)\n GROUP BY m.source_item_id, m.product_id, m.variant_id, v.name, m.source_location_id\n ORDER BY v.name NULLS FIRST\n $q$ USING v_tenant, p_invoice_id, v_inv.order_id;\nEND $function$;\nREVOKE ALL ON FUNCTION public.financial_invoice_stock_link_details(uuid) FROM PUBLIC, anon;\nGRANT EXECUTE ON FUNCTION public.financial_invoice_stock_link_details(uuid) TO authenticated, service_role;\n";
|
|
129
|
+
export declare const MIGRATION_128_A_STOCK_RECEIPT_SHOWS_WHAT_WAS_PAID = "-- ============================================================================\n-- 128 \u2014 a stock receipt shows what was paid\n--\n-- The bill is in reais and the stock is kept in euros. The ledger records both\n-- sides of every receipt (cost in the stock's currency, and what was paid, in\n-- which currency, at which rate), but the reading the bill's screen used only\n-- returned the converted cost \u2014 so the screen could not show the tie it was\n-- asked to prove.\n--\n-- Same reading, with the paid side, the rate and the location's name. The\n-- return type changes, so the function is dropped first; only the bill's\n-- screen calls it.\n-- ============================================================================\n\nDROP FUNCTION IF EXISTS public.financial_invoice_stock_link_details(uuid);\n\nCREATE FUNCTION public.financial_invoice_stock_link_details(p_invoice_id uuid)\n RETURNS TABLE(item_id uuid, product_id uuid, variant_id uuid, variant_name text,\n location_id uuid, location_name text, quantity numeric,\n unit_cost numeric, currency text,\n source_unit_cost numeric, source_currency text, fx_rate numeric,\n entered_on date)\n LANGUAGE plpgsql\n STABLE SECURITY DEFINER\n SET search_path TO ''\nAS $function$\nDECLARE\n v_tenant uuid := public.plg_financial_require_tenant();\n v_inv public.plg_financial_invoices%ROWTYPE;\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 RETURN QUERY EXECUTE $q$\n SELECT m.source_item_id, m.product_id, m.variant_id, v.name, m.source_location_id, l.name,\n sum(m.quantity), m.unit_cost, m.currency, m.source_unit_cost, m.source_currency, m.fx_rate,\n m.movement_date::date\n FROM public.plg_inventory_stock_movements m\n LEFT JOIN public.product_variants v ON v.id = m.variant_id\n LEFT JOIN public.plg_inventory_stock_locations l ON l.id = m.source_location_id\n WHERE m.tenant_id = $1 AND m.kind = 'in'\n AND m.source_item_id IN (SELECT i.id FROM public.items i\n WHERE i.tenant_id = $1 AND (i.invoice_id = $2 OR ($3::uuid IS NOT NULL AND i.order_id = $3)))\n AND NOT EXISTS (SELECT 1 FROM public.plg_inventory_stock_movements r WHERE r.reverses_movement_id = m.id)\n GROUP BY m.source_item_id, m.product_id, m.variant_id, v.name, v.sort_order, m.source_location_id, l.name,\n m.unit_cost, m.currency, m.source_unit_cost, m.source_currency, m.fx_rate, m.movement_date\n ORDER BY m.movement_date, v.sort_order NULLS FIRST\n $q$ USING v_tenant, p_invoice_id, v_inv.order_id;\nEND $function$;\nREVOKE ALL ON FUNCTION public.financial_invoice_stock_link_details(uuid) FROM PUBLIC, anon;\nGRANT EXECUTE ON FUNCTION public.financial_invoice_stock_link_details(uuid) TO authenticated, service_role;\n";
|
|
130
|
+
export declare const MIGRATION_129_A_BILL_CAN_BE_WRITTEN_IN_ANOTHER_CURRENCY = "-- ============================================================================\n-- 129 \u2014 a bill can be written in another currency\n-- ============================================================================\n-- An invoice took the currency of its order or of its unit, and nothing else.\n-- A house that sells in two currencies writes a bill by hand in either one, so\n-- the door now accepts the choice: `metadata.currency` on creation. An order\n-- still wins \u2014 a bill raised from a sale is in the currency of that sale.\n-- The choice is made once: an issued invoice's currency stays immutable.\n-- Idempotent: CREATE OR REPLACE only.\n\nCREATE OR REPLACE FUNCTION public.trg_plg_financial_invoices_currency()\n RETURNS trigger\n LANGUAGE plpgsql\n SECURITY DEFINER\n SET search_path TO ''\nAS $function$\nDECLARE\n v_asked text := upper(btrim(coalesce(NEW.metadata->>'currency', '')));\nBEGIN\n IF nullif(btrim(coalesce(NEW.currency, '')), '') IS NULL THEN\n IF NEW.order_id IS NOT NULL THEN\n SELECT nullif(btrim(o.currency), '') INTO NEW.currency FROM public.orders o WHERE o.id = NEW.order_id;\n END IF;\n IF NEW.currency IS NULL AND v_asked ~ '^[A-Z]{3}$' THEN\n NEW.currency := v_asked;\n END IF;\n NEW.currency := coalesce(NEW.currency, app.unit_currency(NEW.tenant_id, NEW.unit_id));\n END IF;\n NEW.currency := upper(btrim(NEW.currency));\n IF NEW.fx_rate IS NULL THEN\n NEW.fx_rate := public.plg_financial_fx_rate(\n NEW.tenant_id, NEW.currency, app.unit_currency(NEW.tenant_id, NULL), coalesce(NEW.issue_date, current_date));\n END IF;\n RETURN NEW;\nEND $function$;\n";
|
|
131
|
+
export declare const MIGRATION_130_A_BILL_CAN_BE_TAGGED = "-- ============================================================================\n-- 130 \u2014 a bill can be tagged\n-- ============================================================================\n-- Every record of the core has `tags` since spine 150: how THIS house slices\n-- its records, for a purpose the product did not foresee. Bills were the one\n-- place the operator looked for the tag and did not find it. Same shape as\n-- there \u2014 `text[]` read and written with the row, a GIN index for \"who has\n-- this one\". Tags are not part of what an issued invoice freezes.\n-- Idempotent: IF NOT EXISTS throughout.\n\nALTER TABLE public.plg_financial_invoices\n ADD COLUMN IF NOT EXISTS tags text[] DEFAULT '{}'::text[];\n\nCOMMENT ON COLUMN public.plg_financial_invoices.tags IS 'Free labels the house puts on a bill to slice its own lists.';\n\nCREATE INDEX IF NOT EXISTS plg_financial_invoices_tags_idx\n ON public.plg_financial_invoices USING gin (tags);\n";
|
|
124
132
|
export declare const MIGRATIONS: Array<{
|
|
125
133
|
id: string;
|
|
126
134
|
sql: string;
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../../src/migrations/index.ts"],"names":[],"mappings":"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
|
|
1
|
+
{"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../../src/migrations/index.ts"],"names":[],"mappings":"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"}
|
package/dist/types.d.ts
CHANGED
|
@@ -28,6 +28,10 @@ export type InvoiceItemKind = string;
|
|
|
28
28
|
export interface Invoice {
|
|
29
29
|
id: string;
|
|
30
30
|
direction: TransactionDirection;
|
|
31
|
+
/** ISO code the document was written in. Absent = the house's currency. */
|
|
32
|
+
currency?: string;
|
|
33
|
+
/** Free labels the house puts on the bill. */
|
|
34
|
+
tags?: string[];
|
|
31
35
|
/** Competência — o eixo do razão e do DRE. */
|
|
32
36
|
invoiceDate: string;
|
|
33
37
|
/**
|
|
@@ -92,6 +96,8 @@ export interface Invoice {
|
|
|
92
96
|
export interface InvoiceItem {
|
|
93
97
|
id: string;
|
|
94
98
|
invoiceId: string;
|
|
99
|
+
/** The picture of the product this line names, when it names one that has a picture. */
|
|
100
|
+
imageUrl?: string;
|
|
95
101
|
itemKind: InvoiceItemKind;
|
|
96
102
|
referenceId?: string;
|
|
97
103
|
description: string;
|
|
@@ -282,6 +288,9 @@ export interface CreateInvoiceInput {
|
|
|
282
288
|
contactName?: string;
|
|
283
289
|
cashSessionId?: string;
|
|
284
290
|
unitId?: string;
|
|
291
|
+
/** ISO code to write the document in. Absent = the unit's currency. */
|
|
292
|
+
currency?: string;
|
|
293
|
+
tags?: string[];
|
|
285
294
|
observations?: string;
|
|
286
295
|
metadata?: Record<string, unknown>;
|
|
287
296
|
items: CreateInvoiceItemInput[];
|
|
@@ -496,6 +505,18 @@ export interface FinancialSummary {
|
|
|
496
505
|
overdueReceivableAmount: number;
|
|
497
506
|
overduePayableCount: number;
|
|
498
507
|
overduePayableAmount: number;
|
|
508
|
+
/**
|
|
509
|
+
* Amounts in a currency with no rate to the house's on file. They are NOT in
|
|
510
|
+
* the totals above (never counted 1:1), so the screen has to say they exist.
|
|
511
|
+
*/
|
|
512
|
+
excluded?: Array<{
|
|
513
|
+
currency: string;
|
|
514
|
+
balance: number;
|
|
515
|
+
receivable: number;
|
|
516
|
+
payable: number;
|
|
517
|
+
inflow: number;
|
|
518
|
+
outflow: number;
|
|
519
|
+
}>;
|
|
499
520
|
}
|
|
500
521
|
/** What a statement row represents — a normal movement or one leg of a transfer */
|
|
501
522
|
export type StatementEntryKind = 'movement' | 'transfer-in' | 'transfer-out';
|