feat(arcim-migration): import the Fortnox asset register during migration (#1999)

The Fortnox migration now imports the asset register (GET /3/assets + /3/assets/types) as local register rows via createAsset: category from the type's anskaffningskonto BAS class, useful life from the source's depreciation window (K2 schablon fallback), never any journal entries (values arrived via SIE; the source's depreciated-to date is recorded in notes for review of the first proposal). Sold/scrapped/voided assets are skipped, re-runs dedupe, one bad asset counts as skipped. Gated behind FORTNOX_ASSET_SCOPES_APPROVED=false until the portal registration for integration 39254 carries the Assets scope, so hosted consents are unchanged and the wizard shows an honest skipped row.

Co-authored-by: pgronberg <pgronberg@users.noreply.github.com>
This commit is contained in:
Pierre Grönberg
2026-08-31 09:25:55 +01:00
committed by GitHub
co-authored by pgronberg
parent 6ac9679fb5
commit 8b0aa80ea0
11 changed files with 1054 additions and 60 deletions
+2 -2
View File
@@ -2241,9 +2241,9 @@ const PROVIDER_MIGRATION: Record<string, StructuredErrorEntry> = {
PROVIDER_SIE_IMPORT_REQUIRED: {
httpStatus: 409,
message_sv:
'Bokföringsdata (SIE) måste importeras först. Ladda upp en SIE-fil med kontoplan, ingående balanser och verifikationer innan du hämtar kunder, leverantörer och fakturor från den här leverantören.',
'Bokföringsdata (SIE) måste importeras först. Ladda upp en SIE-fil med kontoplan, ingående balanser och verifikationer innan du hämtar kunder, leverantörer, fakturor och anläggningstillgångar från den här leverantören.',
message_en:
'A completed SIE import is required first. Import the SIE file (chart of accounts, opening balances and verifications) before importing customers, suppliers and invoices from this provider.',
'A completed SIE import is required first. Import the SIE file (chart of accounts, opening balances and verifications) before importing customers, suppliers, invoices and fixed assets from this provider.',
},
PROVIDER_MIGRATE_FAILED: {
httpStatus: 500,
@@ -5,6 +5,8 @@ import {
fortnoxConsentScopes,
FORTNOX_DOCUMENT_SCOPES,
FORTNOX_DOCUMENT_SCOPES_APPROVED,
FORTNOX_ASSET_SCOPES,
FORTNOX_ASSET_SCOPES_APPROVED,
} from '../oauth';
describe('Fortnox OAuth scopes', () => {
@@ -47,6 +49,21 @@ describe('Fortnox OAuth scopes', () => {
expect(fortnoxConsentScopes()).not.toContain('connectfile');
});
// The asset register scope is gated on its own portal approval. While the
// flag is false, no consent may request it: an unapproved scope in the
// authorize request is rejected with invalid_scope BEFORE login (the same
// outage mode the document flag above guards against).
it('keeps the asset scope out of every consent until the portal approves it', () => {
expect(FORTNOX_ASSET_SCOPES).toEqual(['assets']);
if (FORTNOX_ASSET_SCOPES_APPROVED) {
expect(fortnoxConsentScopes()).toContain('assets');
expect(fortnoxConsentScopes({ documents: true })).toContain('assets');
} else {
expect(fortnoxConsentScopes()).not.toContain('assets');
expect(fortnoxConsentScopes({ documents: true })).not.toContain('assets');
}
});
// Even once the portal registration lands, opting in must never cost the
// consent its ledger access: the callback overwrites its tokens in place.
it('keeps a document consent a superset of an ordinary one', () => {
+24 -1
View File
@@ -35,6 +35,24 @@ export const FORTNOX_DOCUMENT_SCOPES = ['archive', 'connectfile'];
*/
export const FORTNOX_DOCUMENT_SCOPES_APPROVED: boolean = true;
/** The asset register (anläggningsregistret): what the asset import reads. */
export const FORTNOX_ASSET_SCOPES = ['assets'];
/**
* Whether the registered Fortnox app has the Assets scope (Anläggningsregister)
* enabled in the Fortnox Developer Portal. Ships false until the portal
* registration is confirmed to carry it: requesting a scope the app lacks
* makes the authorize endpoint reject with invalid_scope BEFORE login, the
* same failure mode the document scopes guard against above.
*
* When true, the ordinary connect requests the scope. Unlike Arkivplats and
* Koppla filer, the asset register carries no separate Fortnox customer
* licence, so no per-user opt-in is needed. A consent minted without the
* scope degrades gracefully: the migration reports assets as skipped instead
* of failing (see arcim-migration import-assets).
*/
export const FORTNOX_ASSET_SCOPES_APPROVED: boolean = false;
/**
* The scopes a Fortnox consent is minted with. The document scopes are opt-in
* per authorize call, because Fortnox derives its customer licence
@@ -48,9 +66,14 @@ export const FORTNOX_DOCUMENT_SCOPES_APPROVED: boolean = true;
export function fortnoxConsentScopes(options?: { documents?: boolean }): string[] {
const withDocuments =
options?.documents === true && FORTNOX_DOCUMENT_SCOPES_APPROVED;
return withDocuments
const scopes = withDocuments
? [...BASE_SCOPES, ...FORTNOX_DOCUMENT_SCOPES]
: [...BASE_SCOPES];
// The asset register rides along on every consent once the portal
// registration carries the scope: it needs no extra customer licence, so
// there is nothing to opt in to.
if (FORTNOX_ASSET_SCOPES_APPROVED) scopes.push(...FORTNOX_ASSET_SCOPES);
return scopes;
}
export function buildFortnoxAuthUrl(