@substrat-run/connector-fortnox 0.2.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/dist/index.js ADDED
@@ -0,0 +1,477 @@
1
+ import { z } from 'zod';
2
+ import { connectionActivity, connectionId as connectionIdSchema, instant, money, scopeId as scopeIdSchema, tenantId as tenantIdSchema, } from '@substrat-run/contracts';
3
+ import { FortnoxApi, FortnoxApiError, FORTNOX_API_BASE, FORTNOX_OAUTH_BASE, fortnoxSecret, } from './api.js';
4
+ import { financialYearFor, summarizeLedger } from './aggregate.js';
5
+ import { parseSie4 } from './sie4.js';
6
+ export { FortnoxApi, FortnoxApiError, FORTNOX_API_BASE, FORTNOX_OAUTH_BASE, fortnoxConsentUrl, fortnoxSecret, } from './api.js';
7
+ export { parseSie4, splitSieLine, sieAmount, sieDate, } from './sie4.js';
8
+ export { COST_CENTRE_DIMENSION, financialYearFor, summarizeLedger, } from './aggregate.js';
9
+ export { FortnoxMock } from './mock.js';
10
+ /**
11
+ * The Fortnox connector — the INBOUND half of accounting integration.
12
+ *
13
+ * ## Poll-only, and that is a design fact rather than an omission
14
+ *
15
+ * `connector-scrive` has two halves: a dispatch handler driven by an engine event, and
16
+ * a sweep that polls the provider. This connector has **only the sweep**, and registers
17
+ * no event handler at all, because nothing inside a scope initiates the work. A
18
+ * vertical does not *ask* for last month's bookkeeping the way it asks for a signature;
19
+ * the bookkeeping simply changes at Fortnox, and the platform finds out by looking.
20
+ *
21
+ * So there is no `registerFortnoxConnector`. `sweepFortnoxLedger` is a
22
+ * {@link ConnectorSweeper}, the deployment binds it into the platform sweeper exactly
23
+ * as it binds Scrive's, and that is the whole trigger surface.
24
+ *
25
+ * ## Where the data lands, and why the connector does not decide
26
+ *
27
+ * A sweep has no delivered event, so it has neither a scope to write to nor authority
28
+ * to write with. Scrive gets both from its dispatch ledger — the row it wrote when the
29
+ * event arrived. This connector has no such moment, so the scope is declared **once**,
30
+ * explicitly, by {@link bindFortnoxScope}: which scope, which operation to land the
31
+ * ledger through, and which permission that operation checks.
32
+ *
33
+ * The operation is the *consumer's*, not this connector's, and that is deliberate. What
34
+ * comes out of Fortnox is neutral accounting fact — accounts, cost centres, months,
35
+ * debit-positive sums. What a business *means* by them (which account is
36
+ * `lokal_grundhyra`, which sign normalizes it, which group it rolls into) is vocabulary,
37
+ * and vocabulary is the vertical's layer. A connector that mapped accounts to row keys
38
+ * would be a vertical wearing a connector's clothes, and the second customer with a
39
+ * different chart of accounts would have to fork it.
40
+ */
41
+ /**
42
+ * The standing grants this connector requires — deliberately EMPTY, with a mechanism
43
+ * in place of a declaration.
44
+ *
45
+ * Every other connector names its return-path permissions here so `lint:connector-grants`
46
+ * can prove the dashboard's door is able to grant them. This one genuinely cannot: the
47
+ * permission it needs is whatever the *consumer's* landing operation checks, which
48
+ * differs per vertical and is unknown at this package's build time.
49
+ *
50
+ * That would be an invisible hole — the exact shape of #841, where a connector needed a
51
+ * grant no door could give and the way it surfaced was a failure months later. So the
52
+ * check moves from build time to bind time: {@link bindFortnoxScope} verifies the
53
+ * connection actually holds the named permission in the named scope and **refuses the
54
+ * binding otherwise**, naming what is missing. A sweep can therefore never be configured
55
+ * into a state where it fetches a year of bookkeeping and cannot write it down.
56
+ */
57
+ export const FORTNOX_CONNECTION_GRANTS = [];
58
+ /** The connector-state key prefix every binding lives under — what the sweep enumerates. */
59
+ const BINDING_PREFIX = 'fortnox:binding:';
60
+ const bindingKey = (scope) => `${BINDING_PREFIX}${scope}`;
61
+ /**
62
+ * One page of parsed bookkeeping, as it crosses into a scope.
63
+ *
64
+ * Parsed with this schema on the way OUT, before every `invoke`. The engine-seam rule
65
+ * (`returns()`) exists because a value crossing a version boundary must be pinned to a
66
+ * published shape rather than to whatever the code currently produces, and a connector
67
+ * seam is the same boundary with a network in the middle: a vertical compiled against
68
+ * one version of this package and running against another must get a throw, never a
69
+ * silently-reshaped ledger on a screen.
70
+ */
71
+ export const fortnoxLedgerPage = z.object({
72
+ /**
73
+ * Identifies this sync RUN, and it is the content hash rather than a ULID on purpose:
74
+ * two syncs of unchanged books produce the same `syncId`, so a consumer's upsert is
75
+ * naturally idempotent and a redelivered page cannot double a balance.
76
+ */
77
+ syncId: z.string().min(1),
78
+ connectionId: z.string().min(1),
79
+ company: z.object({
80
+ name: z.string(),
81
+ organizationNumber: z.string(),
82
+ }),
83
+ financialYear: z.object({
84
+ id: z.number(),
85
+ from: z.string(),
86
+ to: z.string(),
87
+ }),
88
+ page: z.number().int().nonnegative(),
89
+ pageCount: z.number().int().positive(),
90
+ /** True on the last page — the signal a consumer commits or swaps on. */
91
+ final: z.boolean(),
92
+ /** Present on page 0 only; the labels every later page's codes refer to. */
93
+ accounts: z.array(z.object({ number: z.string(), name: z.string() })).default([]),
94
+ costCentres: z.array(z.object({ code: z.string(), name: z.string() })).default([]),
95
+ balances: z.array(z.object({
96
+ account: z.string(),
97
+ costCentre: z.string().nullable(),
98
+ month: z.string(),
99
+ /** Debit-positive, as SIE states it. The consumer applies its own sign. */
100
+ amount: money,
101
+ })),
102
+ });
103
+ /** How many balance rows ride one `invoke`. */
104
+ const PAGE_SIZE = 500;
105
+ /**
106
+ * Declare that a connection should sync one scope — the one-time setup a poll-only
107
+ * connector needs in place of a dispatch.
108
+ *
109
+ * **Refuses a binding whose grant is missing**, which is the whole reason this is a
110
+ * function rather than a config object. The alternative — write the binding, discover
111
+ * at sweep time that the connection cannot invoke the operation — fails in the worst
112
+ * possible place: after a year of bookkeeping has been fetched, in a background timer
113
+ * nobody is watching, with a provider round trip already spent. Here it fails in the
114
+ * operator's hands, naming the permission to grant.
115
+ */
116
+ export async function bindFortnoxScope(host, input) {
117
+ const tenant = tenantIdSchema.parse(input.tenantId);
118
+ const scope = scopeIdSchema.parse(input.scopeId);
119
+ // On the host, not `admin`: this is the same projection the permission checker reads,
120
+ // so a binding is verified against the tuples that will actually gate the invoke —
121
+ // not against a directory row that may not have reached the scope yet.
122
+ const granted = await host.connectionGrantsInScope(tenant, scope);
123
+ const held = granted.some((g) => g.connectionId === input.connectionId && g.permission === input.permission);
124
+ if (!held) {
125
+ throw new Error(`connection ${input.connectionId} does not hold '${input.permission}' on scope ${input.scopeId} — ` +
126
+ `a sweep would fetch the ledger and then fail to land it. Grant it first ` +
127
+ `(grantToConnection), then bind.`);
128
+ }
129
+ const binding = {
130
+ scopeId: input.scopeId,
131
+ tenantId: input.tenantId,
132
+ vertical: input.vertical,
133
+ operation: input.operation,
134
+ permission: input.permission,
135
+ boundAt: new Date(input.now?.() ?? Date.now()).toISOString(),
136
+ };
137
+ await host.admin.putConnectorState(input.connectionId, bindingKey(input.scopeId), binding);
138
+ return binding;
139
+ }
140
+ /** Every scope this connection syncs into. */
141
+ export async function listFortnoxBindings(host, connectionId) {
142
+ const rows = await host.admin.listConnectorState(connectionId, BINDING_PREFIX);
143
+ // Tombstones filtered, exactly as the sweep and the activity projection do.
144
+ // `unbindFortnoxScope` writes `null` under the same key, and casting that to
145
+ // `FortnoxBinding` hands a caller a typed value that throws on the first property
146
+ // read — a lie the type system cannot catch.
147
+ return rows
148
+ .filter((r) => r.value !== null && typeof r.value === 'object')
149
+ .map((r) => r.value);
150
+ }
151
+ /**
152
+ * Stop syncing one scope. The binding row is replaced with a tombstone rather than
153
+ * removed, because `putConnectorState` is the only verb this surface has — and an
154
+ * unbound scope that a later sweep silently re-adopts would be worse than a visible
155
+ * dead row.
156
+ */
157
+ export async function unbindFortnoxScope(host, connectionId, scopeId) {
158
+ await host.admin.putConnectorState(connectionId, bindingKey(scopeId), null);
159
+ }
160
+ const sha256Hex = async (text) => {
161
+ const digest = await crypto.subtle.digest('SHA-256', new TextEncoder().encode(text));
162
+ return [...new Uint8Array(digest)].map((b) => b.toString(16).padStart(2, '0')).join('');
163
+ };
164
+ /**
165
+ * Sync ONE bound scope: fetch the year that covers `period`, parse it, sum it, and land
166
+ * it through the binding's operation as the connection itself (#97).
167
+ *
168
+ * Idempotent and cheap on a no-op. The SIE payload is hashed before anything is landed,
169
+ * and an unchanged hash returns `changed: false` without a single `invoke` — which
170
+ * matters because a sweep runs on a timer and most passes find books nobody has touched.
171
+ */
172
+ export async function syncFortnoxScope(host, connectionId, binding, options) {
173
+ const admin = host.admin;
174
+ const conn = await openFortnoxConnection(admin, options.fetch, binding.tenantId, binding.vertical, options.timeoutMs ?? 30_000);
175
+ const api = new FortnoxApi(conn, {
176
+ apiBase: options.apiBase,
177
+ oauthBase: options.oauthBase,
178
+ now: options.now,
179
+ });
180
+ // The period defaults to the calendar year containing "now", which is the sane
181
+ // default for a recurring sync; a caller back-filling names its own.
182
+ const nowMs = options.now?.() ?? Date.now();
183
+ const year = new Date(nowMs).toISOString().slice(0, 4);
184
+ const period = options.period ?? { from: `${year}-01-01`, to: `${year}-12-31` };
185
+ const years = await api.financialYears();
186
+ // Overlap, never containment — a first financial year can start mid-month. See
187
+ // `financialYearFor`.
188
+ const financialYear = financialYearFor(years, period);
189
+ if (!financialYear) {
190
+ return {
191
+ scopeId: binding.scopeId,
192
+ changed: false,
193
+ syncId: '',
194
+ financialYearId: null,
195
+ balances: 0,
196
+ pages: 0,
197
+ outOfRange: 0,
198
+ };
199
+ }
200
+ const sie = await api.sieFile(financialYear.Id);
201
+ // The WINDOW is part of the sync's identity, not just the payload.
202
+ //
203
+ // What lands is `summarizeLedger(ledger, period)` — the payload filtered to the
204
+ // window — so hashing the payload alone makes two genuinely different results share
205
+ // an identity, and the skip below then drops the second one. A broken financial year
206
+ // is all it takes: with `#RAR` running 2026-07-01..2027-06-30, a December sweep uses
207
+ // the default period 2026-01-01..2026-12-31 and lands only the 2026 months; a January
208
+ // sweep selects the SAME year by overlap, downloads the SAME unchanged payload, and
209
+ // returns `changed: false` — so the 2027 months never land until the books happen to
210
+ // change. Non-calendar financial years are ordinary in Sweden, so this was reachable
211
+ // rather than theoretical. An explicit back-fill over a different window inside an
212
+ // already-synced year was a silent no-op for the same reason.
213
+ //
214
+ // Folding the window in fixes the second half too: `syncId` IS this hash, and it is
215
+ // the consumer's idempotency key. Two windows that land different rows must never
216
+ // present the same key to an upsert.
217
+ const contentHash = await sha256Hex(`${financialYear.Id}\n${period.from}\n${period.to}\n${sie}`);
218
+ if (binding.lastSync?.contentHash === contentHash) {
219
+ return {
220
+ scopeId: binding.scopeId,
221
+ changed: false,
222
+ syncId: contentHash,
223
+ financialYearId: financialYear.Id,
224
+ balances: binding.lastSync.balances,
225
+ pages: 0,
226
+ outOfRange: 0,
227
+ };
228
+ }
229
+ const ledger = parseSie4(sie);
230
+ const summary = summarizeLedger(ledger, period);
231
+ // The connection acting as itself (#97). Refuses a scope in another tenant or running
232
+ // another vertical by construction, and the invoke below is gated on the connection's
233
+ // own grant — the one `bindFortnoxScope` verified.
234
+ const scope = await host.getConnectorScope(connectionId, scopeIdSchema.parse(binding.scopeId));
235
+ const pageCount = Math.max(1, Math.ceil(summary.balances.length / PAGE_SIZE));
236
+ for (let page = 0; page < pageCount; page += 1) {
237
+ const slice = summary.balances.slice(page * PAGE_SIZE, (page + 1) * PAGE_SIZE);
238
+ const payload = fortnoxLedgerPage.parse({
239
+ syncId: contentHash,
240
+ connectionId,
241
+ company: { name: ledger.company, organizationNumber: ledger.organizationNumber },
242
+ financialYear: {
243
+ id: financialYear.Id,
244
+ from: financialYear.FromDate,
245
+ to: financialYear.ToDate,
246
+ },
247
+ page,
248
+ pageCount,
249
+ final: page === pageCount - 1,
250
+ // The labels ride page 0 only: they are the same on every page, and repeating a
251
+ // full chart of accounts across twenty pages is bytes through a clone pipe for
252
+ // nothing.
253
+ accounts: page === 0 ? summary.accounts : [],
254
+ costCentres: page === 0 ? summary.costCentres : [],
255
+ balances: slice,
256
+ });
257
+ await scope.invoke(binding.operation, payload);
258
+ }
259
+ const synced = {
260
+ ...binding,
261
+ lastSync: {
262
+ financialYearId: financialYear.Id,
263
+ syncedAt: new Date(nowMs).toISOString(),
264
+ contentHash,
265
+ balances: summary.balances.length,
266
+ },
267
+ };
268
+ // Written only AFTER every page landed, so a failure mid-way leaves the cursor at the
269
+ // previous hash and the next sweep retries the whole year rather than resuming into a
270
+ // half-written ledger.
271
+ await admin.putConnectorState(connectionId, bindingKey(binding.scopeId), synced);
272
+ return {
273
+ scopeId: binding.scopeId,
274
+ changed: true,
275
+ syncId: contentHash,
276
+ financialYearId: financialYear.Id,
277
+ balances: summary.balances.length,
278
+ pages: pageCount,
279
+ outOfRange: summary.outOfRange,
280
+ };
281
+ }
282
+ /**
283
+ * Poll Fortnox for every scope this connection is bound to — the {@link ConnectorSweeper}
284
+ * a deployment schedules.
285
+ *
286
+ * A timer calls this; it holds no timer itself. That keeps the trigger a deployment
287
+ * concern (`startPlatformSweeper` on node, `definePlatformSweeperDO`'s alarm on
288
+ * Cloudflare) and this a plain, testable function.
289
+ *
290
+ * Robust the way a poller must be: an unchanged year is skipped without landing
291
+ * anything, and a failure on one scope is recorded and stepped over rather than sinking
292
+ * the pass — one vertical's missing grant must not stop another tenant's books syncing.
293
+ */
294
+ export async function sweepFortnoxLedger(host, connectionId, options) {
295
+ const rows = await host.admin.listConnectorState(connectionId, BINDING_PREFIX);
296
+ const result = { found: 0, synced: [], unchanged: 0, failed: [] };
297
+ for (const { value } of rows) {
298
+ // A tombstoned binding (`unbindFortnoxScope`) — present as a row, not a target.
299
+ if (value === null || typeof value !== 'object')
300
+ continue;
301
+ const binding = value;
302
+ result.found += 1;
303
+ try {
304
+ const r = await syncFortnoxScope(host, connectionId, binding, options);
305
+ if (r.changed)
306
+ result.synced.push(r);
307
+ else
308
+ result.unchanged += 1;
309
+ }
310
+ catch (err) {
311
+ result.failed.push({
312
+ scopeId: binding.scopeId,
313
+ error: err instanceof Error ? err.message : String(err),
314
+ });
315
+ }
316
+ }
317
+ return result;
318
+ }
319
+ /**
320
+ * Probe a credential that is not stored yet — the connect-time check (#605).
321
+ *
322
+ * Takes the candidate secret directly, touches no connection and no store, and records
323
+ * no health: there may be no connection to record against, and a candidate's failure is
324
+ * not a fact about a live one.
325
+ *
326
+ * The probe is a token mint followed by `companyinformation`, and it must be both:
327
+ * minting alone proves the client pair and the consent, but only the company read proves
328
+ * the `TenantId` names a company this integration can actually see — which is the field
329
+ * an operator is most likely to paste wrong.
330
+ */
331
+ export async function probeFortnoxSecret(secret, options) {
332
+ const parsed = fortnoxSecret.safeParse(secret);
333
+ if (!parsed.success) {
334
+ // A malformed credential IS a refusal — Fortnox would reject it, and there is no
335
+ // point spending a round trip to hear so.
336
+ return {
337
+ ok: false,
338
+ refused: true,
339
+ accountRef: null,
340
+ accountLabel: null,
341
+ facts: [],
342
+ error: `incomplete Fortnox credential: ${parsed.error.issues
343
+ .map((i) => i.path.join('.'))
344
+ .join(', ')}`,
345
+ };
346
+ }
347
+ const timeoutMs = options.timeoutMs ?? 15_000;
348
+ const conn = {
349
+ id: connectionIdSchema.parse('00000000000000000000000000'), // no row yet; never read
350
+ tenantId: '',
351
+ vertical: '',
352
+ provider: 'fortnox',
353
+ secret: parsed.data,
354
+ expiresAt: null,
355
+ fetch: (input, init) => options.fetch(input, { ...init, signal: AbortSignal.timeout(timeoutMs) }),
356
+ };
357
+ return probeWith(conn, options);
358
+ }
359
+ /** Probe the credential a live connection already holds. Verifying is itself a use. */
360
+ export async function probeFortnoxConnection(host, connection, options) {
361
+ const conn = await openFortnoxConnection(host.admin, options.fetch, connection.tenantId, connection.vertical, options.timeoutMs ?? 15_000);
362
+ return probeWith(conn, options);
363
+ }
364
+ async function probeWith(conn, options) {
365
+ const api = new FortnoxApi(conn, {
366
+ apiBase: options.apiBase,
367
+ oauthBase: options.oauthBase,
368
+ now: options.now,
369
+ });
370
+ try {
371
+ const company = await api.companyInformation();
372
+ const years = await api.financialYears();
373
+ const ref = company.DatabaseNumber === undefined ? null : String(company.DatabaseNumber);
374
+ return {
375
+ ok: true,
376
+ refused: false,
377
+ accountRef: ref,
378
+ accountLabel: company.CompanyName || null,
379
+ facts: [
380
+ { label: 'Organisationsnummer', value: company.OrganizationNumber || '—' },
381
+ { label: 'Database number', value: ref ?? '—' },
382
+ {
383
+ label: 'Financial years',
384
+ value: years.length === 0
385
+ ? 'none'
386
+ : `${years.length} (${years
387
+ .map((y) => y.FromDate.slice(0, 4))
388
+ .sort()
389
+ .join(', ')})`,
390
+ },
391
+ ],
392
+ error: null,
393
+ };
394
+ }
395
+ catch (err) {
396
+ return {
397
+ ok: false,
398
+ // Only "not with these credentials" counts. A timeout or a 5xx says nothing about
399
+ // the credential, and treating it as a refusal would make a Fortnox outage look
400
+ // like every tenant's keys going bad at once.
401
+ refused: err instanceof FortnoxApiError && err.refused,
402
+ accountRef: null,
403
+ accountLabel: null,
404
+ facts: [],
405
+ error: err instanceof Error ? err.message : String(err),
406
+ };
407
+ }
408
+ }
409
+ /**
410
+ * What this connection has been doing, for a console — one entry per bound scope.
411
+ *
412
+ * Reads the binding ledger rather than the provider: this answers "what has the platform
413
+ * synced", which is the question an operator asks when a figure looks stale, and it
414
+ * answers it without spending a provider round trip.
415
+ */
416
+ export async function fortnoxConnectionActivity(host, connectionId) {
417
+ const rows = await host.admin.listConnectorState(connectionId, BINDING_PREFIX);
418
+ const entries = rows
419
+ .filter(({ value }) => value !== null && typeof value === 'object')
420
+ .map(({ key, value }) => {
421
+ const b = value;
422
+ return {
423
+ key,
424
+ title: `${b.vertical} — ${b.scopeId}`,
425
+ reference: b.lastSync ? String(b.lastSync.financialYearId) : null,
426
+ status: b.lastSync ? 'synced' : 'bound — not yet synced',
427
+ at: instant.parse(b.lastSync?.syncedAt ?? b.boundAt),
428
+ facts: [
429
+ { label: 'Lands through', value: b.operation },
430
+ { label: 'Permission', value: b.permission },
431
+ ...(b.lastSync
432
+ ? [
433
+ { label: 'Balances', value: String(b.lastSync.balances) },
434
+ { label: 'Financial year', value: String(b.lastSync.financialYearId) },
435
+ // The content hash IS the sync identity, so an operator comparing two
436
+ // scopes can tell "same books" from "same moment" at a glance.
437
+ { label: 'Content hash', value: b.lastSync.contentHash.slice(0, 12) },
438
+ ]
439
+ : []),
440
+ ],
441
+ };
442
+ });
443
+ return connectionActivity.parse({
444
+ source: 'ledger',
445
+ entries,
446
+ // Never live: this reads the binding ledger, never the provider. The ledger knows
447
+ // what the platform synced, not what Fortnox has since booked, and a console that
448
+ // blurs the two invents facts.
449
+ live: false,
450
+ });
451
+ }
452
+ /** Open the live Fortnox connection for a (tenant, vertical), with health recorded. */
453
+ async function openFortnoxConnection(admin, fetchImpl, tenant, vertical, timeoutMs) {
454
+ const parsedTenant = tenantIdSchema.parse(tenant);
455
+ const open = await admin.openConnection(parsedTenant, vertical, 'fortnox');
456
+ if (!open) {
457
+ throw new Error(`no live 'fortnox' connection for tenant ${tenant} / vertical '${vertical}'`);
458
+ }
459
+ return {
460
+ ...open,
461
+ fetch: async (input, init) => {
462
+ try {
463
+ const res = await fetchImpl(input, { ...init, signal: AbortSignal.timeout(timeoutMs) });
464
+ await admin.recordConnectionUse(open.id, res.ok ? { ok: true } : { ok: false, error: `HTTP ${res.status} from fortnox` });
465
+ return res;
466
+ }
467
+ catch (err) {
468
+ await admin.recordConnectionUse(open.id, {
469
+ ok: false,
470
+ error: err instanceof Error ? err.message : String(err),
471
+ });
472
+ throw err;
473
+ }
474
+ },
475
+ };
476
+ }
477
+ //# sourceMappingURL=index.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"index.js","sourceRoot":"","sources":["../src/index.ts"],"names":[],"mappings":"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"}
package/dist/mock.d.ts ADDED
@@ -0,0 +1,59 @@
1
+ import type { FetchLike } from '@substrat-run/kernel';
2
+ /**
3
+ * Fortnox in memory — the token endpoint and the three REST reads, enough to run the
4
+ * whole sweep without a provider account.
5
+ *
6
+ * **What a mock proves:** that our shape works — the client-credentials mint, the
7
+ * latin1 decode, the year-overlap pick, the paging, the unchanged-hash skip.
8
+ * **What it cannot prove:** that our reading of Fortnox's API is correct. The mock IS
9
+ * our reading. Green here means *ready to check against a real company*, which is what
10
+ * `test/live.test.ts` does when a credential is present.
11
+ *
12
+ * The one thing it models with real bytes rather than a convenient string is the SIE
13
+ * response: {@link FortnoxMock.sieBytes} encodes to ISO-8859-1, so a connector that
14
+ * forgot to decode explicitly fails here rather than in production with mangled
15
+ * account names.
16
+ */
17
+ export interface FortnoxMockOptions {
18
+ clientId?: string;
19
+ clientSecret?: string;
20
+ tenantId?: string;
21
+ company?: {
22
+ CompanyName: string;
23
+ OrganizationNumber: string;
24
+ DatabaseNumber: number;
25
+ };
26
+ financialYears?: {
27
+ Id: number;
28
+ FromDate: string;
29
+ ToDate: string;
30
+ }[];
31
+ /** SIE4 text per financial-year id. */
32
+ sie?: Record<number, string>;
33
+ /** Seconds a minted token claims to live. */
34
+ expiresIn?: number;
35
+ }
36
+ /** Encode to ISO-8859-1. Any code point above 0xff becomes `?`, exactly as Fortnox would. */
37
+ export declare function latin1Bytes(text: string): Uint8Array;
38
+ export declare class FortnoxMock {
39
+ readonly apiBase = "https://api.fortnox.test/3";
40
+ readonly oauthBase = "https://apps.fortnox.test/oauth-v1";
41
+ private readonly clientId;
42
+ private readonly clientSecret;
43
+ private readonly tenantId;
44
+ private readonly company;
45
+ private readonly years;
46
+ private readonly sie;
47
+ private readonly expiresIn;
48
+ /** Every token this mock has minted — a test asserts the mint happened exactly once. */
49
+ readonly mints: string[];
50
+ /** Every REST path fetched, in order. */
51
+ readonly calls: string[];
52
+ constructor(options?: FortnoxMockOptions);
53
+ /** Replace one year's SIE payload — how a test simulates "the books changed". */
54
+ setSie(financialYearId: number, text: string): void;
55
+ private json;
56
+ /** The `fetch` a connector is handed. */
57
+ readonly fetch: FetchLike;
58
+ }
59
+ //# sourceMappingURL=mock.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"mock.d.ts","sourceRoot":"","sources":["../src/mock.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,SAAS,EAAE,MAAM,sBAAsB,CAAC;AAMtD;;;;;;;;;;;;;;GAcG;AACH,MAAM,WAAW,kBAAkB;IACjC,QAAQ,CAAC,EAAE,MAAM,CAAC;IAClB,YAAY,CAAC,EAAE,MAAM,CAAC;IACtB,QAAQ,CAAC,EAAE,MAAM,CAAC;IAClB,OAAO,CAAC,EAAE;QAAE,WAAW,EAAE,MAAM,CAAC;QAAC,kBAAkB,EAAE,MAAM,CAAC;QAAC,cAAc,EAAE,MAAM,CAAA;KAAE,CAAC;IACtF,cAAc,CAAC,EAAE;QAAE,EAAE,EAAE,MAAM,CAAC;QAAC,QAAQ,EAAE,MAAM,CAAC;QAAC,MAAM,EAAE,MAAM,CAAA;KAAE,EAAE,CAAC;IACpE,uCAAuC;IACvC,GAAG,CAAC,EAAE,MAAM,CAAC,MAAM,EAAE,MAAM,CAAC,CAAC;IAC7B,6CAA6C;IAC7C,SAAS,CAAC,EAAE,MAAM,CAAC;CACpB;AAED,6FAA6F;AAC7F,wBAAgB,WAAW,CAAC,IAAI,EAAE,MAAM,GAAG,UAAU,CAOpD;AAED,qBAAa,WAAW;IACtB,QAAQ,CAAC,OAAO,gCAAgC;IAChD,QAAQ,CAAC,SAAS,wCAAwC;IAE1D,OAAO,CAAC,QAAQ,CAAC,QAAQ,CAAS;IAClC,OAAO,CAAC,QAAQ,CAAC,YAAY,CAAS;IACtC,OAAO,CAAC,QAAQ,CAAC,QAAQ,CAAS;IAClC,OAAO,CAAC,QAAQ,CAAC,OAAO,CAA6C;IACrE,OAAO,CAAC,QAAQ,CAAC,KAAK,CAAoD;IAC1E,OAAO,CAAC,QAAQ,CAAC,GAAG,CAAyB;IAC7C,OAAO,CAAC,QAAQ,CAAC,SAAS,CAAS;IAEnC,wFAAwF;IACxF,QAAQ,CAAC,KAAK,EAAE,MAAM,EAAE,CAAM;IAC9B,yCAAyC;IACzC,QAAQ,CAAC,KAAK,EAAE,MAAM,EAAE,CAAM;IAE9B,YAAY,OAAO,GAAE,kBAAuB,EAe3C;IAED,iFAAiF;IACjF,MAAM,CAAC,eAAe,EAAE,MAAM,EAAE,IAAI,EAAE,MAAM,GAAG,IAAI,CAElD;IAED,OAAO,CAAC,IAAI;IAUZ,yCAAyC;IACzC,QAAQ,CAAC,KAAK,EAAE,SAAS,CA0DvB;CACH"}