28f7cefc86
* feat(bulk-book): manual booking mode + document inheritance Two pieces of user feedback from PR #606: 1. "How come it is only mallar? Is it not possible to have manuell bokfoering?" - BulkBookDialog was template-only. Added a Tabs primitive with Mall / Manuell tabs. Manual tab pre-fills lines from the selected txs (one line per tx on 1930 + counterparty placeholder on 3001/5800 by direction), then the user edits Konto / Debet / Kredit / Beskrivning. Live balance + bank-leg checks drive the confirm button - same invariants the RPC enforces server-side. 2. "Documents attached does not follow into the bookkeeping. And if there are two different documents attached, none of them follow." The bulk_book_transactions RPC now propagates each tx's document onto the target verifikat (new in Branch B, existing in Branch A) as verifikationsunderlag. Per BFL 5 kap 6§ + BFNAR 2013:2 kap 4 a verifikat may have multiple underlag; every receipt that justified a tx is now retention-protected on the combined entry. The dialog shows a small count chip ("N bilagor foeljer med") so the user sees what will inherit. Also dropped p_user_id from the RPC signature (round-3 hardening pattern applied consistently across all multi-tx RPCs after PR #607). Caller resolves from auth.uid() inside the function. Schema: BulkBookSchema is now a 3-way XOR (existing_journal_entry_id | template_id+mode | manual_lines), with manual_lines validated as accountNumber + nonNegativeAmount per line. pg-real tests: - doc inheritance into a new combined verifikat (mixed: 2 of 3 txs have docs - docs_linked should be 2, not 3) - doc inheritance into an existing posted verifikat (link branch) - manual lines path (no template expansion artifacts in the resulting JE - just the 2 user lines) - unbalanced manual lines still rejected by BULK_BOOK_UNBALANCED Migration applied to remote. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com> * fix(bulk-book): PR #610 review - pg-real signature, account allowlist, account-number validity Three review findings on PR #610: 1. pg-real failure: 2 link-existing tests still used 5-arg SELECT bulk_book_transactions($1::uuid[], $2, $3, $4, $5) after the userId removal. My earlier replace_all caught only the patterns that had ::jsonb on $3; the link-existing tests pass null for new_entry and used a bare $3 so they slipped through. (Greptile P1) 2. Manual lines bypassed chart_of_accounts validation. A typo or adversarial caller could post to a BAS account that doesn't exist in this company's chart, corrupting the hauptbok and breaking SIE export. Both compliance-swarm (OWASP V2.3) and swedish-compliance flagged this. Added a single-roundtrip allowlist check in the route: query chart_of_accounts for distinct account_numbers in manual_lines and reject with BULK_BOOK_INVALID_ACCOUNT if any are missing or inactive. 3. UI canConfirm guard missed invalid account numbers. Account input allows 1-3 digits and JS string comparison '193' >= '1900' is false, so a 3-digit entry escapes bankLineNet, the bank match could pass via other lines, and the server returned 400 only after submit. Added previewLines.every(l => /^\d{4}$/.test(l.account_number)) to canConfirm so the Confirm button stays disabled inline. (Greptile P2) Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com> * fix(bulk-book): PR #610 round 2 - RPC chart-of-accounts, doc tenant isolation, GRANTs Seven compliance findings from the round-1 bot reviews: Migration (20260602121000_bulk_book_round2_fixes.sql): - RPC chart-of-accounts allowlist (defense-in-depth): every line in p_new_entry.lines is now verified to be an active BAS account for p_company_id. Closes the gap where the template branch and direct DB callers (psql, future MCP) bypassed the route's manual-branch check. Returns BULK_BOOK_INVALID_ACCOUNT with the offending list. (OWASP V8.2.1 + SOC 2 CC6.3) - Document inheritance CTE: added "AND d.company_id = p_company_id" to the UPDATE join so the tenant isolation is enforced on both sides (tx + doc), not just the tx side. Four bots converged on this finding (V1.2.5, A.8.2, CC6.6, swedish-compliance). - Bank-leg range check: "length(account_number) = 4 AND account_number BETWEEN '1900' AND '1999'" replaces the bare lexicographic comparison. Lexicographic-on-4-digit is safe today; the length guard is defense-in-depth against schema drift. (swedish-compliance) - Explicit role grants: REVOKE ALL FROM PUBLIC + GRANT EXECUTE TO authenticated on both bulk_book_transactions and match_batch_allocate. (SOC 2 CC6.1) UI (BulkBookDialog): - Manual-mode prefill no longer suggests a hardcoded 3001/5800 counterpart. Reason (swedish-compliance): a user accepting the prefill could submit a verifikat with no VAT line (26xx), under-reporting utgaaende moms. The bank side stays pre-filled (unambiguous); the counterpart row scaffolds blank for the user to choose. Schema (BulkBookSchema): - manual_lines.debit_amount + credit_amount bounded at 99,999,999 SEK per line. Catches typos before the RPC. (compliance-swarm V4.5) i18n: - docs_inherit_hint terminology: "bilaga" -> "verifikationsunderlag" and an explicit "sparas i 7 ar enligt BFL 7 kap" reminder. swedish-compliance flagged that "bilaga" risks users treating the files as deletable attachments rather than retention-bound raekenskapsinformation. Migration applied to remote. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com> * fix(test): seed chart_of_accounts in bulk-book pg-real seedTenant The round-2 RPC fix added a chart_of_accounts allowlist check inside bulk_book_transactions, but the test fixtures don't seed COA — so every existing test that submits lines (1930, 3001, 2611, etc.) now returns BULK_BOOK_INVALID_ACCOUNT instead of the expected error code. Seed the 8 accounts the suite actually uses directly in seedTenant (cheaper than calling seed_chart_of_accounts which inserts the full BAS 2026 chart). Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com> --------- Co-authored-by: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
356 lines
14 KiB
PL/PgSQL
356 lines
14 KiB
PL/PgSQL
-- PR #608 — bulk_book_transactions: drop p_user_id + propagate documents.
|
|
--
|
|
-- Two changes on top of 20260530120000_bulk_book_transactions.sql:
|
|
--
|
|
-- 1. Drop p_user_id from the function signature (round-3 hardening
|
|
-- pattern applied to match_batch_allocate in PR #607). Caller is
|
|
-- resolved from auth.uid() inside the function.
|
|
--
|
|
-- 2. Propagate document_attachments from each constituent tx onto the
|
|
-- target verifikat (new in Branch B, existing in Branch A). User
|
|
-- feedback on PR #606: "the documents attached does not follow into
|
|
-- the bookkeeping. And if there are two different documents
|
|
-- attached, none of them follow." Per BFL 5 kap 6§ + BFNAR 2013:2
|
|
-- kap 4, a verifikat may have multiple verifikationsunderlag —
|
|
-- every receipt that justified a tx remains evidence for the
|
|
-- combined business event, retention-protected under the same
|
|
-- WORM/7-year guarantees.
|
|
--
|
|
-- Manual-mode UX (BulkBookDialog "Manuell" tab) does NOT need a new RPC
|
|
-- parameter. The existing p_new_entry.lines path accepts arbitrary
|
|
-- caller-supplied lines and validates balance + bank-leg match in the
|
|
-- existing loop. The route swaps out template expansion for user-built
|
|
-- lines when the manual_lines schema branch is taken.
|
|
|
|
DROP FUNCTION IF EXISTS public.bulk_book_transactions(uuid[], uuid, jsonb, uuid, uuid);
|
|
|
|
CREATE OR REPLACE FUNCTION public.bulk_book_transactions(
|
|
p_tx_ids uuid[],
|
|
p_existing_journal_entry_id uuid,
|
|
p_new_entry jsonb,
|
|
p_company_id uuid
|
|
)
|
|
RETURNS jsonb
|
|
LANGUAGE plpgsql
|
|
SECURITY DEFINER
|
|
SET search_path TO 'public'
|
|
AS $$
|
|
DECLARE
|
|
v_tx RECORD;
|
|
v_tx_id uuid;
|
|
v_tx_date date;
|
|
v_total_amount numeric := 0;
|
|
v_total_amount_abs numeric;
|
|
v_direction text;
|
|
v_tx_count int := 0;
|
|
|
|
v_voucher RECORD;
|
|
v_voucher_bank_net numeric := 0;
|
|
|
|
v_fiscal_period_id uuid;
|
|
v_period_is_closed boolean;
|
|
v_period_locked_at timestamptz;
|
|
|
|
v_journal_entry_id uuid;
|
|
v_voucher_series text := 'A';
|
|
v_voucher_number int;
|
|
v_entry_description text;
|
|
|
|
v_line jsonb;
|
|
v_line_account text;
|
|
v_line_debit numeric;
|
|
v_line_credit numeric;
|
|
v_line_currency text;
|
|
v_lines_total_debit numeric := 0;
|
|
v_lines_total_credit numeric := 0;
|
|
v_lines_bank_net numeric := 0;
|
|
v_sort_order int := 0;
|
|
|
|
v_docs_linked int := 0;
|
|
v_target_je uuid;
|
|
|
|
v_now timestamptz := now();
|
|
v_caller uuid := auth.uid();
|
|
BEGIN
|
|
IF v_caller IS NULL THEN
|
|
RETURN jsonb_build_object('ok', false, 'code', 'BULK_BOOK_UNAUTHORIZED');
|
|
END IF;
|
|
IF NOT EXISTS (
|
|
SELECT 1 FROM public.company_members
|
|
WHERE user_id = v_caller AND company_id = p_company_id
|
|
) THEN
|
|
RETURN jsonb_build_object('ok', false, 'code', 'BULK_BOOK_UNAUTHORIZED');
|
|
END IF;
|
|
|
|
IF p_tx_ids IS NULL OR array_length(p_tx_ids, 1) IS NULL THEN
|
|
RETURN jsonb_build_object('ok', false, 'code', 'BULK_BOOK_NO_TXS');
|
|
END IF;
|
|
|
|
IF (p_existing_journal_entry_id IS NULL AND p_new_entry IS NULL)
|
|
OR (p_existing_journal_entry_id IS NOT NULL AND p_new_entry IS NOT NULL) THEN
|
|
RETURN jsonb_build_object('ok', false, 'code', 'BULK_BOOK_INVALID_PAYLOAD');
|
|
END IF;
|
|
|
|
FOR v_tx IN
|
|
SELECT * FROM public.transactions
|
|
WHERE id = ANY(p_tx_ids) AND company_id = p_company_id
|
|
ORDER BY id
|
|
FOR UPDATE
|
|
LOOP
|
|
v_tx_count := v_tx_count + 1;
|
|
IF v_tx.journal_entry_id IS NOT NULL THEN
|
|
RETURN jsonb_build_object('ok', false, 'code', 'BULK_BOOK_TX_ALREADY_BOOKED',
|
|
'details', jsonb_build_object('tx_id', v_tx.id));
|
|
END IF;
|
|
IF EXISTS (
|
|
SELECT 1 FROM public.transaction_voucher_links tvl
|
|
WHERE tvl.transaction_id = v_tx.id
|
|
) THEN
|
|
RETURN jsonb_build_object('ok', false, 'code', 'BULK_BOOK_TX_ALREADY_BOOKED',
|
|
'details', jsonb_build_object('tx_id', v_tx.id, 'via', 'transaction_voucher_links'));
|
|
END IF;
|
|
IF v_tx.amount = 0 THEN
|
|
RETURN jsonb_build_object('ok', false, 'code', 'BULK_BOOK_TX_ZERO_AMOUNT',
|
|
'details', jsonb_build_object('tx_id', v_tx.id));
|
|
END IF;
|
|
|
|
IF v_tx_date IS NULL THEN
|
|
v_tx_date := v_tx.date;
|
|
ELSIF v_tx_date <> v_tx.date THEN
|
|
RETURN jsonb_build_object('ok', false, 'code', 'BULK_BOOK_DATE_MISMATCH',
|
|
'details', jsonb_build_object('first_date', v_tx_date, 'other_date', v_tx.date));
|
|
END IF;
|
|
|
|
IF v_direction IS NULL THEN
|
|
v_direction := CASE WHEN v_tx.amount > 0 THEN 'income' ELSE 'expense' END;
|
|
ELSIF (v_direction = 'income' AND v_tx.amount < 0)
|
|
OR (v_direction = 'expense' AND v_tx.amount > 0) THEN
|
|
RETURN jsonb_build_object('ok', false, 'code', 'BULK_BOOK_DIRECTION_MISMATCH',
|
|
'details', jsonb_build_object('expected', v_direction, 'tx_id', v_tx.id));
|
|
END IF;
|
|
|
|
v_total_amount := v_total_amount + v_tx.amount;
|
|
END LOOP;
|
|
|
|
IF v_tx_count = 0 THEN
|
|
RETURN jsonb_build_object('ok', false, 'code', 'BULK_BOOK_TXS_NOT_FOUND');
|
|
END IF;
|
|
|
|
IF v_tx_count <> COALESCE(array_length(p_tx_ids, 1), 0) THEN
|
|
RETURN jsonb_build_object('ok', false, 'code', 'BULK_BOOK_TXS_NOT_FOUND',
|
|
'details', jsonb_build_object('expected', array_length(p_tx_ids, 1), 'found', v_tx_count));
|
|
END IF;
|
|
|
|
v_total_amount_abs := ABS(v_total_amount);
|
|
|
|
-- ── Branch A: link to existing posted verifikat ──────────────────
|
|
IF p_existing_journal_entry_id IS NOT NULL THEN
|
|
SELECT * INTO v_voucher FROM public.journal_entries
|
|
WHERE id = p_existing_journal_entry_id AND company_id = p_company_id
|
|
FOR UPDATE;
|
|
|
|
IF NOT FOUND THEN
|
|
RETURN jsonb_build_object('ok', false, 'code', 'BULK_BOOK_JE_NOT_FOUND',
|
|
'details', jsonb_build_object('journal_entry_id', p_existing_journal_entry_id));
|
|
END IF;
|
|
|
|
IF v_voucher.status <> 'posted' THEN
|
|
RETURN jsonb_build_object('ok', false, 'code', 'BULK_BOOK_JE_NOT_POSTED',
|
|
'details', jsonb_build_object('status', v_voucher.status));
|
|
END IF;
|
|
|
|
SELECT COALESCE(SUM(debit_amount - credit_amount), 0) INTO v_voucher_bank_net
|
|
FROM public.journal_entry_lines
|
|
WHERE journal_entry_id = p_existing_journal_entry_id
|
|
AND account_number >= '1900' AND account_number <= '1999';
|
|
|
|
IF ABS(v_voucher_bank_net - v_total_amount) > 0.005 THEN
|
|
RETURN jsonb_build_object('ok', false, 'code', 'BULK_BOOK_AMOUNT_MISMATCH',
|
|
'details', jsonb_build_object(
|
|
'tx_sum', v_total_amount, 'voucher_bank_net', v_voucher_bank_net));
|
|
END IF;
|
|
|
|
FOR v_tx IN
|
|
SELECT * FROM public.transactions
|
|
WHERE id = ANY(p_tx_ids) AND company_id = p_company_id
|
|
ORDER BY id
|
|
LOOP
|
|
INSERT INTO public.transaction_voucher_links
|
|
(user_id, company_id, transaction_id, journal_entry_id, allocated_amount, role)
|
|
VALUES
|
|
(v_caller, p_company_id, v_tx.id, p_existing_journal_entry_id, v_tx.amount, 'bank_line');
|
|
END LOOP;
|
|
|
|
IF v_tx_count = 1 THEN
|
|
UPDATE public.transactions
|
|
SET journal_entry_id = p_existing_journal_entry_id,
|
|
reconciliation_method = 'manual',
|
|
is_business = TRUE,
|
|
updated_at = v_now
|
|
WHERE id = p_tx_ids[1];
|
|
ELSE
|
|
UPDATE public.transactions
|
|
SET is_business = TRUE, updated_at = v_now
|
|
WHERE id = ANY(p_tx_ids);
|
|
END IF;
|
|
|
|
-- Carry the existing JE's series/number through the merged return.
|
|
v_target_je := p_existing_journal_entry_id;
|
|
v_voucher_series := v_voucher.voucher_series;
|
|
v_voucher_number := v_voucher.voucher_number;
|
|
|
|
ELSE
|
|
-- ── Branch B: create new combined verifikat ─────────────────────
|
|
v_entry_description := p_new_entry->>'description';
|
|
IF v_entry_description IS NULL OR LENGTH(TRIM(v_entry_description)) = 0 THEN
|
|
RETURN jsonb_build_object('ok', false, 'code', 'BULK_BOOK_MISSING_DESCRIPTION');
|
|
END IF;
|
|
|
|
IF jsonb_typeof(p_new_entry->'lines') IS DISTINCT FROM 'array'
|
|
OR jsonb_array_length(p_new_entry->'lines') < 2 THEN
|
|
RETURN jsonb_build_object('ok', false, 'code', 'BULK_BOOK_NO_LINES');
|
|
END IF;
|
|
|
|
FOR v_line IN SELECT * FROM jsonb_array_elements(p_new_entry->'lines')
|
|
LOOP
|
|
v_line_account := v_line->>'account_number';
|
|
v_line_debit := COALESCE((v_line->>'debit_amount')::numeric, 0);
|
|
v_line_credit := COALESCE((v_line->>'credit_amount')::numeric, 0);
|
|
IF v_line_debit < 0 OR v_line_credit < 0 THEN
|
|
RETURN jsonb_build_object('ok', false, 'code', 'BULK_BOOK_NEGATIVE_LINE',
|
|
'details', jsonb_build_object('account', v_line_account));
|
|
END IF;
|
|
IF v_line_debit > 0 AND v_line_credit > 0 THEN
|
|
RETURN jsonb_build_object('ok', false, 'code', 'BULK_BOOK_BOTH_SIDES_NONZERO',
|
|
'details', jsonb_build_object('account', v_line_account));
|
|
END IF;
|
|
v_lines_total_debit := v_lines_total_debit + v_line_debit;
|
|
v_lines_total_credit := v_lines_total_credit + v_line_credit;
|
|
IF v_line_account >= '1900' AND v_line_account <= '1999' THEN
|
|
v_lines_bank_net := v_lines_bank_net + v_line_debit - v_line_credit;
|
|
END IF;
|
|
END LOOP;
|
|
|
|
IF ABS(v_lines_total_debit - v_lines_total_credit) > 0.005 THEN
|
|
RETURN jsonb_build_object('ok', false, 'code', 'BULK_BOOK_UNBALANCED',
|
|
'details', jsonb_build_object(
|
|
'debit_sum', v_lines_total_debit, 'credit_sum', v_lines_total_credit));
|
|
END IF;
|
|
|
|
IF ABS(v_lines_bank_net - v_total_amount) > 0.005 THEN
|
|
RETURN jsonb_build_object('ok', false, 'code', 'BULK_BOOK_AMOUNT_MISMATCH',
|
|
'details', jsonb_build_object(
|
|
'tx_sum', v_total_amount,
|
|
'lines_bank_net', v_lines_bank_net));
|
|
END IF;
|
|
|
|
SELECT id, is_closed, locked_at INTO v_fiscal_period_id, v_period_is_closed, v_period_locked_at
|
|
FROM public.fiscal_periods
|
|
WHERE company_id = p_company_id AND v_tx_date BETWEEN period_start AND period_end
|
|
ORDER BY period_start DESC LIMIT 1;
|
|
|
|
IF v_fiscal_period_id IS NULL THEN
|
|
RETURN jsonb_build_object('ok', false, 'code', 'BULK_BOOK_NO_FISCAL_PERIOD',
|
|
'details', jsonb_build_object('tx_date', v_tx_date));
|
|
END IF;
|
|
|
|
IF v_period_is_closed OR v_period_locked_at IS NOT NULL THEN
|
|
RETURN jsonb_build_object('ok', false, 'code', 'BULK_BOOK_PERIOD_LOCKED',
|
|
'details', jsonb_build_object('fiscal_period_id', v_fiscal_period_id));
|
|
END IF;
|
|
|
|
v_journal_entry_id := gen_random_uuid();
|
|
|
|
INSERT INTO public.journal_entries
|
|
(id, user_id, company_id, fiscal_period_id, voucher_number, voucher_series,
|
|
entry_date, description, source_type, status)
|
|
VALUES
|
|
(v_journal_entry_id, v_caller, p_company_id, v_fiscal_period_id, 0, v_voucher_series,
|
|
v_tx_date, v_entry_description, 'manual', 'draft');
|
|
|
|
v_sort_order := 0;
|
|
FOR v_line IN SELECT * FROM jsonb_array_elements(p_new_entry->'lines')
|
|
LOOP
|
|
v_line_account := v_line->>'account_number';
|
|
v_line_debit := COALESCE((v_line->>'debit_amount')::numeric, 0);
|
|
v_line_credit := COALESCE((v_line->>'credit_amount')::numeric, 0);
|
|
v_line_currency := COALESCE(v_line->>'currency', 'SEK');
|
|
|
|
INSERT INTO public.journal_entry_lines
|
|
(journal_entry_id, account_number, debit_amount, credit_amount, currency,
|
|
sort_order, line_description)
|
|
VALUES
|
|
(v_journal_entry_id, v_line_account, v_line_debit, v_line_credit, v_line_currency,
|
|
COALESCE((v_line->>'sort_order')::int, v_sort_order),
|
|
v_line->>'line_description');
|
|
|
|
v_sort_order := v_sort_order + 1;
|
|
END LOOP;
|
|
|
|
SELECT voucher_number INTO v_voucher_number
|
|
FROM public.commit_journal_entry(p_company_id, v_journal_entry_id);
|
|
|
|
FOR v_tx IN
|
|
SELECT * FROM public.transactions
|
|
WHERE id = ANY(p_tx_ids) AND company_id = p_company_id
|
|
ORDER BY id
|
|
LOOP
|
|
INSERT INTO public.transaction_voucher_links
|
|
(user_id, company_id, transaction_id, journal_entry_id, allocated_amount, role)
|
|
VALUES
|
|
(v_caller, p_company_id, v_tx.id, v_journal_entry_id, v_tx.amount, 'bank_line');
|
|
END LOOP;
|
|
|
|
IF v_tx_count = 1 THEN
|
|
UPDATE public.transactions
|
|
SET journal_entry_id = v_journal_entry_id,
|
|
is_business = TRUE,
|
|
updated_at = v_now
|
|
WHERE id = p_tx_ids[1];
|
|
ELSE
|
|
UPDATE public.transactions
|
|
SET is_business = TRUE, updated_at = v_now
|
|
WHERE id = ANY(p_tx_ids);
|
|
END IF;
|
|
|
|
v_target_je := v_journal_entry_id;
|
|
END IF;
|
|
|
|
-- ── Document inheritance ─────────────────────────────────────────
|
|
-- Each tx has at most one document_id (1:1 relation enforced
|
|
-- elsewhere). Set those documents' journal_entry_id to the target
|
|
-- verifikat so every receipt that justified a tx is now also
|
|
-- verifikationsunderlag for the combined entry. Only updates docs
|
|
-- whose journal_entry_id is currently NULL — never overwrites an
|
|
-- existing link (BFL document immutability via trigger).
|
|
WITH linked AS (
|
|
UPDATE public.document_attachments AS d
|
|
SET journal_entry_id = v_target_je,
|
|
updated_at = v_now
|
|
FROM public.transactions AS t
|
|
WHERE t.id = ANY(p_tx_ids)
|
|
AND t.company_id = p_company_id
|
|
AND t.document_id = d.id
|
|
AND d.journal_entry_id IS NULL
|
|
RETURNING d.id
|
|
)
|
|
SELECT COUNT(*)::int INTO v_docs_linked FROM linked;
|
|
|
|
RETURN jsonb_build_object(
|
|
'ok', true,
|
|
'mode', CASE WHEN p_existing_journal_entry_id IS NOT NULL THEN 'link_existing' ELSE 'create_new' END,
|
|
'journal_entry_id', v_target_je,
|
|
'voucher_series', v_voucher_series,
|
|
'voucher_number', v_voucher_number,
|
|
'linked_tx_count', v_tx_count,
|
|
'tx_sum', v_total_amount,
|
|
'docs_linked', v_docs_linked
|
|
);
|
|
END;
|
|
$$;
|
|
|
|
COMMENT ON FUNCTION public.bulk_book_transactions(uuid[], uuid, jsonb, uuid) IS
|
|
'Bulk-book N bank transactions sharing the same date into a single combined verifikat (samlingsverifikation per BFL 5 kap 6§). Two branches: link to an existing posted verifikat, or create a new one from caller-supplied lines (template expansion OR manual lines done by the route). Documents attached to constituent txs are propagated onto the target verifikat as additional verifikationsunderlag. Caller resolved via auth.uid().';
|
|
|
|
NOTIFY pgrst, 'reload schema';
|