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
@@ -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(