Files
accounted/extensions/general/arcim-migration/__tests__/mapping-targets.test.ts
T
Pierre Grönberg bac9e01e2d fix(arcim): offer the company's own accounts as mapping targets (#2164)
The Fortnox migration's account mapping step builds its target dropdown
from BAS_REFERENCE alone: the 1290 standard accounts. Any account a
company created outside the standard cannot be selected as a target.

Seen on a live Fortnox migration: 3005 "Provisioner inom Sverige" was
active in the chart, returned by /api/bookkeeping/accounts, visible
everywhere else in the app, and missing from this one list, because BAS
defines 3000-3004 and stops there.

The plain SIE import at app/(dashboard)/import already used the
company's own chart (fetchAccounts(false)), so the two routes into the
same AccountMappingStep disagreed about what could be mapped onto.

Targets are now the company chart unioned with BAS, deduplicated by
account number with the company row winning: its name is whatever the
user renamed the account to, and that is the label they look for. BAS
stays for standard accounts a first migration is about to create, and a
failed chart read degrades to BAS rather than throwing, since an
incomplete list still lets the migration proceed while an exception
stops it.
2026-09-04 08:56:56 +02:00

83 lines
3.3 KiB
TypeScript

import { describe, expect, it } from 'vitest'
import type { SupabaseClient } from '@supabase/supabase-js'
import { buildMappingTargets } from '../lib/mapping-targets'
/**
* A chart_of_accounts read that returns the given rows for any range, which is
* all fetchAllRows needs: one page, then an empty one.
*/
function supabaseWith(rows: Array<Record<string, unknown>> | Error): SupabaseClient {
let served = false
const range = () =>
rows instanceof Error
? Promise.resolve({ data: null, error: { message: rows.message } })
: Promise.resolve({ data: served ? [] : ((served = true), rows), error: null })
const builder = {
select: () => builder,
eq: () => builder,
order: () => builder,
range,
}
return { from: () => builder } as unknown as SupabaseClient
}
describe('buildMappingTargets', () => {
// The bug this file exists for: BAS defines 3000-3004 and stops, so a
// company account like 3005 was active in the chart, visible everywhere else
// in the app, and still absent from the migration's mapping dropdown.
it('offers a company account that BAS does not define', async () => {
const targets = await buildMappingTargets(
supabaseWith([
{ account_number: '3005', account_name: 'Provisioner inom Sverige', account_class: 3 },
]),
'company-1',
)
const found = targets.find((t) => t.account_number === '3005')
expect(found?.account_name).toBe('Provisioner inom Sverige')
})
it('still offers standard accounts the company has not created', async () => {
const targets = await buildMappingTargets(supabaseWith([]), 'company-1')
expect(targets.find((t) => t.account_number === '1930')).toBeTruthy()
expect(targets.length).toBeGreaterThan(1000)
})
// The user renamed it; that is the label they will look for.
it('prefers the company name over the BAS name on a collision', async () => {
const targets = await buildMappingTargets(
supabaseWith([
{ account_number: '3001', account_name: 'Försäljning konsulttjänster', account_class: 3 },
]),
'company-1',
)
const matches = targets.filter((t) => t.account_number === '3001')
expect(matches).toHaveLength(1)
expect(matches[0].account_name).toBe('Försäljning konsulttjänster')
})
it('derives the class from the number when the row has none', async () => {
const targets = await buildMappingTargets(
supabaseWith([{ account_number: '3005', account_name: 'X', account_class: null }]),
'company-1',
)
expect(targets.find((t) => t.account_number === '3005')?.account_class).toBe(3)
})
it('sorts by account number so the dropdown groups read in order', async () => {
const targets = await buildMappingTargets(
supabaseWith([{ account_number: '3005', account_name: 'X', account_class: 3 }]),
'company-1',
)
const numbers = targets.map((t) => t.account_number)
expect(numbers).toEqual([...numbers].sort((a, b) => a.localeCompare(b)))
})
// An incomplete list still lets the migration run; an exception stops it.
it('falls back to BAS when the chart cannot be read', async () => {
const targets = await buildMappingTargets(supabaseWith(new Error('boom')), 'company-1')
expect(targets.length).toBeGreaterThan(1000)
expect(targets.find((t) => t.account_number === '3005')).toBeUndefined()
})
})