fix(db): reconcile prod-orphaned migrations blocking Supabase branching (#942)
* fix(db): reconcile prod-orphaned migrations blocking Supabase branching Prod's schema_migrations carries three versions with no committed file on main, leaving the default Supabase branch in MIGRATIONS_FAILED and stopping preview branches from being created: 20260707113729 add_transactions_enrichment (adopted from #927) 20260708120000 ledger_stats_committed_at_lag (adopted from #935) 20260708130000 ledger_deep_context (adopted from #935) Adopt the byte-identical SQL under the exact apply-time versions, plus the matching pg-tests and fixtures for the two RPCs so pg-real stays green: 20260708120000 switches get_ledger_usage_stats' median_booking_lag_days to committed_at, so the existing test now asserts the new behavior. Idempotent (ADD COLUMN IF NOT EXISTS / CREATE OR REPLACE FUNCTION): no-op on prod, clean on fresh replays, no-op on #927/#935's next rebase. The knowledge-page UI/lib/i18n stay in #935. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com> * fix(deps): pin @anthropic-ai/bedrock-sdk to 0.29.1 0.32.0 (grouped dependabot bump #884) broke Bedrock streaming in prod: empty stream / "request ended without sending any chunks", taking down the in-app AI assistant and invoice OCR. Local dev ran the stale 0.29.1 in node_modules, so it only failed on deploys built fresh from the lockfile. Revert to the six-week-stable 0.29.1; creds/region were never the cause (proven AKIA key + eu-west-1). Guard against an accidental re-bump three ways: exact pin (no caret), a dependabot ignore, and a pinned-dep check in scripts/checks/no-new-antipatterns.mjs (check:guards). Unpin only once 0.32.x streaming is verified against Bedrock. See DECISIONS.md.
This commit is contained in:
co-authored by
Claude Opus 4.8
parent
c068638d24
commit
8dde46ad96
@@ -0,0 +1,17 @@
|
||||
-- Third-party transaction enrichment (Gokind counterparty identification).
|
||||
-- Stores a trimmed projection of the enrichment response, not the raw payload:
|
||||
-- { provider, fetched_at, identified, counterparty { id, name, org_numbers,
|
||||
-- logo_url }, industries [], tags [], flags [], payment { subscription,
|
||||
-- vendor_name } }
|
||||
-- Nullable: enrichment is optional and fail-soft; rows ingested while the
|
||||
-- provider is unconfigured or unavailable simply have NULL here.
|
||||
--
|
||||
-- Adopted reconciliation migration: this SQL was applied directly to prod on
|
||||
-- 2026-07-07 (apply-time version 20260707113729) but the file was never
|
||||
-- committed. Committed after the fact so the prod migration ledger and the
|
||||
-- repo agree; prod already has the column, so this only runs on fresh
|
||||
-- branches and staging. See DECISIONS.md 2026-07-08.
|
||||
ALTER TABLE public.transactions
|
||||
ADD COLUMN IF NOT EXISTS enrichment jsonb;
|
||||
|
||||
NOTIFY pgrst, 'reload schema';
|
||||
@@ -0,0 +1,260 @@
|
||||
-- Fix: median_booking_lag_days measures real posting promptness (committed_at),
|
||||
-- not the accounting date (entry_date).
|
||||
--
|
||||
-- The prior version computed median(entry_date - transaction.date). But the
|
||||
-- bank-booking flow dates the voucher (entry_date) TO the transaction date, so
|
||||
-- entry_date - date is ~0 by construction: a tautology, not a signal. Verified
|
||||
-- on prod (company ed461bc1...): entry_date == transaction date for 151 of 152
|
||||
-- booked transactions, so the median came out 0, while the REAL lag,
|
||||
-- committed_at (when the voucher was actually posted) minus the transaction
|
||||
-- date, had median 90 days. This switches the metric to committed_at, the
|
||||
-- honest "how promptly does this company book" signal the field was meant to
|
||||
-- be. committed_at is set by set_committed_at() on draft->posted and is
|
||||
-- non-null for posted entries; the IS NOT NULL guard is belt-and-braces.
|
||||
--
|
||||
-- Everything else is unchanged from 20260708110000 (26xx dominant-contra
|
||||
-- exclusion, storno filter, normalize_counterparty_key keying). Full
|
||||
-- CREATE OR REPLACE, no signature change.
|
||||
--
|
||||
-- pg-test: tests/pg/ledger-usage-stats-rpc.pg.test.ts
|
||||
|
||||
CREATE OR REPLACE FUNCTION public.get_ledger_usage_stats(
|
||||
p_company_id uuid,
|
||||
p_from_date date
|
||||
)
|
||||
RETURNS jsonb
|
||||
LANGUAGE sql
|
||||
STABLE
|
||||
SECURITY INVOKER
|
||||
SET search_path TO 'public'
|
||||
AS $$
|
||||
SELECT jsonb_build_object(
|
||||
'account_usage',
|
||||
(
|
||||
SELECT coalesce(
|
||||
jsonb_agg(
|
||||
jsonb_build_object(
|
||||
'account_number', au.account_number,
|
||||
'account_name', au.account_name,
|
||||
'postings', au.postings,
|
||||
'last_used', au.last_used
|
||||
)
|
||||
ORDER BY au.postings DESC, au.account_number
|
||||
),
|
||||
'[]'::jsonb
|
||||
)
|
||||
FROM (
|
||||
SELECT
|
||||
l.account_number,
|
||||
max(coa.account_name) AS account_name,
|
||||
count(*)::bigint AS postings,
|
||||
max(je.entry_date) AS last_used
|
||||
FROM public.journal_entry_lines l
|
||||
JOIN public.journal_entries je ON je.id = l.journal_entry_id
|
||||
LEFT JOIN public.chart_of_accounts coa
|
||||
ON coa.company_id = p_company_id
|
||||
AND coa.account_number = l.account_number
|
||||
WHERE je.company_id = p_company_id
|
||||
AND je.status = 'posted'
|
||||
-- Stornos annul: counting their swapped lines re-inflates the
|
||||
-- account the correction moved away from. Corrections stay.
|
||||
AND je.source_type <> 'storno'
|
||||
AND je.entry_date >= p_from_date
|
||||
GROUP BY l.account_number
|
||||
ORDER BY count(*) DESC, l.account_number
|
||||
LIMIT 20
|
||||
) au
|
||||
),
|
||||
'counterparty_patterns',
|
||||
(
|
||||
WITH booked AS (
|
||||
SELECT
|
||||
public.normalize_counterparty_key(t.merchant_name) AS counterparty_key,
|
||||
t.merchant_name,
|
||||
t.category,
|
||||
t.journal_entry_id,
|
||||
t.date
|
||||
FROM public.transactions t
|
||||
JOIN public.journal_entries je ON je.id = t.journal_entry_id
|
||||
WHERE t.company_id = p_company_id
|
||||
AND t.journal_entry_id IS NOT NULL
|
||||
AND je.status = 'posted'
|
||||
-- Defensive: no code path should link a transaction to a storno
|
||||
-- (correctEntry relinks to the correction, reverseEntry unlinks),
|
||||
-- but legacy rows may predate the unlink behavior. Corrections are
|
||||
-- deliberately NOT excluded: they are the live booking.
|
||||
AND je.source_type <> 'storno'
|
||||
AND t.merchant_name IS NOT NULL
|
||||
AND trim(t.merchant_name) <> ''
|
||||
AND t.date >= p_from_date
|
||||
),
|
||||
keyed AS (
|
||||
-- All-digit/reference-only merchant labels normalize to '': no
|
||||
-- identity, no pattern.
|
||||
SELECT * FROM booked WHERE counterparty_key <> ''
|
||||
),
|
||||
totals AS (
|
||||
SELECT
|
||||
counterparty_key,
|
||||
mode() WITHIN GROUP (ORDER BY merchant_name) AS display_name,
|
||||
count(*)::bigint AS occurrences,
|
||||
max(date) AS last_booked
|
||||
FROM keyed
|
||||
GROUP BY counterparty_key
|
||||
),
|
||||
dominant_category AS (
|
||||
SELECT DISTINCT ON (counterparty_key)
|
||||
counterparty_key,
|
||||
category,
|
||||
cnt
|
||||
FROM (
|
||||
SELECT counterparty_key, category, count(*)::bigint AS cnt
|
||||
FROM keyed
|
||||
WHERE category IS NOT NULL AND category <> 'uncategorized'
|
||||
GROUP BY counterparty_key, category
|
||||
) c
|
||||
ORDER BY counterparty_key, cnt DESC, category
|
||||
),
|
||||
dominant_account AS (
|
||||
SELECT DISTINCT ON (counterparty_key)
|
||||
counterparty_key,
|
||||
account_number
|
||||
FROM (
|
||||
SELECT b.counterparty_key, l.account_number, count(*)::bigint AS cnt
|
||||
FROM keyed b
|
||||
JOIN public.journal_entry_lines l ON l.journal_entry_id = b.journal_entry_id
|
||||
WHERE l.account_number NOT LIKE '19%'
|
||||
-- Exclude VAT accounts (26xx): on a reverse-charge purchase the
|
||||
-- expense, 2645 and 2614 lines tie, and the account_number
|
||||
-- tiebreak would otherwise pick the low VAT number over the
|
||||
-- expense. 26xx is always moms, never the informative contra.
|
||||
AND l.account_number NOT LIKE '26%'
|
||||
GROUP BY b.counterparty_key, l.account_number
|
||||
) a
|
||||
ORDER BY counterparty_key, cnt DESC, account_number
|
||||
)
|
||||
SELECT coalesce(
|
||||
jsonb_agg(
|
||||
jsonb_build_object(
|
||||
'counterparty', t.display_name,
|
||||
'counterparty_key', t.counterparty_key,
|
||||
'occurrences', t.occurrences,
|
||||
'last_booked', t.last_booked,
|
||||
'dominant_category', dc.category,
|
||||
'dominant_category_count', coalesce(dc.cnt, 0),
|
||||
'dominant_account_number', da.account_number
|
||||
)
|
||||
ORDER BY t.occurrences DESC, t.display_name
|
||||
),
|
||||
'[]'::jsonb
|
||||
)
|
||||
FROM (
|
||||
SELECT * FROM totals ORDER BY occurrences DESC, display_name LIMIT 25
|
||||
) t
|
||||
LEFT JOIN dominant_category dc ON dc.counterparty_key = t.counterparty_key
|
||||
LEFT JOIN dominant_account da ON da.counterparty_key = t.counterparty_key
|
||||
),
|
||||
'supplier_patterns',
|
||||
(
|
||||
-- AP-side booking patterns: bank-transaction patterns only see rows
|
||||
-- with a merchant_name, so an invoice-heavy company would be half
|
||||
-- blind without this. Supplier identity here is exact (FK), no
|
||||
-- normalization needed.
|
||||
WITH sinv AS (
|
||||
SELECT si.id, si.supplier_id, s.name AS supplier_name,
|
||||
si.invoice_date, si.vat_treatment
|
||||
FROM public.supplier_invoices si
|
||||
JOIN public.suppliers s ON s.id = si.supplier_id
|
||||
WHERE si.company_id = p_company_id
|
||||
AND si.invoice_date >= p_from_date
|
||||
-- Reversed bookings and credited invoices are undone business;
|
||||
-- credit notes repeat their original's accounts with flipped sign.
|
||||
AND si.status NOT IN ('reversed', 'credited')
|
||||
AND si.is_credit_note = false
|
||||
),
|
||||
totals AS (
|
||||
SELECT
|
||||
supplier_id,
|
||||
max(supplier_name) AS supplier_name,
|
||||
count(*)::bigint AS invoices,
|
||||
max(invoice_date) AS last_invoice,
|
||||
mode() WITHIN GROUP (ORDER BY vat_treatment) AS dominant_vat
|
||||
FROM sinv
|
||||
GROUP BY supplier_id
|
||||
),
|
||||
dominant_account AS (
|
||||
-- Invoices (not lines) touching each account, so a many-line invoice
|
||||
-- does not outvote ten single-line ones.
|
||||
SELECT DISTINCT ON (supplier_id)
|
||||
supplier_id,
|
||||
account_number,
|
||||
cnt
|
||||
FROM (
|
||||
SELECT v.supplier_id, i.account_number, count(DISTINCT v.id)::bigint AS cnt
|
||||
FROM sinv v
|
||||
JOIN public.supplier_invoice_items i ON i.supplier_invoice_id = v.id
|
||||
GROUP BY v.supplier_id, i.account_number
|
||||
) a
|
||||
ORDER BY supplier_id, cnt DESC, account_number
|
||||
)
|
||||
SELECT coalesce(
|
||||
jsonb_agg(
|
||||
jsonb_build_object(
|
||||
'supplier', t.supplier_name,
|
||||
'invoices', t.invoices,
|
||||
'last_invoice', t.last_invoice,
|
||||
'vat_treatment', t.dominant_vat,
|
||||
'dominant_account_number', da.account_number,
|
||||
'dominant_account_count', coalesce(da.cnt, 0)
|
||||
)
|
||||
ORDER BY t.invoices DESC, t.supplier_name
|
||||
),
|
||||
'[]'::jsonb
|
||||
)
|
||||
FROM (
|
||||
SELECT * FROM totals ORDER BY invoices DESC, supplier_name LIMIT 15
|
||||
) t
|
||||
LEFT JOIN dominant_account da ON da.supplier_id = t.supplier_id
|
||||
),
|
||||
'vat_treatments_used',
|
||||
(
|
||||
SELECT coalesce(jsonb_agg(DISTINCT vt), '[]'::jsonb)
|
||||
FROM (
|
||||
SELECT i.vat_treatment AS vt
|
||||
FROM public.invoices i
|
||||
WHERE i.company_id = p_company_id
|
||||
AND i.invoice_date >= p_from_date
|
||||
AND i.vat_treatment IS NOT NULL
|
||||
UNION
|
||||
SELECT si.vat_treatment AS vt
|
||||
FROM public.supplier_invoices si
|
||||
WHERE si.company_id = p_company_id
|
||||
AND si.invoice_date >= p_from_date
|
||||
AND si.vat_treatment IS NOT NULL
|
||||
) treatments
|
||||
),
|
||||
'median_booking_lag_days',
|
||||
(
|
||||
-- committed_at (when the voucher was POSTED), not entry_date (the
|
||||
-- accounting date the bank flow sets to the transaction date, making
|
||||
-- entry_date - date a ~0 tautology). This is real posting promptness.
|
||||
SELECT percentile_cont(0.5) WITHIN GROUP (ORDER BY (je.committed_at::date - t.date))
|
||||
FROM public.transactions t
|
||||
JOIN public.journal_entries je ON je.id = t.journal_entry_id
|
||||
WHERE t.company_id = p_company_id
|
||||
AND je.status = 'posted'
|
||||
-- A legacy transaction still linked to a storno (a reversal posted
|
||||
-- long after the transaction) would inject a spurious large lag into
|
||||
-- the very metric this migration exists to make honest. Exclude it,
|
||||
-- matching account_usage and counterparty_patterns.
|
||||
AND je.source_type <> 'storno'
|
||||
AND je.committed_at IS NOT NULL
|
||||
AND t.date >= p_from_date
|
||||
)
|
||||
);
|
||||
$$;
|
||||
|
||||
REVOKE ALL ON FUNCTION public.get_ledger_usage_stats(uuid, date) FROM PUBLIC, anon;
|
||||
GRANT EXECUTE ON FUNCTION public.get_ledger_usage_stats(uuid, date) TO authenticated, service_role;
|
||||
|
||||
NOTIFY pgrst, 'reload schema';
|
||||
@@ -0,0 +1,211 @@
|
||||
-- RPC: get_ledger_deep_context — the deep, entity-resolved analysis behind the
|
||||
-- "Vad din agent vet" page. Where get_ledger_usage_stats is the light,
|
||||
-- 12-month digest the agent reads at session start, THIS is the full-history
|
||||
-- deep pass: it merges counterparties across name variants, mines the booked
|
||||
-- verifikat for real spend, and detects recurrence.
|
||||
--
|
||||
-- For each counterparty entity (bank-feed side) and supplier entity (AP side)
|
||||
-- it returns:
|
||||
-- name display label (modal raw variant)
|
||||
-- key the normalized identity key (counterparties) / supplier id
|
||||
-- variants up to 8 distinct raw labels that merged into this entity
|
||||
-- variant_count true number of distinct raw labels
|
||||
-- occurrences number of bookings
|
||||
-- total_amount total paid (gross, sum of abs bank amount / invoice total)
|
||||
-- first_seen/last_seen
|
||||
-- cadence_days median gap between distinct booking dates (null if <2)
|
||||
-- dominant_account_number + dominant_account_share (consistency 0..1)
|
||||
-- dominant_vat (supplier side; counterparties carry it via the light RPC)
|
||||
--
|
||||
-- Determinism: entities merge by normalize_counterparty_key() (the SQL mirror
|
||||
-- of normalizeCounterpartyName), which collapses "KLARNA AB" / "SWISH KLARNA
|
||||
-- AB" / "KORTKÖP KLARNA AB 2026-07-01" into one "klarna". Cross-name merges
|
||||
-- that need the counterparty bank account or fuzzy matching ("Klarna" +
|
||||
-- "Klarna Bank AB") are the persistent-entity-table layer
|
||||
-- (bank_transaction_ai_normalization.md, deferred); this is the deterministic
|
||||
-- read-side v1.
|
||||
--
|
||||
-- Storno excluded (source_type <> 'storno'); corrections kept (they are the
|
||||
-- live booking, the transaction is relinked to them). 26xx VAT and 19xx bank
|
||||
-- are excluded from the dominant contra pick, same asymmetry as the light RPC.
|
||||
--
|
||||
-- p_from_date NULL = full history; a date bounds the lookback (large tenants
|
||||
-- pass a bound; the cache layer is deferred, same "measure before enforce"
|
||||
-- posture as the light RPC).
|
||||
--
|
||||
-- SECURITY INVOKER: RLS on the base tables scopes to the caller's company.
|
||||
--
|
||||
-- pg-test: tests/pg/ledger-deep-context-rpc.pg.test.ts
|
||||
|
||||
CREATE OR REPLACE FUNCTION public.get_ledger_deep_context(
|
||||
p_company_id uuid,
|
||||
p_from_date date DEFAULT NULL
|
||||
)
|
||||
RETURNS jsonb
|
||||
LANGUAGE sql
|
||||
STABLE
|
||||
SECURITY INVOKER
|
||||
SET search_path TO 'public'
|
||||
AS $$
|
||||
SELECT jsonb_build_object(
|
||||
'counterparty_entities',
|
||||
(
|
||||
WITH booked AS (
|
||||
SELECT
|
||||
public.normalize_counterparty_key(t.merchant_name) AS k,
|
||||
t.merchant_name,
|
||||
t.journal_entry_id,
|
||||
t.date,
|
||||
-- SEK: amount_sek carries the converted value on foreign-currency
|
||||
-- rows and is null on SEK rows, so coalesce keeps everything in SEK
|
||||
-- (mirrors the supplier side's coalesce(total_sek, total)).
|
||||
abs(coalesce(t.amount_sek, t.amount)) AS amount
|
||||
FROM public.transactions t
|
||||
JOIN public.journal_entries je ON je.id = t.journal_entry_id
|
||||
WHERE t.company_id = p_company_id
|
||||
AND t.journal_entry_id IS NOT NULL
|
||||
AND je.status = 'posted'
|
||||
AND je.source_type <> 'storno'
|
||||
AND t.merchant_name IS NOT NULL
|
||||
AND trim(t.merchant_name) <> ''
|
||||
AND (p_from_date IS NULL OR t.date >= p_from_date)
|
||||
),
|
||||
keyed AS (SELECT * FROM booked WHERE k <> ''),
|
||||
-- Median gap between distinct booking dates -> recurrence cadence.
|
||||
distinct_dates AS (SELECT DISTINCT k, date FROM keyed),
|
||||
gaps AS (
|
||||
SELECT k, (date - lag(date) OVER (PARTITION BY k ORDER BY date)) AS gap
|
||||
FROM distinct_dates
|
||||
),
|
||||
recur AS (
|
||||
SELECT k, round(percentile_cont(0.5) WITHIN GROUP (ORDER BY gap))::int AS cadence_days
|
||||
FROM gaps WHERE gap IS NOT NULL GROUP BY k
|
||||
),
|
||||
-- Dominant contra account + its share, over the entity's verifikat lines.
|
||||
acct_counts AS (
|
||||
SELECT b.k, l.account_number, count(*)::bigint AS cnt
|
||||
FROM keyed b
|
||||
JOIN public.journal_entry_lines l ON l.journal_entry_id = b.journal_entry_id
|
||||
WHERE l.account_number NOT LIKE '19%'
|
||||
AND l.account_number NOT LIKE '26%'
|
||||
GROUP BY b.k, l.account_number
|
||||
),
|
||||
acct_totals AS (SELECT k, sum(cnt) AS total FROM acct_counts GROUP BY k),
|
||||
dominant_account AS (
|
||||
SELECT DISTINCT ON (ac.k) ac.k, ac.account_number, ac.cnt, at.total
|
||||
FROM acct_counts ac JOIN acct_totals at ON at.k = ac.k
|
||||
ORDER BY ac.k, ac.cnt DESC, ac.account_number
|
||||
),
|
||||
agg AS (
|
||||
SELECT
|
||||
k,
|
||||
mode() WITHIN GROUP (ORDER BY merchant_name) AS display_name,
|
||||
count(*)::bigint AS occurrences,
|
||||
count(DISTINCT merchant_name)::int AS variant_count,
|
||||
(array_agg(DISTINCT merchant_name))[1:8] AS variants,
|
||||
sum(amount) AS total_amount,
|
||||
min(date) AS first_seen,
|
||||
max(date) AS last_seen
|
||||
FROM keyed GROUP BY k
|
||||
)
|
||||
SELECT coalesce(
|
||||
jsonb_agg(
|
||||
jsonb_build_object(
|
||||
'name', a.display_name,
|
||||
'key', a.k,
|
||||
'variants', to_jsonb(a.variants),
|
||||
'variant_count', a.variant_count,
|
||||
'occurrences', a.occurrences,
|
||||
'total_amount', round(a.total_amount)::bigint,
|
||||
'first_seen', a.first_seen,
|
||||
'last_seen', a.last_seen,
|
||||
'cadence_days', r.cadence_days,
|
||||
'dominant_account_number', da.account_number,
|
||||
'dominant_account_share',
|
||||
CASE WHEN da.total > 0 THEN round(da.cnt::numeric / da.total, 2) ELSE NULL END
|
||||
)
|
||||
ORDER BY a.occurrences DESC, a.total_amount DESC, a.display_name
|
||||
),
|
||||
'[]'::jsonb
|
||||
)
|
||||
FROM (SELECT * FROM agg ORDER BY occurrences DESC, total_amount DESC, display_name LIMIT 40) a
|
||||
LEFT JOIN recur r ON r.k = a.k
|
||||
LEFT JOIN dominant_account da ON da.k = a.k
|
||||
),
|
||||
'supplier_entities',
|
||||
(
|
||||
WITH sinv AS (
|
||||
SELECT si.id, si.supplier_id, s.name AS supplier_name,
|
||||
si.invoice_date, si.vat_treatment,
|
||||
coalesce(si.total_sek, si.total, 0) AS amount
|
||||
FROM public.supplier_invoices si
|
||||
JOIN public.suppliers s ON s.id = si.supplier_id
|
||||
WHERE si.company_id = p_company_id
|
||||
AND si.status NOT IN ('reversed', 'credited')
|
||||
AND si.is_credit_note = false
|
||||
AND (p_from_date IS NULL OR si.invoice_date >= p_from_date)
|
||||
),
|
||||
distinct_dates AS (SELECT DISTINCT supplier_id, invoice_date FROM sinv),
|
||||
gaps AS (
|
||||
SELECT supplier_id,
|
||||
(invoice_date - lag(invoice_date) OVER (PARTITION BY supplier_id ORDER BY invoice_date)) AS gap
|
||||
FROM distinct_dates
|
||||
),
|
||||
recur AS (
|
||||
SELECT supplier_id, round(percentile_cont(0.5) WITHIN GROUP (ORDER BY gap))::int AS cadence_days
|
||||
FROM gaps WHERE gap IS NOT NULL GROUP BY supplier_id
|
||||
),
|
||||
acct_counts AS (
|
||||
SELECT v.supplier_id, i.account_number, count(DISTINCT v.id)::bigint AS cnt
|
||||
FROM sinv v JOIN public.supplier_invoice_items i ON i.supplier_invoice_id = v.id
|
||||
GROUP BY v.supplier_id, i.account_number
|
||||
),
|
||||
acct_totals AS (SELECT supplier_id, sum(cnt) AS total FROM acct_counts GROUP BY supplier_id),
|
||||
dominant_account AS (
|
||||
SELECT DISTINCT ON (ac.supplier_id) ac.supplier_id, ac.account_number, ac.cnt, at.total
|
||||
FROM acct_counts ac JOIN acct_totals at ON at.supplier_id = ac.supplier_id
|
||||
ORDER BY ac.supplier_id, ac.cnt DESC, ac.account_number
|
||||
),
|
||||
agg AS (
|
||||
SELECT
|
||||
supplier_id,
|
||||
max(supplier_name) AS supplier_name,
|
||||
count(*)::bigint AS occurrences,
|
||||
sum(amount) AS total_amount,
|
||||
min(invoice_date) AS first_seen,
|
||||
max(invoice_date) AS last_seen,
|
||||
mode() WITHIN GROUP (ORDER BY vat_treatment) AS dominant_vat
|
||||
FROM sinv GROUP BY supplier_id
|
||||
)
|
||||
SELECT coalesce(
|
||||
jsonb_agg(
|
||||
jsonb_build_object(
|
||||
'name', a.supplier_name,
|
||||
'key', a.supplier_id::text,
|
||||
'variants', to_jsonb(ARRAY[a.supplier_name]),
|
||||
'variant_count', 1,
|
||||
'occurrences', a.occurrences,
|
||||
'total_amount', round(a.total_amount)::bigint,
|
||||
'first_seen', a.first_seen,
|
||||
'last_seen', a.last_seen,
|
||||
'cadence_days', r.cadence_days,
|
||||
'dominant_account_number', da.account_number,
|
||||
'dominant_account_share',
|
||||
CASE WHEN da.total > 0 THEN round(da.cnt::numeric / da.total, 2) ELSE NULL END,
|
||||
'dominant_vat', a.dominant_vat
|
||||
)
|
||||
ORDER BY a.occurrences DESC, a.total_amount DESC, a.supplier_name
|
||||
),
|
||||
'[]'::jsonb
|
||||
)
|
||||
FROM (SELECT * FROM agg ORDER BY occurrences DESC, total_amount DESC, supplier_name LIMIT 20) a
|
||||
LEFT JOIN recur r ON r.supplier_id = a.supplier_id
|
||||
LEFT JOIN dominant_account da ON da.supplier_id = a.supplier_id
|
||||
)
|
||||
);
|
||||
$$;
|
||||
|
||||
REVOKE ALL ON FUNCTION public.get_ledger_deep_context(uuid, date) FROM PUBLIC, anon;
|
||||
GRANT EXECUTE ON FUNCTION public.get_ledger_deep_context(uuid, date) TO authenticated, service_role;
|
||||
|
||||
NOTIFY pgrst, 'reload schema';
|
||||
Reference in New Issue
Block a user