Files
accounted/extensions/general/arcim-migration/__tests__/mapping-resolution.test.ts
T
162de2128a fix(import): show "created on import" as the mapping target for source accounts the chart lacks (#2342)
The guided Fortnox import self-mapped a source account that exists in
neither the company chart nor BAS (4599) and then rendered its Malkonto
select blank, because the dropdown only knew chart + BAS accounts. The
row looked unmapped and unmappable while the import created the account
correctly. A nameless account (referenced by #TRANS without #KONTO) was
worse: the mapper refused the self-map, so it stayed unmapped with no
self-target to pick.

- account-mapper: the bas_range self-map no longer requires a #KONTO
  name; unmapped now means exactly "outside 1000-8999". isValidBASRange
  exported as the auto-create boundary.
- AccountMappingStep (shared by both wizards): a target the list cannot
  name is an explicit "<nr> <name> (skapas vid importen)" option, a
  "nya konton skapas" badge/filter lists them, out-of-range accounts
  that block Continue are named, nameless sources say so.
- sie-import: skippedVouchers.unmappedAccounts (per account, voucher
  count) via summarizeUnmappedSkips; warning names the accounts.
- Migration result step: names created accounts and the accounts behind
  "med ej kopplade konton".

Closes #2212


Claude-Session: https://claude.ai/code/session_019SaJfqNi4VmsG8FMKq99G6

Co-authored-by: Jakob Wennberg <311770904+jakobwennberg-oss@users.noreply.github.com>
Co-authored-by: Claude Fable 5.1 <noreply@anthropic.com>
2026-09-06 18:39:36 +02:00

98 lines
4.9 KiB
TypeScript

/**
* Issue #2212: the guided import's mapping step offered no target for a
* Fortnox account that exists in neither the company chart nor BAS (4599),
* and the user could not tell whether the import handled it.
*
* These tests run the exact resolution /sie-data runs (buildMappingTargets
* then suggestMappings) and pin the contract the mapping step now renders:
* - an in-range source account the chart lacks resolves to ITSELF and is
* created on import with the class and type the importer derives from
* the number (classifyAccount: the same helper syncMappedAccounts uses);
* - that holds with or without a #KONTO name;
* - a number outside 1000-8999 stays unresolved and blocks the step.
*/
import { describe, expect, it } from 'vitest'
import type { SupabaseClient } from '@supabase/supabase-js'
import { buildMappingTargets } from '../lib/mapping-targets'
import { suggestMappings, validateMappings, isValidBASRange } from '@/lib/import/account-mapper'
import { classifyAccount } from '@/lib/bookkeeping/account-classifier'
import { getBASReference } from '@/lib/bookkeeping/bas-reference'
/** A company whose chart holds only the given rows (one page, then empty). */
function supabaseWithChart(rows: Array<Record<string, unknown>>): SupabaseClient {
let served = false
const builder = {
select: () => builder,
eq: () => builder,
order: () => builder,
range: () => Promise.resolve({ data: served ? [] : ((served = true), rows), error: null }),
}
return { from: () => builder } as unknown as SupabaseClient
}
describe('guided import: resolving a source account the target chart lacks', () => {
it('4599 is in neither BAS nor an empty chart, so no dropdown target exists for it', async () => {
expect(getBASReference('4599')).toBeUndefined()
const targets = await buildMappingTargets(supabaseWithChart([]), 'company-1')
expect(targets.find((t) => t.account_number === '4599')).toBeUndefined()
})
// The user's case: a #KONTO row with a name and no postings in any exported
// year. It resolves to itself; the import creates it under the file's name.
it('self-maps a named #KONTO-only account onto its own number', async () => {
const targets = await buildMappingTargets(supabaseWithChart([]), 'company-1')
const [mapping] = suggestMappings([{ number: '4599', name: 'Justering inköp' }], targets)
expect(mapping.targetAccount).toBe('4599')
expect(mapping.targetName).toBe('Justering inköp')
expect(mapping.matchType).toBe('bas_range')
expect(validateMappings([mapping]).valid).toBe(true)
})
// The other route to the same screen: an account referenced by #TRANS/#IB
// without a #KONTO row arrives nameless. It must resolve the same way; the
// importer names it "Konto 4599" (account-sync fallback, tested there).
it('self-maps a nameless account referenced only by transactions', async () => {
const targets = await buildMappingTargets(supabaseWithChart([]), 'company-1')
const [mapping] = suggestMappings([{ number: '4599', name: '' }], targets)
expect(mapping.targetAccount).toBe('4599')
expect(mapping.matchType).toBe('bas_range')
expect(validateMappings([mapping]).valid).toBe(true)
})
// The type the created account gets is derived from the number range by
// the importer's own classifier, never asked of the user.
it('derives the created account type from the number range', () => {
expect(classifyAccount('4599')).toEqual({ account_type: 'expense', normal_balance: 'debit' })
expect(classifyAccount('1932')).toEqual({ account_type: 'asset', normal_balance: 'debit' })
expect(classifyAccount('2093')).toEqual({ account_type: 'equity', normal_balance: 'credit' })
})
// Nothing can be created for a number outside BAS: the step must block and
// the user must pick a chart account for its postings.
it('leaves an out-of-range account unresolved so the step blocks', async () => {
const targets = await buildMappingTargets(supabaseWithChart([]), 'company-1')
const [mapping] = suggestMappings([{ number: '9100', name: 'Internt konto' }], targets)
expect(mapping.targetAccount).toBe('')
expect(isValidBASRange('9100')).toBe(false)
expect(validateMappings([mapping])).toMatchObject({ valid: false, unmappedAccounts: ['9100'] })
})
// A company that already renamed the account keeps its own row as the
// target (exact match on the chart), so nothing is created twice.
it('prefers the company row when the account already exists in the chart', async () => {
const targets = await buildMappingTargets(
supabaseWithChart([{ account_number: '4599', account_name: 'Eget namn', account_class: 4 }]),
'company-1',
)
const [mapping] = suggestMappings([{ number: '4599', name: 'Justering inköp' }], targets)
expect(mapping.targetAccount).toBe('4599')
expect(mapping.targetName).toBe('Eget namn')
expect(mapping.matchType).toBe('exact')
})
})