Migrants arrive with vouchers, not bank transactions, so the bank-keyed ledger context is empty for them. This adds the description-keyed twin. - public.ledger_key(text): legibility key on top of the frozen normalize_counterparty_key mirror: strips AP-register prefixes (levfakt, leverantörsfaktura från N, levbet, faktura, kvitto, utgift), the supplier number that follows them, and trailing 1-3 digit runs, never "inköp". Mirrored by lib/parties/ledger-key.ts; the pair is pinned by a shared fixture list in the pg test. - public.get_observed_parties(company, from_date, limit): posted vouchers grouped by ledger_key(description) with occurrences, variants, expense and revenue SEK from the lines, first/last seen, median cadence and the Laplace-smoothed dominant result account. Excludes storno, opening balance, year-end and VAT settlement, and vouchers that carry a bank merchant name (those stay with get_ledger_deep_context). SECURITY INVOKER, so RLS scopes it. Never stored. - lib/parties/classify.ts: the deterministic pre-classifier moved out of the evaluation script so product and evaluation share one implementation (0.965 agreement with the founder labels, party recall 0.99). - lib/parties/observed.ts: RPC wrapper that classifies each row and derives a display rhythm from the cadence. Co-authored-by: Jakob Wennberg <311770904+jakobwennberg-oss@users.noreply.github.com> Co-authored-by: Claude Fable 5.1 <noreply@anthropic.com>
34 lines
1.4 KiB
TypeScript
34 lines
1.4 KiB
TypeScript
import { normalizeCounterpartyName } from '@/lib/bookkeeping/counterparty-templates'
|
|
|
|
/**
|
|
* Legibility key for a voucher description: the identity string an observed
|
|
* party is grouped and displayed by.
|
|
*
|
|
* Built on top of normalizeCounterpartyName() (mirrored in SQL by
|
|
* normalize_counterparty_key) and adds the stages the AP registers of Fortnox,
|
|
* Visma and BL make necessary: "Levfakt BEIJER BYGGMATERIAL AB (2089)" and
|
|
* "Levfakt Beijer Byggmaterial AB, 097" must land on one key,
|
|
* "Leverantörsfaktura från 18 Loopia" on "loopia".
|
|
*
|
|
* Mirrored in SQL by public.ledger_key() (migration 20260902170000) and pinned
|
|
* by tests/pg/observed-parties-rpc.pg.test.ts. Change both or neither.
|
|
*
|
|
* "inköp" is deliberately not a stripped prefix: it turns the generic
|
|
* "inköp av varor" into a vendor-looking "varor" (measured 2026-07-27).
|
|
*/
|
|
const AP_PREFIX = /^(levfakt|levfkt|leverantörsfaktura från|leverantörsfaktura|levbet|faktura|kvitto|utgift)\s+/
|
|
const LEADING_SUPPLIER_NUMBER = /^\d{1,5}\s+/
|
|
const TRAILING_SHORT_DIGITS = /(\s+\d{1,3})+$/
|
|
|
|
export function ledgerKey(raw: string | null | undefined): string {
|
|
const k = normalizeCounterpartyName(raw ?? '')
|
|
if (!k) return ''
|
|
const stripped = k
|
|
.replace(AP_PREFIX, '')
|
|
.replace(LEADING_SUPPLIER_NUMBER, '')
|
|
.replace(TRAILING_SHORT_DIGITS, '')
|
|
.replace(/\s+/g, ' ')
|
|
.trim()
|
|
return stripped === '' ? k : stripped
|
|
}
|