fix(parties): readable suggestions from assistant vouchers, auto-build queue, SCB fetch after promotion (#2259)
* fix(parties): readable suggestions from assistant-written vouchers, auto-build queue, SCB fetch after promotion Live feedback on a real company (2026-09-03): the queue showed 35 one-off suggestions with sentence-long names, wide empty rows, a "Hämta förslag" step nobody could predict, no SCB fetch after promotion, and an empty supplier created from a Finansinspektionen fee line. - ledger_key v2 (migration 20260904002000): keep the counterpart head of "<counterpart> · <note>" descriptions, drop bank method tokens and long references before normalising; JS mirror in lib/parties/ledger-key.ts with shared LEDGER_KEY_CASES. Suggested parties nobody has touched are rebuilt under the new keys (repair in the same migration). - apply_party_suggestions attaches by VAT number too, so ledger keys with a VAT number but no org number reach existing roles. - Queue: fixed name/reason column widths, inline "Hitta i företagsregistret" for rows without an org number. - Page: builds the queue automatically on first visit when nothing has been suggested yet; after promotion, fetches SCB facts for every promoted legal person (spaced under the 10 calls/10 s limit) and fills the role's VAT number; confirm dialog says how many rows lack an org number. - Classifier: more authorities (Finansinspektionen, Arbetsförmedlingen, Pensionsmyndigheten, ...) and fee words (registreringsavgift, tillsynsavgift, ...) so fee lines stop becoming suppliers. Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com> * fix(parties): scope the suggestion repair to keys the new ledger_key no longer produces Superagent flagged the repair DELETE as global. It now only removes untouched pipeline suggestions that no posted voucher of the company maps to under the new function; suggestions whose key is unchanged stay. Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com> --------- Co-authored-by: Jakob Wennberg <311770904+jakobwennberg-oss@users.noreply.github.com> Co-authored-by: Claude Fable 5.1 <noreply@anthropic.com>
This commit is contained in:
co-authored by
Claude Fable 5.1
Jakob Wennberg
parent
d670fe6663
commit
91e2c66afc
@@ -38,3 +38,13 @@ describe('classifyKey', () => {
|
||||
expect(classifyKey({ key: '12 34' })).toBe('unsure')
|
||||
})
|
||||
})
|
||||
|
||||
describe('classifyKey on fee descriptions and authorities (prod 2026-09-03)', () => {
|
||||
it('keeps state fees and authorities out of the party queue', async () => {
|
||||
const { classifyKey } = await import('../classify')
|
||||
expect(classifyKey({ key: 'registeringsavgift finansinspektionen', acct: '6991' })).toBe('authority')
|
||||
expect(classifyKey({ key: 'finansinspektionen', acct: '6991' })).toBe('authority')
|
||||
expect(classifyKey({ key: 'utlägg', acct: '5420' })).toBe('category')
|
||||
expect(classifyKey({ key: 'utlägg anthropic', acct: '5420' })).toBe('party')
|
||||
})
|
||||
})
|
||||
|
||||
@@ -4,6 +4,14 @@ import { ledgerKey } from '../ledger-key'
|
||||
// Fixture pairs are shared with tests/pg/observed-parties-rpc.pg.test.ts,
|
||||
// which runs the same inputs through public.ledger_key() and asserts parity.
|
||||
export const LEDGER_KEY_CASES: [string, string][] = [
|
||||
// Descriptions our own booking flows write (seen on prod 2026-09-03).
|
||||
['Utlägg Anthropic · Anthropic PBC, 206,12 EUR inkl. 41,22 EUR VAT-Sweden 25% via OSS.', 'utlägg anthropic'],
|
||||
['1511768101 · Visma Spcs AB, faktura 2025-10-02, programvarulicens/abonnemang', 'visma spcs'],
|
||||
['TIC identity BG 0000005786439 Bg-bet. via internet · Faktura 20250746, The Intelligence Company AB (publ).', 'tic identity'],
|
||||
['Hotel at Booking.com K3667 Kortköp/uttag · Hotell, svenskt boende, 12% moms', 'hotel at bookingcom'],
|
||||
['Utlägg · Utlägg, mjukvara, leverantör ej angiven', 'utlägg'],
|
||||
['1260424603197 Pris betalning', 'pris betalning'],
|
||||
['leverantörsfaktura 20250928, The Intelligence Company AB (publ)', 'the intelligence company publ'],
|
||||
['Levfakt BEIJER BYGGMATERIAL AB (2089)', 'beijer byggmaterial'],
|
||||
['Levfakt Beijer Byggmaterial AB, 097 (1001)', 'beijer byggmaterial'],
|
||||
['Leverantörsfaktura från 18 Loopia, 1009146000', 'loopia'],
|
||||
@@ -35,3 +43,15 @@ describe('ledgerKey', () => {
|
||||
expect(ledgerKey('Levfakt BEIJER BYGGMATERIAL AB (2089)')).toBe(ledgerKey('Levfakt Beijer Byggmaterial AB, 097 (1001)'))
|
||||
})
|
||||
})
|
||||
|
||||
describe('displayNameFromVoucherText on assistant-written descriptions', () => {
|
||||
it('keeps the counterpart, drops the note, method tokens and references', async () => {
|
||||
const { displayNameFromVoucherText } = await import('../ledger-key')
|
||||
expect(displayNameFromVoucherText('Utlägg Anthropic · Anthropic PBC, 206,12 EUR inkl. VAT')).toBe('Utlägg Anthropic')
|
||||
expect(displayNameFromVoucherText('1511768101 · Visma Spcs AB, faktura 2025-10-02')).toBe('Visma Spcs AB')
|
||||
expect(displayNameFromVoucherText('TIC identity BG 0000005786439 Bg-bet. via internet · Faktura 20250746')).toBe('TIC identity')
|
||||
expect(displayNameFromVoucherText('Hotel at Booking.com K3667 Kortköp/uttag · Hotell')).toBe('Hotel at Booking.com')
|
||||
expect(displayNameFromVoucherText('leverantörsfaktura 20250928, The Intelligence Company AB (publ)')).toBe('The Intelligence Company AB (publ)')
|
||||
expect(displayNameFromVoucherText('1260424603197 Pris betalning')).toBe('Pris betalning')
|
||||
})
|
||||
})
|
||||
|
||||
@@ -32,6 +32,7 @@ const STOP = new Set([
|
||||
const GENERIC = [
|
||||
'inköp', 'inkp', 'kvitto', 'kvitton', 'fika', 'diesel', 'bensin', 'bränsle', 'försäkring', 'telefon', 'mobil', 'hyra',
|
||||
'lokalhyra', 'frakt', 'hosting', 'julklapp', 'frimärken', 'utlägg', 'hotell', 'resa', 'resor', 'resekostnader',
|
||||
'registreringsavgift', 'registeringsavgift', 'tillsynsavgift', 'årsavgift', 'medlemsavgift', 'serviceavgift', 'anmälningsavgift', 'expeditionsavgift',
|
||||
'biljett', 'biljetter', 'biljettkostnad', 'taxi', 'taxiresor', 'parkering', 'parkeringsavgifter', 'representation',
|
||||
'måltidsrepresentation', 'kollektivtrafik', 'kollektivtra', 'kontorsmaterial', 'förbrukning', 'förbrukningsmateriel',
|
||||
'frbrukningsmateriel', 'programvara', 'mjukvara', 'licens', 'avgift', 'avgifter', 'traktamente', 'traktamenten',
|
||||
@@ -66,7 +67,7 @@ const PAYROLL = /\b(lön|löner|löne\w*|lneutbetalning|lönebesked|salary|semes
|
||||
const ADJUSTMENT =
|
||||
/(periodisering|omföring|omforing|lagerförändring|lagerforandring|nedskrivning|rättelse|rattelse|kostnadsföring|avskrivning|bokslut|kursdiff|valutakurs|eur till sek|omvänd betalningsskyldighet)/
|
||||
const BANK = /(bankkostnad|bankavgift|banktjänst|baspaket bank|bank årsavg|årsavg|avi överdrag|företagspaket)/
|
||||
const AUTHORITY = /\b(skatteverket|bolagsverket|transportstyrelsen|försäkringskassan|kronofogden|tullverket|skattekonto)\b/
|
||||
const AUTHORITY = /\b(skatteverket|bolagsverket|transportstyrelsen|försäkringskassan|kronofogden|tullverket|skattekonto|finansinspektionen|arbetsförmedlingen|pensionsmyndigheten|migrationsverket|lantmäteriet|csn|polisen|domstol|tingsrätt|förvaltningsrätt)\b/
|
||||
const INTERMEDIARY = /\b(klarna|paypal|zettle|izettle|swish|payex|bankgirot|adyen|nets)\b/
|
||||
|
||||
function acctNum(a: string | null | undefined): number {
|
||||
|
||||
@@ -19,9 +19,30 @@ import { normalizeCounterpartyName } from '@/lib/bookkeeping/counterparty-templa
|
||||
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})+$/
|
||||
const BANK_METHOD = /(kortköp\/uttag|kortkp\/uttag|överföring via internet|bg-bet\.? via internet|pg-bet\.? via internet|bg-bet\.?|autogiro)/gi
|
||||
const GIRO_REFERENCE = /\b(bg|pg)\s*\d{5,}\b/gi
|
||||
const CARD_REFERENCE = /\bk\d{3,6}\b/gi
|
||||
const LONG_DIGITS = /\b\d{6,}\b/g
|
||||
|
||||
/**
|
||||
* What our own booking flows write: "<counterpart> · <note>", bank method
|
||||
* tokens and long references. Keep the head before " · " (or, when the head
|
||||
* has no letters, the text after it up to the first comma), drop the tokens.
|
||||
* Mirrors the pre-clean in the SQL ledger_key (20260904002000).
|
||||
*/
|
||||
export function preClean(raw: string): string {
|
||||
let pre = raw
|
||||
const sep = pre.indexOf(' · ')
|
||||
if (sep >= 0) {
|
||||
let head = pre.slice(0, sep)
|
||||
if (!/\p{L}/u.test(head)) head = pre.slice(sep + 3).split(',')[0] ?? ''
|
||||
pre = head
|
||||
}
|
||||
return pre.replace(BANK_METHOD, ' ').replace(GIRO_REFERENCE, ' ').replace(CARD_REFERENCE, ' ').replace(LONG_DIGITS, ' ')
|
||||
}
|
||||
|
||||
export function ledgerKey(raw: string | null | undefined): string {
|
||||
const k = normalizeCounterpartyName(raw ?? '')
|
||||
const k = normalizeCounterpartyName(preClean(raw ?? ''))
|
||||
if (!k) return ''
|
||||
const stripped = k
|
||||
.replace(AP_PREFIX, '')
|
||||
@@ -67,6 +88,13 @@ const DISPLAY_SUFFIX = /(\s*[,(]\s*\d{1,6}\s*\)?|\s+\d{1,4})+$/
|
||||
* a printed name; nothing here is generated, only removed.
|
||||
*/
|
||||
export function displayNameFromVoucherText(raw: string): string {
|
||||
const cleaned = raw.trim().replace(DISPLAY_PREFIX, '').replace(DISPLAY_SUFFIX, '').trim()
|
||||
const cleaned = preClean(raw)
|
||||
.trim()
|
||||
.replace(DISPLAY_PREFIX, '')
|
||||
.replace(/^\d{6,10}[,\s]+/, '')
|
||||
.replace(DISPLAY_SUFFIX, '')
|
||||
.replace(/\s+/g, ' ')
|
||||
.replace(/^[,\s]+|[,\s]+$/g, '')
|
||||
.trim()
|
||||
return cleaned.length >= 2 ? cleaned : raw.trim()
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user