fix(sie): selectable IB voucher series, smarter IB toggle on re-import, orphan-IB guard (#1896)

* fix(sie): selectable IB voucher series that never collides with the file's numbering

The Ingående balanser voucher was hardcoded to series A and created before
the file's vouchers, so it consumed the A series' next number and shifted
every imported A voucher one number higher than in the source system
(issue #1882).

- IB voucher series is now selectable in the import wizard; the default is
  the first of M,O,P,Q,R,S,T,V,W,X,Y,Z not used by the file's #VER records
  (M matches the existing migration-adjustment series).
- Plumbed end to end: wizard -> /api/import/sie/execute -> executeSIEImport,
  v1 REST options.openingBalanceSeries, MCP gnubok_import_sie
  opening_balance_series -> commitImportSie.
- The wizard's 'Importera ingående balanser' toggle now defaults OFF when a
  posted IB voucher already exists inside the file's fiscal year, with a
  hint saying why.
- Orphan-IB guard in executeSIEImport: replace_sie_import deletes only
  source_type='import' entries and clears the period's OB pointer, so a
  prior import's IB voucher survived every replace cycle and each re-import
  created another one (field report: five accumulated). The import now
  skips IB creation with a warning when a posted opening_balance entry
  already exists in the period.
- MCP import_opening_balances default (false) vs web (true) documented as
  deliberate in the tool schema and DECISIONS.md.

Fixes #1882

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>

* fix(sie): harden IB series fix after skeptic review (relink orphan, exclude fallback series, type-check option)

Skeptic findings on PR #1896, all four blocking items:

- Orphan-IB guard now relinks a single surviving opening-balance voucher
  as the period's OB entry (permitted by the immutability trigger while
  the pointer is NULL): without it, reports showed IB 0, year-end's
  duplicate-IB blocker never armed, and the manual IB flow could
  double-book. It also diffs the survivor's lines against the file's IB
  and calls out stale amounts in the warning instead of keeping them
  silently; reverseEntry clears the pointer again for the
  storno-then-reimport path.
- Series-less #VER records resolve to the transaction fallback series at
  import time, so the IB default picker now treats that series as used by
  the file (the same #1882 shift pattern through the fallback). The
  wizard recomputes its IB default with the effective transaction series
  once loaded.
- openingBalanceSeries is type-checked on the web execute route, the MCP
  stage, and the staged-operation commit: a non-string falls back to the
  default instead of crashing mid-import after side effects.
- The wizard's IB series select flags series used by the file and shows
  an attention line when the chosen series collides; the engine warns
  when an explicitly chosen series collides with the file's series (the
  choice is honored).

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>

* fix(sie): uppercase caller-chosen IB series before persisting

Swedish accounting review on PR #1896: a lowercase series from v1 or
MCP was persisted as-is, booking a case-distinct parallel series next
to its uppercase sibling (BFL 5 kap requires one systematic series)
and slipping past the file-collision warning. Normalize centrally in
executeSIEImport, the single funnel for web, v1, and MCP.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>

---------

Co-authored-by: Claude Fable 5 <noreply@anthropic.com>
This commit is contained in:
Mattsson
2026-08-25 15:14:59 +02:00
committed by GitHub
co-authored by Claude Fable 5
parent 79013cf092
commit c6f2bebab9
14 changed files with 1020 additions and 6 deletions
@@ -234,4 +234,57 @@ describe('gnubok_import_sie: update_account_names staging', () => {
expect((staged[0].params as Record<string, unknown>).update_account_names).toBe(false)
})
it('passes opening_balance_series through to the staged params (issue #1882)', async () => {
const { supabase, staged } = buildCapturingSupabase()
await importSie.execute(
{
file_content: VALID_SIE,
filename: 'bok.se',
mappings: COVER_VALID_SIE,
import_opening_balances: true,
opening_balance_series: 'K',
},
'company-1',
'user-1',
supabase as never,
{ type: 'api_key' },
)
expect((staged[0].params as Record<string, unknown>).opening_balance_series).toBe('K')
})
it('drops a non-string opening_balance_series instead of staging it (hosts do not always enforce inputSchema)', async () => {
const { supabase, staged } = buildCapturingSupabase()
await importSie.execute(
{
file_content: VALID_SIE,
filename: 'bok.se',
mappings: COVER_VALID_SIE,
opening_balance_series: 123,
},
'company-1',
'user-1',
supabase as never,
{ type: 'api_key' },
)
expect((staged[0].params as Record<string, unknown>).opening_balance_series).toBeUndefined()
})
it('leaves opening_balance_series unset when omitted so the engine picks a non-colliding default', async () => {
const { supabase, staged } = buildCapturingSupabase()
await importSie.execute(
{ file_content: VALID_SIE, filename: 'bok.se', mappings: COVER_VALID_SIE },
'company-1',
'user-1',
supabase as never,
{ type: 'api_key' },
)
expect((staged[0].params as Record<string, unknown>).opening_balance_series).toBeUndefined()
})
})
+8 -1
View File
@@ -15862,9 +15862,10 @@ export const tools: McpTool[] = [
items: { type: 'object' },
},
create_fiscal_period: { type: 'boolean' },
import_opening_balances: { type: 'boolean' },
import_opening_balances: { type: 'boolean', description: 'Default false (web wizard defaults true)' },
import_transactions: { type: 'boolean' },
voucher_series: { type: 'string', description: 'Override voucher series for imported vouchers' },
opening_balance_series: { type: 'string', description: 'Series for the IB voucher; default avoids series used by the file' },
update_account_names: { type: 'boolean', description: 'Use #KONTO names from the file for created and existing accounts (default true). Set false to keep BAS default names.' },
},
required: ['file_content', 'filename', 'mappings'],
@@ -15944,6 +15945,12 @@ export const tools: McpTool[] = [
import_opening_balances: Boolean(args.import_opening_balances),
import_transactions: Boolean(args.import_transactions),
voucher_series: args.voucher_series,
// Hosts do not always enforce inputSchema: a non-string must fall
// back to the engine default, not crash at commit time.
opening_balance_series:
typeof args.opening_balance_series === 'string'
? args.opening_balance_series
: undefined,
// Default true: Boolean(undefined) would silently flip it off.
update_account_names:
args.update_account_names === undefined ? true : Boolean(args.update_account_names),