* 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>
340 lines
19 KiB
TypeScript
340 lines
19 KiB
TypeScript
import { randomUUID } from 'node:crypto'
|
|
import { describe, expect, it } from 'vitest'
|
|
import { getPool, withUserContext } from './setup'
|
|
import { insertPostedJournalEntry, seedCompany } from './fixtures'
|
|
|
|
const ORG = '5564300142'
|
|
const OTHER_ORG = '5560125790'
|
|
const LOOPIA_ORG = '5566661012'
|
|
|
|
const expense = (account: string, amount: number) => [
|
|
{ accountNumber: account, debitAmount: amount, creditAmount: 0 },
|
|
{ accountNumber: '2440', debitAmount: 0, creditAmount: amount },
|
|
]
|
|
|
|
async function linkDocument(params: {
|
|
companyId: string
|
|
userId: string
|
|
journalEntryId: string
|
|
supplier: Record<string, string | null>
|
|
}): Promise<string> {
|
|
const id = randomUUID()
|
|
await getPool().query(
|
|
`INSERT INTO public.document_attachments (id, company_id, user_id, uploaded_by, storage_path, file_name, sha256_hash, mime_type, upload_source, journal_entry_id, extracted_data)
|
|
VALUES ($1, $2, $3, $3, $4, 'f.pdf', md5($4), 'application/pdf', 'file_upload', $5, $6::jsonb)`,
|
|
[id, params.companyId, params.userId, `docs/${id}.pdf`, params.journalEntryId, JSON.stringify({ supplier: params.supplier })],
|
|
)
|
|
return id
|
|
}
|
|
|
|
interface Evidence {
|
|
key: string
|
|
docs: number
|
|
self_docs: number
|
|
orgs: Array<{ org: string; n: number }>
|
|
vat_numbers: Array<{ vat: string; n: number }>
|
|
names: Array<{ name: string; n: number }>
|
|
bankgiro: Array<{ value: string; n: number; first_seen: string; last_seen: string }>
|
|
plusgiro: Array<{ value: string; n: number }>
|
|
}
|
|
|
|
async function evidence(companyId: string, userId: string): Promise<Evidence[]> {
|
|
return withUserContext(userId, async (client) => {
|
|
const { rows } = await client.query<{ r: Evidence[] }>(`SELECT public.get_ledger_key_evidence($1) AS r`, [companyId])
|
|
return rows[0]!.r
|
|
})
|
|
}
|
|
|
|
// Writes run on the pool connection (service presentation: auth.uid() NULL,
|
|
// committed). withUserContext rolls back and is used for RLS assertions only.
|
|
async function apply(companyId: string, userId: string, items: unknown[]) {
|
|
const { rows } = await getPool().query<{ r: Record<string, number> }>(
|
|
`SELECT public.apply_party_suggestions($1, $2, $3::jsonb) AS r`,
|
|
[companyId, userId, JSON.stringify(items)],
|
|
)
|
|
return rows[0]!.r
|
|
}
|
|
|
|
async function decide(companyId: string, userId: string, ids: string[], kind: string, note: string | null = null): Promise<number> {
|
|
const { rows } = await getPool().query<{ n: number }>(
|
|
`SELECT public.decide_parties($1, $2, $3::uuid[], $4, $5) AS n`,
|
|
[companyId, userId, ids, kind, note],
|
|
)
|
|
return rows[0]!.n
|
|
}
|
|
|
|
describe('get_ledger_key_evidence (pg)', () => {
|
|
it('aggregates hard keys per ledger key and counts own-org documents as self_docs only', async () => {
|
|
const c = await seedCompany()
|
|
await getPool().query(`UPDATE public.companies SET org_number = '556430-0142' WHERE id = $1`, [c.companyId])
|
|
const base = { userId: c.userId, companyId: c.companyId, fiscalPeriodId: c.fiscalPeriodId, sourceType: 'import' }
|
|
const e1 = await insertPostedJournalEntry({ ...base, entryDate: '2026-01-10', description: 'Levfakt Loopia AB (17)', lines: expense('6540', 100) })
|
|
const e2 = await insertPostedJournalEntry({ ...base, entryDate: '2026-02-10', description: 'Levfakt Loopia AB (17)', lines: expense('6540', 100) })
|
|
const e3 = await insertPostedJournalEntry({ ...base, entryDate: '2026-03-10', description: 'Levfakt Loopia AB (17)', lines: expense('6540', 100) })
|
|
await linkDocument({ ...c, journalEntryId: e1, supplier: { name: 'Loopia AB', orgNumber: '556666-1012', vatNumber: 'SE556666101201', bankgiro: '5317-0900', plusgiro: null } })
|
|
await linkDocument({ ...c, journalEntryId: e2, supplier: { name: 'Loopia AB', orgNumber: LOOPIA_ORG, vatNumber: 'SE556666101201', bankgiro: '53170900', plusgiro: null } })
|
|
// Own sales invoice uploaded as underlag: the company's own org number.
|
|
await linkDocument({ ...c, journalEntryId: e3, supplier: { name: 'Me AB', orgNumber: ORG, vatNumber: null, bankgiro: '11112222', plusgiro: null } })
|
|
|
|
const rows = await evidence(c.companyId, c.userId)
|
|
const loopia = rows.find((r) => r.key === 'loopia')
|
|
expect(loopia).toBeDefined()
|
|
expect(loopia!.docs).toBe(3)
|
|
expect(loopia!.self_docs).toBe(1)
|
|
expect(loopia!.orgs).toEqual([{ org: LOOPIA_ORG, n: 2 }])
|
|
expect(loopia!.vat_numbers).toEqual([{ vat: 'SE556666101201', n: 2 }])
|
|
expect(loopia!.names).toEqual([{ name: 'Loopia AB', n: 2 }])
|
|
expect(loopia!.bankgiro).toEqual([{ value: '53170900', n: 2, first_seen: '2026-01-10', last_seen: '2026-02-10' }])
|
|
expect(loopia!.plusgiro).toEqual([])
|
|
})
|
|
|
|
it('keeps hard keys for a company that has no org number of its own', async () => {
|
|
const c = await seedCompany()
|
|
await getPool().query(`UPDATE public.companies SET org_number = NULL WHERE id = $1`, [c.companyId])
|
|
const e = await insertPostedJournalEntry({ userId: c.userId, companyId: c.companyId, fiscalPeriodId: c.fiscalPeriodId, sourceType: 'import', description: 'Levfakt Loopia AB', lines: expense('6540', 100) })
|
|
await linkDocument({ ...c, journalEntryId: e, supplier: { name: 'Loopia AB', orgNumber: '556666-1012', vatNumber: null, bankgiro: '5317-0900', plusgiro: null } })
|
|
const rows = await evidence(c.companyId, c.userId)
|
|
const loopia = rows.find((r) => r.key === 'loopia')
|
|
expect(loopia).toBeDefined()
|
|
expect(loopia!.self_docs).toBe(0)
|
|
expect(loopia!.orgs).toEqual([{ org: LOOPIA_ORG, n: 1 }])
|
|
expect(loopia!.names).toEqual([{ name: 'Loopia AB', n: 1 }])
|
|
expect(loopia!.bankgiro.map((b) => b.value)).toEqual(['53170900'])
|
|
})
|
|
|
|
it('is invisible across companies', async () => {
|
|
const mine = await seedCompany()
|
|
const theirs = await seedCompany()
|
|
const e = await insertPostedJournalEntry({ userId: theirs.userId, companyId: theirs.companyId, fiscalPeriodId: theirs.fiscalPeriodId, sourceType: 'import', description: 'Levfakt Loopia AB', lines: expense('6540', 100) })
|
|
await linkDocument({ ...theirs, journalEntryId: e, supplier: { name: 'Loopia AB', orgNumber: LOOPIA_ORG, vatNumber: null, bankgiro: null, plusgiro: null } })
|
|
expect(await evidence(theirs.companyId, mine.userId)).toEqual([])
|
|
})
|
|
})
|
|
|
|
describe('apply_party_suggestions (pg)', () => {
|
|
it('inserts suggested parties, attaches by org and by alias key, and is idempotent', async () => {
|
|
const c = await seedCompany()
|
|
const item = (over: Record<string, unknown>) => ({
|
|
key: 'loopia',
|
|
display_name: 'Loopia AB',
|
|
kind: 'company',
|
|
origin: 'document',
|
|
alias_keys: ['loopia'],
|
|
reason: { attach: 'new', occurrences: 3 },
|
|
facts: [{ field: 'dominant_account', value: { account: '6540' }, source: 'ledger' }],
|
|
identities: [{ scheme: 'bankgiro', value: '53170900', first_seen: '2026-01-10', last_seen: '2026-02-10', seen_count: 1 }],
|
|
...over,
|
|
})
|
|
|
|
const first = await apply(c.companyId, c.userId, [item({ org_number: '556666-1012' })])
|
|
expect(first).toEqual({ created: 1, attached: 0, identities: 1, facts: 1 })
|
|
const party = await getPool().query<{ id: string; status: string; org_number: string; alias_keys: string[]; suggested_reason: { attach: string }; origin: string }>(
|
|
`SELECT id, status, org_number, alias_keys, suggested_reason, origin FROM public.parties WHERE company_id = $1`,
|
|
[c.companyId],
|
|
)
|
|
expect(party.rows).toHaveLength(1)
|
|
expect(party.rows[0]).toMatchObject({ status: 'suggested', org_number: LOOPIA_ORG, alias_keys: ['loopia'], origin: 'document' })
|
|
expect(party.rows[0]!.suggested_reason.attach).toBe('new')
|
|
const partyId = party.rows[0]!.id
|
|
|
|
// Same org under another key: attaches, unions aliases, promotes the identity to known.
|
|
const second = await apply(c.companyId, c.userId, [
|
|
item({ key: 'loopia webbhotell', alias_keys: ['loopia webbhotell'], org_number: LOOPIA_ORG, identities: [{ scheme: 'bankgiro', value: '53170900', first_seen: '2026-03-10', last_seen: '2026-03-10', seen_count: 2 }] }),
|
|
])
|
|
expect(second).toEqual({ created: 0, attached: 1, identities: 1, facts: 0 })
|
|
const after = await getPool().query<{ n: string; alias_keys: string[] }>(
|
|
`SELECT (SELECT count(*) FROM public.parties WHERE company_id = $1)::text AS n, alias_keys FROM public.parties WHERE id = $2`,
|
|
[c.companyId, partyId],
|
|
)
|
|
expect(after.rows[0]!.n).toBe('1')
|
|
expect([...after.rows[0]!.alias_keys].sort()).toEqual(['loopia', 'loopia webbhotell'])
|
|
const ident = await getPool().query<{ status: string; seen_count: number; first_seen: string; last_seen: string }>(
|
|
`SELECT status, seen_count, first_seen::text, last_seen::text FROM public.party_identities WHERE party_id = $1`,
|
|
[partyId],
|
|
)
|
|
expect(ident.rows).toEqual([{ status: 'known', seen_count: 2, first_seen: '2026-01-10', last_seen: '2026-03-10' }])
|
|
|
|
// Exact alias key without org: attaches too. Re-running creates nothing new.
|
|
const third = await apply(c.companyId, c.userId, [item({ key: 'loopia webbhotell', alias_keys: ['loopia webbhotell'] })])
|
|
expect(third).toMatchObject({ created: 0, attached: 1, facts: 0 })
|
|
const facts = await getPool().query(`SELECT 1 FROM public.party_facts WHERE party_id = $1`, [partyId])
|
|
expect(facts.rowCount).toBe(1)
|
|
})
|
|
|
|
it('attaches by VAT number when there is no org number (foreign suppliers)', async () => {
|
|
const c = await seedCompany()
|
|
await apply(c.companyId, c.userId, [{ key: 'utlägg framer', display_name: 'Framer B.V.', vat_number: 'NL853695386B01', origin: 'document' }])
|
|
const second = await apply(c.companyId, c.userId, [{ key: 'framer utlägg', display_name: 'Framer B.V.', vat_number: 'nl 853695386 b01', origin: 'document' }])
|
|
expect(second).toMatchObject({ created: 0, attached: 1 })
|
|
const rows = await getPool().query<{ n: string; alias_keys: string[] }>(
|
|
`SELECT (SELECT count(*) FROM public.parties WHERE company_id = $1)::text AS n, alias_keys FROM public.parties WHERE company_id = $1`,
|
|
[c.companyId],
|
|
)
|
|
expect(rows.rows[0]!.n).toBe('1')
|
|
expect([...rows.rows[0]!.alias_keys].sort()).toEqual(['framer utlägg', 'utlägg framer'])
|
|
})
|
|
|
|
it('never merges on name: same core text becomes a second suggested party', async () => {
|
|
const c = await seedCompany()
|
|
await apply(c.companyId, c.userId, [{ key: 'fortnox', display_name: 'Fortnox AB', org_number: ORG }])
|
|
await apply(c.companyId, c.userId, [{ key: 'fortnox finans', display_name: 'Fortnox Finans AB', org_number: OTHER_ORG }])
|
|
await apply(c.companyId, c.userId, [{ key: 'fortnox ab', display_name: 'FORTNOX AB' }])
|
|
const { rows } = await getPool().query<{ display_name: string }>(
|
|
`SELECT display_name FROM public.parties WHERE company_id = $1 ORDER BY created_at`,
|
|
[c.companyId],
|
|
)
|
|
expect(rows.map((r) => r.display_name)).toEqual(['Fortnox AB', 'Fortnox Finans AB', 'FORTNOX AB'])
|
|
})
|
|
|
|
it('rejects an explicit party_id from another company and a spoofed user id', async () => {
|
|
const mine = await seedCompany()
|
|
const theirs = await seedCompany()
|
|
const { rows } = await getPool().query<{ id: string }>(
|
|
`INSERT INTO public.parties (company_id, user_id, display_name) VALUES ($1, $2, 'Theirs') RETURNING id`,
|
|
[theirs.companyId, theirs.userId],
|
|
)
|
|
await expect(apply(mine.companyId, mine.userId, [{ key: 'x', display_name: 'X', party_id: rows[0]!.id }])).rejects.toMatchObject({ code: '23503' })
|
|
await expect(
|
|
withUserContext(mine.userId, (client) =>
|
|
client.query(`SELECT public.apply_party_suggestions($1, $2, '[]'::jsonb)`, [mine.companyId, theirs.userId]),
|
|
),
|
|
).rejects.toMatchObject({ code: '42501' })
|
|
// RLS: a member of another company cannot write into mine.
|
|
await expect(
|
|
withUserContext(theirs.userId, (client) =>
|
|
client.query(`SELECT public.apply_party_suggestions($1, $2, $3::jsonb)`, [mine.companyId, theirs.userId, JSON.stringify([{ key: 'x', display_name: 'X' }])]),
|
|
),
|
|
).rejects.toMatchObject({ code: '42501' })
|
|
})
|
|
})
|
|
|
|
describe('decide_parties (pg)', () => {
|
|
it('confirms and dismisses in bulk, logging one decision per party', async () => {
|
|
const c = await seedCompany()
|
|
await apply(c.companyId, c.userId, [
|
|
{ key: 'loopia', display_name: 'Loopia AB', reason: { attach: 'new' } },
|
|
{ key: 'beijer', display_name: 'Beijer AB', reason: { attach: 'new' } },
|
|
{ key: 'noise', display_name: 'Noise', reason: { attach: 'new' } },
|
|
])
|
|
const ids = await getPool().query<{ id: string; display_name: string }>(
|
|
`SELECT id, display_name FROM public.parties WHERE company_id = $1`,
|
|
[c.companyId],
|
|
)
|
|
const byName = Object.fromEntries(ids.rows.map((r) => [r.display_name, r.id]))
|
|
|
|
const confirmed = await decide(c.companyId, c.userId, [byName['Loopia AB']!, byName['Beijer AB']!], 'confirm', 'bulk from queue')
|
|
expect(confirmed).toBe(2)
|
|
const dismissed = await decide(c.companyId, c.userId, [byName['Noise']!], 'dismiss')
|
|
expect(dismissed).toBe(1)
|
|
|
|
const state = await getPool().query<{ display_name: string; status: string; archived: boolean; reason: unknown }>(
|
|
`SELECT display_name, status, archived_at IS NOT NULL AS archived, suggested_reason AS reason FROM public.parties WHERE company_id = $1 ORDER BY display_name`,
|
|
[c.companyId],
|
|
)
|
|
expect(state.rows).toEqual([
|
|
{ display_name: 'Beijer AB', status: 'confirmed', archived: false, reason: null },
|
|
{ display_name: 'Loopia AB', status: 'confirmed', archived: false, reason: null },
|
|
{ display_name: 'Noise', status: 'suggested', archived: true, reason: { attach: 'new' } },
|
|
])
|
|
const decisions = await getPool().query<{ kind: string; n: string }>(
|
|
`SELECT kind, count(*)::text AS n FROM public.party_decisions WHERE company_id = $1 GROUP BY kind ORDER BY kind`,
|
|
[c.companyId],
|
|
)
|
|
expect(decisions.rows).toEqual([
|
|
{ kind: 'confirm', n: '2' },
|
|
{ kind: 'dismiss', n: '1' },
|
|
])
|
|
|
|
// Second confirm of the same ids is a no-op: no duplicate decisions.
|
|
const again = await decide(c.companyId, c.userId, [byName['Loopia AB']!], 'confirm')
|
|
expect(again).toBe(0)
|
|
// Dismiss never touches a confirmed party.
|
|
const notDismissed = await decide(c.companyId, c.userId, [byName['Loopia AB']!], 'dismiss')
|
|
expect(notDismissed).toBe(0)
|
|
const loopia = await getPool().query<{ archived: boolean }>(`SELECT archived_at IS NOT NULL AS archived FROM public.parties WHERE id = $1`, [byName['Loopia AB']])
|
|
expect(loopia.rows[0]!.archived).toBe(false)
|
|
})
|
|
|
|
it('rejects unknown kinds and other companies', async () => {
|
|
const mine = await seedCompany()
|
|
const theirs = await seedCompany()
|
|
await apply(theirs.companyId, theirs.userId, [{ key: 'loopia', display_name: 'Loopia AB' }])
|
|
const { rows } = await getPool().query<{ id: string }>(`SELECT id FROM public.parties WHERE company_id = $1`, [theirs.companyId])
|
|
await expect(
|
|
withUserContext(mine.userId, (client) =>
|
|
client.query(`SELECT public.decide_parties($1, $2, $3::uuid[], 'merge', NULL)`, [mine.companyId, mine.userId, [rows[0]!.id]]),
|
|
),
|
|
).rejects.toMatchObject({ code: '22023' })
|
|
// Under RLS a member of another company sees no rows to update.
|
|
const n = await withUserContext(mine.userId, async (client) => {
|
|
const r = await client.query<{ n: number }>(`SELECT public.decide_parties($1, $2, $3::uuid[], 'confirm', NULL) AS n`, [
|
|
theirs.companyId,
|
|
mine.userId,
|
|
[rows[0]!.id],
|
|
])
|
|
return r.rows[0]!.n
|
|
})
|
|
expect(n).toBe(0)
|
|
const still = await getPool().query<{ status: string }>(`SELECT status FROM public.parties WHERE id = $1`, [rows[0]!.id])
|
|
expect(still.rows[0]!.status).toBe('suggested')
|
|
})
|
|
})
|
|
|
|
describe('undo_party_decisions (pg)', () => {
|
|
async function undoDecisions(companyId: string, userId: string, ids: string[]): Promise<number> {
|
|
const { rows } = await getPool().query<{ n: number }>(`SELECT public.undo_party_decisions($1, $2, $3::uuid[]) AS n`, [companyId, userId, ids])
|
|
return rows[0]!.n
|
|
}
|
|
|
|
it('reverses a confirm (reason restored) and a dismiss, logs undo, and refuses a second undo', async () => {
|
|
const c = await seedCompany()
|
|
await apply(c.companyId, c.userId, [
|
|
{ key: 'loopia', display_name: 'Loopia AB', reason: { attach: 'new', occurrences: 3 } },
|
|
{ key: 'noise', display_name: 'Noise', reason: { attach: 'new', occurrences: 1 } },
|
|
])
|
|
const ids = await getPool().query<{ id: string; display_name: string }>(`SELECT id, display_name FROM public.parties WHERE company_id = $1`, [c.companyId])
|
|
const byName = Object.fromEntries(ids.rows.map((r) => [r.display_name, r.id]))
|
|
expect(await decide(c.companyId, c.userId, [byName['Loopia AB']!], 'confirm')).toBe(1)
|
|
expect(await decide(c.companyId, c.userId, [byName['Noise']!], 'dismiss')).toBe(1)
|
|
const snap = await getPool().query<{ before: { suggested_reason: { occurrences: number } } }>(
|
|
`SELECT before FROM public.party_decisions WHERE party_id = $1 AND kind = 'confirm'`,
|
|
[byName['Loopia AB']],
|
|
)
|
|
expect(snap.rows[0]!.before.suggested_reason.occurrences).toBe(3)
|
|
|
|
expect(await undoDecisions(c.companyId, c.userId, [byName['Loopia AB']!, byName['Noise']!])).toBe(2)
|
|
const state = await getPool().query<{ display_name: string; status: string; archived: boolean; reason: { occurrences: number } | null }>(
|
|
`SELECT display_name, status, archived_at IS NOT NULL AS archived, suggested_reason AS reason FROM public.parties WHERE company_id = $1 ORDER BY display_name`,
|
|
[c.companyId],
|
|
)
|
|
expect(state.rows).toEqual([
|
|
{ display_name: 'Loopia AB', status: 'suggested', archived: false, reason: { attach: 'new', occurrences: 3 } },
|
|
{ display_name: 'Noise', status: 'suggested', archived: false, reason: { attach: 'new', occurrences: 1 } },
|
|
])
|
|
const kinds = await getPool().query<{ kind: string; n: string }>(
|
|
`SELECT kind, count(*)::text AS n FROM public.party_decisions WHERE company_id = $1 GROUP BY kind ORDER BY kind`,
|
|
[c.companyId],
|
|
)
|
|
expect(kinds.rows).toEqual([
|
|
{ kind: 'confirm', n: '1' },
|
|
{ kind: 'dismiss', n: '1' },
|
|
{ kind: 'undo', n: '2' },
|
|
])
|
|
// The latest decision is now the undo itself: nothing left to reverse.
|
|
expect(await undoDecisions(c.companyId, c.userId, [byName['Loopia AB']!])).toBe(0)
|
|
})
|
|
|
|
it('refuses after 30 days and ignores other companies', async () => {
|
|
const c = await seedCompany()
|
|
const other = await seedCompany()
|
|
await apply(c.companyId, c.userId, [{ key: 'loopia', display_name: 'Loopia AB', reason: { attach: 'new' } }])
|
|
const { rows } = await getPool().query<{ id: string }>(`SELECT id FROM public.parties WHERE company_id = $1`, [c.companyId])
|
|
await decide(c.companyId, c.userId, [rows[0]!.id], 'confirm')
|
|
expect(await undoDecisions(other.companyId, other.userId, [rows[0]!.id])).toBe(0)
|
|
await getPool().query(`UPDATE public.party_decisions SET created_at = now() - interval '31 days' WHERE party_id = $1`, [rows[0]!.id])
|
|
expect(await undoDecisions(c.companyId, c.userId, [rows[0]!.id])).toBe(0)
|
|
const still = await getPool().query<{ status: string }>(`SELECT status FROM public.parties WHERE id = $1`, [rows[0]!.id])
|
|
expect(still.rows[0]!.status).toBe('confirmed')
|
|
})
|
|
})
|