@fayz-ai/plugin-financial 0.14.5 → 0.14.6
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.
|
@@ -61,7 +61,7 @@ export declare const MIGRATION_059_O_CLIPE_SAI_DA_VIEW = "-- O clipe sai da view
|
|
|
61
61
|
export declare const MIGRATION_060_O_RECEBIVEL_VOLTA_DA_ARVORE_DERRUBADA = "-- O receb\u00EDvel volta da \u00E1rvore derrubada.\n--\n-- Mesma causa da 007 da agenda: o `DROP VIEW \u2026 CASCADE` das migrations 044 e\n-- 045 derrubou tudo o que pendurava em `v_invoice_balances`, e elas s\u00F3\n-- recriaram as duas views que o autor tinha em mente.\n--\n-- `plg_financial_rep_receivables` \u00E9 a que o plugin-reports l\u00EA para o relat\u00F3rio\n-- de contas a receber \u2014 o alias `plg_*` \u00E9 o nome pelo qual o cat\u00E1logo de\n-- relat\u00F3rios a encontra. Ela sumiu junto e ningu\u00E9m percebeu, porque quem abre\n-- contas a receber no dia a dia abre a LISTA, n\u00E3o o relat\u00F3rio.\n--\n-- Corpo id\u00EAntico ao do baseline: nenhuma coluna muda, nenhuma regra muda.\n-- `security_invoker` e GRANT expl\u00EDcitos, porque uma view recriada n\u00E3o herda\n-- nem as op\u00E7\u00F5es nem as permiss\u00F5es da que morreu.\n--\n-- Idempotente.\n\nCREATE OR REPLACE VIEW public.plg_financial_rep_receivables WITH (security_invoker = true) AS\n SELECT b.invoice_id,\n b.tenant_id,\n b.created_at,\n b.reference_number,\n b.kind,\n b.status,\n b.party_id,\n p.name AS party_name,\n b.amount,\n b.paid,\n b.balance,\n CASE\n WHEN (b.status = ANY (ARRAY['open'::text, 'overdue'::text])) THEN b.balance\n ELSE (0)::numeric\n END AS open_balance,\n CASE\n WHEN (b.status = 'overdue'::text) THEN b.balance\n ELSE (0)::numeric\n END AS overdue_balance,\n CASE\n WHEN (b.status = 'paid'::text) THEN b.amount\n ELSE (0)::numeric\n END AS settled_amount,\n CASE\n WHEN (b.status = 'overdue'::text) THEN 1\n ELSE 0\n END AS is_overdue,\n CASE\n WHEN (b.status = 'paid'::text) THEN 1\n ELSE 0\n END AS is_settled\n FROM (public.v_invoice_balances b\n LEFT JOIN public.people p ON ((p.id = b.party_id)))\n WHERE (b.flow = 'receivable'::text);\n\nGRANT SELECT ON TABLE public.plg_financial_rep_receivables TO authenticated, service_role;\n";
|
|
62
62
|
export declare const MIGRATION_061_CONTAS_A_PAGAR_GANHAM_ONDE_MORAR = "-- Contas a pagar ganham onde morar.\n--\n-- `recurring_bills`, `recurring_bill_items` e `recurring_bill_occurrences` do V1\n-- estavam em `migration.excluded_tables` com este motivo: *\"no destination\n-- contract \u2026 plg_financial_recurring_bills nunca foi constru\u00EDda (#188)\"*.\n--\n-- **A nota est\u00E1 vencida.** `public.plg_financial_recurring_bills` existe, com\n-- forma pr\u00F3pria e diferente da do V1 \u2014 e \u00E9 essa forma que manda:\n--\n-- V1 V2\n-- recurring_bills plg_financial_recurring_bills\n-- recurring_bill_items (tabela) .items (jsonb na pr\u00F3pria conta)\n-- recurring_bill_occurrences (tabela) \u2014 n\u00E3o existe\n-- interval_unit + interval_count frequency (weekly|monthly|quarterly|yearly)\n-- first_due_date starts_on + due_day (1..31)\n-- account_central_id \u2192 contacts counterparty_person_id \u2192 public.people\n-- (soma dos itens) amount NOT NULL, CHECK (amount > 0)\n-- (primeira descri\u00E7\u00E3o de item) description NOT NULL\n--\n-- Este arquivo faz tr\u00EAs coisas, e s\u00F3 a terceira cria tabela:\n--\n-- 1. escreve os writers que levam a conta e os itens para a forma do V2\n-- 2. escreve o de-para e tira as tr\u00EAs tabelas da lista de adiadas\n-- 3. cria `plg_financial_recurring_bill_occurrences`, que \u00E9 a \u00FAnica lacuna\n-- DE VERDADE\n--\n-- \u2500\u2500 Por que a ocorr\u00EAncia precisa existir \u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\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 V2 deriva os vencimentos de `frequency` + `due_day`: a conta \u00E9 a regra, e o\n-- calend\u00E1rio sai dela. Isso funciona enquanto ningu\u00E9m mexe num vencimento \u2014 e o\n-- V1 deixa mexer. `recurring_bill_occurrences.overridden` marca o vencimento que\n-- algu\u00E9m editou \u00E0 m\u00E3o (mudou o valor de mar\u00E7o sem mexer na regra), e\n-- `paid_value` guarda o que de fato foi pago. Nenhum dos dois \u00E9 deriv\u00E1vel: a\n-- f\u00F3rmula devolve o valor da regra, n\u00E3o o que aconteceu.\n--\n-- Medido na origem em 18/09/2026 (licen\u00E7a 8, Pertinho do C\u00E9u): 35 contas, 35\n-- itens, 833 ocorr\u00EAncias, R$ 1.937.827,67 entre 25/05/2026 e 02/09/2028, 111 j\u00E1\n-- consolidadas. Descartar as ocorr\u00EAncias apagaria o hist\u00F3rico de pagamento e\n-- toda edi\u00E7\u00E3o manual junto.\n--\n-- Idempotente.\n\n-- \u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\n-- 1. A \u00FAnica tabela que falta\n-- \u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\n\nCREATE TABLE IF NOT EXISTS public.plg_financial_recurring_bill_occurrences (\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 REFERENCES app.units(id),\n recurring_bill_id uuid NOT NULL\n REFERENCES public.plg_financial_recurring_bills(id) ON DELETE CASCADE,\n seq integer NOT NULL,\n competence_date date NOT NULL,\n due_date date NOT NULL,\n amount numeric NOT NULL,\n paid_amount numeric NOT NULL DEFAULT 0,\n status text NOT NULL DEFAULT 'provisioned'\n CHECK (status IN ('provisioned', 'consolidated', 'cancelled')),\n -- Algu\u00E9m editou ESTE vencimento, fora da regra. \u00C9 a raz\u00E3o de a ocorr\u00EAncia ser\n -- uma linha e n\u00E3o um c\u00E1lculo.\n overridden boolean NOT NULL DEFAULT false,\n -- O t\u00EDtulo que o vencimento virou, quando virou. Fica nulo at\u00E9 consolidar, e\n -- n\u00E3o vem do V1: l\u00E1 o campo aponta para `invoices`, cuja contraparte no V2 \u00E9\n -- uma VENDA, e ligar uma conta a pagar a uma venda inverteria o sentido do\n -- dinheiro para ganhar um link. A refer\u00EAncia crua do V1 fica em metadata.\n invoice_id uuid REFERENCES public.plg_financial_invoices(id) ON DELETE SET NULL,\n notes text,\n metadata jsonb NOT NULL DEFAULT '{}'::jsonb,\n created_at timestamptz NOT NULL DEFAULT now(),\n updated_at timestamptz NOT NULL DEFAULT now(),\n UNIQUE (recurring_bill_id, seq)\n);\n\n-- A pergunta que a tela faz: \"o que vence neste per\u00EDodo?\". Sem este \u00EDndice ela\n-- varre as ocorr\u00EAncias de todos os tenants.\nCREATE INDEX IF NOT EXISTS plg_financial_recurring_bill_occurrences_due_idx\n ON public.plg_financial_recurring_bill_occurrences (tenant_id, due_date, status);\n\nDROP TRIGGER IF EXISTS plg_financial_recurring_bill_occurrences_updated_at\n ON public.plg_financial_recurring_bill_occurrences;\nCREATE TRIGGER plg_financial_recurring_bill_occurrences_updated_at\n BEFORE UPDATE ON public.plg_financial_recurring_bill_occurrences\n FOR EACH ROW EXECUTE FUNCTION public.handle_updated_at();\n\nGRANT SELECT, INSERT, UPDATE, DELETE\n ON public.plg_financial_recurring_bill_occurrences TO authenticated, service_role;\n\n-- O scaffold \u00E9 quem sabe desenhar a pol\u00EDtica deste cluster (tenant, unidade,\n-- permiss\u00E3o do plugin, escotilhas de compartilhamento). Escrever a policy \u00E0 m\u00E3o\n-- criaria a segunda regra de autoriza\u00E7\u00E3o do financeiro, que diverge da primeira\n-- na pr\u00F3xima mudan\u00E7a do scaffold.\n--\n-- `unit_optional`, e n\u00E3o `unit`: `unit_id` aceita NULL aqui porque a conta pode\n-- n\u00E3o ter unidade. Declarar `unit` numa coluna anul\u00E1vel \u00E9 o erro que j\u00E1 escondeu\n-- todo movimento financeiro sem unidade e zerou um relat\u00F3rio inteiro.\nSELECT app.scaffold_table('public.plg_financial_recurring_bill_occurrences'::regclass,\n 'financial.recurring_bill_occurrence', 'financial', false, 'enforce');\n\n-- \u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\n-- 2. Os writers\n-- \u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\n\n-- O vocabul\u00E1rio do de-para \u00E9 lista fechada de prop\u00F3sito: uma linha que nomeia um\n-- writer inexistente falha na hora, n\u00E3o na terceira hora da onda.\n--\n-- A LISTA \u00C9 A UNI\u00C3O, E N\u00C3O UMA LISTA LITERAL. Este CHECK \u00E9 de um recurso\n-- COMPARTILHADO: cada onda de porte acrescenta os seus writers a ele. Escrito\n-- como literal, o \u00FAltimo arquivo a rodar apaga os writers dos outros \u2014 e foi\n-- exatamente o que aconteceu aqui: este arquivo nasceu antes de o workforce\n-- declarar os `wf_*`, e ao ser replayed derrubava todos eles, violando o\n-- pr\u00F3prio CHECK com as linhas que j\u00E1 existiam (\"is violated by some row\").\n--\n-- Somar o que EST\u00C1 EM USO n\u00E3o afrouxa nada: um writer j\u00E1 gravado na allowlist\n-- j\u00E1 passou por este mesmo port\u00E3o alguma vez. O que o CHECK continua pegando \u00E9\n-- o writer NOVO com nome errado, que \u00E9 para o que ele existe.\nDO $$\nDECLARE v_list text;\nBEGIN\n SELECT string_agg(DISTINCT quote_literal(w), ', ') INTO v_list FROM (\n SELECT unnest(ARRAY[\n 'person', 'unit', 'category', 'product', 'order', 'invoice', 'price_table',\n 'price_table_item', 'package', 'package_item', 'field_rule', 'holiday', 'facet',\n 'fin_account', 'fin_payment_method_type', 'fin_payment_method', 'fin_card_brand',\n 'fin_chart_account', 'fin_cost_center', 'inv_measurement_unit', 'inv_unit_conversion',\n 'inv_location', 'inv_movement', 'order_item', 'fin_movement', 'authz_role',\n 'authz_role_permission', 'person_registration',\n -- Os tr\u00EAs que esta migration traz.\n 'fin_recurring_bill', 'fin_recurring_bill_item', 'fin_recurring_bill_occurrence']) AS w\n UNION\n SELECT a.writer FROM migration.allowlist a WHERE a.writer IS NOT NULL\n ) s;\n\n ALTER TABLE migration.allowlist DROP CONSTRAINT IF EXISTS allowlist_writer_check;\n EXECUTE format(\n 'ALTER TABLE migration.allowlist ADD CONSTRAINT allowlist_writer_check CHECK (writer IS NULL OR writer = ANY (ARRAY[%s]))',\n v_list);\nEND $$;\n\nCREATE OR REPLACE FUNCTION migration.upsert_fin_recurring_bill(\n p_tenant uuid, p_id uuid, p_row jsonb, p_args jsonb DEFAULT '{}'::jsonb\n) RETURNS jsonb\nLANGUAGE plpgsql SECURITY DEFINER SET search_path TO ''\nAS $function$\nDECLARE\n v_meta jsonb := coalesce(p_row -> 'metadata', '{}'::jsonb);\n v_dir_v1 text := nullif(btrim(upper(coalesce(p_row ->> 'direction', ''))), '');\n v_dir text;\n v_unit_v1 text := nullif(btrim(lower(coalesce(p_row ->> 'interval_unit', ''))), '');\n v_count integer := coalesce(nullif(btrim(coalesce(p_row ->> 'interval_count', '')), '')::integer, 1);\n v_freq text;\n v_amount numeric := (p_row ->> 'amount')::numeric;\n v_desc text := nullif(btrim(coalesce(p_row ->> 'description', '')), '');\n v_due_day integer;\n v_starts date := (p_row ->> 'starts_on')::date;\n v_created boolean := false;\n v_standalone boolean := migration._standalone();\nBEGIN\n IF v_standalone THEN PERFORM migration._assert_fence(p_tenant); PERFORM set_config('migration.writer', 'on', true); END IF;\n IF p_id IS NULL OR v_starts IS NULL THEN\n RETURN jsonb_build_object('status', 'quarantined', 'kind', 'state',\n 'reason', 'uma conta recorrente precisa de id e de primeiro vencimento');\n END IF;\n\n -- Sem corre\u00E7\u00E3o silenciosa: o V1 grava 'D' (despesa) e 'R' (receita) em `type`.\n -- Um vencimento com o sentido do dinheiro trocado some do contas a pagar e\n -- aparece no contas a receber, e ningu\u00E9m confere um n\u00FAmero que parece plaus\u00EDvel.\n v_dir := CASE v_dir_v1 WHEN 'D' THEN 'payable' WHEN 'R' THEN 'receivable' ELSE NULL END;\n IF v_dir IS NULL THEN\n RETURN jsonb_build_object('status', 'quarantined', 'kind', 'value',\n 'reason', format('sentido do dinheiro desconhecido: type=%L (o V1 usa D=despesa, R=receita)', v_dir_v1));\n END IF;\n\n -- Duas colunas do V1 (unidade + quantidade) viram uma palavra no V2. A tradu\u00E7\u00E3o\n -- \u00E9 exaustiva de prop\u00F3sito: \"a cada 2 meses\" n\u00E3o tem nome no vocabul\u00E1rio do V2,\n -- e arredondar para 'monthly' dobraria a previs\u00E3o de caixa daquela conta.\n v_freq := CASE\n WHEN v_unit_v1 = 'week' AND v_count = 1 THEN 'weekly'\n WHEN v_unit_v1 = 'month' AND v_count = 1 THEN 'monthly'\n WHEN v_unit_v1 = 'month' AND v_count = 3 THEN 'quarterly'\n WHEN v_unit_v1 = 'month' AND v_count = 12 THEN 'yearly'\n WHEN v_unit_v1 = 'year' AND v_count = 1 THEN 'yearly'\n ELSE NULL END;\n IF v_freq IS NULL THEN\n RETURN jsonb_build_object('status', 'quarantined', 'kind', 'value',\n 'reason', format('periodicidade sem equivalente no V2: a cada %s %s (o V2 conhece '\n 'weekly, monthly, quarterly e yearly)', v_count, v_unit_v1));\n END IF;\n\n -- `description` e `amount` s\u00E3o NOT NULL no V2 e n\u00E3o existem na conta do V1:\n -- l\u00E1 eles moram nos itens. O plano de extra\u00E7\u00E3o os reconstr\u00F3i a partir da\n -- pr\u00F3pria origem (Step.augment) \u2014 se vierem vazios, a conta n\u00E3o tem item, e\n -- isso \u00E9 uma linha quebrada na origem, n\u00E3o um default a inventar.\n IF v_desc IS NULL THEN\n RETURN jsonb_build_object('status', 'quarantined', 'kind', 'state',\n 'reason', 'conta recorrente sem descri\u00E7\u00E3o: no V1 ela vem do item, e esta conta n\u00E3o tem item');\n END IF;\n IF v_amount IS NULL OR v_amount <= 0 THEN\n RETURN jsonb_build_object('status', 'quarantined', 'kind', 'value',\n 'reason', format('valor da conta recorrente \u00E9 %s: o V2 exige amount > 0, e no V1 ele \u00E9 a '\n 'soma dos itens', coalesce(v_amount::text, 'nulo')));\n END IF;\n\n v_due_day := extract(day from v_starts)::integer;\n\n IF NOT EXISTS (SELECT 1 FROM public.plg_financial_recurring_bills WHERE id = p_id) THEN\n INSERT INTO public.plg_financial_recurring_bills\n (id, tenant_id, unit_id, description, direction, counterparty_person_id, amount,\n chart_account_id, cost_center_id, frequency, due_day, starts_on, ends_on,\n is_active, notes, items, metadata)\n VALUES (p_id, p_tenant, (p_row ->> 'unit_id')::uuid, v_desc, v_dir,\n (p_row ->> 'counterparty_person_id')::uuid, v_amount,\n (p_row ->> 'chart_account_id')::uuid, (p_row ->> 'cost_center_id')::uuid,\n v_freq, v_due_day, v_starts, (p_row ->> 'ends_on')::date,\n coalesce((p_row ->> 'is_active')::boolean, true), p_row ->> 'notes',\n '[]'::jsonb, v_meta);\n v_created := true;\n ELSE\n UPDATE public.plg_financial_recurring_bills SET\n unit_id = CASE WHEN p_row ? 'unit_id' THEN (p_row ->> 'unit_id')::uuid ELSE unit_id END,\n description = v_desc,\n direction = v_dir,\n counterparty_person_id = CASE WHEN p_row ? 'counterparty_person_id' THEN (p_row ->> 'counterparty_person_id')::uuid ELSE counterparty_person_id END,\n amount = v_amount,\n chart_account_id = CASE WHEN p_row ? 'chart_account_id' THEN (p_row ->> 'chart_account_id')::uuid ELSE chart_account_id END,\n cost_center_id = CASE WHEN p_row ? 'cost_center_id' THEN (p_row ->> 'cost_center_id')::uuid ELSE cost_center_id END,\n frequency = v_freq,\n due_day = v_due_day,\n starts_on = v_starts,\n ends_on = CASE WHEN p_row ? 'ends_on' THEN (p_row ->> 'ends_on')::date ELSE ends_on END,\n is_active = CASE WHEN p_row ? 'is_active' THEN coalesce((p_row ->> 'is_active')::boolean, true) ELSE is_active END,\n notes = CASE WHEN p_row ? 'notes' THEN p_row ->> 'notes' ELSE notes END,\n metadata = migration._merge_meta(metadata, v_meta)\n WHERE id = p_id;\n END IF;\n\n IF v_standalone THEN PERFORM migration._audit(p_tenant, 'public.plg_financial_recurring_bills', p_id, '{}'::jsonb); END IF;\n RETURN jsonb_build_object('status', 'migrated', 'target_id', p_id,\n 'reason', CASE WHEN v_created THEN 'created' ELSE 'updated' END);\nEND $function$;\n\n-- O item do V1 \u00E9 uma LINHA; no V2 ele \u00E9 um elemento do jsonb `items` da conta.\n-- Por isso este writer n\u00E3o tem tabela pr\u00F3pria e devolve o id da CONTA: o ledger\n-- passa a apontar a linha de item do V1 para a conta que a cont\u00E9m, que \u00E9 onde o\n-- dado foi parar de verdade.\nCREATE OR REPLACE FUNCTION migration.upsert_fin_recurring_bill_item(\n p_tenant uuid, p_id uuid, p_row jsonb, p_args jsonb DEFAULT '{}'::jsonb\n) RETURNS jsonb\nLANGUAGE plpgsql SECURITY DEFINER SET search_path TO ''\nAS $function$\nDECLARE\n v_bill uuid := (p_row ->> 'recurring_bill_id')::uuid;\n v_item jsonb;\n v_items jsonb;\n v_standalone boolean := migration._standalone();\nBEGIN\n IF v_standalone THEN PERFORM migration._assert_fence(p_tenant); PERFORM set_config('migration.writer', 'on', true); END IF;\n IF p_id IS NULL OR v_bill IS NULL THEN\n RETURN jsonb_build_object('status', 'quarantined', 'kind', 'state',\n 'reason', 'um item precisa de id e da conta recorrente a que pertence');\n END IF;\n IF NOT EXISTS (SELECT 1 FROM public.plg_financial_recurring_bills\n WHERE id = v_bill AND tenant_id = p_tenant) THEN\n RETURN jsonb_build_object('status', 'quarantined', 'kind', 'fk',\n 'reason', format('a conta recorrente %s n\u00E3o existe neste tenant', v_bill));\n END IF;\n\n v_item := jsonb_strip_nulls(jsonb_build_object(\n 'id', p_id,\n 'description', p_row ->> 'description',\n 'quantity', (p_row ->> 'quantity')::numeric,\n 'unit_price', (p_row ->> 'unit_price')::numeric,\n 'discount', (p_row ->> 'discount')::numeric,\n 'addition', (p_row ->> 'addition')::numeric,\n 'total_amount', (p_row ->> 'total_amount')::numeric,\n 'chart_account_id', p_row ->> 'chart_account_id',\n 'cost_center_id', p_row ->> 'cost_center_id',\n -- No V2 um servi\u00E7o \u00C9 um produto: as duas colunas do V1 desaguam aqui.\n 'product_id', coalesce(p_row ->> 'product_id', p_row ->> 'service_product_id')\n ));\n\n -- Substitui o item de mesmo id em vez de acrescentar: reoferecer a mesma linha\n -- numa onda seguinte tem que atualizar, n\u00E3o duplicar o valor da conta.\n SELECT coalesce(jsonb_agg(x), '[]'::jsonb) INTO v_items\n FROM (SELECT value AS x FROM jsonb_array_elements(\n (SELECT items FROM public.plg_financial_recurring_bills WHERE id = v_bill))\n WHERE value ->> 'id' IS DISTINCT FROM p_id::text) s;\n\n UPDATE public.plg_financial_recurring_bills\n SET items = v_items || jsonb_build_array(v_item)\n WHERE id = v_bill;\n\n IF v_standalone THEN PERFORM migration._audit(p_tenant, 'public.plg_financial_recurring_bills', v_bill, '{}'::jsonb); END IF;\n -- target_id \u00E9 a CONTA: \u00E9 l\u00E1 que o item mora no V2.\n RETURN jsonb_build_object('status', 'migrated', 'target_id', v_bill,\n 'reason', 'item gravado em plg_financial_recurring_bills.items');\nEND $function$;\n\nCREATE OR REPLACE FUNCTION migration.upsert_fin_recurring_bill_occurrence(\n p_tenant uuid, p_id uuid, p_row jsonb, p_args jsonb DEFAULT '{}'::jsonb\n) RETURNS jsonb\nLANGUAGE plpgsql SECURITY DEFINER SET search_path TO ''\nAS $function$\nDECLARE\n v_meta jsonb := coalesce(p_row -> 'metadata', '{}'::jsonb);\n v_bill uuid := (p_row ->> 'recurring_bill_id')::uuid;\n v_status_v1 text := nullif(btrim(lower(coalesce(p_row ->> 'status', ''))), '');\n v_status text;\n v_unit uuid;\n v_created boolean := false;\n v_standalone boolean := migration._standalone();\nBEGIN\n IF v_standalone THEN PERFORM migration._assert_fence(p_tenant); PERFORM set_config('migration.writer', 'on', true); END IF;\n IF p_id IS NULL OR v_bill IS NULL OR p_row ->> 'due_date' IS NULL THEN\n RETURN jsonb_build_object('status', 'quarantined', 'kind', 'state',\n 'reason', 'uma ocorr\u00EAncia precisa de id, da conta recorrente e de vencimento');\n END IF;\n SELECT unit_id INTO v_unit FROM public.plg_financial_recurring_bills\n WHERE id = v_bill AND tenant_id = p_tenant;\n IF NOT FOUND THEN\n RETURN jsonb_build_object('status', 'quarantined', 'kind', 'fk',\n 'reason', format('a conta recorrente %s n\u00E3o existe neste tenant', v_bill));\n END IF;\n\n -- O V1 usa provisioned | consolidated; 'cancelled' existe do lado do V2 para o\n -- vencimento cancelado DEPOIS da virada. Estado desconhecido vira quarentena\n -- com a palavra \u00E0 vista, n\u00E3o um estado aproximado.\n v_status := CASE v_status_v1\n WHEN 'provisioned' THEN 'provisioned'\n WHEN 'consolidated' THEN 'consolidated'\n WHEN 'cancelled' THEN 'cancelled'\n WHEN 'canceled' THEN 'cancelled'\n ELSE NULL END;\n IF v_status IS NULL THEN\n RETURN jsonb_build_object('status', 'quarantined', 'kind', 'value',\n 'reason', format('estado de vencimento desconhecido: %L', v_status_v1));\n END IF;\n\n IF NOT EXISTS (SELECT 1 FROM public.plg_financial_recurring_bill_occurrences WHERE id = p_id) THEN\n INSERT INTO public.plg_financial_recurring_bill_occurrences\n (id, tenant_id, unit_id, recurring_bill_id, seq, competence_date, due_date,\n amount, paid_amount, status, overridden, notes, metadata)\n VALUES (p_id, p_tenant, v_unit, v_bill,\n coalesce((p_row ->> 'seq')::integer, 1),\n coalesce((p_row ->> 'competence_date')::date, (p_row ->> 'due_date')::date),\n (p_row ->> 'due_date')::date,\n coalesce((p_row ->> 'amount')::numeric, 0),\n coalesce((p_row ->> 'paid_amount')::numeric, 0),\n v_status, coalesce((p_row ->> 'overridden')::boolean, false),\n p_row ->> 'notes', v_meta);\n v_created := true;\n ELSE\n UPDATE public.plg_financial_recurring_bill_occurrences SET\n unit_id = v_unit,\n seq = CASE WHEN p_row ? 'seq' THEN coalesce((p_row ->> 'seq')::integer, seq) ELSE seq END,\n competence_date = CASE WHEN p_row ? 'competence_date' THEN coalesce((p_row ->> 'competence_date')::date, competence_date) ELSE competence_date END,\n due_date = (p_row ->> 'due_date')::date,\n amount = CASE WHEN p_row ? 'amount' THEN coalesce((p_row ->> 'amount')::numeric, amount) ELSE amount END,\n paid_amount = CASE WHEN p_row ? 'paid_amount' THEN coalesce((p_row ->> 'paid_amount')::numeric, paid_amount) ELSE paid_amount END,\n status = v_status,\n overridden = CASE WHEN p_row ? 'overridden' THEN coalesce((p_row ->> 'overridden')::boolean, overridden) ELSE overridden END,\n notes = CASE WHEN p_row ? 'notes' THEN p_row ->> 'notes' ELSE notes END,\n metadata = migration._merge_meta(metadata, v_meta)\n WHERE id = p_id;\n END IF;\n\n IF v_standalone THEN PERFORM migration._audit(p_tenant, 'public.plg_financial_recurring_bill_occurrences', p_id, '{}'::jsonb); END IF;\n RETURN jsonb_build_object('status', 'migrated', 'target_id', p_id,\n 'reason', CASE WHEN v_created THEN 'created' ELSE 'updated' END);\nEND $function$;\n\n-- \u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\n-- 3. O de-para\n-- \u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\n\n-- Saem da lista de adiadas: a nota delas dizia que o destino n\u00E3o existia, e ele\n-- existe (duas das tr\u00EAs) ou passou a existir (a terceira, acima).\nDELETE FROM migration.excluded_tables\n WHERE source_table IN ('recurring_bills', 'recurring_bill_items', 'recurring_bill_occurrences');\n\n-- `_allow` \u00E9 o construtor oficial: a allowlist tem gatilho de congelamento, e um\n-- INSERT direto por cima de uma linha `frozen` seria recusado.\n--\n-- As quatro colunas que o V2 exige e o V1 n\u00E3o tem na conta \u2014 description, amount,\n-- starts_on e o comprador \u2014 s\u00E3o reconstru\u00EDdas a partir da PR\u00D3PRIA origem pelo\n-- plano de extra\u00E7\u00E3o (`Step.augment` em fayz_etl/sources/beautysoft_v1.py), que \u00E9\n-- a ponte declarada para isso. Nada aqui inventa valor.\n-- \u2500\u2500 a allowlist est\u00E1 CONGELADA, e destravar \u00E9 o caminho previsto \u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\n--\n-- `trg_allowlist_frozen` recusa UPDATE numa linha `frozen`: mudan\u00E7a de core-A\n-- \u00E9 arquivo de migration, n\u00E3o escrita solta \u2014 e esta \u00C9 um arquivo de migration.\n-- `migration.allowlist_unlock` \u00E9 a escotilha que o pr\u00F3prio gatilho l\u00EA, e ela \u00E9\n-- `true` no terceiro argumento: vale s\u00F3 at\u00E9 o fim desta transa\u00E7\u00E3o, ent\u00E3o n\u00E3o\n-- fica uma porta aberta atr\u00E1s deste arquivo.\n--\n-- Sem isto, `_allow` falha ao reescrever `recurring_bills`, que uma onda\n-- anterior j\u00E1 havia declarado.\nSELECT set_config('migration.allowlist_unlock', 'on', true);\n\nSELECT migration._allow(\n 'recurring_bills', 'public.plg_financial_recurring_bills', 'financial',\n 'fin_recurring_bill', '{}'::jsonb, ARRAY['amount'],\n jsonb_build_object(\n 'uuid', 'id',\n 'unit_id', 'unit_id@companies:app.units',\n 'type', 'direction',\n 'first_due_date', 'starts_on',\n 'end_date', 'ends_on',\n 'interval_count', 'interval_count',\n 'interval_unit', 'interval_unit',\n 'active', 'is_active',\n 'observations', 'notes',\n -- Derivadas pelo plano, a partir dos itens e do account_central da conta.\n 'description', 'description',\n 'amount', 'amount',\n 'counterparty_contact_id','counterparty_person_id@contacts:public.people',\n 'chart_account_id', 'chart_account_id@chart_of_accounts',\n 'cost_center_id', 'cost_center_id@cost_centers',\n -- A conta financeira da contraparte continua registrada, ainda que o V2\n -- prefira a PESSOA: \u00E9 por ela que o V1 alcan\u00E7a quem recebe.\n 'account_central_id', 'jsonb:v1.account_central_id',\n -- O autor \u00E9 um login do V1, e nada liga login do V1 a usu\u00E1rio do V2 (#115).\n -- A refer\u00EAncia crua vale mais que perd\u00EA-la e mais que fingir que resolve.\n 'created_by', 'jsonb:v1.created_by',\n 'payment_method_id', 'jsonb:v1.payment_method_id'\n ),\n 'Regra de conta recorrente. description/amount/contraparte s\u00E3o derivados dos itens e do account_central pelo plano de extra\u00E7\u00E3o.'\n);\n\nSELECT migration._allow(\n 'recurring_bill_items', 'public.plg_financial_recurring_bills', 'financial',\n 'fin_recurring_bill_item', '{}'::jsonb, ARRAY[]::text[],\n jsonb_build_object(\n 'uuid', 'id',\n 'recurring_bill_id', 'recurring_bill_id@recurring_bills',\n 'description', 'description',\n 'quantity', 'quantity',\n 'unit_price', 'unit_price',\n 'discount', 'discount',\n 'additional', 'addition',\n 'total_amount', 'total_amount',\n 'account_id', 'chart_account_id@chart_of_accounts',\n 'cost_center_id', 'cost_center_id@cost_centers',\n 'product_id', 'product_id@products',\n 'service_id', 'service_product_id@services:public.products',\n -- \u00DAnico valor medido: 'other'. Traduzi-lo exigiria um vocabul\u00E1rio de destino\n -- que n\u00E3o existe; no metadata ele fica recuper\u00E1vel sem inventar nada.\n 'item_type', 'jsonb:v1.item_type'\n ),\n 'Item da conta. No V2 ele \u00E9 elemento do jsonb `items` da pr\u00F3pria conta \u2014 n\u00E3o tem tabela.'\n);\n\nSELECT migration._allow(\n 'recurring_bill_occurrences', 'public.plg_financial_recurring_bill_occurrences',\n 'financial',\n 'fin_recurring_bill_occurrence', '{}'::jsonb, ARRAY['amount', 'paid_amount'],\n jsonb_build_object(\n 'uuid', 'id',\n 'recurring_bill_id', 'recurring_bill_id@recurring_bills',\n 'seq', 'seq',\n 'competence_date', 'competence_date',\n 'due_date', 'due_date',\n 'value', 'amount',\n 'paid_value', 'paid_amount',\n 'status', 'status',\n 'overridden', 'overridden',\n 'observations', 'notes',\n -- N\u00C3O vira FK: no V1 aponta para `invoices`, cuja contraparte no V2 \u00E9 uma\n -- VENDA. Ligar uma conta a pagar a uma venda inverteria o sentido do\n -- dinheiro para ganhar um link. O estado `consolidated` j\u00E1 diz o que importa.\n 'invoice_id', 'jsonb:v1.invoice_id'\n ),\n 'O vencimento. `overridden` marca o que algu\u00E9m editou fora da regra \u2014 n\u00E3o \u00E9 deriv\u00E1vel.'\n);\n\n-- Fecha a escotilha antes de qualquer outra coisa neste arquivo tocar a\n-- allowlist sem querer.\nSELECT set_config('migration.allowlist_unlock', 'off', true);\n\n\n-- \u2500\u2500 quem pode executar \u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\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-- Fun\u00E7\u00E3o nova nasce execut\u00E1vel por PUBLIC, e `anon`/`authenticated` herdam\n-- dali. Estas s\u00E3o de PORTE: SECURITY DEFINER, escrevem dados de qualquer\n-- inquilino, e uma delas alcan\u00E7\u00E1vel pela chave an\u00F4nima \u00E9 a base inteira\n-- alcan\u00E7\u00E1vel pela chave an\u00F4nima. S\u00F3 o service_role, que \u00E9 quem roda a onda.\nREVOKE ALL ON FUNCTION migration.upsert_fin_recurring_bill(uuid, uuid, jsonb, jsonb) FROM PUBLIC, anon, authenticated;\nGRANT EXECUTE ON FUNCTION migration.upsert_fin_recurring_bill(uuid, uuid, jsonb, jsonb) TO service_role;\nREVOKE ALL ON FUNCTION migration.upsert_fin_recurring_bill_item(uuid, uuid, jsonb, jsonb) FROM PUBLIC, anon, authenticated;\nGRANT EXECUTE ON FUNCTION migration.upsert_fin_recurring_bill_item(uuid, uuid, jsonb, jsonb) TO service_role;\nREVOKE ALL ON FUNCTION migration.upsert_fin_recurring_bill_occurrence(uuid, uuid, jsonb, jsonb) FROM PUBLIC, anon, authenticated;\nGRANT EXECUTE ON FUNCTION migration.upsert_fin_recurring_bill_occurrence(uuid, uuid, jsonb, jsonb) TO service_role;\n";
|
|
63
63
|
export declare const MIGRATION_062_O_VENCIMENTO_APONTA_PARA_O_TITULO = "-- O vencimento aponta para o t\u00EDtulo.\n--\n-- Corre\u00E7\u00E3o de um mapeamento da 046. L\u00E1, `recurring_bill_occurrences.invoice_id`\n-- foi jogado em `metadata` com esta justificativa:\n--\n-- \"N\u00C3O vira FK: no V1 aponta para `invoices`, cuja contraparte no V2 \u00E9 uma\n-- VENDA. Ligar uma conta a pagar a uma venda inverteria o sentido do\n-- dinheiro para ganhar um link.\"\n--\n-- A primeira metade est\u00E1 certa e a segunda \u00E9 falsa. `invoices` do V1 tem DOIS\n-- destinos no V2, n\u00E3o um: o writer `order` grava a Venda em `public.orders` e\n-- devolve, em `extra`, o T\u00CDTULO em `public.plg_financial_invoices`. O ledger\n-- guarda as duas linhas. Medido no tenant 8: 2.365 para `public.orders` e 2.365\n-- para `public.plg_financial_invoices`.\n--\n-- E o t\u00EDtulo \u00E9 exatamente o objeto certo: das 111 faturas que as ocorr\u00EAncias\n-- consolidadas referenciam, as 111 t\u00EAm t\u00EDtulo no V2, e **todas com\n-- `direction = 'payable'`**. N\u00E3o h\u00E1 invers\u00E3o nenhuma a evitar.\n--\n-- \u2500\u2500 O que o erro custava \u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\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-- `recurring_bill_occurrences.paid_value` \u00E9 0 nas 833 linhas da origem \u2014 no V1 o\n-- pagamento n\u00E3o mora no vencimento, mora no movimento financeiro ligado ao\n-- t\u00EDtulo. Com `invoice_id` em metadata, a ocorr\u00EAncia consolidada chegava ao V2\n-- sem nenhum caminho at\u00E9 o dinheiro: 123 vencimentos marcados `consolidated` e\n-- indistingu\u00EDveis, na tela, de 710 em aberto. A tela dizia a verdade sobre o\n-- campo e mentia sobre o fato.\n--\n-- Com a FK, o caminho existe:\n--\n-- ocorr\u00EAncia \u2192 plg_financial_invoices \u2192 plg_financial_movements (paid_amount)\n--\n-- S\u00E3o R$ 311.006,00 pagos em 181 movimentos, que passam a ser alcan\u00E7\u00E1veis a\n-- partir do vencimento.\n--\n-- Idempotente.\n\n-- A linha j\u00E1 est\u00E1 `frozen`, e o gatilho `trg_allowlist_frozen` recusa a escrita\n-- com a frase certa: \"core-A allowlist changes are a migration file, not a\n-- write\". Este \u00C9 o arquivo de migration, e a escotilha existe exatamente para\n-- ele \u2014 `SET LOCAL`, para a destravada morrer com a transa\u00E7\u00E3o e n\u00E3o vazar para a\n-- sess\u00E3o seguinte.\nSET LOCAL migration.allowlist_unlock = 'on';\n\n-- `_allow` reescreve a linha inteira (ON CONFLICT DO UPDATE), ent\u00E3o o mapa vai\n-- completo \u2014 inclusive as colunas que n\u00E3o mudaram.\nSELECT migration._allow(\n 'recurring_bill_occurrences', 'public.plg_financial_recurring_bill_occurrences',\n 'financial',\n 'fin_recurring_bill_occurrence', '{}'::jsonb, ARRAY['amount', 'paid_amount'],\n jsonb_build_object(\n 'uuid', 'id',\n 'recurring_bill_id', 'recurring_bill_id@recurring_bills',\n 'seq', 'seq',\n 'competence_date', 'competence_date',\n 'due_date', 'due_date',\n 'value', 'amount',\n 'paid_value', 'paid_amount',\n 'status', 'status',\n 'overridden', 'overridden',\n 'observations', 'notes',\n -- O T\u00CDTULO, n\u00E3o a venda. `invoices` tem dois destinos no V2 e este \u00E9 o\n -- financeiro; o `:public.plg_financial_invoices` escolhe qual dos dois o\n -- ledger deve resolver.\n 'invoice_id', 'invoice_id@invoices:public.plg_financial_invoices'\n ),\n 'O vencimento. `overridden` marca o que algu\u00E9m editou fora da regra \u2014 n\u00E3o \u00E9 '\n 'deriv\u00E1vel. `invoice_id` liga o vencimento consolidado ao t\u00EDtulo, que \u00E9 onde o '\n 'pagamento mora: no V1 paid_value \u00E9 sempre 0.'\n);\n\n-- O writer precisa gravar a coluna que o de-para agora entrega. Sem isto o mapa\n-- resolveria a refer\u00EAncia e o writer a jogaria fora em sil\u00EAncio \u2014 que \u00E9 a forma\n-- mais cara de errar aqui, porque tudo parece ter funcionado.\nCREATE OR REPLACE FUNCTION migration.upsert_fin_recurring_bill_occurrence(\n p_tenant uuid, p_id uuid, p_row jsonb, p_args jsonb DEFAULT '{}'::jsonb\n) RETURNS jsonb\nLANGUAGE plpgsql SECURITY DEFINER SET search_path TO ''\nAS $function$\nDECLARE\n v_meta jsonb := coalesce(p_row -> 'metadata', '{}'::jsonb);\n v_bill uuid := (p_row ->> 'recurring_bill_id')::uuid;\n v_invoice uuid := (p_row ->> 'invoice_id')::uuid;\n v_status_v1 text := nullif(btrim(lower(coalesce(p_row ->> 'status', ''))), '');\n v_status text;\n v_unit uuid;\n v_created boolean := false;\n v_standalone boolean := migration._standalone();\nBEGIN\n IF v_standalone THEN PERFORM migration._assert_fence(p_tenant); PERFORM set_config('migration.writer', 'on', true); END IF;\n IF p_id IS NULL OR v_bill IS NULL OR p_row ->> 'due_date' IS NULL THEN\n RETURN jsonb_build_object('status', 'quarantined', 'kind', 'state',\n 'reason', 'uma ocorr\u00EAncia precisa de id, da conta recorrente e de vencimento');\n END IF;\n SELECT unit_id INTO v_unit FROM public.plg_financial_recurring_bills\n WHERE id = v_bill AND tenant_id = p_tenant;\n IF NOT FOUND THEN\n RETURN jsonb_build_object('status', 'quarantined', 'kind', 'fk',\n 'reason', format('a conta recorrente %s n\u00E3o existe neste tenant', v_bill));\n END IF;\n\n -- O V1 usa provisioned | consolidated; 'cancelled' existe do lado do V2 para o\n -- vencimento cancelado DEPOIS da virada. Estado desconhecido vira quarentena\n -- com a palavra \u00E0 vista, n\u00E3o um estado aproximado.\n v_status := CASE v_status_v1\n WHEN 'provisioned' THEN 'provisioned'\n WHEN 'consolidated' THEN 'consolidated'\n WHEN 'cancelled' THEN 'cancelled'\n WHEN 'canceled' THEN 'cancelled'\n ELSE NULL END;\n IF v_status IS NULL THEN\n RETURN jsonb_build_object('status', 'quarantined', 'kind', 'value',\n 'reason', format('estado de vencimento desconhecido: %L', v_status_v1));\n END IF;\n\n -- Um t\u00EDtulo que n\u00E3o existe (ou \u00E9 de outro tenant) n\u00E3o derruba o vencimento: o\n -- vencimento \u00E9 o dado, o v\u00EDnculo \u00E9 o enriquecimento. Perder o v\u00EDnculo \u00E9 ruim;\n -- perder o vencimento por causa dele seria pior.\n IF v_invoice IS NOT NULL AND NOT EXISTS (\n SELECT 1 FROM public.plg_financial_invoices\n WHERE id = v_invoice AND tenant_id = p_tenant) THEN\n v_meta := migration._deep_set(v_meta, ARRAY['v1', 'invoice_unresolved'], to_jsonb(v_invoice));\n v_invoice := NULL;\n END IF;\n\n IF NOT EXISTS (SELECT 1 FROM public.plg_financial_recurring_bill_occurrences WHERE id = p_id) THEN\n INSERT INTO public.plg_financial_recurring_bill_occurrences\n (id, tenant_id, unit_id, recurring_bill_id, seq, competence_date, due_date,\n amount, paid_amount, status, overridden, invoice_id, notes, metadata)\n VALUES (p_id, p_tenant, v_unit, v_bill,\n coalesce((p_row ->> 'seq')::integer, 1),\n coalesce((p_row ->> 'competence_date')::date, (p_row ->> 'due_date')::date),\n (p_row ->> 'due_date')::date,\n coalesce((p_row ->> 'amount')::numeric, 0),\n coalesce((p_row ->> 'paid_amount')::numeric, 0),\n v_status, coalesce((p_row ->> 'overridden')::boolean, false),\n v_invoice, p_row ->> 'notes', v_meta);\n v_created := true;\n ELSE\n UPDATE public.plg_financial_recurring_bill_occurrences SET\n unit_id = v_unit,\n seq = CASE WHEN p_row ? 'seq' THEN coalesce((p_row ->> 'seq')::integer, seq) ELSE seq END,\n competence_date = CASE WHEN p_row ? 'competence_date' THEN coalesce((p_row ->> 'competence_date')::date, competence_date) ELSE competence_date END,\n due_date = (p_row ->> 'due_date')::date,\n amount = CASE WHEN p_row ? 'amount' THEN coalesce((p_row ->> 'amount')::numeric, amount) ELSE amount END,\n paid_amount = CASE WHEN p_row ? 'paid_amount' THEN coalesce((p_row ->> 'paid_amount')::numeric, paid_amount) ELSE paid_amount END,\n status = v_status,\n overridden = CASE WHEN p_row ? 'overridden' THEN coalesce((p_row ->> 'overridden')::boolean, overridden) ELSE overridden END,\n invoice_id = coalesce(v_invoice, invoice_id),\n notes = CASE WHEN p_row ? 'notes' THEN p_row ->> 'notes' ELSE notes END,\n metadata = migration._merge_meta(metadata, v_meta)\n WHERE id = p_id;\n END IF;\n\n IF v_standalone THEN PERFORM migration._audit(p_tenant, 'public.plg_financial_recurring_bill_occurrences', p_id, '{}'::jsonb); END IF;\n RETURN jsonb_build_object('status', 'migrated', 'target_id', p_id,\n 'reason', CASE WHEN v_created THEN 'created' ELSE 'updated' END);\nEND $function$;\n\n-- \u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\n-- A leitura de contas a pagar, num lugar s\u00F3\n-- \u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\n--\n-- A pergunta que a tela faz \u00E9 \"o que vence, e o que j\u00E1 foi pago?\", e respond\u00EA-la\n-- exige tr\u00EAs jun\u00E7\u00F5es e o conhecimento de que o pagamento mora no movimento, n\u00E3o\n-- no vencimento. Uma view p\u00F5e esse conhecimento no banco em vez de em cada tela\n-- que vier a perguntar.\n-- `security_invoker` porque uma view sem ele roda com os direitos do DONO e\n-- atravessa a RLS das tabelas por baixo: quem abrisse esta leitura pelo\n-- PostgREST leria as contas a pagar de TODOS os inquilinos do cluster. \u00C9 a\n-- postura N16 da bancada, e ela existe porque essa falha j\u00E1 aconteceu aqui.\nCREATE OR REPLACE VIEW public.v_financial_payables WITH (security_invoker = true) AS\nSELECT\n o.tenant_id,\n o.unit_id,\n o.id AS occurrence_id,\n b.id AS recurring_bill_id,\n b.description,\n b.counterparty_person_id,\n p.name AS counterparty_name,\n b.frequency,\n o.seq,\n o.competence_date,\n o.due_date,\n o.amount,\n -- O pagamento vem do movimento do t\u00EDtulo; `o.paid_amount` \u00E9 0 em tudo que veio\n -- do V1, e somar os dois contaria duas vezes o dia em que o V2 come\u00E7ar a\n -- escrever no vencimento.\n coalesce(o.paid_amount, 0) + coalesce(m.pago, 0) AS paid_amount,\n o.amount - (coalesce(o.paid_amount, 0) + coalesce(m.pago, 0)) AS open_amount,\n o.status,\n o.overridden,\n o.invoice_id,\n CASE\n WHEN o.status = 'cancelled' THEN 'cancelado'\n WHEN o.amount <= coalesce(o.paid_amount, 0) + coalesce(m.pago, 0) THEN 'pago'\n WHEN coalesce(m.pago, 0) > 0 THEN 'parcial'\n WHEN o.due_date < current_date THEN 'vencido'\n ELSE 'em aberto'\n END AS situacao\nFROM public.plg_financial_recurring_bill_occurrences o\nJOIN public.plg_financial_recurring_bills b ON b.id = o.recurring_bill_id\nLEFT JOIN public.people p ON p.id = b.counterparty_person_id\nLEFT JOIN LATERAL (\n SELECT sum(mv.paid_amount) AS pago\n FROM public.plg_financial_movements mv\n WHERE mv.financial_invoice_id = o.invoice_id\n) m ON o.invoice_id IS NOT NULL;\n\nCOMMENT ON VIEW public.v_financial_payables IS\n 'Contas a pagar por vencimento, com o pago somado do movimento do t\u00EDtulo. '\n 'No V1 recurring_bill_occurrences.paid_value \u00E9 sempre 0: o pagamento mora no '\n 'movimento, e sem esta jun\u00E7\u00E3o todo vencimento consolidado parece em aberto.';\n\nGRANT SELECT ON public.v_financial_payables TO authenticated, service_role;\n\n\n-- \u2500\u2500 quem pode executar \u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\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-- Fun\u00E7\u00E3o nova nasce execut\u00E1vel por PUBLIC, e `anon`/`authenticated` herdam\n-- dali. Estas s\u00E3o de PORTE: SECURITY DEFINER, escrevem dados de qualquer\n-- inquilino, e uma delas alcan\u00E7\u00E1vel pela chave an\u00F4nima \u00E9 a base inteira\n-- alcan\u00E7\u00E1vel pela chave an\u00F4nima. S\u00F3 o service_role, que \u00E9 quem roda a onda.\nREVOKE ALL ON FUNCTION migration.upsert_fin_recurring_bill_occurrence(uuid, uuid, jsonb, jsonb) FROM PUBLIC, anon, authenticated;\nGRANT EXECUTE ON FUNCTION migration.upsert_fin_recurring_bill_occurrence(uuid, uuid, jsonb, jsonb) TO service_role;\n";
|
|
64
|
-
export declare const MIGRATION_063_A_UNIDADE_TEM_CONTA_E_A_PESSOA_TEM_UMA_SO = "-- A unidade tem conta, e a pessoa tem uma s\u00F3.\n--\n-- Duas decis\u00F5es sobre o raz\u00E3o financeiro, tomadas depois de medir o que cada uma\n-- custava. As duas estavam registradas como \"adiar\" com uma estimativa de custo,\n-- e a estimativa veio do cliente errado.\n--\n-- \u2500\u2500 1. `account_type = 'unit'` \u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\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 plano de extra\u00E7\u00E3o do V1 traduz o vocabul\u00E1rio de conta do V1 para as oito\n-- esp\u00E9cies do V2, e deixava `unit` passar sem tradu\u00E7\u00E3o de prop\u00F3sito, com este\n-- coment\u00E1rio:\n--\n-- \"conta da unidade n\u00E3o \u00E9 pessoa, nem banco, nem caixa. Encaixar em 'system'\n-- seria escolher a sem\u00E2ntica no lugar de quem \u00E9 dono do contrato\n-- financeiro. Custa 6 movimentos na onda 1.\"\n--\n-- A recusa em escolher estava certa. A estimativa n\u00E3o: os 6 movimentos s\u00E3o do\n-- Espa\u00E7o Facial, uma rede de est\u00E9tica onde o dinheiro entra pela venda. Num\n-- restaurante o caixa passa pela conta da pr\u00F3pria unidade, e no Pertinho do C\u00E9u\n-- (licen\u00E7a 8) DUAS contas travam **1.359 movimentos** \u2014 30% do raz\u00E3o:\n--\n-- account_central #195 \"Pertinho do C\u00E9u\" \u2192 777 movimentos\n-- account_central #654100 \"PC Servi\u00E7os\" \u2192 582 movimentos\n--\n-- E n\u00E3o \u00E9 s\u00F3 a contagem: sem elas, `v_financial_payables` mostra 833 vencimentos\n-- em aberto num cliente que pagou R$ 311.006. O n\u00FAmero certo some junto com a\n-- conta.\n--\n-- A escolha foi **criar a esp\u00E9cie**, n\u00E3o encaixar em `system`. `system` j\u00E1 tem\n-- dono sem\u00E2ntico (`CHECK (kind <> 'system' OR code IS NOT NULL)` \u2014 conta de\n-- plano de contas, identificada por c\u00F3digo) e jogar a conta da casa l\u00E1 dentro\n-- faria os dois conceitos compartilharem um balde que nenhum relat\u00F3rio consegue\n-- separar depois.\n--\n-- \u2500\u2500 2. Uma conta por pessoa \u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\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_accounts_person_uq` diz que uma pessoa tem UMA conta no tenant.\n-- O V1 permite v\u00E1rias (48 casos no Pertinho do C\u00E9u), e cada colis\u00E3o derrubava a\n-- conta E todos os movimentos que a referenciavam.\n--\n-- A constraint FICA. A decis\u00E3o foi **fundir**: da segunda conta em diante, o\n-- writer devolve o id da conta que j\u00E1 existe, o ledger amarra a linha do V1 a\n-- ela, e todo movimento daquela conta passa a apontar para a conta \u00FAnica da\n-- pessoa. O saldo da pessoa fica certo; o que se perde \u00E9 a separa\u00E7\u00E3o que o V1\n-- fazia entre as contas dela, e isso fica escrito em\n-- `metadata.migration.merged_from`.\n--\n-- Fundir \u00E9 o mesmo mecanismo que `upsert_person` j\u00E1 usa para pessoa duplicada, e\n-- \u00E9 revers\u00EDvel pela leitura do ledger: `source_numeric_id` distintos apontando\n-- para o mesmo `target_id` \u00E9 a marca da fus\u00E3o.\n--\n-- Idempotente.\n\n-- \u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\n-- 1. A esp\u00E9cie nova\n-- \u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\n\nDO $$\nBEGIN\n ALTER TABLE public.plg_financial_accounts DROP CONSTRAINT IF EXISTS plg_financial_accounts_kind_check;\n ALTER TABLE public.plg_financial_accounts ADD CONSTRAINT plg_financial_accounts_kind_check\n CHECK (kind = ANY (ARRAY['bank', 'cash_physical', 'cash_session',\n 'card_receivable_credit', 'card_receivable_debit',\n 'card_payable', 'person', 'system', 'unit']));\n\n -- Uma conta de unidade sem unidade seria a conta de ningu\u00E9m. O par esp\u00E9cie \u2194\n -- coluna obrigat\u00F3ria \u00E9 a mesma disciplina que `person`/`person_id` j\u00E1 tem.\n ALTER TABLE public.plg_financial_accounts DROP CONSTRAINT IF EXISTS plg_financial_accounts_unit_check;\n ALTER TABLE public.plg_financial_accounts ADD CONSTRAINT plg_financial_accounts_unit_check\n CHECK (kind <> 'unit' OR unit_id IS NOT NULL);\nEND $$;\n\nCOMMENT ON CONSTRAINT plg_financial_accounts_unit_check ON public.plg_financial_accounts IS\n 'A conta da pr\u00F3pria unidade (a conta da casa) nomeia a unidade a que pertence. '\n 'Sem isso ela seria indistingu\u00EDvel de uma conta de sistema sem dono.';\n\n-- \u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\n-- 2. O writer\n-- \u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\n\nCREATE OR REPLACE FUNCTION migration.upsert_fin_account(\n p_tenant uuid, p_id uuid, p_row jsonb, p_args jsonb DEFAULT '{}'::jsonb\n) RETURNS jsonb\nLANGUAGE plpgsql SECURITY DEFINER SET search_path TO ''\nAS $function$\nDECLARE\n v_meta jsonb := coalesce(p_row -> 'metadata', '{}'::jsonb);\n v_kind text := coalesce(p_row ->> 'kind', 'bank');\n v_person uuid;\n v_unit uuid := (p_row ->> 'unit_id')::uuid;\n v_merge_into uuid;\n v_existing_tenant uuid;\n v_created boolean := false;\n v_standalone boolean := migration._standalone();\nBEGIN\n IF v_standalone THEN PERFORM migration._assert_fence(p_tenant); PERFORM set_config('migration.writer', 'on', true); END IF;\n IF p_id IS NULL OR nullif(btrim(coalesce(p_row ->> 'name', '')), '') IS NULL THEN\n RETURN jsonb_build_object('status', 'quarantined', 'kind', 'state', 'reason', 'this row needs an id and a name');\n END IF;\n IF v_kind NOT IN ('bank', 'cash_physical', 'cash_session', 'card_receivable_credit',\n 'card_receivable_debit', 'card_payable', 'person', 'system', 'unit') THEN\n RETURN jsonb_build_object('status', 'quarantined', 'kind', 'value',\n 'reason', format('unknown account kind %L \u2014 V1 account_type does not map onto the ledger''s nine', v_kind));\n END IF;\n\n -- A pessoa da conta polim\u00F3rfica. O ledger \u00E9 a \u00FAnica fonte que sabe o de-para,\n -- e ele guarda a tabela de origem \u2014 ent\u00E3o `clients`, `contacts` e\n -- `professionals` resolvem pelo mesmo caminho, sem o writer conhecer nenhuma.\n IF v_kind = 'person' THEN\n SELECT l.target_id INTO v_person\n FROM migration.ledger l\n WHERE l.tenant_id = p_tenant\n AND l.source_table = (v_meta #>> '{v1,reference_table}')\n AND l.source_numeric_id = nullif(v_meta #>> '{v1,reference_id}', '')::bigint\n AND l.target_table = 'public.people'\n AND l.status = 'migrated'\n LIMIT 1;\n -- Recusar \u00E9 o certo, e \u00E9 tempor\u00E1rio: a pessoa entra na mesma onda, e a\n -- segunda passada oferece a conta de novo. Inventar uma pessoa aqui seria\n -- pendurar dinheiro em quem n\u00E3o existe.\n IF v_person IS NULL THEN\n RETURN jsonb_build_object('status', 'quarantined', 'kind', 'fk',\n 'reason', format('conta de pessoa cuja pessoa ainda n\u00E3o migrou (%s #%s)',\n coalesce(v_meta #>> '{v1,reference_table}', '<sem tabela>'),\n coalesce(v_meta #>> '{v1,reference_id}', '<sem id>')));\n END IF;\n\n -- FUS\u00C3O. O V2 admite uma conta por pessoa e o V1 admite v\u00E1rias; da segunda\n -- em diante a conta do V1 passa a APONTAR para a que j\u00E1 existe, em vez de\n -- derrubar a si mesma e a todo movimento que a referencia.\n SELECT id INTO v_merge_into\n FROM public.plg_financial_accounts\n WHERE tenant_id = p_tenant AND person_id = v_person AND id <> p_id\n LIMIT 1;\n END IF;\n\n -- A unidade da conta da casa, pelo mesmo caminho da pessoa: o ledger sabe qual\n -- `app.units` a `companies` do V1 virou.\n IF v_kind = 'unit' AND v_unit IS NULL THEN\n SELECT l.target_id INTO v_unit\n FROM migration.ledger l\n WHERE l.tenant_id = p_tenant\n AND l.source_table = coalesce(v_meta #>> '{v1,reference_table}', 'companies')\n AND l.source_numeric_id = nullif(v_meta #>> '{v1,reference_id}', '')::bigint\n AND l.target_table = 'app.units'\n AND l.status = 'migrated'\n LIMIT 1;\n IF v_unit IS NULL THEN\n RETURN jsonb_build_object('status', 'quarantined', 'kind', 'fk',\n 'reason', format('conta de unidade cuja unidade ainda n\u00E3o migrou (%s #%s)',\n coalesce(v_meta #>> '{v1,reference_table}', 'companies'),\n coalesce(v_meta #>> '{v1,reference_id}', '<sem id>')));\n END IF;\n END IF;\n\n IF v_merge_into IS NOT NULL THEN\n -- Enriquece a conta que fica e devolve O ID DELA. O motor grava esse\n -- target_id no ledger, e a partir da\u00ED todo movimento que referenciava a\n -- conta do V1 resolve para a conta \u00FAnica da pessoa no V2.\n UPDATE public.plg_financial_accounts SET\n is_active = is_active OR coalesce((p_row ->> 'is_active')::boolean, true),\n metadata = migration._merge_meta(\n metadata,\n migration._deep_set(v_meta, ARRAY['migration', 'merged_from'],\n coalesce(metadata #> '{migration,merged_from}', '[]'::jsonb)\n || jsonb_build_array(jsonb_build_object(\n 'v1_account', v_meta #> '{v1,reference_id}',\n 'name', p_row ->> 'name', 'account_id', p_id))))\n WHERE id = v_merge_into;\n IF v_standalone THEN PERFORM migration._audit(p_tenant, 'public.plg_financial_accounts', v_merge_into, jsonb_build_object('reason', 'merged')); END IF;\n RETURN jsonb_build_object('status', 'migrated', 'target_id', v_merge_into, 'merged', true,\n 'reason', 'conta fundida na conta \u00FAnica da pessoa (o V2 admite uma por pessoa)');\n END IF;\n\n SELECT tenant_id INTO v_existing_tenant FROM public.plg_financial_accounts WHERE id = p_id;\n IF v_existing_tenant IS NOT NULL AND v_existing_tenant <> p_tenant THEN\n RETURN jsonb_build_object('status', 'quarantined', 'kind', 'tenant', 'reason', format('%s already exists in another tenant', p_id));\n END IF;\n IF v_existing_tenant IS NULL THEN\n INSERT INTO public.plg_financial_accounts (id, tenant_id, unit_id, kind, code, name, bank_name, settlement_days, is_active, person_id, metadata)\n VALUES (p_id, p_tenant, v_unit, v_kind,\n p_row ->> 'code',\n p_row ->> 'name',\n p_row ->> 'bank_name',\n (p_row ->> 'settlement_days')::integer,\n coalesce((p_row ->> 'is_active')::boolean, true),\n v_person,\n v_meta);\n v_created := true;\n ELSE\n -- kind e person_id andam JUNTOS: a constraint amarra os dois, ent\u00E3o mudar\n -- um sem o outro derruba a linha que j\u00E1 estava l\u00E1.\n UPDATE public.plg_financial_accounts SET\n unit_id = coalesce(v_unit, unit_id),\n kind = CASE WHEN p_row ? 'kind' THEN v_kind ELSE kind END,\n person_id = CASE WHEN p_row ? 'kind' THEN v_person ELSE person_id END,\n code = CASE WHEN p_row ? 'code' THEN p_row ->> 'code' ELSE code END,\n name = CASE WHEN p_row ? 'name' THEN p_row ->> 'name' ELSE name END,\n bank_name = CASE WHEN p_row ? 'bank_name' THEN p_row ->> 'bank_name' ELSE bank_name END,\n settlement_days = CASE WHEN p_row ? 'settlement_days' THEN (p_row ->> 'settlement_days')::integer ELSE settlement_days END,\n is_active = CASE WHEN p_row ? 'is_active' THEN coalesce((p_row ->> 'is_active')::boolean, true) ELSE is_active END,\n metadata = migration._merge_meta(metadata, v_meta)\n WHERE id = p_id;\n END IF;\n IF v_standalone THEN PERFORM migration._audit(p_tenant, 'public.plg_financial_accounts', p_id, jsonb_build_object('reason', CASE WHEN v_created THEN 'created' ELSE 'updated' END)); END IF;\n RETURN jsonb_build_object('status', 'migrated', 'target_id', p_id, 'reason', CASE WHEN v_created THEN 'created' ELSE 'updated' END);\nEND $function$;\n\n\n-- \u2500\u2500 quem pode executar \u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\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-- Fun\u00E7\u00E3o nova nasce execut\u00E1vel por PUBLIC, e `anon`/`authenticated` herdam\n-- dali. Estas s\u00E3o de PORTE: SECURITY DEFINER, escrevem dados de qualquer\n-- inquilino, e uma delas alcan\u00E7\u00E1vel pela chave an\u00F4nima \u00E9 a base inteira\n-- alcan\u00E7\u00E1vel pela chave an\u00F4nima. S\u00F3 o service_role, que \u00E9 quem roda a onda.\nREVOKE ALL ON FUNCTION migration.upsert_fin_account(uuid, uuid, jsonb, jsonb) FROM PUBLIC, anon, authenticated;\nGRANT EXECUTE ON FUNCTION migration.upsert_fin_account(uuid, uuid, jsonb, jsonb) TO service_role;\n";
|
|
64
|
+
export declare const MIGRATION_063_A_UNIDADE_TEM_CONTA_E_A_PESSOA_TEM_UMA_SO = "-- A unidade tem conta, e a pessoa tem uma s\u00F3.\n--\n-- Duas decis\u00F5es sobre o raz\u00E3o financeiro, tomadas depois de medir o que cada uma\n-- custava. As duas estavam registradas como \"adiar\" com uma estimativa de custo,\n-- e a estimativa veio do cliente errado.\n--\n-- \u2500\u2500 1. `account_type = 'unit'` \u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\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 plano de extra\u00E7\u00E3o do V1 traduz o vocabul\u00E1rio de conta do V1 para as oito\n-- esp\u00E9cies do V2, e deixava `unit` passar sem tradu\u00E7\u00E3o de prop\u00F3sito, com este\n-- coment\u00E1rio:\n--\n-- \"conta da unidade n\u00E3o \u00E9 pessoa, nem banco, nem caixa. Encaixar em 'system'\n-- seria escolher a sem\u00E2ntica no lugar de quem \u00E9 dono do contrato\n-- financeiro. Custa 6 movimentos na onda 1.\"\n--\n-- A recusa em escolher estava certa. A estimativa n\u00E3o: os 6 movimentos s\u00E3o do\n-- Espa\u00E7o Facial, uma rede de est\u00E9tica onde o dinheiro entra pela venda. Num\n-- restaurante o caixa passa pela conta da pr\u00F3pria unidade, e no Pertinho do C\u00E9u\n-- (licen\u00E7a 8) DUAS contas travam **1.359 movimentos** \u2014 30% do raz\u00E3o:\n--\n-- account_central #195 \"Pertinho do C\u00E9u\" \u2192 777 movimentos\n-- account_central #654100 \"PC Servi\u00E7os\" \u2192 582 movimentos\n--\n-- E n\u00E3o \u00E9 s\u00F3 a contagem: sem elas, `v_financial_payables` mostra 833 vencimentos\n-- em aberto num cliente que pagou R$ 311.006. O n\u00FAmero certo some junto com a\n-- conta.\n--\n-- A escolha foi **criar a esp\u00E9cie**, n\u00E3o encaixar em `system`. `system` j\u00E1 tem\n-- dono sem\u00E2ntico (`CHECK (kind <> 'system' OR code IS NOT NULL)` \u2014 conta de\n-- plano de contas, identificada por c\u00F3digo) e jogar a conta da casa l\u00E1 dentro\n-- faria os dois conceitos compartilharem um balde que nenhum relat\u00F3rio consegue\n-- separar depois.\n--\n-- \u2500\u2500 2. Uma conta por pessoa \u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\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_accounts_person_uq` diz que uma pessoa tem UMA conta no tenant.\n-- O V1 permite v\u00E1rias (48 casos no Pertinho do C\u00E9u), e cada colis\u00E3o derrubava a\n-- conta E todos os movimentos que a referenciavam.\n--\n-- A constraint FICA. A decis\u00E3o foi **fundir**: da segunda conta em diante, o\n-- writer devolve o id da conta que j\u00E1 existe, o ledger amarra a linha do V1 a\n-- ela, e todo movimento daquela conta passa a apontar para a conta \u00FAnica da\n-- pessoa. O saldo da pessoa fica certo; o que se perde \u00E9 a separa\u00E7\u00E3o que o V1\n-- fazia entre as contas dela, e isso fica escrito em\n-- `metadata.migration.merged_from`.\n--\n-- Fundir \u00E9 o mesmo mecanismo que `upsert_person` j\u00E1 usa para pessoa duplicada, e\n-- \u00E9 revers\u00EDvel pela leitura do ledger: `source_numeric_id` distintos apontando\n-- para o mesmo `target_id` \u00E9 a marca da fus\u00E3o.\n--\n-- Idempotente.\n\n-- \u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\n-- 1. A esp\u00E9cie nova\n-- \u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\n\nDO $$\nBEGIN\n ALTER TABLE public.plg_financial_accounts DROP CONSTRAINT IF EXISTS plg_financial_accounts_kind_check;\n ALTER TABLE public.plg_financial_accounts ADD CONSTRAINT plg_financial_accounts_kind_check\n -- `unidentified` JUNTO: a 037 j\u00E1 o cria (\"Cliente n\u00E3o identificado\" e\n -- \"M\u00FAltiplos participantes\", a venda de balc\u00E3o sem quem a levou), e a 066\n -- o rep\u00F5e tr\u00EAs arquivos adiante. Sem ele aqui, esta lista REGRIDE o\n -- vocabul\u00E1rio por tr\u00EAs arquivos \u2014 e num banco vazio isso n\u00E3o aparece,\n -- porque n\u00E3o h\u00E1 linha para violar a restri\u00E7\u00E3o. Num cluster vivo aparece\n -- na hora: `23514 ... is violated by some row`, medido com 2 contas.\n CHECK (kind = ANY (ARRAY['bank', 'cash_physical', 'cash_session',\n 'card_receivable_credit', 'card_receivable_debit',\n 'card_payable', 'person', 'system', 'unit',\n 'unidentified']));\n\n -- Uma conta de unidade sem unidade seria a conta de ningu\u00E9m. O par esp\u00E9cie \u2194\n -- coluna obrigat\u00F3ria \u00E9 a mesma disciplina que `person`/`person_id` j\u00E1 tem.\n ALTER TABLE public.plg_financial_accounts DROP CONSTRAINT IF EXISTS plg_financial_accounts_unit_check;\n ALTER TABLE public.plg_financial_accounts ADD CONSTRAINT plg_financial_accounts_unit_check\n CHECK (kind <> 'unit' OR unit_id IS NOT NULL);\nEND $$;\n\nCOMMENT ON CONSTRAINT plg_financial_accounts_unit_check ON public.plg_financial_accounts IS\n 'A conta da pr\u00F3pria unidade (a conta da casa) nomeia a unidade a que pertence. '\n 'Sem isso ela seria indistingu\u00EDvel de uma conta de sistema sem dono.';\n\n-- \u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\n-- 2. O writer\n-- \u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\n\nCREATE OR REPLACE FUNCTION migration.upsert_fin_account(\n p_tenant uuid, p_id uuid, p_row jsonb, p_args jsonb DEFAULT '{}'::jsonb\n) RETURNS jsonb\nLANGUAGE plpgsql SECURITY DEFINER SET search_path TO ''\nAS $function$\nDECLARE\n v_meta jsonb := coalesce(p_row -> 'metadata', '{}'::jsonb);\n v_kind text := coalesce(p_row ->> 'kind', 'bank');\n v_person uuid;\n v_unit uuid := (p_row ->> 'unit_id')::uuid;\n v_merge_into uuid;\n v_existing_tenant uuid;\n v_created boolean := false;\n v_standalone boolean := migration._standalone();\nBEGIN\n IF v_standalone THEN PERFORM migration._assert_fence(p_tenant); PERFORM set_config('migration.writer', 'on', true); END IF;\n IF p_id IS NULL OR nullif(btrim(coalesce(p_row ->> 'name', '')), '') IS NULL THEN\n RETURN jsonb_build_object('status', 'quarantined', 'kind', 'state', 'reason', 'this row needs an id and a name');\n END IF;\n IF v_kind NOT IN ('bank', 'cash_physical', 'cash_session', 'card_receivable_credit',\n 'card_receivable_debit', 'card_payable', 'person', 'system', 'unit') THEN\n RETURN jsonb_build_object('status', 'quarantined', 'kind', 'value',\n 'reason', format('unknown account kind %L \u2014 V1 account_type does not map onto the ledger''s nine', v_kind));\n END IF;\n\n -- A pessoa da conta polim\u00F3rfica. O ledger \u00E9 a \u00FAnica fonte que sabe o de-para,\n -- e ele guarda a tabela de origem \u2014 ent\u00E3o `clients`, `contacts` e\n -- `professionals` resolvem pelo mesmo caminho, sem o writer conhecer nenhuma.\n IF v_kind = 'person' THEN\n SELECT l.target_id INTO v_person\n FROM migration.ledger l\n WHERE l.tenant_id = p_tenant\n AND l.source_table = (v_meta #>> '{v1,reference_table}')\n AND l.source_numeric_id = nullif(v_meta #>> '{v1,reference_id}', '')::bigint\n AND l.target_table = 'public.people'\n AND l.status = 'migrated'\n LIMIT 1;\n -- Recusar \u00E9 o certo, e \u00E9 tempor\u00E1rio: a pessoa entra na mesma onda, e a\n -- segunda passada oferece a conta de novo. Inventar uma pessoa aqui seria\n -- pendurar dinheiro em quem n\u00E3o existe.\n IF v_person IS NULL THEN\n RETURN jsonb_build_object('status', 'quarantined', 'kind', 'fk',\n 'reason', format('conta de pessoa cuja pessoa ainda n\u00E3o migrou (%s #%s)',\n coalesce(v_meta #>> '{v1,reference_table}', '<sem tabela>'),\n coalesce(v_meta #>> '{v1,reference_id}', '<sem id>')));\n END IF;\n\n -- FUS\u00C3O. O V2 admite uma conta por pessoa e o V1 admite v\u00E1rias; da segunda\n -- em diante a conta do V1 passa a APONTAR para a que j\u00E1 existe, em vez de\n -- derrubar a si mesma e a todo movimento que a referencia.\n SELECT id INTO v_merge_into\n FROM public.plg_financial_accounts\n WHERE tenant_id = p_tenant AND person_id = v_person AND id <> p_id\n LIMIT 1;\n END IF;\n\n -- A unidade da conta da casa, pelo mesmo caminho da pessoa: o ledger sabe qual\n -- `app.units` a `companies` do V1 virou.\n IF v_kind = 'unit' AND v_unit IS NULL THEN\n SELECT l.target_id INTO v_unit\n FROM migration.ledger l\n WHERE l.tenant_id = p_tenant\n AND l.source_table = coalesce(v_meta #>> '{v1,reference_table}', 'companies')\n AND l.source_numeric_id = nullif(v_meta #>> '{v1,reference_id}', '')::bigint\n AND l.target_table = 'app.units'\n AND l.status = 'migrated'\n LIMIT 1;\n IF v_unit IS NULL THEN\n RETURN jsonb_build_object('status', 'quarantined', 'kind', 'fk',\n 'reason', format('conta de unidade cuja unidade ainda n\u00E3o migrou (%s #%s)',\n coalesce(v_meta #>> '{v1,reference_table}', 'companies'),\n coalesce(v_meta #>> '{v1,reference_id}', '<sem id>')));\n END IF;\n END IF;\n\n IF v_merge_into IS NOT NULL THEN\n -- Enriquece a conta que fica e devolve O ID DELA. O motor grava esse\n -- target_id no ledger, e a partir da\u00ED todo movimento que referenciava a\n -- conta do V1 resolve para a conta \u00FAnica da pessoa no V2.\n UPDATE public.plg_financial_accounts SET\n is_active = is_active OR coalesce((p_row ->> 'is_active')::boolean, true),\n metadata = migration._merge_meta(\n metadata,\n migration._deep_set(v_meta, ARRAY['migration', 'merged_from'],\n coalesce(metadata #> '{migration,merged_from}', '[]'::jsonb)\n || jsonb_build_array(jsonb_build_object(\n 'v1_account', v_meta #> '{v1,reference_id}',\n 'name', p_row ->> 'name', 'account_id', p_id))))\n WHERE id = v_merge_into;\n IF v_standalone THEN PERFORM migration._audit(p_tenant, 'public.plg_financial_accounts', v_merge_into, jsonb_build_object('reason', 'merged')); END IF;\n RETURN jsonb_build_object('status', 'migrated', 'target_id', v_merge_into, 'merged', true,\n 'reason', 'conta fundida na conta \u00FAnica da pessoa (o V2 admite uma por pessoa)');\n END IF;\n\n SELECT tenant_id INTO v_existing_tenant FROM public.plg_financial_accounts WHERE id = p_id;\n IF v_existing_tenant IS NOT NULL AND v_existing_tenant <> p_tenant THEN\n RETURN jsonb_build_object('status', 'quarantined', 'kind', 'tenant', 'reason', format('%s already exists in another tenant', p_id));\n END IF;\n IF v_existing_tenant IS NULL THEN\n INSERT INTO public.plg_financial_accounts (id, tenant_id, unit_id, kind, code, name, bank_name, settlement_days, is_active, person_id, metadata)\n VALUES (p_id, p_tenant, v_unit, v_kind,\n p_row ->> 'code',\n p_row ->> 'name',\n p_row ->> 'bank_name',\n (p_row ->> 'settlement_days')::integer,\n coalesce((p_row ->> 'is_active')::boolean, true),\n v_person,\n v_meta);\n v_created := true;\n ELSE\n -- kind e person_id andam JUNTOS: a constraint amarra os dois, ent\u00E3o mudar\n -- um sem o outro derruba a linha que j\u00E1 estava l\u00E1.\n UPDATE public.plg_financial_accounts SET\n unit_id = coalesce(v_unit, unit_id),\n kind = CASE WHEN p_row ? 'kind' THEN v_kind ELSE kind END,\n person_id = CASE WHEN p_row ? 'kind' THEN v_person ELSE person_id END,\n code = CASE WHEN p_row ? 'code' THEN p_row ->> 'code' ELSE code END,\n name = CASE WHEN p_row ? 'name' THEN p_row ->> 'name' ELSE name END,\n bank_name = CASE WHEN p_row ? 'bank_name' THEN p_row ->> 'bank_name' ELSE bank_name END,\n settlement_days = CASE WHEN p_row ? 'settlement_days' THEN (p_row ->> 'settlement_days')::integer ELSE settlement_days END,\n is_active = CASE WHEN p_row ? 'is_active' THEN coalesce((p_row ->> 'is_active')::boolean, true) ELSE is_active END,\n metadata = migration._merge_meta(metadata, v_meta)\n WHERE id = p_id;\n END IF;\n IF v_standalone THEN PERFORM migration._audit(p_tenant, 'public.plg_financial_accounts', p_id, jsonb_build_object('reason', CASE WHEN v_created THEN 'created' ELSE 'updated' END)); END IF;\n RETURN jsonb_build_object('status', 'migrated', 'target_id', p_id, 'reason', CASE WHEN v_created THEN 'created' ELSE 'updated' END);\nEND $function$;\n\n\n-- \u2500\u2500 quem pode executar \u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\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-- Fun\u00E7\u00E3o nova nasce execut\u00E1vel por PUBLIC, e `anon`/`authenticated` herdam\n-- dali. Estas s\u00E3o de PORTE: SECURITY DEFINER, escrevem dados de qualquer\n-- inquilino, e uma delas alcan\u00E7\u00E1vel pela chave an\u00F4nima \u00E9 a base inteira\n-- alcan\u00E7\u00E1vel pela chave an\u00F4nima. S\u00F3 o service_role, que \u00E9 quem roda a onda.\nREVOKE ALL ON FUNCTION migration.upsert_fin_account(uuid, uuid, jsonb, jsonb) FROM PUBLIC, anon, authenticated;\nGRANT EXECUTE ON FUNCTION migration.upsert_fin_account(uuid, uuid, jsonb, jsonb) TO service_role;\n";
|
|
65
65
|
export declare const MIGRATION_064_A_CLASSIFICACAO_CONTABIL_CHEGA_INTEIRA = "-- A classifica\u00E7\u00E3o cont\u00E1bil chega inteira.\n--\n-- Sem esta migration n\u00E3o existe DRE no V2 \u2014 e o motivo n\u00E3o \u00E9 a tela. O DRE do V1\n-- \u00E9 montado sobre DOIS eixos, e os dois se perdiam no caminho:\n--\n-- 1. a NATUREZA da conta (receita \u00D7 despesa)\n-- 2. a CONTA CONT\u00C1BIL DO ITEM (qual linha do DRE aquele lan\u00E7amento \u00E9)\n--\n-- \u2500\u2500 Eixo 1: a natureza estava na coluna errada \u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\n--\n-- O de-para mapeava `chart_of_accounts.account_type \u2192 node_type`. S\u00E3o coisas\n-- diferentes: no V1 `account_type` \u00E9 a NATUREZA (asset|liability|equity|revenue|\n-- expense); no V2 `node_type` \u00E9 a FORMA DO N\u00D3 (group|leaf) e `account_type` \u00E9 a\n-- natureza. O resultado, medido no cluster em 18/09/2026:\n--\n-- node_type = 'expense' \u2192 302 linhas account_type = NULL\n-- node_type = 'revenue' \u2192 38 linhas account_type = NULL\n-- node_type = 'leaf' \u2192 244 linhas account_type preenchido\n--\n-- As 244 corretas s\u00E3o as que algu\u00E9m criou pelo app; as 340 erradas s\u00E3o as que o\n-- porte escreveu. E `upsert_fin_chart_account` **nunca gravou `account_type`** \u2014\n-- a coluna n\u00E3o aparece no INSERT nem no UPDATE dele.\n--\n-- Um DRE sem natureza n\u00E3o separa receita de despesa. N\u00E3o erra por pouco: n\u00E3o\n-- monta.\n--\n-- \u2500\u2500 Eixo 2: a conta do item virava texto opaco \u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\n--\n-- `invoice_items.account_id \u2192 jsonb:financial.v1_account_ref`. A classifica\u00E7\u00E3o\n-- cont\u00E1bil, que no V1 \u00E9 do ITEM e n\u00E3o do movimento, chegava ao V2 como o id\n-- num\u00E9rico cru do V1 dentro de `metadata` \u2014 sem FK, sem join poss\u00EDvel. E n\u00E3o\n-- havia para onde resolver: `public.items` n\u00E3o tinha coluna de conta cont\u00E1bil.\n--\n-- (`plg_financial_invoice_items.chart_account_id` existe e tem ZERO linhas: o\n-- item do V1 cai s\u00F3 em `public.items`; n\u00E3o h\u00E1 split financeiro para ele.)\n--\n-- \u2500\u2500 O terceiro eixo N\u00C3O precisou de nada \u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\n--\n-- A dire\u00E7\u00E3o (receita \u00D7 despesa do documento) parecia perdida tamb\u00E9m \u2014\n-- `invoices.type \u2192 jsonb:financial.v1_type`. N\u00E3o est\u00E1: o writer `order` devolve\n-- em `extra` o T\u00CDTULO em `plg_financial_invoices`, e l\u00E1 `direction` j\u00E1 chega\n-- certa. Medido: 8.175 `receivable` + 2.503 `payable`. O DRE l\u00EA o t\u00EDtulo, n\u00E3o o\n-- pedido, e o eixo j\u00E1 existe.\n--\n-- Idempotente.\n\n-- \u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\n-- 1. O item ganha onde guardar a classifica\u00E7\u00E3o\n-- \u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\n\nALTER TABLE public.items\n ADD COLUMN IF NOT EXISTS chart_account_id uuid REFERENCES public.plg_financial_chart_of_accounts(id),\n ADD COLUMN IF NOT EXISTS cost_center_id uuid REFERENCES public.plg_financial_cost_centers(id);\n\nCOMMENT ON COLUMN public.items.chart_account_id IS\n 'Conta cont\u00E1bil DESTE item. A classifica\u00E7\u00E3o \u00E9 da linha, n\u00E3o do documento: no '\n 'V1 dois itens da mesma venda podem cair em ramos diferentes do DRE, e \u00E9 essa '\n 'granularidade que o relat\u00F3rio precisa.';\nCOMMENT ON COLUMN public.items.cost_center_id IS\n 'Centro de custo do item. O DRE n\u00E3o usa \u2014 \u00E9 a segunda dimens\u00E3o, para an\u00E1lise '\n 'de custo \u2014 mas perder o dado na entrada seria irrevers\u00EDvel.';\n\n-- A pergunta do DRE \u00E9 \"some os itens por conta, no per\u00EDodo\". Sem \u00EDndice ela\n-- varre `items` inteira por linha do relat\u00F3rio.\nCREATE INDEX IF NOT EXISTS items_chart_account_idx\n ON public.items (tenant_id, chart_account_id) WHERE chart_account_id IS NOT NULL;\n\n-- \u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\n-- 2. Conserto do dado j\u00E1 migrado\n-- \u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\n\n-- Devolve a natureza para a coluna certa nas linhas que o porte escreveu. O\n-- teste \u00E9 o pr\u00F3prio vocabul\u00E1rio: `node_type` s\u00F3 pode ser group|leaf|account, e\n-- qualquer outra coisa ali \u00E9 natureza que foi parar no lugar errado.\nUPDATE public.plg_financial_chart_of_accounts\n SET account_type = node_type,\n node_type = 'leaf'\n WHERE node_type IN ('asset', 'liability', 'equity', 'revenue', 'expense')\n AND account_type IS NULL;\n\n-- `node_type` de verdade: \u00E9 grupo quem tem filho. O V1 n\u00E3o tem essa coluna, e\n-- deriv\u00E1-la da pr\u00F3pria \u00E1rvore \u00E9 mais honesto que herdar um default.\nUPDATE public.plg_financial_chart_of_accounts p\n SET node_type = 'group'\n WHERE node_type <> 'group'\n AND EXISTS (SELECT 1 FROM public.plg_financial_chart_of_accounts c WHERE c.parent_id = p.id);\n\n-- \u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\n-- 3. Os writers\n-- \u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\n\nCREATE OR REPLACE FUNCTION migration.upsert_fin_chart_account(\n p_tenant uuid, p_id uuid, p_row jsonb, p_args jsonb DEFAULT '{}'::jsonb\n) RETURNS jsonb\nLANGUAGE plpgsql SECURITY DEFINER SET search_path TO ''\nAS $function$\nDECLARE\n v_meta jsonb := coalesce(p_row -> 'metadata', '{}'::jsonb);\n v_type text := nullif(btrim(lower(coalesce(p_row ->> 'account_type', ''))), '');\n v_parent uuid := (p_row ->> 'parent_id')::uuid;\n v_existing_tenant uuid;\n v_created boolean := false;\n v_standalone boolean := migration._standalone();\nBEGIN\n IF v_standalone THEN PERFORM migration._assert_fence(p_tenant); PERFORM set_config('migration.writer', 'on', true); END IF;\n IF p_id IS NULL OR nullif(btrim(coalesce(p_row ->> 'name', '')), '') IS NULL THEN\n RETURN jsonb_build_object('status', 'quarantined', 'kind', 'state', 'reason', 'this row needs an id and a name');\n END IF;\n\n -- Sem corre\u00E7\u00E3o silenciosa: uma natureza fora do vocabul\u00E1rio do raz\u00E3o faria a\n -- conta sumir do DRE sem aparecer em lugar nenhum \u2014 nem na receita, nem na\n -- despesa, nem num aviso.\n IF v_type IS NOT NULL AND v_type NOT IN ('asset', 'liability', 'equity', 'revenue', 'expense') THEN\n RETURN jsonb_build_object('status', 'quarantined', 'kind', 'value',\n 'reason', format('natureza de conta desconhecida: %L (o raz\u00E3o conhece asset, liability, equity, revenue e expense)', v_type));\n END IF;\n\n SELECT tenant_id INTO v_existing_tenant FROM public.plg_financial_chart_of_accounts WHERE id = p_id;\n IF v_existing_tenant IS NOT NULL AND v_existing_tenant <> p_tenant THEN\n RETURN jsonb_build_object('status', 'quarantined', 'kind', 'tenant', 'reason', format('%s already exists in another tenant', p_id));\n END IF;\n IF v_existing_tenant IS NULL THEN\n INSERT INTO public.plg_financial_chart_of_accounts\n (id, tenant_id, code, name, node_type, account_type, parent_id, is_active)\n VALUES (p_id, p_tenant, p_row ->> 'code', p_row ->> 'name',\n -- Nasce folha; vira grupo abaixo, quando o primeiro filho chegar.\n coalesce(p_row ->> 'node_type', 'leaf'),\n v_type, v_parent,\n coalesce((p_row ->> 'is_active')::boolean, true));\n v_created := true;\n ELSE\n UPDATE public.plg_financial_chart_of_accounts SET\n code = CASE WHEN p_row ? 'code' THEN p_row ->> 'code' ELSE code END,\n name = CASE WHEN p_row ? 'name' THEN p_row ->> 'name' ELSE name END,\n node_type = CASE WHEN p_row ? 'node_type' THEN coalesce(p_row ->> 'node_type', node_type) ELSE node_type END,\n account_type = coalesce(v_type, account_type),\n parent_id = CASE WHEN p_row ? 'parent_id' THEN v_parent ELSE parent_id END,\n is_active = CASE WHEN p_row ? 'is_active' THEN coalesce((p_row ->> 'is_active')::boolean, true) ELSE is_active END\n WHERE id = p_id;\n END IF;\n\n -- Quem tem filho \u00E9 grupo. Marcar aqui, e n\u00E3o num passe no fim, faz a \u00E1rvore\n -- ficar correta mesmo se a onda parar no meio.\n IF v_parent IS NOT NULL THEN\n UPDATE public.plg_financial_chart_of_accounts\n SET node_type = 'group'\n WHERE id = v_parent AND tenant_id = p_tenant AND node_type <> 'group';\n END IF;\n\n IF v_standalone THEN PERFORM migration._audit(p_tenant, 'public.plg_financial_chart_of_accounts', p_id, jsonb_build_object('reason', CASE WHEN v_created THEN 'created' ELSE 'updated' END)); END IF;\n RETURN jsonb_build_object('status', 'migrated', 'target_id', p_id, 'reason', CASE WHEN v_created THEN 'created' ELSE 'updated' END);\nEND $function$;\n\n-- O writer do item passa a gravar as duas colunas novas. O resto do corpo \u00E9 o\n-- que j\u00E1 estava l\u00E1 \u2014 est\u00E1 reemitido inteiro porque \u00E9 assim que se troca uma\n-- fun\u00E7\u00E3o no Postgres, n\u00E3o porque mudou.\nCREATE OR REPLACE FUNCTION migration.upsert_order_item(\n p_tenant uuid, p_id uuid, p_row jsonb, p_args jsonb DEFAULT '{}'::jsonb\n) RETURNS jsonb\nLANGUAGE plpgsql SECURITY DEFINER SET search_path TO ''\nAS $function$\nDECLARE\n v_meta jsonb := coalesce(p_row -> 'metadata', '{}'::jsonb);\n v_order uuid := (p_row ->> 'order_id')::uuid;\n v_chart uuid := (p_row ->> 'chart_account_id')::uuid;\n v_center uuid := (p_row ->> 'cost_center_id')::uuid;\n v_order_tenant uuid;\n v_existing uuid;\n v_cancelled timestamptz;\n v_created boolean := false;\n v_standalone boolean := migration._standalone();\nBEGIN\n IF v_standalone THEN PERFORM migration._assert_fence(p_tenant); PERFORM set_config('migration.writer', 'on', true); END IF;\n IF p_id IS NULL OR v_order IS NULL THEN\n RETURN jsonb_build_object('status', 'quarantined', 'kind', 'state', 'reason', 'an order line needs an id and an order');\n END IF;\n SELECT tenant_id INTO v_order_tenant FROM public.orders WHERE id = v_order;\n IF v_order_tenant IS NULL THEN\n RETURN jsonb_build_object('status', 'quarantined', 'kind', 'fk', 'reason', format('order %s is not in the destination yet', v_order));\n END IF;\n IF v_order_tenant <> p_tenant THEN\n RETURN jsonb_build_object('status', 'quarantined', 'kind', 'tenant', 'reason', format('order %s belongs to another tenant', v_order));\n END IF;\n\n v_cancelled := nullif(v_meta #>> '{cancellation,at}', '')::timestamptz;\n\n SELECT tenant_id INTO v_existing FROM public.items WHERE id = p_id;\n IF v_existing IS NOT NULL AND v_existing <> p_tenant THEN\n RETURN jsonb_build_object('status', 'quarantined', 'kind', 'tenant', 'reason', format('order line %s already exists in another tenant', p_id));\n END IF;\n IF v_existing IS NULL THEN\n -- `order_id` e S\u00D3 ele: a fatura alcan\u00E7a a linha pelo pedido (items_one_parent).\n INSERT INTO public.items (id, tenant_id, order_id, product_id, name, description,\n quantity, unit_price, discount, total, sort_order,\n cancelled_at, cancel_reason, chart_account_id, cost_center_id, metadata)\n VALUES (p_id, p_tenant, v_order, (p_row ->> 'product_id')::uuid,\n coalesce(nullif(btrim(coalesce(p_row ->> 'name', '')), ''), 'Item'),\n coalesce(p_row ->> 'description', nullif(btrim(coalesce(p_row ->> 'name', '')), '')),\n greatest(coalesce((p_row ->> 'quantity')::numeric, 1), 0.0001),\n round(coalesce((p_row ->> 'unit_price')::numeric, 0), 2),\n round(coalesce((p_row ->> 'discount')::numeric, 0), 2),\n round(coalesce((p_row ->> 'total')::numeric, 0), 2),\n coalesce((p_row ->> 'sort_order')::integer, 0),\n v_cancelled, v_meta #>> '{cancellation,reason}',\n v_chart, v_center, v_meta);\n v_created := true;\n ELSE\n UPDATE public.items SET\n order_id = v_order,\n product_id = CASE WHEN p_row ? 'product_id' THEN (p_row ->> 'product_id')::uuid ELSE product_id END,\n name = CASE WHEN p_row ? 'name' THEN coalesce(nullif(btrim(p_row ->> 'name'), ''), name) ELSE name END,\n quantity = CASE WHEN p_row ? 'quantity' THEN greatest(coalesce((p_row ->> 'quantity')::numeric, 1), 0.0001) ELSE quantity END,\n unit_price = CASE WHEN p_row ? 'unit_price' THEN round(coalesce((p_row ->> 'unit_price')::numeric, 0), 2) ELSE unit_price END,\n discount = CASE WHEN p_row ? 'discount' THEN round(coalesce((p_row ->> 'discount')::numeric, 0), 2) ELSE discount END,\n total = CASE WHEN p_row ? 'total' THEN round(coalesce((p_row ->> 'total')::numeric, 0), 2) ELSE total END,\n cancelled_at = coalesce(v_cancelled, cancelled_at),\n cancel_reason = coalesce(v_meta #>> '{cancellation,reason}', cancel_reason),\n -- `coalesce` e n\u00E3o CASE: a classifica\u00E7\u00E3o \u00E9 enriquecimento. Uma reoferta\n -- sem a coluna n\u00E3o deve apagar a conta que j\u00E1 est\u00E1 l\u00E1.\n chart_account_id = coalesce(v_chart, chart_account_id),\n cost_center_id = coalesce(v_center, cost_center_id),\n metadata = migration._merge_meta(metadata, v_meta)\n WHERE id = p_id;\n END IF;\n\n IF v_standalone THEN PERFORM migration._audit(p_tenant, 'public.items', p_id, jsonb_build_object('reason', CASE WHEN v_created THEN 'created' ELSE 'updated' END)); END IF;\n RETURN jsonb_build_object('status', 'migrated', 'target_id', p_id, 'reason', CASE WHEN v_created THEN 'created' ELSE 'updated' END);\nEND $function$;\n\n-- \u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\n-- 4. O de-para\n-- \u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\n\nSET LOCAL migration.allowlist_unlock = 'on';\n\n-- `account_type` para `account_type`. O mapa anterior mandava a natureza para\n-- `node_type`, e \u00E9 a corre\u00E7\u00E3o que faz o DRE existir.\nSELECT migration._allow(\n 'chart_of_accounts', 'public.plg_financial_chart_of_accounts', 'financial',\n 'fin_chart_account', '{}'::jsonb, ARRAY[]::text[],\n jsonb_build_object(\n 'uuid', 'id',\n 'code', 'code',\n 'name', 'name',\n 'account_type', 'account_type',\n 'parent_id', 'parent_id@chart_of_accounts',\n 'active', 'is_active'\n ),\n 'Plano de contas. `account_type` \u00E9 a NATUREZA (o eixo do DRE); `node_type` \u00E9 '\n 'derivado da \u00E1rvore pelo writer \u2014 quem tem filho \u00E9 grupo.'\n);\n\n-- O item passa a resolver a conta cont\u00E1bil e o centro de custo em vez de\n-- carreg\u00E1-los como id cru do V1 em metadata.\nSELECT migration._allow(\n 'invoice_items', 'public.items', 'financial',\n 'order_item', '{}'::jsonb, ARRAY['unit_price', 'total_amount'],\n jsonb_build_object(\n 'uuid', 'id',\n 'invoice_id', 'order_id@invoices:public.orders',\n 'description', 'name',\n 'quantity', 'quantity',\n 'unit_price', 'unit_price',\n 'discount', 'discount',\n 'total_amount', 'total',\n 'additional', 'jsonb:financial.additional',\n 'product_id', 'product_id@products',\n 'service_id', 'product_id@services',\n 'account_id', 'chart_account_id@chart_of_accounts',\n 'cost_center_id', 'cost_center_id@cost_centers',\n 'item_type', 'jsonb:item_type',\n 'canceled_at', 'jsonb:cancellation.at',\n 'cancel_reason', 'jsonb:cancellation.reason',\n 'appointment_id', 'jsonb:agenda.v1_appointment_ref',\n 'execution_date', 'jsonb:execution.date',\n 'execution_start_time', 'jsonb:execution.start_time',\n 'execution_end_time', 'jsonb:execution.end_time',\n 'execution_photos', '-',\n 'executed_by_professional_id', 'jsonb:execution.v1_professional_ref',\n 'is_executed', 'jsonb:execution.is_executed',\n 'gift_card_id', 'jsonb:v1_gift_card_ref',\n 'service_package_id', 'jsonb:v1_package_ref'\n ),\n 'A linha da venda. `account_id` \u00E9 a classifica\u00E7\u00E3o cont\u00E1bil DO ITEM \u2014 o eixo '\n 'que o DRE agrupa; antes ela vinha como id cru do V1 em metadata.'\n);\n\n\n-- \u2500\u2500 quem pode executar \u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\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-- Fun\u00E7\u00E3o nova nasce execut\u00E1vel por PUBLIC, e `anon`/`authenticated` herdam\n-- dali. Estas s\u00E3o de PORTE: SECURITY DEFINER, escrevem dados de qualquer\n-- inquilino, e uma delas alcan\u00E7\u00E1vel pela chave an\u00F4nima \u00E9 a base inteira\n-- alcan\u00E7\u00E1vel pela chave an\u00F4nima. S\u00F3 o service_role, que \u00E9 quem roda a onda.\nREVOKE ALL ON FUNCTION migration.upsert_fin_chart_account(uuid, uuid, jsonb, jsonb) FROM PUBLIC, anon, authenticated;\nGRANT EXECUTE ON FUNCTION migration.upsert_fin_chart_account(uuid, uuid, jsonb, jsonb) TO service_role;\nREVOKE ALL ON FUNCTION migration.upsert_order_item(uuid, uuid, jsonb, jsonb) FROM PUBLIC, anon, authenticated;\nGRANT EXECUTE ON FUNCTION migration.upsert_order_item(uuid, uuid, jsonb, jsonb) TO service_role;\n";
|
|
66
66
|
export declare const MIGRATION_065_A_FUSAO_PARA_DE_DOBRAR_A_PROPRIA_MEMORIA = "-- A fus\u00E3o para de dobrar a pr\u00F3pria mem\u00F3ria.\n--\n-- Corre\u00E7\u00E3o de um defeito que eu introduzi na 048 e que derrubou o cluster quatro\n-- vezes em 18/09/2026.\n--\n-- \u2500\u2500 O que eu escrevi \u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\n--\n-- Na fus\u00E3o de conta de pessoa, para registrar de onde a conta veio:\n--\n-- metadata = migration._merge_meta(\n-- metadata,\n-- migration._deep_set(v_meta, ARRAY['migration','merged_from'],\n-- coalesce(metadata #> '{migration,merged_from}', '[]'::jsonb)\n-- || jsonb_build_array(...)))\n--\n-- Leio o array que j\u00E1 est\u00E1 na linha, acrescento um elemento \u2014 e entrego o\n-- resultado a `_merge_meta`, que **concatena com o array que j\u00E1 est\u00E1 na linha de\n-- novo**. Cada oferta n\u00E3o acrescenta um: ela DOBRA.\n--\n-- oferta 1 \u2192 1 elemento\n-- oferta 5 \u2192 16\n-- oferta 10 \u2192 512\n-- oferta 18 \u2192 262.143 \u2190 medido\n--\n-- \u2500\u2500 O que isso custou \u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\n--\n-- Medido antes deste conserto, no tenant Pertinho do C\u00E9u:\n--\n-- maior merged_from 262.143 elementos (2\u00B9\u2078 \u2212 1)\n-- maior metadata 1.378 kB numa \u00FAnica linha\n--\n-- Montar e mesclar um jsonb desse tamanho, uma vez por linha, dentro de um\n-- statement, num cluster com `shared_buffers` de 224 MB, mata o backend. E o\n-- sintoma n\u00E3o apontava para c\u00E1: a conex\u00E3o ca\u00EDa no meio de `account_central`, o\n-- transporte reportava \"server closed the connection unexpectedly\", e eu passei\n-- duas rodadas reduzindo o tamanho do lote \u2014 de 2.000 para 250, depois para 100.\n-- Nenhuma ajudou, porque 417 linhas nunca foram o problema.\n--\n-- \u2500\u2500 O conserto \u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\n--\n-- `merged_from` sai. N\u00E3o vira \"append idempotente\", n\u00E3o vira \"array com limite\":\n-- sai inteiro, porque ele nunca deveria ter existido \u2014 **o ledger j\u00E1 registra a\n-- fus\u00E3o**, e melhor: dois `source_numeric_id` distintos apontando para o mesmo\n-- `target_id` \u00C9 a defini\u00E7\u00E3o de fus\u00E3o, \u00E9 consult\u00E1vel, e \u00E9 o lugar onde o resto\n-- deste schema j\u00E1 procura essa resposta.\n--\n-- select source_numeric_id, target_id\n-- from migration.ledger\n-- where source_table = 'account_central' and target_table = 'public.plg_financial_accounts'\n-- order by target_id;\n--\n-- Guardar a mesma informa\u00E7\u00E3o num jsonb ao lado era a segunda c\u00F3pia da regra \u2014\n-- exatamente o que o cabe\u00E7alho do `sdkmig.py` diz para n\u00E3o fazer. A segunda\n-- c\u00F3pia divergiu no primeiro dia, e de forma exponencial.\n--\n-- Idempotente.\n\n-- \u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\n-- 1. Desarmar o que j\u00E1 explodiu\n-- \u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\n\n-- Primeiro o dado, depois a fun\u00E7\u00E3o: enquanto as linhas gordas estiverem l\u00E1,\n-- qualquer rodada que as toque derruba o banco de novo \u2014 inclusive a que\n-- aplicaria esta migration mais adiante.\nUPDATE public.plg_financial_accounts\n SET metadata = (coalesce(metadata, '{}'::jsonb) #- '{migration,merged_from}')\n WHERE metadata #> '{migration,merged_from}' IS NOT NULL;\n\n-- \u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\n-- 2. O writer para de acumular\n-- \u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\n\nCREATE OR REPLACE FUNCTION migration.upsert_fin_account(\n p_tenant uuid, p_id uuid, p_row jsonb, p_args jsonb DEFAULT '{}'::jsonb\n) RETURNS jsonb\nLANGUAGE plpgsql SECURITY DEFINER SET search_path TO ''\nAS $function$\nDECLARE\n v_meta jsonb := coalesce(p_row -> 'metadata', '{}'::jsonb);\n v_kind text := coalesce(p_row ->> 'kind', 'bank');\n v_person uuid;\n v_unit uuid := (p_row ->> 'unit_id')::uuid;\n v_merge_into uuid;\n v_existing_tenant uuid;\n v_created boolean := false;\n v_standalone boolean := migration._standalone();\nBEGIN\n IF v_standalone THEN PERFORM migration._assert_fence(p_tenant); PERFORM set_config('migration.writer', 'on', true); END IF;\n IF p_id IS NULL OR nullif(btrim(coalesce(p_row ->> 'name', '')), '') IS NULL THEN\n RETURN jsonb_build_object('status', 'quarantined', 'kind', 'state', 'reason', 'this row needs an id and a name');\n END IF;\n IF v_kind NOT IN ('bank', 'cash_physical', 'cash_session', 'card_receivable_credit',\n 'card_receivable_debit', 'card_payable', 'person', 'system', 'unit') THEN\n RETURN jsonb_build_object('status', 'quarantined', 'kind', 'value',\n 'reason', format('unknown account kind %L \u2014 V1 account_type does not map onto the ledger''s nine', v_kind));\n END IF;\n\n IF v_kind = 'person' THEN\n SELECT l.target_id INTO v_person\n FROM migration.ledger l\n WHERE l.tenant_id = p_tenant\n AND l.source_table = (v_meta #>> '{v1,reference_table}')\n AND l.source_numeric_id = nullif(v_meta #>> '{v1,reference_id}', '')::bigint\n AND l.target_table = 'public.people'\n AND l.status = 'migrated'\n LIMIT 1;\n IF v_person IS NULL THEN\n RETURN jsonb_build_object('status', 'quarantined', 'kind', 'fk',\n 'reason', format('conta de pessoa cuja pessoa ainda n\u00E3o migrou (%s #%s)',\n coalesce(v_meta #>> '{v1,reference_table}', '<sem tabela>'),\n coalesce(v_meta #>> '{v1,reference_id}', '<sem id>')));\n END IF;\n\n SELECT id INTO v_merge_into\n FROM public.plg_financial_accounts\n WHERE tenant_id = p_tenant AND person_id = v_person AND id <> p_id\n LIMIT 1;\n END IF;\n\n IF v_kind = 'unit' AND v_unit IS NULL THEN\n SELECT l.target_id INTO v_unit\n FROM migration.ledger l\n WHERE l.tenant_id = p_tenant\n AND l.source_table = coalesce(v_meta #>> '{v1,reference_table}', 'companies')\n AND l.source_numeric_id = nullif(v_meta #>> '{v1,reference_id}', '')::bigint\n AND l.target_table = 'app.units'\n AND l.status = 'migrated'\n LIMIT 1;\n IF v_unit IS NULL THEN\n RETURN jsonb_build_object('status', 'quarantined', 'kind', 'fk',\n 'reason', format('conta de unidade cuja unidade ainda n\u00E3o migrou (%s #%s)',\n coalesce(v_meta #>> '{v1,reference_table}', 'companies'),\n coalesce(v_meta #>> '{v1,reference_id}', '<sem id>')));\n END IF;\n END IF;\n\n IF v_merge_into IS NOT NULL THEN\n -- A fus\u00E3o N\u00C3O escreve hist\u00F3rico aqui. Quem registra que duas contas do V1\n -- viraram uma no V2 \u00E9 o ledger: dois `source_numeric_id` apontando para o\n -- mesmo `target_id`. Acumular a mesma informa\u00E7\u00E3o num array de metadata foi o\n -- defeito que dobrava de tamanho a cada oferta e derrubou o cluster.\n UPDATE public.plg_financial_accounts\n SET is_active = is_active OR coalesce((p_row ->> 'is_active')::boolean, true)\n WHERE id = v_merge_into;\n IF v_standalone THEN PERFORM migration._audit(p_tenant, 'public.plg_financial_accounts', v_merge_into, jsonb_build_object('reason', 'merged')); END IF;\n RETURN jsonb_build_object('status', 'migrated', 'target_id', v_merge_into, 'merged', true,\n 'reason', 'conta fundida na conta \u00FAnica da pessoa (o V2 admite uma por pessoa)');\n END IF;\n\n SELECT tenant_id INTO v_existing_tenant FROM public.plg_financial_accounts WHERE id = p_id;\n IF v_existing_tenant IS NOT NULL AND v_existing_tenant <> p_tenant THEN\n RETURN jsonb_build_object('status', 'quarantined', 'kind', 'tenant', 'reason', format('%s already exists in another tenant', p_id));\n END IF;\n IF v_existing_tenant IS NULL THEN\n INSERT INTO public.plg_financial_accounts (id, tenant_id, unit_id, kind, code, name, bank_name, settlement_days, is_active, person_id, metadata)\n VALUES (p_id, p_tenant, v_unit, v_kind,\n p_row ->> 'code', p_row ->> 'name', p_row ->> 'bank_name',\n (p_row ->> 'settlement_days')::integer,\n coalesce((p_row ->> 'is_active')::boolean, true), v_person, v_meta);\n v_created := true;\n ELSE\n UPDATE public.plg_financial_accounts SET\n unit_id = coalesce(v_unit, unit_id),\n kind = CASE WHEN p_row ? 'kind' THEN v_kind ELSE kind END,\n person_id = CASE WHEN p_row ? 'kind' THEN v_person ELSE person_id END,\n code = CASE WHEN p_row ? 'code' THEN p_row ->> 'code' ELSE code END,\n name = CASE WHEN p_row ? 'name' THEN p_row ->> 'name' ELSE name END,\n bank_name = CASE WHEN p_row ? 'bank_name' THEN p_row ->> 'bank_name' ELSE bank_name END,\n settlement_days = CASE WHEN p_row ? 'settlement_days' THEN (p_row ->> 'settlement_days')::integer ELSE settlement_days END,\n is_active = CASE WHEN p_row ? 'is_active' THEN coalesce((p_row ->> 'is_active')::boolean, true) ELSE is_active END,\n metadata = migration._merge_meta(metadata, v_meta)\n WHERE id = p_id;\n END IF;\n IF v_standalone THEN PERFORM migration._audit(p_tenant, 'public.plg_financial_accounts', p_id, jsonb_build_object('reason', CASE WHEN v_created THEN 'created' ELSE 'updated' END)); END IF;\n RETURN jsonb_build_object('status', 'migrated', 'target_id', p_id, 'reason', CASE WHEN v_created THEN 'created' ELSE 'updated' END);\nEND $function$;\n\n\n-- \u2500\u2500 quem pode executar \u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\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-- Fun\u00E7\u00E3o nova nasce execut\u00E1vel por PUBLIC, e `anon`/`authenticated` herdam\n-- dali. Estas s\u00E3o de PORTE: SECURITY DEFINER, escrevem dados de qualquer\n-- inquilino, e uma delas alcan\u00E7\u00E1vel pela chave an\u00F4nima \u00E9 a base inteira\n-- alcan\u00E7\u00E1vel pela chave an\u00F4nima. S\u00F3 o service_role, que \u00E9 quem roda a onda.\nREVOKE ALL ON FUNCTION migration.upsert_fin_account(uuid, uuid, jsonb, jsonb) FROM PUBLIC, anon, authenticated;\nGRANT EXECUTE ON FUNCTION migration.upsert_fin_account(uuid, uuid, jsonb, jsonb) TO service_role;\n";
|
|
67
67
|
export declare const MIGRATION_066_A_VENDA_DE_BALCAO_TEM_CONTA = "-- A venda de balc\u00E3o tem conta.\n--\n-- Terceira esp\u00E9cie de conta que o V1 tem e o raz\u00E3o do V2 n\u00E3o tinha \u2014 depois de\n-- `unit` (a conta da casa, migration 048) vem `unidentified`: a conta da venda\n-- para quem n\u00E3o deixou cadastro.\n--\n-- No V1 ela \u00E9 uma linha de `account_central` com `account_type = 'client'` e\n-- `reference_id = 0`. O tipo diz \"pessoa\"; a refer\u00EAncia \u00E9 o sentinela de \"n\u00E3o\n-- h\u00E1 pessoa\". O V2 n\u00E3o aceita a combina\u00E7\u00E3o, e com raz\u00E3o:\n--\n-- CHECK ((kind = 'person') = (person_id IS NOT NULL))\n--\n-- Uma conta de pessoa sem pessoa \u00E9 dinheiro pendurado em ningu\u00E9m. O CHECK est\u00E1\n-- certo; o que faltava era a palavra para o caso em que **n\u00E3o h\u00E1 contraparte e\n-- isso \u00E9 o fato**, n\u00E3o uma falha de cadastro.\n--\n-- \u2500\u2500 Por que esp\u00E9cie pr\u00F3pria, e n\u00E3o `system` \u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\n--\n-- `system` j\u00E1 tem dono: \u00E9 a conta do plano de contas, identificada por c\u00F3digo\n-- (`CHECK (kind <> 'system' OR code IS NOT NULL)`). Jogar a venda de balc\u00E3o l\u00E1\n-- dentro faria dois conceitos dividirem um balde que nenhum relat\u00F3rio separa\n-- depois \u2014 e o de balc\u00E3o n\u00E3o \u00E9 detalhe: num restaurante ele \u00E9 a maior parte do\n-- movimento.\n--\n-- `unidentified` diz a verdade: existe contraparte, ela comprou, e ela n\u00E3o se\n-- identificou. \u00C9 diferente de `system` (n\u00E3o h\u00E1 contraparte) e de `person` (h\u00E1, e\n-- sabemos quem).\n--\n-- \u2500\u2500 O que estava preso \u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\n--\n-- Medido em 18/09/2026 no tenant Pertinho do C\u00E9u, antes deste arquivo:\n--\n-- account_central em quarentena por `clients #0` 5\n-- financial_movements travados atr\u00E1s delas 110\n-- valor R$ 15.102,78\n--\n-- A asser\u00E7\u00E3o de paridade (`make parity`) apontava exatamente isso: contas a\n-- pagar com \u0394 \u221210.218,30 contra a origem, e a cadeia inteira levava a uma conta.\n--\n-- Idempotente.\n\n-- \u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\n-- 1. A esp\u00E9cie\n-- \u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\n\nDO $$\nBEGIN\n ALTER TABLE public.plg_financial_accounts DROP CONSTRAINT IF EXISTS plg_financial_accounts_kind_check;\n ALTER TABLE public.plg_financial_accounts ADD CONSTRAINT plg_financial_accounts_kind_check\n CHECK (kind = ANY (ARRAY['bank', 'cash_physical', 'cash_session',\n 'card_receivable_credit', 'card_receivable_debit',\n 'card_payable', 'person', 'system', 'unit',\n 'unidentified']));\nEND $$;\n\nCOMMENT ON COLUMN public.plg_financial_accounts.kind IS\n 'bank | cash_physical | cash_session | card_receivable_credit | '\n 'card_receivable_debit | card_payable | person | system | unit | unidentified. '\n '`unidentified` \u00E9 a contraparte que comprou e n\u00E3o se identificou (venda de '\n 'balc\u00E3o): diferente de `system`, onde n\u00E3o h\u00E1 contraparte, e de `person`, onde '\n 'h\u00E1 e sabemos quem.';\n\n-- \u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\n-- 2. O writer reconhece o sentinela\n-- \u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\n\n-- S\u00F3 a perna que muda: quando a conta \u00E9 de pessoa e a refer\u00EAncia da origem \u00E9 o\n-- sentinela (`reference_id = 0`) ou aponta para linha que n\u00E3o existe mais, ela\n-- deixa de ser `person` e passa a ser `unidentified` \u2014 em vez de recusar a conta\n-- e, com ela, todo movimento que a cita.\n--\n-- A distin\u00E7\u00E3o importa e est\u00E1 no c\u00F3digo: refer\u00EAncia AUSENTE (0) vira\n-- `unidentified`; refer\u00EAncia PRESENTE cuja pessoa ainda n\u00E3o migrou continua\n-- caindo em quarentena de `fk`, porque a\u00ED a pessoa existe e a ordem \u00E9 que est\u00E1\n-- errada \u2014 e a segunda passada resolve.\nCREATE OR REPLACE FUNCTION migration.upsert_fin_account(\n p_tenant uuid, p_id uuid, p_row jsonb, p_args jsonb DEFAULT '{}'::jsonb\n) RETURNS jsonb\nLANGUAGE plpgsql SECURITY DEFINER SET search_path TO ''\nAS $function$\nDECLARE\n v_meta jsonb := coalesce(p_row -> 'metadata', '{}'::jsonb);\n v_kind text := coalesce(p_row ->> 'kind', 'bank');\n v_ref_id bigint := nullif(v_meta #>> '{v1,reference_id}', '')::bigint;\n v_ref_table text := v_meta #>> '{v1,reference_table}';\n v_person uuid;\n v_unit uuid := (p_row ->> 'unit_id')::uuid;\n v_merge_into uuid;\n v_existing_tenant uuid;\n v_created boolean := false;\n v_standalone boolean := migration._standalone();\nBEGIN\n IF v_standalone THEN PERFORM migration._assert_fence(p_tenant); PERFORM set_config('migration.writer', 'on', true); END IF;\n IF p_id IS NULL OR nullif(btrim(coalesce(p_row ->> 'name', '')), '') IS NULL THEN\n RETURN jsonb_build_object('status', 'quarantined', 'kind', 'state', 'reason', 'this row needs an id and a name');\n END IF;\n\n -- O sentinela ANTES da checagem de vocabul\u00E1rio: `clients#0` n\u00E3o \u00E9 uma conta de\n -- pessoa mal formada, \u00E9 uma conta sem contraparte.\n IF v_kind = 'person' AND coalesce(v_ref_id, 0) = 0 THEN\n v_kind := 'unidentified';\n END IF;\n\n IF v_kind NOT IN ('bank', 'cash_physical', 'cash_session', 'card_receivable_credit',\n 'card_receivable_debit', 'card_payable', 'person', 'system', 'unit',\n 'unidentified') THEN\n RETURN jsonb_build_object('status', 'quarantined', 'kind', 'value',\n 'reason', format('unknown account kind %L \u2014 V1 account_type does not map onto the ledger''s ten', v_kind));\n END IF;\n\n IF v_kind = 'person' THEN\n SELECT l.target_id INTO v_person\n FROM migration.ledger l\n WHERE l.tenant_id = p_tenant\n AND l.source_table = v_ref_table\n AND l.source_numeric_id = v_ref_id\n AND l.target_table = 'public.people'\n AND l.status = 'migrated'\n LIMIT 1;\n IF v_person IS NULL THEN\n RETURN jsonb_build_object('status', 'quarantined', 'kind', 'fk',\n 'reason', format('conta de pessoa cuja pessoa ainda n\u00E3o migrou (%s #%s)',\n coalesce(v_ref_table, '<sem tabela>'), coalesce(v_ref_id::text, '<sem id>')));\n END IF;\n\n SELECT id INTO v_merge_into\n FROM public.plg_financial_accounts\n WHERE tenant_id = p_tenant AND person_id = v_person AND id <> p_id\n LIMIT 1;\n END IF;\n\n IF v_kind = 'unit' AND v_unit IS NULL THEN\n SELECT l.target_id INTO v_unit\n FROM migration.ledger l\n WHERE l.tenant_id = p_tenant\n AND l.source_table = coalesce(v_ref_table, 'companies')\n AND l.source_numeric_id = v_ref_id\n AND l.target_table = 'app.units'\n AND l.status = 'migrated'\n LIMIT 1;\n IF v_unit IS NULL THEN\n RETURN jsonb_build_object('status', 'quarantined', 'kind', 'fk',\n 'reason', format('conta de unidade cuja unidade ainda n\u00E3o migrou (%s #%s)',\n coalesce(v_ref_table, 'companies'), coalesce(v_ref_id::text, '<sem id>')));\n END IF;\n END IF;\n\n IF v_merge_into IS NOT NULL THEN\n UPDATE public.plg_financial_accounts\n SET is_active = is_active OR coalesce((p_row ->> 'is_active')::boolean, true)\n WHERE id = v_merge_into;\n IF v_standalone THEN PERFORM migration._audit(p_tenant, 'public.plg_financial_accounts', v_merge_into, jsonb_build_object('reason', 'merged')); END IF;\n RETURN jsonb_build_object('status', 'migrated', 'target_id', v_merge_into, 'merged', true,\n 'reason', 'conta fundida na conta \u00FAnica da pessoa (o V2 admite uma por pessoa)');\n END IF;\n\n SELECT tenant_id INTO v_existing_tenant FROM public.plg_financial_accounts WHERE id = p_id;\n IF v_existing_tenant IS NOT NULL AND v_existing_tenant <> p_tenant THEN\n RETURN jsonb_build_object('status', 'quarantined', 'kind', 'tenant', 'reason', format('%s already exists in another tenant', p_id));\n END IF;\n IF v_existing_tenant IS NULL THEN\n INSERT INTO public.plg_financial_accounts (id, tenant_id, unit_id, kind, code, name, bank_name, settlement_days, is_active, person_id, metadata)\n VALUES (p_id, p_tenant, v_unit, v_kind,\n p_row ->> 'code', p_row ->> 'name', p_row ->> 'bank_name',\n (p_row ->> 'settlement_days')::integer,\n coalesce((p_row ->> 'is_active')::boolean, true), v_person, v_meta);\n v_created := true;\n ELSE\n UPDATE public.plg_financial_accounts SET\n unit_id = coalesce(v_unit, unit_id),\n kind = CASE WHEN p_row ? 'kind' THEN v_kind ELSE kind END,\n person_id = CASE WHEN p_row ? 'kind' THEN v_person ELSE person_id END,\n code = CASE WHEN p_row ? 'code' THEN p_row ->> 'code' ELSE code END,\n name = CASE WHEN p_row ? 'name' THEN p_row ->> 'name' ELSE name END,\n bank_name = CASE WHEN p_row ? 'bank_name' THEN p_row ->> 'bank_name' ELSE bank_name END,\n settlement_days = CASE WHEN p_row ? 'settlement_days' THEN (p_row ->> 'settlement_days')::integer ELSE settlement_days END,\n is_active = CASE WHEN p_row ? 'is_active' THEN coalesce((p_row ->> 'is_active')::boolean, true) ELSE is_active END,\n metadata = migration._merge_meta(metadata, v_meta)\n WHERE id = p_id;\n END IF;\n IF v_standalone THEN PERFORM migration._audit(p_tenant, 'public.plg_financial_accounts', p_id, jsonb_build_object('reason', CASE WHEN v_created THEN 'created' ELSE 'updated' END)); END IF;\n RETURN jsonb_build_object('status', 'migrated', 'target_id', p_id, 'reason', CASE WHEN v_created THEN 'created' ELSE 'updated' END);\nEND $function$;\n\n\n-- \u2500\u2500 quem pode executar \u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\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-- Fun\u00E7\u00E3o nova nasce execut\u00E1vel por PUBLIC, e `anon`/`authenticated` herdam\n-- dali. Estas s\u00E3o de PORTE: SECURITY DEFINER, escrevem dados de qualquer\n-- inquilino, e uma delas alcan\u00E7\u00E1vel pela chave an\u00F4nima \u00E9 a base inteira\n-- alcan\u00E7\u00E1vel pela chave an\u00F4nima. S\u00F3 o service_role, que \u00E9 quem roda a onda.\nREVOKE ALL ON FUNCTION migration.upsert_fin_account(uuid, uuid, jsonb, jsonb) FROM PUBLIC, anon, authenticated;\nGRANT EXECUTE ON FUNCTION migration.upsert_fin_account(uuid, uuid, jsonb, jsonb) TO service_role;\n";
|
|
@@ -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/package.json
CHANGED
|
@@ -4,7 +4,7 @@
|
|
|
4
4
|
"status": "beta",
|
|
5
5
|
"dependencies": []
|
|
6
6
|
},
|
|
7
|
-
"version": "0.14.
|
|
7
|
+
"version": "0.14.6",
|
|
8
8
|
"description": "Fayz SDK — plugin-financial plugin",
|
|
9
9
|
"type": "module",
|
|
10
10
|
"sideEffects": false,
|
|
@@ -28,9 +28,9 @@
|
|
|
28
28
|
},
|
|
29
29
|
"dependencies": {
|
|
30
30
|
"@tanstack/react-table": "^8.20.0",
|
|
31
|
-
"@fayz-ai/admin": "^0.
|
|
31
|
+
"@fayz-ai/admin": "^0.24.0",
|
|
32
32
|
"@fayz-ai/core": "^0.23.0",
|
|
33
|
-
"@fayz-ai/ui": "^0.
|
|
33
|
+
"@fayz-ai/ui": "^0.24.0"
|
|
34
34
|
},
|
|
35
35
|
"devDependencies": {
|
|
36
36
|
"@types/node": "^20.0.0",
|