{
  "name": "Conta intercompany checker — private pilot",
  "active": false,
  "nodes": [
    {
      "id": "84a2e40670df461bea03eebd7b34de11",
      "name": "Manual run",
      "type": "n8n-nodes-base.manualTrigger",
      "typeVersion": 1,
      "parameters": {},
      "position": [
        -880,
        -80
      ]
    },
    {
      "id": "3709608962d9e35e101fee34a4d7ab00",
      "name": "Monthly run (disabled)",
      "type": "n8n-nodes-base.scheduleTrigger",
      "typeVersion": 1.2,
      "parameters": {
        "rule": {
          "interval": [
            {
              "field": "cronExpression",
              "expression": "0 6 1 * *"
            }
          ]
        }
      },
      "position": [
        -880,
        100
      ],
      "disabled": true
    },
    {
      "id": "b332c3492d5eb10a118eb6d8b0dcd689",
      "name": "Configuration",
      "type": "n8n-nodes-base.code",
      "typeVersion": 2,
      "parameters": {
        "jsCode": "// No API keys here. Select separate Header Auth credentials in Conta GET A/B.\nreturn [{json: {\n  \"environment\": \"sandbox\",\n  \"period\": \"last-completed-month\",\n  \"tolerance\": \"0.01\",\n  \"companies\": {\n    \"A\": {\n      \"organizationId\": \"REPLACE_ME\",\n      \"name\": \"Company A\",\n      \"authorized\": false,\n      \"currency\": \"NOK\",\n      \"accounts\": [\n        {\n          \"number\": \"1560\",\n          \"dedicatedToOtherCompany\": false,\n          \"positionsInNok\": false\n        }\n      ]\n    },\n    \"B\": {\n      \"organizationId\": \"REPLACE_ME\",\n      \"name\": \"Company B\",\n      \"authorized\": false,\n      \"currency\": \"NOK\",\n      \"accounts\": [\n        {\n          \"number\": \"2960\",\n          \"dedicatedToOtherCompany\": false,\n          \"positionsInNok\": false\n        }\n      ]\n    }\n  },\n  \"sharedInvoiceNumbersConfirmed\": false,\n  \"candidateDateWindowDays\": 3,\n  \"liveValidation\": {\n    \"reference\": \"\",\n    \"signsDatesOpeningAndOmissionsVerified\": false\n  }\n}}];"
      },
      "position": [
        -660,
        -80
      ]
    },
    {
      "id": "281ceeeaecb3aa413380971348e8165a",
      "name": "Validate scope",
      "type": "n8n-nodes-base.code",
      "typeVersion": 2,
      "parameters": {
        "mode": "runOnceForAllItems",
        "jsCode": "// src/money.ts\nfunction ore(value) {\n  if (typeof value !== \"string\" || !/^-?(0|[1-9]\\d*)(\\.\\d{1,2})?$/.test(value)) {\n    throw new Error(\"Ugyldig beløp: forventet desimaltall med høyst to desimaler.\");\n  }\n  const negative = value.startsWith(\"-\");\n  const [whole = \"\", fraction = \"\"] = (negative ? value.slice(1) : value).split(\".\");\n  return (BigInt(whole) * 100n + BigInt(fraction.padEnd(2, \"0\"))) * (negative ? -1n : 1n);\n}\nfunction decimal(value) {\n  const sign = value < 0n ? \"-\" : \"\";\n  const n = value < 0n ? -value : value;\n  return `${sign}${n / 100n}.${String(n % 100n).padStart(2, \"0\")}`;\n}\nfunction abs(n) {\n  return n < 0n ? -n : n;\n}\nfunction nok(value) {\n  const [whole = \"\", fraction = \"00\"] = value.split(\".\");\n  return `${whole.replace(/\\B(?=(\\d{3})+(?!\\d))/g, \" \")},${fraction.padEnd(2, \"0\")}`;\n}\n\n// src/config.ts\nfunction validDate(value) {\n  return typeof value === \"string\" && /^\\d{4}-\\d{2}-\\d{2}$/.test(value) && !Number.isNaN(Date.parse(value)) && new Date(value).toISOString().slice(0, 10) === value;\n}\nfunction scopeFor(input, now) {\n  if (!input || typeof input !== \"object\") throw new Error(\"Konfigurasjonen mangler.\");\n  const c = input;\n  if (![\"sandbox\", \"production\"].includes(c.environment)) throw new Error(\"Ugyldig Conta-miljø.\");\n  if (!Number.isFinite(now.getTime())) throw new Error(\"Ugyldig kjøringstidspunkt.\");\n  const oslo = new Intl.DateTimeFormat(\"en-CA\", { timeZone: \"Europe/Oslo\", year: \"numeric\", month: \"2-digit\", day: \"2-digit\" }).formatToParts(now);\n  const part = (name) => oslo.find((p) => p.type === name).value;\n  const currentMonth = `${part(\"year\")}-${part(\"month\")}`;\n  let month = c.period;\n  if (month === \"last-completed-month\") {\n    const date = /* @__PURE__ */ new Date(`${currentMonth}-01T12:00:00Z`);\n    date.setUTCMonth(date.getUTCMonth() - 1);\n    month = date.toISOString().slice(0, 7);\n  }\n  if (typeof month !== \"string\" || !/^20\\d{2}-(0[1-9]|1[0-2])$/.test(month) || month >= currentMonth) {\n    throw new Error(\"Velg én avsluttet kalendermåned (YYYY-MM).\");\n  }\n  const startDate = `${month}-01`;\n  const end = /* @__PURE__ */ new Date(`${startDate}T12:00:00Z`);\n  end.setUTCMonth(end.getUTCMonth() + 1, 0);\n  const tolerance = ore(c.tolerance);\n  if (tolerance < 0n || tolerance > 10000n) throw new Error(\"Toleranse må være mellom 0 og 100 NOK.\");\n  for (const side of [\"A\", \"B\"]) {\n    const company = c.companies?.[side];\n    if (!company || !/^[1-9]\\d{0,18}$/.test(company.organizationId) || typeof company.organizationId !== \"string\" || typeof company.name !== \"string\" || !company.name.trim() || company.name.length > 200) throw new Error(`Ugyldig selskap ${side}.`);\n    if (company.authorized !== true || company.currency !== \"NOK\") throw new Error(`Selskap ${side} må være autorisert og føre regnskap i NOK.`);\n    if (!Array.isArray(company.accounts) || !company.accounts.length) throw new Error(`Kontovalg mangler for ${side}.`);\n    const seen = /* @__PURE__ */ new Set();\n    for (const account of company.accounts) {\n      if (!account || typeof account.number !== \"string\" || !/^[12]\\d{3}$/.test(account.number) || seen.has(account.number)) throw new Error(`Ugyldig eller gjentatt balansekonto for ${side}.`);\n      if (account.dedicatedToOtherCompany !== true || account.positionsInNok !== true) throw new Error(`Konto ${account.number} må kun gjelde motparten og posisjoner i NOK.`);\n      seen.add(account.number);\n    }\n  }\n  if (c.companies.A.organizationId === c.companies.B.organizationId) throw new Error(\"Selskapene må ha ulike organisasjons-ID-er.\");\n  if (typeof c.sharedInvoiceNumbersConfirmed !== \"boolean\" || !Number.isInteger(c.candidateDateWindowDays) || c.candidateDateWindowDays < 0 || c.candidateDateWindowDays > 31) throw new Error(\"Ugyldige regler for kandidatforslag.\");\n  if (!c.liveValidation || typeof c.liveValidation.reference !== \"string\" || typeof c.liveValidation.signsDatesOpeningAndOmissionsVerified !== \"boolean\") throw new Error(\"Angi status for kontroll mot Conta-rapporter.\");\n  const clean = {\n    environment: c.environment,\n    period: month,\n    tolerance: c.tolerance,\n    companies: Object.fromEntries([\"A\", \"B\"].map((side) => [side, {\n      organizationId: c.companies[side].organizationId,\n      name: c.companies[side].name,\n      authorized: true,\n      currency: \"NOK\",\n      accounts: c.companies[side].accounts.map((a) => ({ number: a.number, dedicatedToOtherCompany: true, positionsInNok: true }))\n    }])),\n    sharedInvoiceNumbersConfirmed: c.sharedInvoiceNumbersConfirmed,\n    candidateDateWindowDays: c.candidateDateWindowDays,\n    liveValidation: { reference: c.liveValidation.reference, signsDatesOpeningAndOmissionsVerified: c.liveValidation.signsDatesOpeningAndOmissionsVerified }\n  };\n  return { config: clean, startDate, endDate: end.toISOString().slice(0, 10) };\n}\n\n// src/adapter.ts\nfunction record(value) {\n  if (!value || typeof value !== \"object\" || Array.isArray(value)) throw new Error(\"Uventet objektskjema fra Conta.\");\n  return value;\n}\nfunction list(value) {\n  if (!Array.isArray(value)) throw new Error(\"Uventet listeskjema fra Conta.\");\n  return value;\n}\nfunction id(value) {\n  if (typeof value !== \"string\" || !/^[1-9]\\d{0,18}$/.test(value)) throw new Error(\"Kilde-ID mangler eller er ugyldig.\");\n  return value;\n}\nfunction text(value, name) {\n  if (typeof value !== \"string\") throw new Error(`${name} mangler eller er ugyldig.`);\n  return value;\n}\nfunction validateAccounts(body, scope, side) {\n  const rows = list(body).map(record);\n  const ids = {};\n  for (const a of scope.config.companies[side].accounts) {\n    const found = rows.filter((r) => r.bookkeepingAccountNo === a.number);\n    if (found.length !== 1) throw new Error(`${side}: Konto ${a.number} mangler eller er tvetydig i kontoplanen.`);\n    ids[a.number] = id(found[0].id);\n    if (typeof found[0].isActive !== \"boolean\") throw new Error(`${side}: Kontostatus mangler for ${a.number}.`);\n  }\n  return ids;\n}\nfunction balances(body, scope, side) {\n  const rows = list(record(body).trialBalanceOutputSub).map(record);\n  return scope.config.companies[side].accounts.map((a) => {\n    const found = rows.filter((r) => r.bookkeepingAccountNo === a.number);\n    if (found.length !== 1) throw new Error(`${side}: Konto ${a.number} mangler eller er tvetydig i saldobalansen. Fravær er ikke null.`);\n    const row = found[0];\n    const opening = ore(row.sumIngoing), movement = ore(row.sumChanged), closing = ore(row.sumOutgoing);\n    if (opening + movement !== closing) throw new Error(`${side}: Inngående saldo + bevegelse stemmer ikke med utgående saldo på ${a.number}.`);\n    return {\n      side,\n      account: a.number,\n      opening: decimal(opening),\n      movement: decimal(movement),\n      closing: decimal(closing),\n      original: { sumIngoing: row.sumIngoing, sumChanged: row.sumChanged, sumOutgoing: row.sumOutgoing }\n    };\n  });\n}\nfunction ledger(receipt, scope) {\n  return list(receipt.body).map((value) => {\n    const row = record(value);\n    const account = text(row.bookkeepingAccountNo, \"Kontonummer\");\n    if (account !== receipt.request.account) throw new Error(\"Detaljsvaret inneholder en annen konto enn forespurt.\");\n    if (!validDate(row.date) || row.date < scope.startDate || row.date > scope.endDate) throw new Error(\"Detaljsvaret inneholder en ugyldig dato eller en dato utenfor perioden.\");\n    const lineId = id(row.id);\n    id(row.transactionId);\n    if (typeof row.isResetTransaction !== \"boolean\" || typeof row.isCorrection !== \"boolean\") throw new Error(\"Status for reversering eller korrigering mangler.\");\n    const original = {};\n    for (const key of [\"id\", \"transactionId\", \"transactionNo\", \"transactionNoYear\", \"date\", \"amount\", \"bookkeepingAccountNo\", \"description\", \"invoiceId\", \"invoiceNo\", \"isResetTransaction\", \"resetByTransactionId\", \"creditNoteId\", \"creditNoteNo\", \"isCorrection\"]) {\n      if (row[key] !== void 0) {\n        if (row[key] !== null && ![\"string\", \"boolean\"].includes(typeof row[key])) throw new Error(\"Uventet felt i kildebevis.\");\n        original[key] = row[key];\n      }\n    }\n    const invoiceNo = row.invoiceNo === void 0 || row.invoiceNo === null || row.invoiceNo === \"\" ? null : text(row.invoiceNo, \"Fakturanummer\");\n    return {\n      side: receipt.request.side,\n      account,\n      id: lineId,\n      date: row.date,\n      amount: decimal(ore(row.amount)),\n      invoiceNo,\n      description: row.description === void 0 ? \"\" : text(row.description, \"Beskrivelse\"),\n      correction: row.isResetTransaction || row.isCorrection || row.resetByTransactionId != null || row.creditNoteId != null,\n      original\n    };\n  });\n}\n\n// src/candidates.ts\nfunction suggest(lines, config) {\n  const candidates = [], unresolved = [];\n  const groups = /* @__PURE__ */ new Map();\n  for (const l of lines) if (l.invoiceNo) {\n    if (!groups.has(l.invoiceNo)) groups.set(l.invoiceNo, { A: [], B: [] });\n    groups.get(l.invoiceNo)[l.side].push(l);\n  }\n  for (const line of lines) {\n    let reason = \"\";\n    const group = line.invoiceNo ? groups.get(line.invoiceNo) : { A: [], B: [] };\n    const a = group.A, b = group.B;\n    if (!config.sharedInvoiceNumbersConfirmed) reason = \"Felles fakturareferanse er ikke bekreftet for relasjonen.\";\n    else if (!line.invoiceNo) reason = \"Mangler felles fakturareferanse.\";\n    else if (a.length > 1 || b.length > 1) reason = \"Tvetydig referanse: flere linjer eller delt oppgjør.\";\n    else if (!a.length || !b.length) reason = \"Ingen linje med samme fakturareferanse hos motparten.\";\n    else if (a[0].correction || b[0].correction) reason = \"Reversering eller korrigering krever manuell vurdering.\";\n    else if (ore(a[0].amount) === 0n || ore(a[0].amount) + ore(b[0].amount) !== 0n) reason = \"Referansen har ulike eller ikke motsatte beløp.\";\n    else if (Math.abs(Date.parse(a[0].date) - Date.parse(b[0].date)) / 864e5 > config.candidateDateWindowDays) reason = \"Referansen ligger utenfor valgt datovindu.\";\n    else {\n      if (line.side === \"A\") candidates.push({ a: a[0].id, b: b[0].id, reference: line.invoiceNo, reason: \"Samme bekreftede fakturareferanse, motsatte like beløp og dato innenfor datovindu. Kun forslag.\" });\n      continue;\n    }\n    unresolved.push({ side: line.side, lineId: line.id, reason });\n  }\n  return { candidates, unresolved };\n}\n\n// src/transport.ts\nfunction parseContaJson(text2) {\n  if (typeof text2 !== \"string\") throw new Error(\"Conta-svaret må hentes som tekst for å bevare beløp og ID-er.\");\n  try {\n    JSON.parse(text2);\n  } catch {\n    throw new Error(\"Conta returnerte ugyldig JSON.\");\n  }\n  const protectedNumbers = text2.replace(\n    /\"(?:\\\\.|[^\"\\\\])*\"|-?\\d+(?:\\.\\d+)?(?:[eE][+-]?\\d+)?/g,\n    (token) => token.startsWith('\"') ? token : JSON.stringify(token)\n  );\n  return JSON.parse(protectedNumbers);\n}\nfunction requestPlan(scope) {\n  const root = scope.config.environment === \"production\" ? \"https://api.gateway.conta.no\" : \"https://api.gateway.conta-sandbox.no\";\n  const requests = [];\n  const add = (side, kind, account) => {\n    const base = `${root}/accounting/organizations/${scope.config.companies[side].organizationId}`;\n    const dates = `startDate=${scope.startDate}&endDate=${scope.endDate}`;\n    const path = kind === \"accounts\" ? \"/bookkeeping-accounts?hits=0&page=0\" : kind === \"details\" ? `/reports/trial-balance/details?${dates}&bookkeepingAccountNo=${account}` : `/reports/trial-balance?${dates}`;\n    requests.push({ side, kind, ...account ? { account } : {}, key: `${side}:${kind}${account ? \":\" + account : \"\"}`, url: base + path });\n  };\n  for (const side of [\"A\", \"B\"]) {\n    add(side, \"accounts\");\n    add(side, \"before\");\n  }\n  for (const side of [\"A\", \"B\"]) for (const a of scope.config.companies[side].accounts) add(side, \"details\", a.number);\n  for (const side of [\"A\", \"B\"]) add(side, \"after\");\n  return requests;\n}\nfunction assertAllowed(request, scope) {\n  const expected = requestPlan(scope).find((r) => r.key === request.key);\n  if (!expected || JSON.stringify(expected) !== JSON.stringify(request)) throw new Error(\"Forespørselen er ikke tillatt.\");\n}\nfunction retryDelay(status, retryAfter, attempt, now) {\n  if (![0, 408, 429, 500, 502, 503, 504].includes(status) || attempt >= 3) return null;\n  let delay = 2 ** attempt;\n  if (retryAfter !== void 0) {\n    if (typeof retryAfter !== \"string\") return null;\n    if (/^\\d+$/.test(retryAfter)) delay = Math.max(delay, Number(retryAfter));\n    else {\n      const at = Date.parse(retryAfter);\n      if (!Number.isFinite(at)) return null;\n      delay = Math.max(delay, Math.ceil((at - now) / 1e3));\n    }\n  }\n  return delay <= 300 ? delay : null;\n}\n\n// src/compare.ts\nfunction emptyReport(scope, generatedAt, synthetic = false) {\n  return {\n    version: \"0.1.0\",\n    synthetic,\n    generatedAt,\n    completeness: \"INCOMPLETE\",\n    agreement: null,\n    scope,\n    issues: [],\n    accounts: [],\n    totals: null,\n    residual: null,\n    lines: [],\n    candidates: [],\n    unresolved: [],\n    sources: []\n  };\n}\nfunction compare(scope, receipts, now, synthetic = false) {\n  const report = emptyReport(scope, now.toISOString(), synthetic);\n  try {\n    const plan = requestPlan(scope);\n    if (receipts.length !== plan.length) throw new Error(\"Datainnhentingen er ufullstendig eller inneholder ekstra svar.\");\n    let lastTime = -Infinity;\n    for (let i = 0; i < plan.length; i++) {\n      const r = receipts[i];\n      if (JSON.stringify(r.request) !== JSON.stringify(plan[i])) throw new Error(\"Kildesvar har feil rekkefølge eller omfang.\");\n      const time = Date.parse(r.retrievedAt);\n      if (!Number.isFinite(time) || time < lastTime || time > now.getTime()) throw new Error(\"Ugyldig tidsstempel for datainnhenting.\");\n      lastTime = time;\n      report.sources.push({ key: r.request.key, url: r.request.url, retrievedAt: r.retrievedAt, status: r.status });\n      if (r.status !== 200 || r.failure || r.body === void 0) throw new Error(`${r.request.key}: Innhenting mislyktes (HTTP ${r.status || \"ukjent\"}).`);\n    }\n    for (const side of [\"A\", \"B\"]) {\n      const get = (kind) => receipts.find((r) => r.request.side === side && r.request.kind === kind);\n      const accountIds = validateAccounts(get(\"accounts\").body, scope, side);\n      const before = balances(get(\"before\").body, scope, side);\n      const after = balances(get(\"after\").body, scope, side);\n      const values = (list2) => list2.map(({ original: _original, ...balance }) => balance);\n      if (JSON.stringify(values(before)) !== JSON.stringify(values(after))) throw new Error(`${side}: Saldoene endret seg under innhenting. Kjør kontrollen på nytt.`);\n      const lines = receipts.filter((r) => r.request.side === side && r.request.kind === \"details\").flatMap((r) => ledger(r, scope));\n      const ids = /* @__PURE__ */ new Set();\n      for (const l of lines) {\n        if (ids.has(l.id)) throw new Error(`${side}: Gjentatt kildelinje-ID ${l.id}.`);\n        ids.add(l.id);\n      }\n      for (const balance of before) {\n        const sum = lines.filter((l) => l.account === balance.account).reduce((n, l) => n + ore(l.amount), 0n);\n        if (sum !== ore(balance.movement)) throw new Error(`${side}: Detaljlinjene stemmer ikke med periodens bevegelse på konto ${balance.account}.`);\n      }\n      report.accounts.push(...before.map((a) => ({ ...a, accountId: accountIds[a.account] })));\n      report.lines.push(...lines);\n    }\n    if (!synthetic && (!scope.config.liveValidation.signsDatesOpeningAndOmissionsVerified || !scope.config.liveValidation.reference.trim())) {\n      throw new Error(\"Fortegn, datoavgrensning, inngående saldo og utelatte kontoer er ikke verifisert mot autoriserte Conta-rapporter.\");\n    }\n    const total = (side) => Object.fromEntries([\"opening\", \"movement\", \"closing\"].map((k) => [\n      k,\n      decimal(report.accounts.filter((a) => a.side === side).reduce((n, a) => n + ore(a[k]), 0n))\n    ]));\n    report.totals = { A: total(\"A\"), B: total(\"B\") };\n    const residual = Object.fromEntries([\"opening\", \"movement\", \"closing\"].map((k) => [\n      k,\n      decimal(ore(report.totals.A[k]) + ore(report.totals.B[k]))\n    ]));\n    report.residual = { ...residual, magnitude: decimal(abs(ore(residual.closing))) };\n    Object.assign(report, suggest(report.lines, scope.config));\n    report.completeness = \"COMPLETE\";\n    report.agreement = abs(ore(residual.closing)) <= ore(scope.config.tolerance) ? \"BALANCES_AGREE\" : \"DIFFERENCE\";\n  } catch (error) {\n    report.issues.push(error instanceof Error ? error.message : \"Kontrollen kunne ikke fullføres.\");\n    report.accounts = [];\n    report.totals = null;\n    report.residual = null;\n    report.unresolved = report.lines.map((l) => ({ side: l.side, lineId: l.id, reason: \"Ufullstendig kontroll. Linjen er ikke vurdert.\" }));\n  }\n  return report;\n}\n\n// src/engine.ts\nfunction initialize(config, now) {\n  try {\n    const scope = scopeFor(config, now);\n    return { scope, plan: requestPlan(scope), index: 0, attempt: 1, receipts: [], route: \"A\" };\n  } catch (error) {\n    const report = emptyReport(null, now.toISOString());\n    report.issues.push(error instanceof TypeError ? \"Konfigurasjonen har ugyldig struktur.\" : error instanceof Error ? error.message : \"Ugyldig konfigurasjon.\");\n    return { scope: null, plan: [], index: 0, attempt: 1, receipts: [], route: \"DONE\", report };\n  }\n}\nfunction dispatch(state, now) {\n  if (state.report) return { ...state, route: \"DONE\" };\n  if (!state.scope) throw new Error(\"Mangler validert omfang.\");\n  if (state.waitUntil && state.waitUntil > now.getTime()) return { ...state, route: \"WAIT\", waitSeconds: Math.max(1, Math.ceil((state.waitUntil - now.getTime()) / 1e3)) };\n  if (state.index >= state.plan.length) return { ...state, route: \"DONE\", report: compare(state.scope, state.receipts, now) };\n  const request = state.plan[state.index];\n  assertAllowed(request, state.scope);\n  return { ...state, waitUntil: void 0, waitSeconds: void 0, route: request.side, request };\n}\nfunction accept(state, response, now) {\n  if (!state.scope || !state.request) throw new Error(\"Svar uten en validert forespørsel.\");\n  assertAllowed(state.request, state.scope);\n  const value = response && typeof response === \"object\" ? response : {};\n  const status = typeof value.statusCode === \"number\" && Number.isInteger(value.statusCode) ? value.statusCode : 0;\n  const headers = value.headers && typeof value.headers === \"object\" ? value.headers : {};\n  if (status !== 200) {\n    const retryAfter = Object.entries(headers).find(([key]) => key.toLowerCase() === \"retry-after\")?.[1];\n    const delay = retryDelay(status, retryAfter, state.attempt, now.getTime());\n    if (delay !== null) return { ...state, attempt: state.attempt + 1, waitUntil: now.getTime() + delay * 1e3, route: \"WAIT\", waitSeconds: delay };\n  }\n  const receipt = { request: state.request, retrievedAt: now.toISOString(), status };\n  try {\n    if (status !== 200) throw new Error(\"Forespørselen mislyktes.\");\n    receipt.body = parseContaJson(value.body);\n  } catch {\n    receipt.failure = \"Innhenting eller lesing av svaret mislyktes.\";\n  }\n  const next = { ...state, receipts: [...state.receipts, receipt], index: state.index + 1, attempt: 1, waitUntil: void 0 };\n  if (receipt.failure) {\n    const report = emptyReport(state.scope, now.toISOString());\n    report.issues.push(`${receipt.request.key}: ${receipt.failure} HTTP ${status || \"ukjent\"}.`);\n    report.sources = next.receipts.map((r) => ({ key: r.request.key, url: r.request.url, status: r.status, retrievedAt: r.retrievedAt }));\n    return { ...next, report, route: \"DONE\" };\n  }\n  return next;\n}\n\n// src/report.ts\nfunction escapeHtml(value) {\n  return String(value ?? \"\").replace(/[&<>\"']/g, (c) => ({ \"&\": \"&amp;\", \"<\": \"&lt;\", \">\": \"&gt;\", '\"': \"&quot;\", \"'\": \"&#39;\" })[c]);\n}\nfunction csvCell(value) {\n  let text2 = String(value ?? \"\");\n  if (/^[\\s\\u0000-\\u001f\\ufeff]*[=+@-]/.test(text2) || /^[\\t\\r\\n]/.test(text2)) text2 = \"'\" + text2;\n  return '\"' + text2.replace(/\"/g, '\"\"') + '\"';\n}\nvar table = (heads, rows) => `<div class=\"scroll\"><table><thead><tr>${heads.map((h) => `<th scope=\"col\">${escapeHtml(h)}</th>`).join(\"\")}</tr></thead><tbody>${rows.map((row) => `<tr>${row.map((cell) => `<td>${escapeHtml(cell)}</td>`).join(\"\")}</tr>`).join(\"\")}</tbody></table></div>`;\nfunction render(report) {\n  const r = report, s = r.scope;\n  const complete = r.completeness === \"COMPLETE\";\n  const status = r.completeness === \"INCOMPLETE\" ? \"Ufullstendig kontroll\" : r.agreement === \"BALANCES_AGREE\" ? \"Saldoene stemmer innenfor toleransen\" : \"Differanse i saldoene\";\n  const candidateIds = new Set(r.candidates.flatMap((c) => [`A:${c.a}`, `B:${c.b}`]));\n  const reasons = new Map(r.unresolved.map((u) => [`${u.side}:${u.lineId}`, u.reason]));\n  const lineRows = r.lines.map((l) => [\n    l.side,\n    l.account,\n    l.id,\n    l.original.transactionId,\n    l.original.transactionNoYear ?? l.original.transactionNo ?? \"\",\n    l.date,\n    l.amount,\n    l.invoiceNo ?? \"\",\n    l.correction ? \"Ja\" : \"Nei\",\n    l.description,\n    candidateIds.has(`${l.side}:${l.id}`) ? \"Kandidat – krever vurdering\" : reasons.get(`${l.side}:${l.id}`) ?? \"Ikke vurdert\"\n  ]);\n  const heads = [\"Side\", \"Konto\", \"Kildelinje-ID\", \"Transaksjons-ID\", \"Bilagsnummer\", \"Dato\", \"Beløp NOK\", \"Fakturareferanse\", \"Korreksjon/reversering\", \"Beskrivelse\", \"Vurdering\"];\n  const csvRows = [\n    [\"Mellomværendekontroll for Conta\", r.synthetic ? \"KUN SYNTETISKE DATA\" : \"Privat regnskapsmateriale\"],\n    [\"Datastatus\", r.completeness],\n    [\"Saldostatus\", r.agreement ?? \"IKKE VURDERT\"],\n    [\"Selskap A\", s?.config.companies.A.name ?? \"\", s?.config.companies.A.organizationId ?? \"\"],\n    [\"Selskap B\", s?.config.companies.B.name ?? \"\", s?.config.companies.B.organizationId ?? \"\"],\n    [\"Valgte kontoer A\", s?.config.companies.A.accounts.map((a) => a.number).join(\", \") ?? \"UKJENT\"],\n    [\"Valgte kontoer B\", s?.config.companies.B.accounts.map((a) => a.number).join(\", \") ?? \"UKJENT\"],\n    [\"Fra\", s?.startDate ?? \"\", \"Til\", s?.endDate ?? \"\"],\n    [\"Toleranse NOK\", s?.config.tolerance ?? \"\"],\n    [\"Side\", \"Konto\", \"Inngående NOK\", \"Bevegelse NOK\", \"Utgående NOK\"],\n    ...r.accounts.map((a) => [a.side, a.account, a.opening, a.movement, a.closing]),\n    [\"Restsaldo A + B\", \"\", r.residual?.opening ?? \"UKJENT\", r.residual?.movement ?? \"UKJENT\", r.residual?.closing ?? \"UKJENT\"],\n    [\"Absolutt utgående differanse NOK\", r.residual?.magnitude ?? \"UKJENT\"],\n    [\"Antall kandidatpar\", complete ? r.candidates.length : \"IKKE VURDERT\"],\n    [\"Antall linjer uten kandidat\", complete ? r.unresolved.length : \"IKKE VURDERT\"],\n    ...r.issues.map((i) => [\"Problem\", i]),\n    [\"Saldoenighet er ikke ferdig avstemming. Kandidater og uavklarte linjer krever regnskapsførers vurdering.\"],\n    [],\n    [\"Linje A\", \"Linje B\", \"Felles referanse\", \"Begrunnelse\"],\n    ...r.candidates.map((c) => [c.a, c.b, c.reference, c.reason]),\n    [],\n    heads,\n    ...lineRows,\n    [],\n    [\"Kilde\", \"URL\", \"Hentet\", \"HTTP\"],\n    ...r.sources.map((source) => [source.key, source.url, source.retrievedAt, source.status])\n  ];\n  const html = `<!doctype html><html lang=\"nb\"><head><meta charset=\"utf-8\"><meta name=\"viewport\" content=\"width=device-width,initial-scale=1\"><meta http-equiv=\"Content-Security-Policy\" content=\"default-src 'none'; style-src 'unsafe-inline'; base-uri 'none'; form-action 'none'\"><title>Mellomværendekontroll for Conta</title><style>\n  :root{font-family:system-ui,sans-serif;color:#172b36;background:#f2f5f4}body{margin:0}main{max-width:1120px;margin:32px auto;padding:32px;background:white;border-radius:12px}h1{font-size:30px;line-height:1.2}h2{margin-top:32px;font-size:21px}.eyebrow{font-size:13px;letter-spacing:.07em;text-transform:uppercase;color:#526b71}.status{padding:20px;background:#edf5f1;border-left:5px solid #1c6955}.amount{font-size:36px;font-weight:700;margin:10px 0}.note{color:#465961;line-height:1.6}.scroll{overflow-x:auto}table{border-collapse:collapse;width:100%;font-size:13px}th,td{padding:10px;text-align:left;border-bottom:1px solid #dce4e2;vertical-align:top;overflow-wrap:anywhere}th{background:#edf2f1}ul{line-height:1.7}footer{margin-top:32px;border-top:1px solid #dce4e2;padding-top:16px;font-size:12px;color:#465961}@media(max-width:600px){main{margin:0;padding:20px;border-radius:0}h1{font-size:25px}.amount{font-size:30px}}@media print{main{margin:0;padding:0;max-width:none}.scroll{overflow:visible}thead{display:table-header-group}tr{break-inside:avoid}}\n  </style></head><body><main><p class=\"eyebrow\">${r.synthetic ? \"Demonstrasjon · kun syntetiske data\" : \"Privat · grunnlag for regnskapsførers vurdering\"}</p><h1>Mellomværendekontroll for Conta</h1>\n  <p>${escapeHtml(s?.config.companies.A.name ?? \"Ukjent selskap\")} ↔ ${escapeHtml(s?.config.companies.B.name ?? \"Ukjent selskap\")}</p><p>${escapeHtml(s?.startDate)}–${escapeHtml(s?.endDate)} · NOK · toleranse ${escapeHtml(s ? nok(s.config.tolerance) : \"ukjent\")}</p>\n  <section class=\"status\"><strong>${escapeHtml(status)}</strong><p class=\"amount\">${r.residual ? escapeHtml(nok(r.residual.closing)) + \" NOK\" : \"Ikke beregnet\"}</p><p>Signert utgående restsaldo: A + B${r.residual ? \". Absolutt differanse: \" + escapeHtml(nok(r.residual.magnitude)) + \" NOK.\" : \".\"}</p><p>Datastatus: ${r.completeness} · Saldostatus: ${r.agreement ?? \"IKKE VURDERT\"}</p></section>\n  <p class=\"note\">Saldoenighet er ikke en ferdig avstemming eller bevis på at alle transaksjoner er bokført. Uavklarte linjer er ikke bekreftede bokføringsfeil. Regnskapsføreren godkjenner kontovalg og konklusjoner.</p>\n${r.issues.length ? \"<h2>Problemer som må løses</h2><ul>\" + r.issues.map((i) => \"<li>\" + escapeHtml(i) + \"</li>\").join(\"\") + \"</ul>\" : \"\"}\n  <h2>Omfang og kontovalg</h2>${table([\"Side\", \"Selskap\", \"Conta-ID\", \"Dedikerte kontoer\"], s ? [\"A\", \"B\"].map((side) => [side, s.config.companies[side].name, s.config.companies[side].organizationId, s.config.companies[side].accounts.map((a) => a.number).join(\", \")]) : [])}\n  <h2>Inngående saldo, bevegelse og utgående saldo</h2>${table([\"Side/konto\", \"Inngående NOK\", \"Bevegelse NOK\", \"Utgående NOK\"], [...r.accounts.map((a) => [a.side + \" / \" + a.account, nok(a.opening), nok(a.movement), nok(a.closing)]), ...r.residual ? [[\"Restsaldo A + B\", nok(r.residual.opening), nok(r.residual.movement), nok(r.residual.closing)]] : []])}\n  <p class=\"note\">Inngående restsaldo + periodens bevegelsesdifferanse = utgående restsaldo. En inngående differanse oppsto før denne måneden.</p>\n  <h2>Kandidatforslag (${complete ? r.candidates.length : \"ikke vurdert\"})</h2>${table([\"Linje A\", \"Linje B\", \"Felles referanse\", \"Begrunnelse\"], r.candidates.map((c) => [c.a, c.b, c.reference, c.reason]))}<p class=\"note\">${complete ? `Ingen forslag er automatisk bekreftet. ${r.unresolved.length} linjer gjenstår uten kandidat.` : \"Kandidater og antall uavklarte linjer kan ikke vurderes før kontrollen er fullført.\"}</p>\n  <h2>${complete ? `Linjegrunnlag (${r.lines.length})` : \"Tilgjengelig linjegrunnlag – ufullstendig\"}</h2>${complete ? \"\" : '<p class=\"note\">Tabellen viser bare linjer som er validert før kontrollen stoppet. En tom tabell betyr ikke at perioden er uten transaksjoner.</p>'}${table(heads, lineRows)}\n  <h2>Kilder og innhenting</h2>${table([\"Kilde\", \"Hentet (UTC)\", \"HTTP\"], r.sources.map((source) => [source.key, source.retrievedAt, source.status]))}\n  <p class=\"note\">Saldoene hentes på nytt etter detaljene. Dette oppdager enkelte endringer underveis, men gir ikke et atomisk historisk øyeblikksbilde. Endringer som utligner hverandre kan forbli uoppdaget.</p><footer>Generert ${escapeHtml(r.generatedAt)} · versjon ${r.version} · Oppbevar rapport og kildegrunnlag privat etter virksomhetens rutiner.</footer></main></body></html>`;\n  return { html, csv: \"\\uFEFF\" + csvRows.map((row) => row.map(csvCell).join(\";\")).join(\"\\r\\n\") + \"\\r\\n\", json: JSON.stringify(report, null, 2) };\n}\n\n// <stdin>\nvar Checker = { initialize, dispatch, accept, compare, render, scopeFor };\n\nreturn [{ json: Checker.initialize($input.first().json, new Date()), pairedItem: { item: 0 } }];"
      },
      "position": [
        -440,
        -80
      ]
    },
    {
      "id": "d7cd89078fd1f52df1efedf11244188f",
      "name": "Dispatch request",
      "type": "n8n-nodes-base.code",
      "typeVersion": 2,
      "parameters": {
        "mode": "runOnceForAllItems",
        "jsCode": "// src/money.ts\nfunction ore(value) {\n  if (typeof value !== \"string\" || !/^-?(0|[1-9]\\d*)(\\.\\d{1,2})?$/.test(value)) {\n    throw new Error(\"Ugyldig beløp: forventet desimaltall med høyst to desimaler.\");\n  }\n  const negative = value.startsWith(\"-\");\n  const [whole = \"\", fraction = \"\"] = (negative ? value.slice(1) : value).split(\".\");\n  return (BigInt(whole) * 100n + BigInt(fraction.padEnd(2, \"0\"))) * (negative ? -1n : 1n);\n}\nfunction decimal(value) {\n  const sign = value < 0n ? \"-\" : \"\";\n  const n = value < 0n ? -value : value;\n  return `${sign}${n / 100n}.${String(n % 100n).padStart(2, \"0\")}`;\n}\nfunction abs(n) {\n  return n < 0n ? -n : n;\n}\nfunction nok(value) {\n  const [whole = \"\", fraction = \"00\"] = value.split(\".\");\n  return `${whole.replace(/\\B(?=(\\d{3})+(?!\\d))/g, \" \")},${fraction.padEnd(2, \"0\")}`;\n}\n\n// src/config.ts\nfunction validDate(value) {\n  return typeof value === \"string\" && /^\\d{4}-\\d{2}-\\d{2}$/.test(value) && !Number.isNaN(Date.parse(value)) && new Date(value).toISOString().slice(0, 10) === value;\n}\nfunction scopeFor(input, now) {\n  if (!input || typeof input !== \"object\") throw new Error(\"Konfigurasjonen mangler.\");\n  const c = input;\n  if (![\"sandbox\", \"production\"].includes(c.environment)) throw new Error(\"Ugyldig Conta-miljø.\");\n  if (!Number.isFinite(now.getTime())) throw new Error(\"Ugyldig kjøringstidspunkt.\");\n  const oslo = new Intl.DateTimeFormat(\"en-CA\", { timeZone: \"Europe/Oslo\", year: \"numeric\", month: \"2-digit\", day: \"2-digit\" }).formatToParts(now);\n  const part = (name) => oslo.find((p) => p.type === name).value;\n  const currentMonth = `${part(\"year\")}-${part(\"month\")}`;\n  let month = c.period;\n  if (month === \"last-completed-month\") {\n    const date = /* @__PURE__ */ new Date(`${currentMonth}-01T12:00:00Z`);\n    date.setUTCMonth(date.getUTCMonth() - 1);\n    month = date.toISOString().slice(0, 7);\n  }\n  if (typeof month !== \"string\" || !/^20\\d{2}-(0[1-9]|1[0-2])$/.test(month) || month >= currentMonth) {\n    throw new Error(\"Velg én avsluttet kalendermåned (YYYY-MM).\");\n  }\n  const startDate = `${month}-01`;\n  const end = /* @__PURE__ */ new Date(`${startDate}T12:00:00Z`);\n  end.setUTCMonth(end.getUTCMonth() + 1, 0);\n  const tolerance = ore(c.tolerance);\n  if (tolerance < 0n || tolerance > 10000n) throw new Error(\"Toleranse må være mellom 0 og 100 NOK.\");\n  for (const side of [\"A\", \"B\"]) {\n    const company = c.companies?.[side];\n    if (!company || !/^[1-9]\\d{0,18}$/.test(company.organizationId) || typeof company.organizationId !== \"string\" || typeof company.name !== \"string\" || !company.name.trim() || company.name.length > 200) throw new Error(`Ugyldig selskap ${side}.`);\n    if (company.authorized !== true || company.currency !== \"NOK\") throw new Error(`Selskap ${side} må være autorisert og føre regnskap i NOK.`);\n    if (!Array.isArray(company.accounts) || !company.accounts.length) throw new Error(`Kontovalg mangler for ${side}.`);\n    const seen = /* @__PURE__ */ new Set();\n    for (const account of company.accounts) {\n      if (!account || typeof account.number !== \"string\" || !/^[12]\\d{3}$/.test(account.number) || seen.has(account.number)) throw new Error(`Ugyldig eller gjentatt balansekonto for ${side}.`);\n      if (account.dedicatedToOtherCompany !== true || account.positionsInNok !== true) throw new Error(`Konto ${account.number} må kun gjelde motparten og posisjoner i NOK.`);\n      seen.add(account.number);\n    }\n  }\n  if (c.companies.A.organizationId === c.companies.B.organizationId) throw new Error(\"Selskapene må ha ulike organisasjons-ID-er.\");\n  if (typeof c.sharedInvoiceNumbersConfirmed !== \"boolean\" || !Number.isInteger(c.candidateDateWindowDays) || c.candidateDateWindowDays < 0 || c.candidateDateWindowDays > 31) throw new Error(\"Ugyldige regler for kandidatforslag.\");\n  if (!c.liveValidation || typeof c.liveValidation.reference !== \"string\" || typeof c.liveValidation.signsDatesOpeningAndOmissionsVerified !== \"boolean\") throw new Error(\"Angi status for kontroll mot Conta-rapporter.\");\n  const clean = {\n    environment: c.environment,\n    period: month,\n    tolerance: c.tolerance,\n    companies: Object.fromEntries([\"A\", \"B\"].map((side) => [side, {\n      organizationId: c.companies[side].organizationId,\n      name: c.companies[side].name,\n      authorized: true,\n      currency: \"NOK\",\n      accounts: c.companies[side].accounts.map((a) => ({ number: a.number, dedicatedToOtherCompany: true, positionsInNok: true }))\n    }])),\n    sharedInvoiceNumbersConfirmed: c.sharedInvoiceNumbersConfirmed,\n    candidateDateWindowDays: c.candidateDateWindowDays,\n    liveValidation: { reference: c.liveValidation.reference, signsDatesOpeningAndOmissionsVerified: c.liveValidation.signsDatesOpeningAndOmissionsVerified }\n  };\n  return { config: clean, startDate, endDate: end.toISOString().slice(0, 10) };\n}\n\n// src/adapter.ts\nfunction record(value) {\n  if (!value || typeof value !== \"object\" || Array.isArray(value)) throw new Error(\"Uventet objektskjema fra Conta.\");\n  return value;\n}\nfunction list(value) {\n  if (!Array.isArray(value)) throw new Error(\"Uventet listeskjema fra Conta.\");\n  return value;\n}\nfunction id(value) {\n  if (typeof value !== \"string\" || !/^[1-9]\\d{0,18}$/.test(value)) throw new Error(\"Kilde-ID mangler eller er ugyldig.\");\n  return value;\n}\nfunction text(value, name) {\n  if (typeof value !== \"string\") throw new Error(`${name} mangler eller er ugyldig.`);\n  return value;\n}\nfunction validateAccounts(body, scope, side) {\n  const rows = list(body).map(record);\n  const ids = {};\n  for (const a of scope.config.companies[side].accounts) {\n    const found = rows.filter((r) => r.bookkeepingAccountNo === a.number);\n    if (found.length !== 1) throw new Error(`${side}: Konto ${a.number} mangler eller er tvetydig i kontoplanen.`);\n    ids[a.number] = id(found[0].id);\n    if (typeof found[0].isActive !== \"boolean\") throw new Error(`${side}: Kontostatus mangler for ${a.number}.`);\n  }\n  return ids;\n}\nfunction balances(body, scope, side) {\n  const rows = list(record(body).trialBalanceOutputSub).map(record);\n  return scope.config.companies[side].accounts.map((a) => {\n    const found = rows.filter((r) => r.bookkeepingAccountNo === a.number);\n    if (found.length !== 1) throw new Error(`${side}: Konto ${a.number} mangler eller er tvetydig i saldobalansen. Fravær er ikke null.`);\n    const row = found[0];\n    const opening = ore(row.sumIngoing), movement = ore(row.sumChanged), closing = ore(row.sumOutgoing);\n    if (opening + movement !== closing) throw new Error(`${side}: Inngående saldo + bevegelse stemmer ikke med utgående saldo på ${a.number}.`);\n    return {\n      side,\n      account: a.number,\n      opening: decimal(opening),\n      movement: decimal(movement),\n      closing: decimal(closing),\n      original: { sumIngoing: row.sumIngoing, sumChanged: row.sumChanged, sumOutgoing: row.sumOutgoing }\n    };\n  });\n}\nfunction ledger(receipt, scope) {\n  return list(receipt.body).map((value) => {\n    const row = record(value);\n    const account = text(row.bookkeepingAccountNo, \"Kontonummer\");\n    if (account !== receipt.request.account) throw new Error(\"Detaljsvaret inneholder en annen konto enn forespurt.\");\n    if (!validDate(row.date) || row.date < scope.startDate || row.date > scope.endDate) throw new Error(\"Detaljsvaret inneholder en ugyldig dato eller en dato utenfor perioden.\");\n    const lineId = id(row.id);\n    id(row.transactionId);\n    if (typeof row.isResetTransaction !== \"boolean\" || typeof row.isCorrection !== \"boolean\") throw new Error(\"Status for reversering eller korrigering mangler.\");\n    const original = {};\n    for (const key of [\"id\", \"transactionId\", \"transactionNo\", \"transactionNoYear\", \"date\", \"amount\", \"bookkeepingAccountNo\", \"description\", \"invoiceId\", \"invoiceNo\", \"isResetTransaction\", \"resetByTransactionId\", \"creditNoteId\", \"creditNoteNo\", \"isCorrection\"]) {\n      if (row[key] !== void 0) {\n        if (row[key] !== null && ![\"string\", \"boolean\"].includes(typeof row[key])) throw new Error(\"Uventet felt i kildebevis.\");\n        original[key] = row[key];\n      }\n    }\n    const invoiceNo = row.invoiceNo === void 0 || row.invoiceNo === null || row.invoiceNo === \"\" ? null : text(row.invoiceNo, \"Fakturanummer\");\n    return {\n      side: receipt.request.side,\n      account,\n      id: lineId,\n      date: row.date,\n      amount: decimal(ore(row.amount)),\n      invoiceNo,\n      description: row.description === void 0 ? \"\" : text(row.description, \"Beskrivelse\"),\n      correction: row.isResetTransaction || row.isCorrection || row.resetByTransactionId != null || row.creditNoteId != null,\n      original\n    };\n  });\n}\n\n// src/candidates.ts\nfunction suggest(lines, config) {\n  const candidates = [], unresolved = [];\n  const groups = /* @__PURE__ */ new Map();\n  for (const l of lines) if (l.invoiceNo) {\n    if (!groups.has(l.invoiceNo)) groups.set(l.invoiceNo, { A: [], B: [] });\n    groups.get(l.invoiceNo)[l.side].push(l);\n  }\n  for (const line of lines) {\n    let reason = \"\";\n    const group = line.invoiceNo ? groups.get(line.invoiceNo) : { A: [], B: [] };\n    const a = group.A, b = group.B;\n    if (!config.sharedInvoiceNumbersConfirmed) reason = \"Felles fakturareferanse er ikke bekreftet for relasjonen.\";\n    else if (!line.invoiceNo) reason = \"Mangler felles fakturareferanse.\";\n    else if (a.length > 1 || b.length > 1) reason = \"Tvetydig referanse: flere linjer eller delt oppgjør.\";\n    else if (!a.length || !b.length) reason = \"Ingen linje med samme fakturareferanse hos motparten.\";\n    else if (a[0].correction || b[0].correction) reason = \"Reversering eller korrigering krever manuell vurdering.\";\n    else if (ore(a[0].amount) === 0n || ore(a[0].amount) + ore(b[0].amount) !== 0n) reason = \"Referansen har ulike eller ikke motsatte beløp.\";\n    else if (Math.abs(Date.parse(a[0].date) - Date.parse(b[0].date)) / 864e5 > config.candidateDateWindowDays) reason = \"Referansen ligger utenfor valgt datovindu.\";\n    else {\n      if (line.side === \"A\") candidates.push({ a: a[0].id, b: b[0].id, reference: line.invoiceNo, reason: \"Samme bekreftede fakturareferanse, motsatte like beløp og dato innenfor datovindu. Kun forslag.\" });\n      continue;\n    }\n    unresolved.push({ side: line.side, lineId: line.id, reason });\n  }\n  return { candidates, unresolved };\n}\n\n// src/transport.ts\nfunction parseContaJson(text2) {\n  if (typeof text2 !== \"string\") throw new Error(\"Conta-svaret må hentes som tekst for å bevare beløp og ID-er.\");\n  try {\n    JSON.parse(text2);\n  } catch {\n    throw new Error(\"Conta returnerte ugyldig JSON.\");\n  }\n  const protectedNumbers = text2.replace(\n    /\"(?:\\\\.|[^\"\\\\])*\"|-?\\d+(?:\\.\\d+)?(?:[eE][+-]?\\d+)?/g,\n    (token) => token.startsWith('\"') ? token : JSON.stringify(token)\n  );\n  return JSON.parse(protectedNumbers);\n}\nfunction requestPlan(scope) {\n  const root = scope.config.environment === \"production\" ? \"https://api.gateway.conta.no\" : \"https://api.gateway.conta-sandbox.no\";\n  const requests = [];\n  const add = (side, kind, account) => {\n    const base = `${root}/accounting/organizations/${scope.config.companies[side].organizationId}`;\n    const dates = `startDate=${scope.startDate}&endDate=${scope.endDate}`;\n    const path = kind === \"accounts\" ? \"/bookkeeping-accounts?hits=0&page=0\" : kind === \"details\" ? `/reports/trial-balance/details?${dates}&bookkeepingAccountNo=${account}` : `/reports/trial-balance?${dates}`;\n    requests.push({ side, kind, ...account ? { account } : {}, key: `${side}:${kind}${account ? \":\" + account : \"\"}`, url: base + path });\n  };\n  for (const side of [\"A\", \"B\"]) {\n    add(side, \"accounts\");\n    add(side, \"before\");\n  }\n  for (const side of [\"A\", \"B\"]) for (const a of scope.config.companies[side].accounts) add(side, \"details\", a.number);\n  for (const side of [\"A\", \"B\"]) add(side, \"after\");\n  return requests;\n}\nfunction assertAllowed(request, scope) {\n  const expected = requestPlan(scope).find((r) => r.key === request.key);\n  if (!expected || JSON.stringify(expected) !== JSON.stringify(request)) throw new Error(\"Forespørselen er ikke tillatt.\");\n}\nfunction retryDelay(status, retryAfter, attempt, now) {\n  if (![0, 408, 429, 500, 502, 503, 504].includes(status) || attempt >= 3) return null;\n  let delay = 2 ** attempt;\n  if (retryAfter !== void 0) {\n    if (typeof retryAfter !== \"string\") return null;\n    if (/^\\d+$/.test(retryAfter)) delay = Math.max(delay, Number(retryAfter));\n    else {\n      const at = Date.parse(retryAfter);\n      if (!Number.isFinite(at)) return null;\n      delay = Math.max(delay, Math.ceil((at - now) / 1e3));\n    }\n  }\n  return delay <= 300 ? delay : null;\n}\n\n// src/compare.ts\nfunction emptyReport(scope, generatedAt, synthetic = false) {\n  return {\n    version: \"0.1.0\",\n    synthetic,\n    generatedAt,\n    completeness: \"INCOMPLETE\",\n    agreement: null,\n    scope,\n    issues: [],\n    accounts: [],\n    totals: null,\n    residual: null,\n    lines: [],\n    candidates: [],\n    unresolved: [],\n    sources: []\n  };\n}\nfunction compare(scope, receipts, now, synthetic = false) {\n  const report = emptyReport(scope, now.toISOString(), synthetic);\n  try {\n    const plan = requestPlan(scope);\n    if (receipts.length !== plan.length) throw new Error(\"Datainnhentingen er ufullstendig eller inneholder ekstra svar.\");\n    let lastTime = -Infinity;\n    for (let i = 0; i < plan.length; i++) {\n      const r = receipts[i];\n      if (JSON.stringify(r.request) !== JSON.stringify(plan[i])) throw new Error(\"Kildesvar har feil rekkefølge eller omfang.\");\n      const time = Date.parse(r.retrievedAt);\n      if (!Number.isFinite(time) || time < lastTime || time > now.getTime()) throw new Error(\"Ugyldig tidsstempel for datainnhenting.\");\n      lastTime = time;\n      report.sources.push({ key: r.request.key, url: r.request.url, retrievedAt: r.retrievedAt, status: r.status });\n      if (r.status !== 200 || r.failure || r.body === void 0) throw new Error(`${r.request.key}: Innhenting mislyktes (HTTP ${r.status || \"ukjent\"}).`);\n    }\n    for (const side of [\"A\", \"B\"]) {\n      const get = (kind) => receipts.find((r) => r.request.side === side && r.request.kind === kind);\n      const accountIds = validateAccounts(get(\"accounts\").body, scope, side);\n      const before = balances(get(\"before\").body, scope, side);\n      const after = balances(get(\"after\").body, scope, side);\n      const values = (list2) => list2.map(({ original: _original, ...balance }) => balance);\n      if (JSON.stringify(values(before)) !== JSON.stringify(values(after))) throw new Error(`${side}: Saldoene endret seg under innhenting. Kjør kontrollen på nytt.`);\n      const lines = receipts.filter((r) => r.request.side === side && r.request.kind === \"details\").flatMap((r) => ledger(r, scope));\n      const ids = /* @__PURE__ */ new Set();\n      for (const l of lines) {\n        if (ids.has(l.id)) throw new Error(`${side}: Gjentatt kildelinje-ID ${l.id}.`);\n        ids.add(l.id);\n      }\n      for (const balance of before) {\n        const sum = lines.filter((l) => l.account === balance.account).reduce((n, l) => n + ore(l.amount), 0n);\n        if (sum !== ore(balance.movement)) throw new Error(`${side}: Detaljlinjene stemmer ikke med periodens bevegelse på konto ${balance.account}.`);\n      }\n      report.accounts.push(...before.map((a) => ({ ...a, accountId: accountIds[a.account] })));\n      report.lines.push(...lines);\n    }\n    if (!synthetic && (!scope.config.liveValidation.signsDatesOpeningAndOmissionsVerified || !scope.config.liveValidation.reference.trim())) {\n      throw new Error(\"Fortegn, datoavgrensning, inngående saldo og utelatte kontoer er ikke verifisert mot autoriserte Conta-rapporter.\");\n    }\n    const total = (side) => Object.fromEntries([\"opening\", \"movement\", \"closing\"].map((k) => [\n      k,\n      decimal(report.accounts.filter((a) => a.side === side).reduce((n, a) => n + ore(a[k]), 0n))\n    ]));\n    report.totals = { A: total(\"A\"), B: total(\"B\") };\n    const residual = Object.fromEntries([\"opening\", \"movement\", \"closing\"].map((k) => [\n      k,\n      decimal(ore(report.totals.A[k]) + ore(report.totals.B[k]))\n    ]));\n    report.residual = { ...residual, magnitude: decimal(abs(ore(residual.closing))) };\n    Object.assign(report, suggest(report.lines, scope.config));\n    report.completeness = \"COMPLETE\";\n    report.agreement = abs(ore(residual.closing)) <= ore(scope.config.tolerance) ? \"BALANCES_AGREE\" : \"DIFFERENCE\";\n  } catch (error) {\n    report.issues.push(error instanceof Error ? error.message : \"Kontrollen kunne ikke fullføres.\");\n    report.accounts = [];\n    report.totals = null;\n    report.residual = null;\n    report.unresolved = report.lines.map((l) => ({ side: l.side, lineId: l.id, reason: \"Ufullstendig kontroll. Linjen er ikke vurdert.\" }));\n  }\n  return report;\n}\n\n// src/engine.ts\nfunction initialize(config, now) {\n  try {\n    const scope = scopeFor(config, now);\n    return { scope, plan: requestPlan(scope), index: 0, attempt: 1, receipts: [], route: \"A\" };\n  } catch (error) {\n    const report = emptyReport(null, now.toISOString());\n    report.issues.push(error instanceof TypeError ? \"Konfigurasjonen har ugyldig struktur.\" : error instanceof Error ? error.message : \"Ugyldig konfigurasjon.\");\n    return { scope: null, plan: [], index: 0, attempt: 1, receipts: [], route: \"DONE\", report };\n  }\n}\nfunction dispatch(state, now) {\n  if (state.report) return { ...state, route: \"DONE\" };\n  if (!state.scope) throw new Error(\"Mangler validert omfang.\");\n  if (state.waitUntil && state.waitUntil > now.getTime()) return { ...state, route: \"WAIT\", waitSeconds: Math.max(1, Math.ceil((state.waitUntil - now.getTime()) / 1e3)) };\n  if (state.index >= state.plan.length) return { ...state, route: \"DONE\", report: compare(state.scope, state.receipts, now) };\n  const request = state.plan[state.index];\n  assertAllowed(request, state.scope);\n  return { ...state, waitUntil: void 0, waitSeconds: void 0, route: request.side, request };\n}\nfunction accept(state, response, now) {\n  if (!state.scope || !state.request) throw new Error(\"Svar uten en validert forespørsel.\");\n  assertAllowed(state.request, state.scope);\n  const value = response && typeof response === \"object\" ? response : {};\n  const status = typeof value.statusCode === \"number\" && Number.isInteger(value.statusCode) ? value.statusCode : 0;\n  const headers = value.headers && typeof value.headers === \"object\" ? value.headers : {};\n  if (status !== 200) {\n    const retryAfter = Object.entries(headers).find(([key]) => key.toLowerCase() === \"retry-after\")?.[1];\n    const delay = retryDelay(status, retryAfter, state.attempt, now.getTime());\n    if (delay !== null) return { ...state, attempt: state.attempt + 1, waitUntil: now.getTime() + delay * 1e3, route: \"WAIT\", waitSeconds: delay };\n  }\n  const receipt = { request: state.request, retrievedAt: now.toISOString(), status };\n  try {\n    if (status !== 200) throw new Error(\"Forespørselen mislyktes.\");\n    receipt.body = parseContaJson(value.body);\n  } catch {\n    receipt.failure = \"Innhenting eller lesing av svaret mislyktes.\";\n  }\n  const next = { ...state, receipts: [...state.receipts, receipt], index: state.index + 1, attempt: 1, waitUntil: void 0 };\n  if (receipt.failure) {\n    const report = emptyReport(state.scope, now.toISOString());\n    report.issues.push(`${receipt.request.key}: ${receipt.failure} HTTP ${status || \"ukjent\"}.`);\n    report.sources = next.receipts.map((r) => ({ key: r.request.key, url: r.request.url, status: r.status, retrievedAt: r.retrievedAt }));\n    return { ...next, report, route: \"DONE\" };\n  }\n  return next;\n}\n\n// src/report.ts\nfunction escapeHtml(value) {\n  return String(value ?? \"\").replace(/[&<>\"']/g, (c) => ({ \"&\": \"&amp;\", \"<\": \"&lt;\", \">\": \"&gt;\", '\"': \"&quot;\", \"'\": \"&#39;\" })[c]);\n}\nfunction csvCell(value) {\n  let text2 = String(value ?? \"\");\n  if (/^[\\s\\u0000-\\u001f\\ufeff]*[=+@-]/.test(text2) || /^[\\t\\r\\n]/.test(text2)) text2 = \"'\" + text2;\n  return '\"' + text2.replace(/\"/g, '\"\"') + '\"';\n}\nvar table = (heads, rows) => `<div class=\"scroll\"><table><thead><tr>${heads.map((h) => `<th scope=\"col\">${escapeHtml(h)}</th>`).join(\"\")}</tr></thead><tbody>${rows.map((row) => `<tr>${row.map((cell) => `<td>${escapeHtml(cell)}</td>`).join(\"\")}</tr>`).join(\"\")}</tbody></table></div>`;\nfunction render(report) {\n  const r = report, s = r.scope;\n  const complete = r.completeness === \"COMPLETE\";\n  const status = r.completeness === \"INCOMPLETE\" ? \"Ufullstendig kontroll\" : r.agreement === \"BALANCES_AGREE\" ? \"Saldoene stemmer innenfor toleransen\" : \"Differanse i saldoene\";\n  const candidateIds = new Set(r.candidates.flatMap((c) => [`A:${c.a}`, `B:${c.b}`]));\n  const reasons = new Map(r.unresolved.map((u) => [`${u.side}:${u.lineId}`, u.reason]));\n  const lineRows = r.lines.map((l) => [\n    l.side,\n    l.account,\n    l.id,\n    l.original.transactionId,\n    l.original.transactionNoYear ?? l.original.transactionNo ?? \"\",\n    l.date,\n    l.amount,\n    l.invoiceNo ?? \"\",\n    l.correction ? \"Ja\" : \"Nei\",\n    l.description,\n    candidateIds.has(`${l.side}:${l.id}`) ? \"Kandidat – krever vurdering\" : reasons.get(`${l.side}:${l.id}`) ?? \"Ikke vurdert\"\n  ]);\n  const heads = [\"Side\", \"Konto\", \"Kildelinje-ID\", \"Transaksjons-ID\", \"Bilagsnummer\", \"Dato\", \"Beløp NOK\", \"Fakturareferanse\", \"Korreksjon/reversering\", \"Beskrivelse\", \"Vurdering\"];\n  const csvRows = [\n    [\"Mellomværendekontroll for Conta\", r.synthetic ? \"KUN SYNTETISKE DATA\" : \"Privat regnskapsmateriale\"],\n    [\"Datastatus\", r.completeness],\n    [\"Saldostatus\", r.agreement ?? \"IKKE VURDERT\"],\n    [\"Selskap A\", s?.config.companies.A.name ?? \"\", s?.config.companies.A.organizationId ?? \"\"],\n    [\"Selskap B\", s?.config.companies.B.name ?? \"\", s?.config.companies.B.organizationId ?? \"\"],\n    [\"Valgte kontoer A\", s?.config.companies.A.accounts.map((a) => a.number).join(\", \") ?? \"UKJENT\"],\n    [\"Valgte kontoer B\", s?.config.companies.B.accounts.map((a) => a.number).join(\", \") ?? \"UKJENT\"],\n    [\"Fra\", s?.startDate ?? \"\", \"Til\", s?.endDate ?? \"\"],\n    [\"Toleranse NOK\", s?.config.tolerance ?? \"\"],\n    [\"Side\", \"Konto\", \"Inngående NOK\", \"Bevegelse NOK\", \"Utgående NOK\"],\n    ...r.accounts.map((a) => [a.side, a.account, a.opening, a.movement, a.closing]),\n    [\"Restsaldo A + B\", \"\", r.residual?.opening ?? \"UKJENT\", r.residual?.movement ?? \"UKJENT\", r.residual?.closing ?? \"UKJENT\"],\n    [\"Absolutt utgående differanse NOK\", r.residual?.magnitude ?? \"UKJENT\"],\n    [\"Antall kandidatpar\", complete ? r.candidates.length : \"IKKE VURDERT\"],\n    [\"Antall linjer uten kandidat\", complete ? r.unresolved.length : \"IKKE VURDERT\"],\n    ...r.issues.map((i) => [\"Problem\", i]),\n    [\"Saldoenighet er ikke ferdig avstemming. Kandidater og uavklarte linjer krever regnskapsførers vurdering.\"],\n    [],\n    [\"Linje A\", \"Linje B\", \"Felles referanse\", \"Begrunnelse\"],\n    ...r.candidates.map((c) => [c.a, c.b, c.reference, c.reason]),\n    [],\n    heads,\n    ...lineRows,\n    [],\n    [\"Kilde\", \"URL\", \"Hentet\", \"HTTP\"],\n    ...r.sources.map((source) => [source.key, source.url, source.retrievedAt, source.status])\n  ];\n  const html = `<!doctype html><html lang=\"nb\"><head><meta charset=\"utf-8\"><meta name=\"viewport\" content=\"width=device-width,initial-scale=1\"><meta http-equiv=\"Content-Security-Policy\" content=\"default-src 'none'; style-src 'unsafe-inline'; base-uri 'none'; form-action 'none'\"><title>Mellomværendekontroll for Conta</title><style>\n  :root{font-family:system-ui,sans-serif;color:#172b36;background:#f2f5f4}body{margin:0}main{max-width:1120px;margin:32px auto;padding:32px;background:white;border-radius:12px}h1{font-size:30px;line-height:1.2}h2{margin-top:32px;font-size:21px}.eyebrow{font-size:13px;letter-spacing:.07em;text-transform:uppercase;color:#526b71}.status{padding:20px;background:#edf5f1;border-left:5px solid #1c6955}.amount{font-size:36px;font-weight:700;margin:10px 0}.note{color:#465961;line-height:1.6}.scroll{overflow-x:auto}table{border-collapse:collapse;width:100%;font-size:13px}th,td{padding:10px;text-align:left;border-bottom:1px solid #dce4e2;vertical-align:top;overflow-wrap:anywhere}th{background:#edf2f1}ul{line-height:1.7}footer{margin-top:32px;border-top:1px solid #dce4e2;padding-top:16px;font-size:12px;color:#465961}@media(max-width:600px){main{margin:0;padding:20px;border-radius:0}h1{font-size:25px}.amount{font-size:30px}}@media print{main{margin:0;padding:0;max-width:none}.scroll{overflow:visible}thead{display:table-header-group}tr{break-inside:avoid}}\n  </style></head><body><main><p class=\"eyebrow\">${r.synthetic ? \"Demonstrasjon · kun syntetiske data\" : \"Privat · grunnlag for regnskapsførers vurdering\"}</p><h1>Mellomværendekontroll for Conta</h1>\n  <p>${escapeHtml(s?.config.companies.A.name ?? \"Ukjent selskap\")} ↔ ${escapeHtml(s?.config.companies.B.name ?? \"Ukjent selskap\")}</p><p>${escapeHtml(s?.startDate)}–${escapeHtml(s?.endDate)} · NOK · toleranse ${escapeHtml(s ? nok(s.config.tolerance) : \"ukjent\")}</p>\n  <section class=\"status\"><strong>${escapeHtml(status)}</strong><p class=\"amount\">${r.residual ? escapeHtml(nok(r.residual.closing)) + \" NOK\" : \"Ikke beregnet\"}</p><p>Signert utgående restsaldo: A + B${r.residual ? \". Absolutt differanse: \" + escapeHtml(nok(r.residual.magnitude)) + \" NOK.\" : \".\"}</p><p>Datastatus: ${r.completeness} · Saldostatus: ${r.agreement ?? \"IKKE VURDERT\"}</p></section>\n  <p class=\"note\">Saldoenighet er ikke en ferdig avstemming eller bevis på at alle transaksjoner er bokført. Uavklarte linjer er ikke bekreftede bokføringsfeil. Regnskapsføreren godkjenner kontovalg og konklusjoner.</p>\n${r.issues.length ? \"<h2>Problemer som må løses</h2><ul>\" + r.issues.map((i) => \"<li>\" + escapeHtml(i) + \"</li>\").join(\"\") + \"</ul>\" : \"\"}\n  <h2>Omfang og kontovalg</h2>${table([\"Side\", \"Selskap\", \"Conta-ID\", \"Dedikerte kontoer\"], s ? [\"A\", \"B\"].map((side) => [side, s.config.companies[side].name, s.config.companies[side].organizationId, s.config.companies[side].accounts.map((a) => a.number).join(\", \")]) : [])}\n  <h2>Inngående saldo, bevegelse og utgående saldo</h2>${table([\"Side/konto\", \"Inngående NOK\", \"Bevegelse NOK\", \"Utgående NOK\"], [...r.accounts.map((a) => [a.side + \" / \" + a.account, nok(a.opening), nok(a.movement), nok(a.closing)]), ...r.residual ? [[\"Restsaldo A + B\", nok(r.residual.opening), nok(r.residual.movement), nok(r.residual.closing)]] : []])}\n  <p class=\"note\">Inngående restsaldo + periodens bevegelsesdifferanse = utgående restsaldo. En inngående differanse oppsto før denne måneden.</p>\n  <h2>Kandidatforslag (${complete ? r.candidates.length : \"ikke vurdert\"})</h2>${table([\"Linje A\", \"Linje B\", \"Felles referanse\", \"Begrunnelse\"], r.candidates.map((c) => [c.a, c.b, c.reference, c.reason]))}<p class=\"note\">${complete ? `Ingen forslag er automatisk bekreftet. ${r.unresolved.length} linjer gjenstår uten kandidat.` : \"Kandidater og antall uavklarte linjer kan ikke vurderes før kontrollen er fullført.\"}</p>\n  <h2>${complete ? `Linjegrunnlag (${r.lines.length})` : \"Tilgjengelig linjegrunnlag – ufullstendig\"}</h2>${complete ? \"\" : '<p class=\"note\">Tabellen viser bare linjer som er validert før kontrollen stoppet. En tom tabell betyr ikke at perioden er uten transaksjoner.</p>'}${table(heads, lineRows)}\n  <h2>Kilder og innhenting</h2>${table([\"Kilde\", \"Hentet (UTC)\", \"HTTP\"], r.sources.map((source) => [source.key, source.retrievedAt, source.status]))}\n  <p class=\"note\">Saldoene hentes på nytt etter detaljene. Dette oppdager enkelte endringer underveis, men gir ikke et atomisk historisk øyeblikksbilde. Endringer som utligner hverandre kan forbli uoppdaget.</p><footer>Generert ${escapeHtml(r.generatedAt)} · versjon ${r.version} · Oppbevar rapport og kildegrunnlag privat etter virksomhetens rutiner.</footer></main></body></html>`;\n  return { html, csv: \"\\uFEFF\" + csvRows.map((row) => row.map(csvCell).join(\";\")).join(\"\\r\\n\") + \"\\r\\n\", json: JSON.stringify(report, null, 2) };\n}\n\n// <stdin>\nvar Checker = { initialize, dispatch, accept, compare, render, scopeFor };\n\nreturn [{ json: Checker.dispatch($input.first().json, new Date()), pairedItem: { item: 0 } }];"
      },
      "position": [
        -220,
        -80
      ]
    },
    {
      "id": "adc74704d65d5cc96612c15b073209d3",
      "name": "Route",
      "type": "n8n-nodes-base.switch",
      "typeVersion": 3.2,
      "parameters": {
        "mode": "expression",
        "numberOutputs": 4,
        "output": "={{ ({A:0,B:1,WAIT:2,DONE:3})[$json.route] }}"
      },
      "position": [
        0,
        -80
      ]
    },
    {
      "id": "05ec7f478814ecb4d65e7f75cd3a1aaa",
      "name": "Conta GET A",
      "type": "n8n-nodes-base.httpRequest",
      "typeVersion": 4.2,
      "parameters": {
        "method": "GET",
        "url": "={{ $json.request.url }}",
        "authentication": "genericCredentialType",
        "genericAuthType": "httpHeaderAuth",
        "options": {
          "redirect": {
            "redirect": {
              "followRedirects": false
            }
          },
          "timeout": 30000,
          "response": {
            "response": {
              "fullResponse": true,
              "neverError": true,
              "responseFormat": "file",
              "outputPropertyName": "raw"
            }
          }
        }
      },
      "position": [
        260,
        -280
      ],
      "onError": "continueRegularOutput",
      "notes": "Select customer-owned Header Auth credential A: header name apiKey. Restrict credential allowed domains to the chosen Conta gateway.",
      "notesInFlow": true
    },
    {
      "id": "2ec3481f71939e9b5d58a6cc02665ad9",
      "name": "Conta GET B",
      "type": "n8n-nodes-base.httpRequest",
      "typeVersion": 4.2,
      "parameters": {
        "method": "GET",
        "url": "={{ $json.request.url }}",
        "authentication": "genericCredentialType",
        "genericAuthType": "httpHeaderAuth",
        "options": {
          "redirect": {
            "redirect": {
              "followRedirects": false
            }
          },
          "timeout": 30000,
          "response": {
            "response": {
              "fullResponse": true,
              "neverError": true,
              "responseFormat": "file",
              "outputPropertyName": "raw"
            }
          }
        }
      },
      "position": [
        260,
        -100
      ],
      "onError": "continueRegularOutput",
      "notes": "Select customer-owned Header Auth credential B: header name apiKey. Restrict credential allowed domains to the chosen Conta gateway.",
      "notesInFlow": true
    },
    {
      "id": "6c7d69b8db1a5c53b92ecec90856490c",
      "name": "Accept response A",
      "type": "n8n-nodes-base.code",
      "typeVersion": 2,
      "parameters": {
        "mode": "runOnceForAllItems",
        "jsCode": "// src/money.ts\nfunction ore(value) {\n  if (typeof value !== \"string\" || !/^-?(0|[1-9]\\d*)(\\.\\d{1,2})?$/.test(value)) {\n    throw new Error(\"Ugyldig beløp: forventet desimaltall med høyst to desimaler.\");\n  }\n  const negative = value.startsWith(\"-\");\n  const [whole = \"\", fraction = \"\"] = (negative ? value.slice(1) : value).split(\".\");\n  return (BigInt(whole) * 100n + BigInt(fraction.padEnd(2, \"0\"))) * (negative ? -1n : 1n);\n}\nfunction decimal(value) {\n  const sign = value < 0n ? \"-\" : \"\";\n  const n = value < 0n ? -value : value;\n  return `${sign}${n / 100n}.${String(n % 100n).padStart(2, \"0\")}`;\n}\nfunction abs(n) {\n  return n < 0n ? -n : n;\n}\nfunction nok(value) {\n  const [whole = \"\", fraction = \"00\"] = value.split(\".\");\n  return `${whole.replace(/\\B(?=(\\d{3})+(?!\\d))/g, \" \")},${fraction.padEnd(2, \"0\")}`;\n}\n\n// src/config.ts\nfunction validDate(value) {\n  return typeof value === \"string\" && /^\\d{4}-\\d{2}-\\d{2}$/.test(value) && !Number.isNaN(Date.parse(value)) && new Date(value).toISOString().slice(0, 10) === value;\n}\nfunction scopeFor(input, now) {\n  if (!input || typeof input !== \"object\") throw new Error(\"Konfigurasjonen mangler.\");\n  const c = input;\n  if (![\"sandbox\", \"production\"].includes(c.environment)) throw new Error(\"Ugyldig Conta-miljø.\");\n  if (!Number.isFinite(now.getTime())) throw new Error(\"Ugyldig kjøringstidspunkt.\");\n  const oslo = new Intl.DateTimeFormat(\"en-CA\", { timeZone: \"Europe/Oslo\", year: \"numeric\", month: \"2-digit\", day: \"2-digit\" }).formatToParts(now);\n  const part = (name) => oslo.find((p) => p.type === name).value;\n  const currentMonth = `${part(\"year\")}-${part(\"month\")}`;\n  let month = c.period;\n  if (month === \"last-completed-month\") {\n    const date = /* @__PURE__ */ new Date(`${currentMonth}-01T12:00:00Z`);\n    date.setUTCMonth(date.getUTCMonth() - 1);\n    month = date.toISOString().slice(0, 7);\n  }\n  if (typeof month !== \"string\" || !/^20\\d{2}-(0[1-9]|1[0-2])$/.test(month) || month >= currentMonth) {\n    throw new Error(\"Velg én avsluttet kalendermåned (YYYY-MM).\");\n  }\n  const startDate = `${month}-01`;\n  const end = /* @__PURE__ */ new Date(`${startDate}T12:00:00Z`);\n  end.setUTCMonth(end.getUTCMonth() + 1, 0);\n  const tolerance = ore(c.tolerance);\n  if (tolerance < 0n || tolerance > 10000n) throw new Error(\"Toleranse må være mellom 0 og 100 NOK.\");\n  for (const side of [\"A\", \"B\"]) {\n    const company = c.companies?.[side];\n    if (!company || !/^[1-9]\\d{0,18}$/.test(company.organizationId) || typeof company.organizationId !== \"string\" || typeof company.name !== \"string\" || !company.name.trim() || company.name.length > 200) throw new Error(`Ugyldig selskap ${side}.`);\n    if (company.authorized !== true || company.currency !== \"NOK\") throw new Error(`Selskap ${side} må være autorisert og føre regnskap i NOK.`);\n    if (!Array.isArray(company.accounts) || !company.accounts.length) throw new Error(`Kontovalg mangler for ${side}.`);\n    const seen = /* @__PURE__ */ new Set();\n    for (const account of company.accounts) {\n      if (!account || typeof account.number !== \"string\" || !/^[12]\\d{3}$/.test(account.number) || seen.has(account.number)) throw new Error(`Ugyldig eller gjentatt balansekonto for ${side}.`);\n      if (account.dedicatedToOtherCompany !== true || account.positionsInNok !== true) throw new Error(`Konto ${account.number} må kun gjelde motparten og posisjoner i NOK.`);\n      seen.add(account.number);\n    }\n  }\n  if (c.companies.A.organizationId === c.companies.B.organizationId) throw new Error(\"Selskapene må ha ulike organisasjons-ID-er.\");\n  if (typeof c.sharedInvoiceNumbersConfirmed !== \"boolean\" || !Number.isInteger(c.candidateDateWindowDays) || c.candidateDateWindowDays < 0 || c.candidateDateWindowDays > 31) throw new Error(\"Ugyldige regler for kandidatforslag.\");\n  if (!c.liveValidation || typeof c.liveValidation.reference !== \"string\" || typeof c.liveValidation.signsDatesOpeningAndOmissionsVerified !== \"boolean\") throw new Error(\"Angi status for kontroll mot Conta-rapporter.\");\n  const clean = {\n    environment: c.environment,\n    period: month,\n    tolerance: c.tolerance,\n    companies: Object.fromEntries([\"A\", \"B\"].map((side) => [side, {\n      organizationId: c.companies[side].organizationId,\n      name: c.companies[side].name,\n      authorized: true,\n      currency: \"NOK\",\n      accounts: c.companies[side].accounts.map((a) => ({ number: a.number, dedicatedToOtherCompany: true, positionsInNok: true }))\n    }])),\n    sharedInvoiceNumbersConfirmed: c.sharedInvoiceNumbersConfirmed,\n    candidateDateWindowDays: c.candidateDateWindowDays,\n    liveValidation: { reference: c.liveValidation.reference, signsDatesOpeningAndOmissionsVerified: c.liveValidation.signsDatesOpeningAndOmissionsVerified }\n  };\n  return { config: clean, startDate, endDate: end.toISOString().slice(0, 10) };\n}\n\n// src/adapter.ts\nfunction record(value) {\n  if (!value || typeof value !== \"object\" || Array.isArray(value)) throw new Error(\"Uventet objektskjema fra Conta.\");\n  return value;\n}\nfunction list(value) {\n  if (!Array.isArray(value)) throw new Error(\"Uventet listeskjema fra Conta.\");\n  return value;\n}\nfunction id(value) {\n  if (typeof value !== \"string\" || !/^[1-9]\\d{0,18}$/.test(value)) throw new Error(\"Kilde-ID mangler eller er ugyldig.\");\n  return value;\n}\nfunction text(value, name) {\n  if (typeof value !== \"string\") throw new Error(`${name} mangler eller er ugyldig.`);\n  return value;\n}\nfunction validateAccounts(body, scope, side) {\n  const rows = list(body).map(record);\n  const ids = {};\n  for (const a of scope.config.companies[side].accounts) {\n    const found = rows.filter((r) => r.bookkeepingAccountNo === a.number);\n    if (found.length !== 1) throw new Error(`${side}: Konto ${a.number} mangler eller er tvetydig i kontoplanen.`);\n    ids[a.number] = id(found[0].id);\n    if (typeof found[0].isActive !== \"boolean\") throw new Error(`${side}: Kontostatus mangler for ${a.number}.`);\n  }\n  return ids;\n}\nfunction balances(body, scope, side) {\n  const rows = list(record(body).trialBalanceOutputSub).map(record);\n  return scope.config.companies[side].accounts.map((a) => {\n    const found = rows.filter((r) => r.bookkeepingAccountNo === a.number);\n    if (found.length !== 1) throw new Error(`${side}: Konto ${a.number} mangler eller er tvetydig i saldobalansen. Fravær er ikke null.`);\n    const row = found[0];\n    const opening = ore(row.sumIngoing), movement = ore(row.sumChanged), closing = ore(row.sumOutgoing);\n    if (opening + movement !== closing) throw new Error(`${side}: Inngående saldo + bevegelse stemmer ikke med utgående saldo på ${a.number}.`);\n    return {\n      side,\n      account: a.number,\n      opening: decimal(opening),\n      movement: decimal(movement),\n      closing: decimal(closing),\n      original: { sumIngoing: row.sumIngoing, sumChanged: row.sumChanged, sumOutgoing: row.sumOutgoing }\n    };\n  });\n}\nfunction ledger(receipt, scope) {\n  return list(receipt.body).map((value) => {\n    const row = record(value);\n    const account = text(row.bookkeepingAccountNo, \"Kontonummer\");\n    if (account !== receipt.request.account) throw new Error(\"Detaljsvaret inneholder en annen konto enn forespurt.\");\n    if (!validDate(row.date) || row.date < scope.startDate || row.date > scope.endDate) throw new Error(\"Detaljsvaret inneholder en ugyldig dato eller en dato utenfor perioden.\");\n    const lineId = id(row.id);\n    id(row.transactionId);\n    if (typeof row.isResetTransaction !== \"boolean\" || typeof row.isCorrection !== \"boolean\") throw new Error(\"Status for reversering eller korrigering mangler.\");\n    const original = {};\n    for (const key of [\"id\", \"transactionId\", \"transactionNo\", \"transactionNoYear\", \"date\", \"amount\", \"bookkeepingAccountNo\", \"description\", \"invoiceId\", \"invoiceNo\", \"isResetTransaction\", \"resetByTransactionId\", \"creditNoteId\", \"creditNoteNo\", \"isCorrection\"]) {\n      if (row[key] !== void 0) {\n        if (row[key] !== null && ![\"string\", \"boolean\"].includes(typeof row[key])) throw new Error(\"Uventet felt i kildebevis.\");\n        original[key] = row[key];\n      }\n    }\n    const invoiceNo = row.invoiceNo === void 0 || row.invoiceNo === null || row.invoiceNo === \"\" ? null : text(row.invoiceNo, \"Fakturanummer\");\n    return {\n      side: receipt.request.side,\n      account,\n      id: lineId,\n      date: row.date,\n      amount: decimal(ore(row.amount)),\n      invoiceNo,\n      description: row.description === void 0 ? \"\" : text(row.description, \"Beskrivelse\"),\n      correction: row.isResetTransaction || row.isCorrection || row.resetByTransactionId != null || row.creditNoteId != null,\n      original\n    };\n  });\n}\n\n// src/candidates.ts\nfunction suggest(lines, config) {\n  const candidates = [], unresolved = [];\n  const groups = /* @__PURE__ */ new Map();\n  for (const l of lines) if (l.invoiceNo) {\n    if (!groups.has(l.invoiceNo)) groups.set(l.invoiceNo, { A: [], B: [] });\n    groups.get(l.invoiceNo)[l.side].push(l);\n  }\n  for (const line of lines) {\n    let reason = \"\";\n    const group = line.invoiceNo ? groups.get(line.invoiceNo) : { A: [], B: [] };\n    const a = group.A, b = group.B;\n    if (!config.sharedInvoiceNumbersConfirmed) reason = \"Felles fakturareferanse er ikke bekreftet for relasjonen.\";\n    else if (!line.invoiceNo) reason = \"Mangler felles fakturareferanse.\";\n    else if (a.length > 1 || b.length > 1) reason = \"Tvetydig referanse: flere linjer eller delt oppgjør.\";\n    else if (!a.length || !b.length) reason = \"Ingen linje med samme fakturareferanse hos motparten.\";\n    else if (a[0].correction || b[0].correction) reason = \"Reversering eller korrigering krever manuell vurdering.\";\n    else if (ore(a[0].amount) === 0n || ore(a[0].amount) + ore(b[0].amount) !== 0n) reason = \"Referansen har ulike eller ikke motsatte beløp.\";\n    else if (Math.abs(Date.parse(a[0].date) - Date.parse(b[0].date)) / 864e5 > config.candidateDateWindowDays) reason = \"Referansen ligger utenfor valgt datovindu.\";\n    else {\n      if (line.side === \"A\") candidates.push({ a: a[0].id, b: b[0].id, reference: line.invoiceNo, reason: \"Samme bekreftede fakturareferanse, motsatte like beløp og dato innenfor datovindu. Kun forslag.\" });\n      continue;\n    }\n    unresolved.push({ side: line.side, lineId: line.id, reason });\n  }\n  return { candidates, unresolved };\n}\n\n// src/transport.ts\nfunction parseContaJson(text2) {\n  if (typeof text2 !== \"string\") throw new Error(\"Conta-svaret må hentes som tekst for å bevare beløp og ID-er.\");\n  try {\n    JSON.parse(text2);\n  } catch {\n    throw new Error(\"Conta returnerte ugyldig JSON.\");\n  }\n  const protectedNumbers = text2.replace(\n    /\"(?:\\\\.|[^\"\\\\])*\"|-?\\d+(?:\\.\\d+)?(?:[eE][+-]?\\d+)?/g,\n    (token) => token.startsWith('\"') ? token : JSON.stringify(token)\n  );\n  return JSON.parse(protectedNumbers);\n}\nfunction requestPlan(scope) {\n  const root = scope.config.environment === \"production\" ? \"https://api.gateway.conta.no\" : \"https://api.gateway.conta-sandbox.no\";\n  const requests = [];\n  const add = (side, kind, account) => {\n    const base = `${root}/accounting/organizations/${scope.config.companies[side].organizationId}`;\n    const dates = `startDate=${scope.startDate}&endDate=${scope.endDate}`;\n    const path = kind === \"accounts\" ? \"/bookkeeping-accounts?hits=0&page=0\" : kind === \"details\" ? `/reports/trial-balance/details?${dates}&bookkeepingAccountNo=${account}` : `/reports/trial-balance?${dates}`;\n    requests.push({ side, kind, ...account ? { account } : {}, key: `${side}:${kind}${account ? \":\" + account : \"\"}`, url: base + path });\n  };\n  for (const side of [\"A\", \"B\"]) {\n    add(side, \"accounts\");\n    add(side, \"before\");\n  }\n  for (const side of [\"A\", \"B\"]) for (const a of scope.config.companies[side].accounts) add(side, \"details\", a.number);\n  for (const side of [\"A\", \"B\"]) add(side, \"after\");\n  return requests;\n}\nfunction assertAllowed(request, scope) {\n  const expected = requestPlan(scope).find((r) => r.key === request.key);\n  if (!expected || JSON.stringify(expected) !== JSON.stringify(request)) throw new Error(\"Forespørselen er ikke tillatt.\");\n}\nfunction retryDelay(status, retryAfter, attempt, now) {\n  if (![0, 408, 429, 500, 502, 503, 504].includes(status) || attempt >= 3) return null;\n  let delay = 2 ** attempt;\n  if (retryAfter !== void 0) {\n    if (typeof retryAfter !== \"string\") return null;\n    if (/^\\d+$/.test(retryAfter)) delay = Math.max(delay, Number(retryAfter));\n    else {\n      const at = Date.parse(retryAfter);\n      if (!Number.isFinite(at)) return null;\n      delay = Math.max(delay, Math.ceil((at - now) / 1e3));\n    }\n  }\n  return delay <= 300 ? delay : null;\n}\n\n// src/compare.ts\nfunction emptyReport(scope, generatedAt, synthetic = false) {\n  return {\n    version: \"0.1.0\",\n    synthetic,\n    generatedAt,\n    completeness: \"INCOMPLETE\",\n    agreement: null,\n    scope,\n    issues: [],\n    accounts: [],\n    totals: null,\n    residual: null,\n    lines: [],\n    candidates: [],\n    unresolved: [],\n    sources: []\n  };\n}\nfunction compare(scope, receipts, now, synthetic = false) {\n  const report = emptyReport(scope, now.toISOString(), synthetic);\n  try {\n    const plan = requestPlan(scope);\n    if (receipts.length !== plan.length) throw new Error(\"Datainnhentingen er ufullstendig eller inneholder ekstra svar.\");\n    let lastTime = -Infinity;\n    for (let i = 0; i < plan.length; i++) {\n      const r = receipts[i];\n      if (JSON.stringify(r.request) !== JSON.stringify(plan[i])) throw new Error(\"Kildesvar har feil rekkefølge eller omfang.\");\n      const time = Date.parse(r.retrievedAt);\n      if (!Number.isFinite(time) || time < lastTime || time > now.getTime()) throw new Error(\"Ugyldig tidsstempel for datainnhenting.\");\n      lastTime = time;\n      report.sources.push({ key: r.request.key, url: r.request.url, retrievedAt: r.retrievedAt, status: r.status });\n      if (r.status !== 200 || r.failure || r.body === void 0) throw new Error(`${r.request.key}: Innhenting mislyktes (HTTP ${r.status || \"ukjent\"}).`);\n    }\n    for (const side of [\"A\", \"B\"]) {\n      const get = (kind) => receipts.find((r) => r.request.side === side && r.request.kind === kind);\n      const accountIds = validateAccounts(get(\"accounts\").body, scope, side);\n      const before = balances(get(\"before\").body, scope, side);\n      const after = balances(get(\"after\").body, scope, side);\n      const values = (list2) => list2.map(({ original: _original, ...balance }) => balance);\n      if (JSON.stringify(values(before)) !== JSON.stringify(values(after))) throw new Error(`${side}: Saldoene endret seg under innhenting. Kjør kontrollen på nytt.`);\n      const lines = receipts.filter((r) => r.request.side === side && r.request.kind === \"details\").flatMap((r) => ledger(r, scope));\n      const ids = /* @__PURE__ */ new Set();\n      for (const l of lines) {\n        if (ids.has(l.id)) throw new Error(`${side}: Gjentatt kildelinje-ID ${l.id}.`);\n        ids.add(l.id);\n      }\n      for (const balance of before) {\n        const sum = lines.filter((l) => l.account === balance.account).reduce((n, l) => n + ore(l.amount), 0n);\n        if (sum !== ore(balance.movement)) throw new Error(`${side}: Detaljlinjene stemmer ikke med periodens bevegelse på konto ${balance.account}.`);\n      }\n      report.accounts.push(...before.map((a) => ({ ...a, accountId: accountIds[a.account] })));\n      report.lines.push(...lines);\n    }\n    if (!synthetic && (!scope.config.liveValidation.signsDatesOpeningAndOmissionsVerified || !scope.config.liveValidation.reference.trim())) {\n      throw new Error(\"Fortegn, datoavgrensning, inngående saldo og utelatte kontoer er ikke verifisert mot autoriserte Conta-rapporter.\");\n    }\n    const total = (side) => Object.fromEntries([\"opening\", \"movement\", \"closing\"].map((k) => [\n      k,\n      decimal(report.accounts.filter((a) => a.side === side).reduce((n, a) => n + ore(a[k]), 0n))\n    ]));\n    report.totals = { A: total(\"A\"), B: total(\"B\") };\n    const residual = Object.fromEntries([\"opening\", \"movement\", \"closing\"].map((k) => [\n      k,\n      decimal(ore(report.totals.A[k]) + ore(report.totals.B[k]))\n    ]));\n    report.residual = { ...residual, magnitude: decimal(abs(ore(residual.closing))) };\n    Object.assign(report, suggest(report.lines, scope.config));\n    report.completeness = \"COMPLETE\";\n    report.agreement = abs(ore(residual.closing)) <= ore(scope.config.tolerance) ? \"BALANCES_AGREE\" : \"DIFFERENCE\";\n  } catch (error) {\n    report.issues.push(error instanceof Error ? error.message : \"Kontrollen kunne ikke fullføres.\");\n    report.accounts = [];\n    report.totals = null;\n    report.residual = null;\n    report.unresolved = report.lines.map((l) => ({ side: l.side, lineId: l.id, reason: \"Ufullstendig kontroll. Linjen er ikke vurdert.\" }));\n  }\n  return report;\n}\n\n// src/engine.ts\nfunction initialize(config, now) {\n  try {\n    const scope = scopeFor(config, now);\n    return { scope, plan: requestPlan(scope), index: 0, attempt: 1, receipts: [], route: \"A\" };\n  } catch (error) {\n    const report = emptyReport(null, now.toISOString());\n    report.issues.push(error instanceof TypeError ? \"Konfigurasjonen har ugyldig struktur.\" : error instanceof Error ? error.message : \"Ugyldig konfigurasjon.\");\n    return { scope: null, plan: [], index: 0, attempt: 1, receipts: [], route: \"DONE\", report };\n  }\n}\nfunction dispatch(state, now) {\n  if (state.report) return { ...state, route: \"DONE\" };\n  if (!state.scope) throw new Error(\"Mangler validert omfang.\");\n  if (state.waitUntil && state.waitUntil > now.getTime()) return { ...state, route: \"WAIT\", waitSeconds: Math.max(1, Math.ceil((state.waitUntil - now.getTime()) / 1e3)) };\n  if (state.index >= state.plan.length) return { ...state, route: \"DONE\", report: compare(state.scope, state.receipts, now) };\n  const request = state.plan[state.index];\n  assertAllowed(request, state.scope);\n  return { ...state, waitUntil: void 0, waitSeconds: void 0, route: request.side, request };\n}\nfunction accept(state, response, now) {\n  if (!state.scope || !state.request) throw new Error(\"Svar uten en validert forespørsel.\");\n  assertAllowed(state.request, state.scope);\n  const value = response && typeof response === \"object\" ? response : {};\n  const status = typeof value.statusCode === \"number\" && Number.isInteger(value.statusCode) ? value.statusCode : 0;\n  const headers = value.headers && typeof value.headers === \"object\" ? value.headers : {};\n  if (status !== 200) {\n    const retryAfter = Object.entries(headers).find(([key]) => key.toLowerCase() === \"retry-after\")?.[1];\n    const delay = retryDelay(status, retryAfter, state.attempt, now.getTime());\n    if (delay !== null) return { ...state, attempt: state.attempt + 1, waitUntil: now.getTime() + delay * 1e3, route: \"WAIT\", waitSeconds: delay };\n  }\n  const receipt = { request: state.request, retrievedAt: now.toISOString(), status };\n  try {\n    if (status !== 200) throw new Error(\"Forespørselen mislyktes.\");\n    receipt.body = parseContaJson(value.body);\n  } catch {\n    receipt.failure = \"Innhenting eller lesing av svaret mislyktes.\";\n  }\n  const next = { ...state, receipts: [...state.receipts, receipt], index: state.index + 1, attempt: 1, waitUntil: void 0 };\n  if (receipt.failure) {\n    const report = emptyReport(state.scope, now.toISOString());\n    report.issues.push(`${receipt.request.key}: ${receipt.failure} HTTP ${status || \"ukjent\"}.`);\n    report.sources = next.receipts.map((r) => ({ key: r.request.key, url: r.request.url, status: r.status, retrievedAt: r.retrievedAt }));\n    return { ...next, report, route: \"DONE\" };\n  }\n  return next;\n}\n\n// src/report.ts\nfunction escapeHtml(value) {\n  return String(value ?? \"\").replace(/[&<>\"']/g, (c) => ({ \"&\": \"&amp;\", \"<\": \"&lt;\", \">\": \"&gt;\", '\"': \"&quot;\", \"'\": \"&#39;\" })[c]);\n}\nfunction csvCell(value) {\n  let text2 = String(value ?? \"\");\n  if (/^[\\s\\u0000-\\u001f\\ufeff]*[=+@-]/.test(text2) || /^[\\t\\r\\n]/.test(text2)) text2 = \"'\" + text2;\n  return '\"' + text2.replace(/\"/g, '\"\"') + '\"';\n}\nvar table = (heads, rows) => `<div class=\"scroll\"><table><thead><tr>${heads.map((h) => `<th scope=\"col\">${escapeHtml(h)}</th>`).join(\"\")}</tr></thead><tbody>${rows.map((row) => `<tr>${row.map((cell) => `<td>${escapeHtml(cell)}</td>`).join(\"\")}</tr>`).join(\"\")}</tbody></table></div>`;\nfunction render(report) {\n  const r = report, s = r.scope;\n  const complete = r.completeness === \"COMPLETE\";\n  const status = r.completeness === \"INCOMPLETE\" ? \"Ufullstendig kontroll\" : r.agreement === \"BALANCES_AGREE\" ? \"Saldoene stemmer innenfor toleransen\" : \"Differanse i saldoene\";\n  const candidateIds = new Set(r.candidates.flatMap((c) => [`A:${c.a}`, `B:${c.b}`]));\n  const reasons = new Map(r.unresolved.map((u) => [`${u.side}:${u.lineId}`, u.reason]));\n  const lineRows = r.lines.map((l) => [\n    l.side,\n    l.account,\n    l.id,\n    l.original.transactionId,\n    l.original.transactionNoYear ?? l.original.transactionNo ?? \"\",\n    l.date,\n    l.amount,\n    l.invoiceNo ?? \"\",\n    l.correction ? \"Ja\" : \"Nei\",\n    l.description,\n    candidateIds.has(`${l.side}:${l.id}`) ? \"Kandidat – krever vurdering\" : reasons.get(`${l.side}:${l.id}`) ?? \"Ikke vurdert\"\n  ]);\n  const heads = [\"Side\", \"Konto\", \"Kildelinje-ID\", \"Transaksjons-ID\", \"Bilagsnummer\", \"Dato\", \"Beløp NOK\", \"Fakturareferanse\", \"Korreksjon/reversering\", \"Beskrivelse\", \"Vurdering\"];\n  const csvRows = [\n    [\"Mellomværendekontroll for Conta\", r.synthetic ? \"KUN SYNTETISKE DATA\" : \"Privat regnskapsmateriale\"],\n    [\"Datastatus\", r.completeness],\n    [\"Saldostatus\", r.agreement ?? \"IKKE VURDERT\"],\n    [\"Selskap A\", s?.config.companies.A.name ?? \"\", s?.config.companies.A.organizationId ?? \"\"],\n    [\"Selskap B\", s?.config.companies.B.name ?? \"\", s?.config.companies.B.organizationId ?? \"\"],\n    [\"Valgte kontoer A\", s?.config.companies.A.accounts.map((a) => a.number).join(\", \") ?? \"UKJENT\"],\n    [\"Valgte kontoer B\", s?.config.companies.B.accounts.map((a) => a.number).join(\", \") ?? \"UKJENT\"],\n    [\"Fra\", s?.startDate ?? \"\", \"Til\", s?.endDate ?? \"\"],\n    [\"Toleranse NOK\", s?.config.tolerance ?? \"\"],\n    [\"Side\", \"Konto\", \"Inngående NOK\", \"Bevegelse NOK\", \"Utgående NOK\"],\n    ...r.accounts.map((a) => [a.side, a.account, a.opening, a.movement, a.closing]),\n    [\"Restsaldo A + B\", \"\", r.residual?.opening ?? \"UKJENT\", r.residual?.movement ?? \"UKJENT\", r.residual?.closing ?? \"UKJENT\"],\n    [\"Absolutt utgående differanse NOK\", r.residual?.magnitude ?? \"UKJENT\"],\n    [\"Antall kandidatpar\", complete ? r.candidates.length : \"IKKE VURDERT\"],\n    [\"Antall linjer uten kandidat\", complete ? r.unresolved.length : \"IKKE VURDERT\"],\n    ...r.issues.map((i) => [\"Problem\", i]),\n    [\"Saldoenighet er ikke ferdig avstemming. Kandidater og uavklarte linjer krever regnskapsførers vurdering.\"],\n    [],\n    [\"Linje A\", \"Linje B\", \"Felles referanse\", \"Begrunnelse\"],\n    ...r.candidates.map((c) => [c.a, c.b, c.reference, c.reason]),\n    [],\n    heads,\n    ...lineRows,\n    [],\n    [\"Kilde\", \"URL\", \"Hentet\", \"HTTP\"],\n    ...r.sources.map((source) => [source.key, source.url, source.retrievedAt, source.status])\n  ];\n  const html = `<!doctype html><html lang=\"nb\"><head><meta charset=\"utf-8\"><meta name=\"viewport\" content=\"width=device-width,initial-scale=1\"><meta http-equiv=\"Content-Security-Policy\" content=\"default-src 'none'; style-src 'unsafe-inline'; base-uri 'none'; form-action 'none'\"><title>Mellomværendekontroll for Conta</title><style>\n  :root{font-family:system-ui,sans-serif;color:#172b36;background:#f2f5f4}body{margin:0}main{max-width:1120px;margin:32px auto;padding:32px;background:white;border-radius:12px}h1{font-size:30px;line-height:1.2}h2{margin-top:32px;font-size:21px}.eyebrow{font-size:13px;letter-spacing:.07em;text-transform:uppercase;color:#526b71}.status{padding:20px;background:#edf5f1;border-left:5px solid #1c6955}.amount{font-size:36px;font-weight:700;margin:10px 0}.note{color:#465961;line-height:1.6}.scroll{overflow-x:auto}table{border-collapse:collapse;width:100%;font-size:13px}th,td{padding:10px;text-align:left;border-bottom:1px solid #dce4e2;vertical-align:top;overflow-wrap:anywhere}th{background:#edf2f1}ul{line-height:1.7}footer{margin-top:32px;border-top:1px solid #dce4e2;padding-top:16px;font-size:12px;color:#465961}@media(max-width:600px){main{margin:0;padding:20px;border-radius:0}h1{font-size:25px}.amount{font-size:30px}}@media print{main{margin:0;padding:0;max-width:none}.scroll{overflow:visible}thead{display:table-header-group}tr{break-inside:avoid}}\n  </style></head><body><main><p class=\"eyebrow\">${r.synthetic ? \"Demonstrasjon · kun syntetiske data\" : \"Privat · grunnlag for regnskapsførers vurdering\"}</p><h1>Mellomværendekontroll for Conta</h1>\n  <p>${escapeHtml(s?.config.companies.A.name ?? \"Ukjent selskap\")} ↔ ${escapeHtml(s?.config.companies.B.name ?? \"Ukjent selskap\")}</p><p>${escapeHtml(s?.startDate)}–${escapeHtml(s?.endDate)} · NOK · toleranse ${escapeHtml(s ? nok(s.config.tolerance) : \"ukjent\")}</p>\n  <section class=\"status\"><strong>${escapeHtml(status)}</strong><p class=\"amount\">${r.residual ? escapeHtml(nok(r.residual.closing)) + \" NOK\" : \"Ikke beregnet\"}</p><p>Signert utgående restsaldo: A + B${r.residual ? \". Absolutt differanse: \" + escapeHtml(nok(r.residual.magnitude)) + \" NOK.\" : \".\"}</p><p>Datastatus: ${r.completeness} · Saldostatus: ${r.agreement ?? \"IKKE VURDERT\"}</p></section>\n  <p class=\"note\">Saldoenighet er ikke en ferdig avstemming eller bevis på at alle transaksjoner er bokført. Uavklarte linjer er ikke bekreftede bokføringsfeil. Regnskapsføreren godkjenner kontovalg og konklusjoner.</p>\n${r.issues.length ? \"<h2>Problemer som må løses</h2><ul>\" + r.issues.map((i) => \"<li>\" + escapeHtml(i) + \"</li>\").join(\"\") + \"</ul>\" : \"\"}\n  <h2>Omfang og kontovalg</h2>${table([\"Side\", \"Selskap\", \"Conta-ID\", \"Dedikerte kontoer\"], s ? [\"A\", \"B\"].map((side) => [side, s.config.companies[side].name, s.config.companies[side].organizationId, s.config.companies[side].accounts.map((a) => a.number).join(\", \")]) : [])}\n  <h2>Inngående saldo, bevegelse og utgående saldo</h2>${table([\"Side/konto\", \"Inngående NOK\", \"Bevegelse NOK\", \"Utgående NOK\"], [...r.accounts.map((a) => [a.side + \" / \" + a.account, nok(a.opening), nok(a.movement), nok(a.closing)]), ...r.residual ? [[\"Restsaldo A + B\", nok(r.residual.opening), nok(r.residual.movement), nok(r.residual.closing)]] : []])}\n  <p class=\"note\">Inngående restsaldo + periodens bevegelsesdifferanse = utgående restsaldo. En inngående differanse oppsto før denne måneden.</p>\n  <h2>Kandidatforslag (${complete ? r.candidates.length : \"ikke vurdert\"})</h2>${table([\"Linje A\", \"Linje B\", \"Felles referanse\", \"Begrunnelse\"], r.candidates.map((c) => [c.a, c.b, c.reference, c.reason]))}<p class=\"note\">${complete ? `Ingen forslag er automatisk bekreftet. ${r.unresolved.length} linjer gjenstår uten kandidat.` : \"Kandidater og antall uavklarte linjer kan ikke vurderes før kontrollen er fullført.\"}</p>\n  <h2>${complete ? `Linjegrunnlag (${r.lines.length})` : \"Tilgjengelig linjegrunnlag – ufullstendig\"}</h2>${complete ? \"\" : '<p class=\"note\">Tabellen viser bare linjer som er validert før kontrollen stoppet. En tom tabell betyr ikke at perioden er uten transaksjoner.</p>'}${table(heads, lineRows)}\n  <h2>Kilder og innhenting</h2>${table([\"Kilde\", \"Hentet (UTC)\", \"HTTP\"], r.sources.map((source) => [source.key, source.retrievedAt, source.status]))}\n  <p class=\"note\">Saldoene hentes på nytt etter detaljene. Dette oppdager enkelte endringer underveis, men gir ikke et atomisk historisk øyeblikksbilde. Endringer som utligner hverandre kan forbli uoppdaget.</p><footer>Generert ${escapeHtml(r.generatedAt)} · versjon ${r.version} · Oppbevar rapport og kildegrunnlag privat etter virksomhetens rutiner.</footer></main></body></html>`;\n  return { html, csv: \"\\uFEFF\" + csvRows.map((row) => row.map(csvCell).join(\";\")).join(\"\\r\\n\") + \"\\r\\n\", json: JSON.stringify(report, null, 2) };\n}\n\n// <stdin>\nvar Checker = { initialize, dispatch, accept, compare, render, scopeFor };\n\nconst response = {...$input.first().json};\ntry { if ($input.first().binary?.raw) response.body = (await this.helpers.getBinaryDataBuffer(0, 'raw')).toString('utf8'); } catch { response.body = undefined; }\nreturn [{ json: Checker.accept($('Dispatch request').item.json, response, new Date()), pairedItem: { item: 0 } }];"
      },
      "position": [
        560,
        -280
      ]
    },
    {
      "id": "836700a798813254805ed9becf7a7cc7",
      "name": "Accept response B",
      "type": "n8n-nodes-base.code",
      "typeVersion": 2,
      "parameters": {
        "mode": "runOnceForAllItems",
        "jsCode": "// src/money.ts\nfunction ore(value) {\n  if (typeof value !== \"string\" || !/^-?(0|[1-9]\\d*)(\\.\\d{1,2})?$/.test(value)) {\n    throw new Error(\"Ugyldig beløp: forventet desimaltall med høyst to desimaler.\");\n  }\n  const negative = value.startsWith(\"-\");\n  const [whole = \"\", fraction = \"\"] = (negative ? value.slice(1) : value).split(\".\");\n  return (BigInt(whole) * 100n + BigInt(fraction.padEnd(2, \"0\"))) * (negative ? -1n : 1n);\n}\nfunction decimal(value) {\n  const sign = value < 0n ? \"-\" : \"\";\n  const n = value < 0n ? -value : value;\n  return `${sign}${n / 100n}.${String(n % 100n).padStart(2, \"0\")}`;\n}\nfunction abs(n) {\n  return n < 0n ? -n : n;\n}\nfunction nok(value) {\n  const [whole = \"\", fraction = \"00\"] = value.split(\".\");\n  return `${whole.replace(/\\B(?=(\\d{3})+(?!\\d))/g, \" \")},${fraction.padEnd(2, \"0\")}`;\n}\n\n// src/config.ts\nfunction validDate(value) {\n  return typeof value === \"string\" && /^\\d{4}-\\d{2}-\\d{2}$/.test(value) && !Number.isNaN(Date.parse(value)) && new Date(value).toISOString().slice(0, 10) === value;\n}\nfunction scopeFor(input, now) {\n  if (!input || typeof input !== \"object\") throw new Error(\"Konfigurasjonen mangler.\");\n  const c = input;\n  if (![\"sandbox\", \"production\"].includes(c.environment)) throw new Error(\"Ugyldig Conta-miljø.\");\n  if (!Number.isFinite(now.getTime())) throw new Error(\"Ugyldig kjøringstidspunkt.\");\n  const oslo = new Intl.DateTimeFormat(\"en-CA\", { timeZone: \"Europe/Oslo\", year: \"numeric\", month: \"2-digit\", day: \"2-digit\" }).formatToParts(now);\n  const part = (name) => oslo.find((p) => p.type === name).value;\n  const currentMonth = `${part(\"year\")}-${part(\"month\")}`;\n  let month = c.period;\n  if (month === \"last-completed-month\") {\n    const date = /* @__PURE__ */ new Date(`${currentMonth}-01T12:00:00Z`);\n    date.setUTCMonth(date.getUTCMonth() - 1);\n    month = date.toISOString().slice(0, 7);\n  }\n  if (typeof month !== \"string\" || !/^20\\d{2}-(0[1-9]|1[0-2])$/.test(month) || month >= currentMonth) {\n    throw new Error(\"Velg én avsluttet kalendermåned (YYYY-MM).\");\n  }\n  const startDate = `${month}-01`;\n  const end = /* @__PURE__ */ new Date(`${startDate}T12:00:00Z`);\n  end.setUTCMonth(end.getUTCMonth() + 1, 0);\n  const tolerance = ore(c.tolerance);\n  if (tolerance < 0n || tolerance > 10000n) throw new Error(\"Toleranse må være mellom 0 og 100 NOK.\");\n  for (const side of [\"A\", \"B\"]) {\n    const company = c.companies?.[side];\n    if (!company || !/^[1-9]\\d{0,18}$/.test(company.organizationId) || typeof company.organizationId !== \"string\" || typeof company.name !== \"string\" || !company.name.trim() || company.name.length > 200) throw new Error(`Ugyldig selskap ${side}.`);\n    if (company.authorized !== true || company.currency !== \"NOK\") throw new Error(`Selskap ${side} må være autorisert og føre regnskap i NOK.`);\n    if (!Array.isArray(company.accounts) || !company.accounts.length) throw new Error(`Kontovalg mangler for ${side}.`);\n    const seen = /* @__PURE__ */ new Set();\n    for (const account of company.accounts) {\n      if (!account || typeof account.number !== \"string\" || !/^[12]\\d{3}$/.test(account.number) || seen.has(account.number)) throw new Error(`Ugyldig eller gjentatt balansekonto for ${side}.`);\n      if (account.dedicatedToOtherCompany !== true || account.positionsInNok !== true) throw new Error(`Konto ${account.number} må kun gjelde motparten og posisjoner i NOK.`);\n      seen.add(account.number);\n    }\n  }\n  if (c.companies.A.organizationId === c.companies.B.organizationId) throw new Error(\"Selskapene må ha ulike organisasjons-ID-er.\");\n  if (typeof c.sharedInvoiceNumbersConfirmed !== \"boolean\" || !Number.isInteger(c.candidateDateWindowDays) || c.candidateDateWindowDays < 0 || c.candidateDateWindowDays > 31) throw new Error(\"Ugyldige regler for kandidatforslag.\");\n  if (!c.liveValidation || typeof c.liveValidation.reference !== \"string\" || typeof c.liveValidation.signsDatesOpeningAndOmissionsVerified !== \"boolean\") throw new Error(\"Angi status for kontroll mot Conta-rapporter.\");\n  const clean = {\n    environment: c.environment,\n    period: month,\n    tolerance: c.tolerance,\n    companies: Object.fromEntries([\"A\", \"B\"].map((side) => [side, {\n      organizationId: c.companies[side].organizationId,\n      name: c.companies[side].name,\n      authorized: true,\n      currency: \"NOK\",\n      accounts: c.companies[side].accounts.map((a) => ({ number: a.number, dedicatedToOtherCompany: true, positionsInNok: true }))\n    }])),\n    sharedInvoiceNumbersConfirmed: c.sharedInvoiceNumbersConfirmed,\n    candidateDateWindowDays: c.candidateDateWindowDays,\n    liveValidation: { reference: c.liveValidation.reference, signsDatesOpeningAndOmissionsVerified: c.liveValidation.signsDatesOpeningAndOmissionsVerified }\n  };\n  return { config: clean, startDate, endDate: end.toISOString().slice(0, 10) };\n}\n\n// src/adapter.ts\nfunction record(value) {\n  if (!value || typeof value !== \"object\" || Array.isArray(value)) throw new Error(\"Uventet objektskjema fra Conta.\");\n  return value;\n}\nfunction list(value) {\n  if (!Array.isArray(value)) throw new Error(\"Uventet listeskjema fra Conta.\");\n  return value;\n}\nfunction id(value) {\n  if (typeof value !== \"string\" || !/^[1-9]\\d{0,18}$/.test(value)) throw new Error(\"Kilde-ID mangler eller er ugyldig.\");\n  return value;\n}\nfunction text(value, name) {\n  if (typeof value !== \"string\") throw new Error(`${name} mangler eller er ugyldig.`);\n  return value;\n}\nfunction validateAccounts(body, scope, side) {\n  const rows = list(body).map(record);\n  const ids = {};\n  for (const a of scope.config.companies[side].accounts) {\n    const found = rows.filter((r) => r.bookkeepingAccountNo === a.number);\n    if (found.length !== 1) throw new Error(`${side}: Konto ${a.number} mangler eller er tvetydig i kontoplanen.`);\n    ids[a.number] = id(found[0].id);\n    if (typeof found[0].isActive !== \"boolean\") throw new Error(`${side}: Kontostatus mangler for ${a.number}.`);\n  }\n  return ids;\n}\nfunction balances(body, scope, side) {\n  const rows = list(record(body).trialBalanceOutputSub).map(record);\n  return scope.config.companies[side].accounts.map((a) => {\n    const found = rows.filter((r) => r.bookkeepingAccountNo === a.number);\n    if (found.length !== 1) throw new Error(`${side}: Konto ${a.number} mangler eller er tvetydig i saldobalansen. Fravær er ikke null.`);\n    const row = found[0];\n    const opening = ore(row.sumIngoing), movement = ore(row.sumChanged), closing = ore(row.sumOutgoing);\n    if (opening + movement !== closing) throw new Error(`${side}: Inngående saldo + bevegelse stemmer ikke med utgående saldo på ${a.number}.`);\n    return {\n      side,\n      account: a.number,\n      opening: decimal(opening),\n      movement: decimal(movement),\n      closing: decimal(closing),\n      original: { sumIngoing: row.sumIngoing, sumChanged: row.sumChanged, sumOutgoing: row.sumOutgoing }\n    };\n  });\n}\nfunction ledger(receipt, scope) {\n  return list(receipt.body).map((value) => {\n    const row = record(value);\n    const account = text(row.bookkeepingAccountNo, \"Kontonummer\");\n    if (account !== receipt.request.account) throw new Error(\"Detaljsvaret inneholder en annen konto enn forespurt.\");\n    if (!validDate(row.date) || row.date < scope.startDate || row.date > scope.endDate) throw new Error(\"Detaljsvaret inneholder en ugyldig dato eller en dato utenfor perioden.\");\n    const lineId = id(row.id);\n    id(row.transactionId);\n    if (typeof row.isResetTransaction !== \"boolean\" || typeof row.isCorrection !== \"boolean\") throw new Error(\"Status for reversering eller korrigering mangler.\");\n    const original = {};\n    for (const key of [\"id\", \"transactionId\", \"transactionNo\", \"transactionNoYear\", \"date\", \"amount\", \"bookkeepingAccountNo\", \"description\", \"invoiceId\", \"invoiceNo\", \"isResetTransaction\", \"resetByTransactionId\", \"creditNoteId\", \"creditNoteNo\", \"isCorrection\"]) {\n      if (row[key] !== void 0) {\n        if (row[key] !== null && ![\"string\", \"boolean\"].includes(typeof row[key])) throw new Error(\"Uventet felt i kildebevis.\");\n        original[key] = row[key];\n      }\n    }\n    const invoiceNo = row.invoiceNo === void 0 || row.invoiceNo === null || row.invoiceNo === \"\" ? null : text(row.invoiceNo, \"Fakturanummer\");\n    return {\n      side: receipt.request.side,\n      account,\n      id: lineId,\n      date: row.date,\n      amount: decimal(ore(row.amount)),\n      invoiceNo,\n      description: row.description === void 0 ? \"\" : text(row.description, \"Beskrivelse\"),\n      correction: row.isResetTransaction || row.isCorrection || row.resetByTransactionId != null || row.creditNoteId != null,\n      original\n    };\n  });\n}\n\n// src/candidates.ts\nfunction suggest(lines, config) {\n  const candidates = [], unresolved = [];\n  const groups = /* @__PURE__ */ new Map();\n  for (const l of lines) if (l.invoiceNo) {\n    if (!groups.has(l.invoiceNo)) groups.set(l.invoiceNo, { A: [], B: [] });\n    groups.get(l.invoiceNo)[l.side].push(l);\n  }\n  for (const line of lines) {\n    let reason = \"\";\n    const group = line.invoiceNo ? groups.get(line.invoiceNo) : { A: [], B: [] };\n    const a = group.A, b = group.B;\n    if (!config.sharedInvoiceNumbersConfirmed) reason = \"Felles fakturareferanse er ikke bekreftet for relasjonen.\";\n    else if (!line.invoiceNo) reason = \"Mangler felles fakturareferanse.\";\n    else if (a.length > 1 || b.length > 1) reason = \"Tvetydig referanse: flere linjer eller delt oppgjør.\";\n    else if (!a.length || !b.length) reason = \"Ingen linje med samme fakturareferanse hos motparten.\";\n    else if (a[0].correction || b[0].correction) reason = \"Reversering eller korrigering krever manuell vurdering.\";\n    else if (ore(a[0].amount) === 0n || ore(a[0].amount) + ore(b[0].amount) !== 0n) reason = \"Referansen har ulike eller ikke motsatte beløp.\";\n    else if (Math.abs(Date.parse(a[0].date) - Date.parse(b[0].date)) / 864e5 > config.candidateDateWindowDays) reason = \"Referansen ligger utenfor valgt datovindu.\";\n    else {\n      if (line.side === \"A\") candidates.push({ a: a[0].id, b: b[0].id, reference: line.invoiceNo, reason: \"Samme bekreftede fakturareferanse, motsatte like beløp og dato innenfor datovindu. Kun forslag.\" });\n      continue;\n    }\n    unresolved.push({ side: line.side, lineId: line.id, reason });\n  }\n  return { candidates, unresolved };\n}\n\n// src/transport.ts\nfunction parseContaJson(text2) {\n  if (typeof text2 !== \"string\") throw new Error(\"Conta-svaret må hentes som tekst for å bevare beløp og ID-er.\");\n  try {\n    JSON.parse(text2);\n  } catch {\n    throw new Error(\"Conta returnerte ugyldig JSON.\");\n  }\n  const protectedNumbers = text2.replace(\n    /\"(?:\\\\.|[^\"\\\\])*\"|-?\\d+(?:\\.\\d+)?(?:[eE][+-]?\\d+)?/g,\n    (token) => token.startsWith('\"') ? token : JSON.stringify(token)\n  );\n  return JSON.parse(protectedNumbers);\n}\nfunction requestPlan(scope) {\n  const root = scope.config.environment === \"production\" ? \"https://api.gateway.conta.no\" : \"https://api.gateway.conta-sandbox.no\";\n  const requests = [];\n  const add = (side, kind, account) => {\n    const base = `${root}/accounting/organizations/${scope.config.companies[side].organizationId}`;\n    const dates = `startDate=${scope.startDate}&endDate=${scope.endDate}`;\n    const path = kind === \"accounts\" ? \"/bookkeeping-accounts?hits=0&page=0\" : kind === \"details\" ? `/reports/trial-balance/details?${dates}&bookkeepingAccountNo=${account}` : `/reports/trial-balance?${dates}`;\n    requests.push({ side, kind, ...account ? { account } : {}, key: `${side}:${kind}${account ? \":\" + account : \"\"}`, url: base + path });\n  };\n  for (const side of [\"A\", \"B\"]) {\n    add(side, \"accounts\");\n    add(side, \"before\");\n  }\n  for (const side of [\"A\", \"B\"]) for (const a of scope.config.companies[side].accounts) add(side, \"details\", a.number);\n  for (const side of [\"A\", \"B\"]) add(side, \"after\");\n  return requests;\n}\nfunction assertAllowed(request, scope) {\n  const expected = requestPlan(scope).find((r) => r.key === request.key);\n  if (!expected || JSON.stringify(expected) !== JSON.stringify(request)) throw new Error(\"Forespørselen er ikke tillatt.\");\n}\nfunction retryDelay(status, retryAfter, attempt, now) {\n  if (![0, 408, 429, 500, 502, 503, 504].includes(status) || attempt >= 3) return null;\n  let delay = 2 ** attempt;\n  if (retryAfter !== void 0) {\n    if (typeof retryAfter !== \"string\") return null;\n    if (/^\\d+$/.test(retryAfter)) delay = Math.max(delay, Number(retryAfter));\n    else {\n      const at = Date.parse(retryAfter);\n      if (!Number.isFinite(at)) return null;\n      delay = Math.max(delay, Math.ceil((at - now) / 1e3));\n    }\n  }\n  return delay <= 300 ? delay : null;\n}\n\n// src/compare.ts\nfunction emptyReport(scope, generatedAt, synthetic = false) {\n  return {\n    version: \"0.1.0\",\n    synthetic,\n    generatedAt,\n    completeness: \"INCOMPLETE\",\n    agreement: null,\n    scope,\n    issues: [],\n    accounts: [],\n    totals: null,\n    residual: null,\n    lines: [],\n    candidates: [],\n    unresolved: [],\n    sources: []\n  };\n}\nfunction compare(scope, receipts, now, synthetic = false) {\n  const report = emptyReport(scope, now.toISOString(), synthetic);\n  try {\n    const plan = requestPlan(scope);\n    if (receipts.length !== plan.length) throw new Error(\"Datainnhentingen er ufullstendig eller inneholder ekstra svar.\");\n    let lastTime = -Infinity;\n    for (let i = 0; i < plan.length; i++) {\n      const r = receipts[i];\n      if (JSON.stringify(r.request) !== JSON.stringify(plan[i])) throw new Error(\"Kildesvar har feil rekkefølge eller omfang.\");\n      const time = Date.parse(r.retrievedAt);\n      if (!Number.isFinite(time) || time < lastTime || time > now.getTime()) throw new Error(\"Ugyldig tidsstempel for datainnhenting.\");\n      lastTime = time;\n      report.sources.push({ key: r.request.key, url: r.request.url, retrievedAt: r.retrievedAt, status: r.status });\n      if (r.status !== 200 || r.failure || r.body === void 0) throw new Error(`${r.request.key}: Innhenting mislyktes (HTTP ${r.status || \"ukjent\"}).`);\n    }\n    for (const side of [\"A\", \"B\"]) {\n      const get = (kind) => receipts.find((r) => r.request.side === side && r.request.kind === kind);\n      const accountIds = validateAccounts(get(\"accounts\").body, scope, side);\n      const before = balances(get(\"before\").body, scope, side);\n      const after = balances(get(\"after\").body, scope, side);\n      const values = (list2) => list2.map(({ original: _original, ...balance }) => balance);\n      if (JSON.stringify(values(before)) !== JSON.stringify(values(after))) throw new Error(`${side}: Saldoene endret seg under innhenting. Kjør kontrollen på nytt.`);\n      const lines = receipts.filter((r) => r.request.side === side && r.request.kind === \"details\").flatMap((r) => ledger(r, scope));\n      const ids = /* @__PURE__ */ new Set();\n      for (const l of lines) {\n        if (ids.has(l.id)) throw new Error(`${side}: Gjentatt kildelinje-ID ${l.id}.`);\n        ids.add(l.id);\n      }\n      for (const balance of before) {\n        const sum = lines.filter((l) => l.account === balance.account).reduce((n, l) => n + ore(l.amount), 0n);\n        if (sum !== ore(balance.movement)) throw new Error(`${side}: Detaljlinjene stemmer ikke med periodens bevegelse på konto ${balance.account}.`);\n      }\n      report.accounts.push(...before.map((a) => ({ ...a, accountId: accountIds[a.account] })));\n      report.lines.push(...lines);\n    }\n    if (!synthetic && (!scope.config.liveValidation.signsDatesOpeningAndOmissionsVerified || !scope.config.liveValidation.reference.trim())) {\n      throw new Error(\"Fortegn, datoavgrensning, inngående saldo og utelatte kontoer er ikke verifisert mot autoriserte Conta-rapporter.\");\n    }\n    const total = (side) => Object.fromEntries([\"opening\", \"movement\", \"closing\"].map((k) => [\n      k,\n      decimal(report.accounts.filter((a) => a.side === side).reduce((n, a) => n + ore(a[k]), 0n))\n    ]));\n    report.totals = { A: total(\"A\"), B: total(\"B\") };\n    const residual = Object.fromEntries([\"opening\", \"movement\", \"closing\"].map((k) => [\n      k,\n      decimal(ore(report.totals.A[k]) + ore(report.totals.B[k]))\n    ]));\n    report.residual = { ...residual, magnitude: decimal(abs(ore(residual.closing))) };\n    Object.assign(report, suggest(report.lines, scope.config));\n    report.completeness = \"COMPLETE\";\n    report.agreement = abs(ore(residual.closing)) <= ore(scope.config.tolerance) ? \"BALANCES_AGREE\" : \"DIFFERENCE\";\n  } catch (error) {\n    report.issues.push(error instanceof Error ? error.message : \"Kontrollen kunne ikke fullføres.\");\n    report.accounts = [];\n    report.totals = null;\n    report.residual = null;\n    report.unresolved = report.lines.map((l) => ({ side: l.side, lineId: l.id, reason: \"Ufullstendig kontroll. Linjen er ikke vurdert.\" }));\n  }\n  return report;\n}\n\n// src/engine.ts\nfunction initialize(config, now) {\n  try {\n    const scope = scopeFor(config, now);\n    return { scope, plan: requestPlan(scope), index: 0, attempt: 1, receipts: [], route: \"A\" };\n  } catch (error) {\n    const report = emptyReport(null, now.toISOString());\n    report.issues.push(error instanceof TypeError ? \"Konfigurasjonen har ugyldig struktur.\" : error instanceof Error ? error.message : \"Ugyldig konfigurasjon.\");\n    return { scope: null, plan: [], index: 0, attempt: 1, receipts: [], route: \"DONE\", report };\n  }\n}\nfunction dispatch(state, now) {\n  if (state.report) return { ...state, route: \"DONE\" };\n  if (!state.scope) throw new Error(\"Mangler validert omfang.\");\n  if (state.waitUntil && state.waitUntil > now.getTime()) return { ...state, route: \"WAIT\", waitSeconds: Math.max(1, Math.ceil((state.waitUntil - now.getTime()) / 1e3)) };\n  if (state.index >= state.plan.length) return { ...state, route: \"DONE\", report: compare(state.scope, state.receipts, now) };\n  const request = state.plan[state.index];\n  assertAllowed(request, state.scope);\n  return { ...state, waitUntil: void 0, waitSeconds: void 0, route: request.side, request };\n}\nfunction accept(state, response, now) {\n  if (!state.scope || !state.request) throw new Error(\"Svar uten en validert forespørsel.\");\n  assertAllowed(state.request, state.scope);\n  const value = response && typeof response === \"object\" ? response : {};\n  const status = typeof value.statusCode === \"number\" && Number.isInteger(value.statusCode) ? value.statusCode : 0;\n  const headers = value.headers && typeof value.headers === \"object\" ? value.headers : {};\n  if (status !== 200) {\n    const retryAfter = Object.entries(headers).find(([key]) => key.toLowerCase() === \"retry-after\")?.[1];\n    const delay = retryDelay(status, retryAfter, state.attempt, now.getTime());\n    if (delay !== null) return { ...state, attempt: state.attempt + 1, waitUntil: now.getTime() + delay * 1e3, route: \"WAIT\", waitSeconds: delay };\n  }\n  const receipt = { request: state.request, retrievedAt: now.toISOString(), status };\n  try {\n    if (status !== 200) throw new Error(\"Forespørselen mislyktes.\");\n    receipt.body = parseContaJson(value.body);\n  } catch {\n    receipt.failure = \"Innhenting eller lesing av svaret mislyktes.\";\n  }\n  const next = { ...state, receipts: [...state.receipts, receipt], index: state.index + 1, attempt: 1, waitUntil: void 0 };\n  if (receipt.failure) {\n    const report = emptyReport(state.scope, now.toISOString());\n    report.issues.push(`${receipt.request.key}: ${receipt.failure} HTTP ${status || \"ukjent\"}.`);\n    report.sources = next.receipts.map((r) => ({ key: r.request.key, url: r.request.url, status: r.status, retrievedAt: r.retrievedAt }));\n    return { ...next, report, route: \"DONE\" };\n  }\n  return next;\n}\n\n// src/report.ts\nfunction escapeHtml(value) {\n  return String(value ?? \"\").replace(/[&<>\"']/g, (c) => ({ \"&\": \"&amp;\", \"<\": \"&lt;\", \">\": \"&gt;\", '\"': \"&quot;\", \"'\": \"&#39;\" })[c]);\n}\nfunction csvCell(value) {\n  let text2 = String(value ?? \"\");\n  if (/^[\\s\\u0000-\\u001f\\ufeff]*[=+@-]/.test(text2) || /^[\\t\\r\\n]/.test(text2)) text2 = \"'\" + text2;\n  return '\"' + text2.replace(/\"/g, '\"\"') + '\"';\n}\nvar table = (heads, rows) => `<div class=\"scroll\"><table><thead><tr>${heads.map((h) => `<th scope=\"col\">${escapeHtml(h)}</th>`).join(\"\")}</tr></thead><tbody>${rows.map((row) => `<tr>${row.map((cell) => `<td>${escapeHtml(cell)}</td>`).join(\"\")}</tr>`).join(\"\")}</tbody></table></div>`;\nfunction render(report) {\n  const r = report, s = r.scope;\n  const complete = r.completeness === \"COMPLETE\";\n  const status = r.completeness === \"INCOMPLETE\" ? \"Ufullstendig kontroll\" : r.agreement === \"BALANCES_AGREE\" ? \"Saldoene stemmer innenfor toleransen\" : \"Differanse i saldoene\";\n  const candidateIds = new Set(r.candidates.flatMap((c) => [`A:${c.a}`, `B:${c.b}`]));\n  const reasons = new Map(r.unresolved.map((u) => [`${u.side}:${u.lineId}`, u.reason]));\n  const lineRows = r.lines.map((l) => [\n    l.side,\n    l.account,\n    l.id,\n    l.original.transactionId,\n    l.original.transactionNoYear ?? l.original.transactionNo ?? \"\",\n    l.date,\n    l.amount,\n    l.invoiceNo ?? \"\",\n    l.correction ? \"Ja\" : \"Nei\",\n    l.description,\n    candidateIds.has(`${l.side}:${l.id}`) ? \"Kandidat – krever vurdering\" : reasons.get(`${l.side}:${l.id}`) ?? \"Ikke vurdert\"\n  ]);\n  const heads = [\"Side\", \"Konto\", \"Kildelinje-ID\", \"Transaksjons-ID\", \"Bilagsnummer\", \"Dato\", \"Beløp NOK\", \"Fakturareferanse\", \"Korreksjon/reversering\", \"Beskrivelse\", \"Vurdering\"];\n  const csvRows = [\n    [\"Mellomværendekontroll for Conta\", r.synthetic ? \"KUN SYNTETISKE DATA\" : \"Privat regnskapsmateriale\"],\n    [\"Datastatus\", r.completeness],\n    [\"Saldostatus\", r.agreement ?? \"IKKE VURDERT\"],\n    [\"Selskap A\", s?.config.companies.A.name ?? \"\", s?.config.companies.A.organizationId ?? \"\"],\n    [\"Selskap B\", s?.config.companies.B.name ?? \"\", s?.config.companies.B.organizationId ?? \"\"],\n    [\"Valgte kontoer A\", s?.config.companies.A.accounts.map((a) => a.number).join(\", \") ?? \"UKJENT\"],\n    [\"Valgte kontoer B\", s?.config.companies.B.accounts.map((a) => a.number).join(\", \") ?? \"UKJENT\"],\n    [\"Fra\", s?.startDate ?? \"\", \"Til\", s?.endDate ?? \"\"],\n    [\"Toleranse NOK\", s?.config.tolerance ?? \"\"],\n    [\"Side\", \"Konto\", \"Inngående NOK\", \"Bevegelse NOK\", \"Utgående NOK\"],\n    ...r.accounts.map((a) => [a.side, a.account, a.opening, a.movement, a.closing]),\n    [\"Restsaldo A + B\", \"\", r.residual?.opening ?? \"UKJENT\", r.residual?.movement ?? \"UKJENT\", r.residual?.closing ?? \"UKJENT\"],\n    [\"Absolutt utgående differanse NOK\", r.residual?.magnitude ?? \"UKJENT\"],\n    [\"Antall kandidatpar\", complete ? r.candidates.length : \"IKKE VURDERT\"],\n    [\"Antall linjer uten kandidat\", complete ? r.unresolved.length : \"IKKE VURDERT\"],\n    ...r.issues.map((i) => [\"Problem\", i]),\n    [\"Saldoenighet er ikke ferdig avstemming. Kandidater og uavklarte linjer krever regnskapsførers vurdering.\"],\n    [],\n    [\"Linje A\", \"Linje B\", \"Felles referanse\", \"Begrunnelse\"],\n    ...r.candidates.map((c) => [c.a, c.b, c.reference, c.reason]),\n    [],\n    heads,\n    ...lineRows,\n    [],\n    [\"Kilde\", \"URL\", \"Hentet\", \"HTTP\"],\n    ...r.sources.map((source) => [source.key, source.url, source.retrievedAt, source.status])\n  ];\n  const html = `<!doctype html><html lang=\"nb\"><head><meta charset=\"utf-8\"><meta name=\"viewport\" content=\"width=device-width,initial-scale=1\"><meta http-equiv=\"Content-Security-Policy\" content=\"default-src 'none'; style-src 'unsafe-inline'; base-uri 'none'; form-action 'none'\"><title>Mellomværendekontroll for Conta</title><style>\n  :root{font-family:system-ui,sans-serif;color:#172b36;background:#f2f5f4}body{margin:0}main{max-width:1120px;margin:32px auto;padding:32px;background:white;border-radius:12px}h1{font-size:30px;line-height:1.2}h2{margin-top:32px;font-size:21px}.eyebrow{font-size:13px;letter-spacing:.07em;text-transform:uppercase;color:#526b71}.status{padding:20px;background:#edf5f1;border-left:5px solid #1c6955}.amount{font-size:36px;font-weight:700;margin:10px 0}.note{color:#465961;line-height:1.6}.scroll{overflow-x:auto}table{border-collapse:collapse;width:100%;font-size:13px}th,td{padding:10px;text-align:left;border-bottom:1px solid #dce4e2;vertical-align:top;overflow-wrap:anywhere}th{background:#edf2f1}ul{line-height:1.7}footer{margin-top:32px;border-top:1px solid #dce4e2;padding-top:16px;font-size:12px;color:#465961}@media(max-width:600px){main{margin:0;padding:20px;border-radius:0}h1{font-size:25px}.amount{font-size:30px}}@media print{main{margin:0;padding:0;max-width:none}.scroll{overflow:visible}thead{display:table-header-group}tr{break-inside:avoid}}\n  </style></head><body><main><p class=\"eyebrow\">${r.synthetic ? \"Demonstrasjon · kun syntetiske data\" : \"Privat · grunnlag for regnskapsførers vurdering\"}</p><h1>Mellomværendekontroll for Conta</h1>\n  <p>${escapeHtml(s?.config.companies.A.name ?? \"Ukjent selskap\")} ↔ ${escapeHtml(s?.config.companies.B.name ?? \"Ukjent selskap\")}</p><p>${escapeHtml(s?.startDate)}–${escapeHtml(s?.endDate)} · NOK · toleranse ${escapeHtml(s ? nok(s.config.tolerance) : \"ukjent\")}</p>\n  <section class=\"status\"><strong>${escapeHtml(status)}</strong><p class=\"amount\">${r.residual ? escapeHtml(nok(r.residual.closing)) + \" NOK\" : \"Ikke beregnet\"}</p><p>Signert utgående restsaldo: A + B${r.residual ? \". Absolutt differanse: \" + escapeHtml(nok(r.residual.magnitude)) + \" NOK.\" : \".\"}</p><p>Datastatus: ${r.completeness} · Saldostatus: ${r.agreement ?? \"IKKE VURDERT\"}</p></section>\n  <p class=\"note\">Saldoenighet er ikke en ferdig avstemming eller bevis på at alle transaksjoner er bokført. Uavklarte linjer er ikke bekreftede bokføringsfeil. Regnskapsføreren godkjenner kontovalg og konklusjoner.</p>\n${r.issues.length ? \"<h2>Problemer som må løses</h2><ul>\" + r.issues.map((i) => \"<li>\" + escapeHtml(i) + \"</li>\").join(\"\") + \"</ul>\" : \"\"}\n  <h2>Omfang og kontovalg</h2>${table([\"Side\", \"Selskap\", \"Conta-ID\", \"Dedikerte kontoer\"], s ? [\"A\", \"B\"].map((side) => [side, s.config.companies[side].name, s.config.companies[side].organizationId, s.config.companies[side].accounts.map((a) => a.number).join(\", \")]) : [])}\n  <h2>Inngående saldo, bevegelse og utgående saldo</h2>${table([\"Side/konto\", \"Inngående NOK\", \"Bevegelse NOK\", \"Utgående NOK\"], [...r.accounts.map((a) => [a.side + \" / \" + a.account, nok(a.opening), nok(a.movement), nok(a.closing)]), ...r.residual ? [[\"Restsaldo A + B\", nok(r.residual.opening), nok(r.residual.movement), nok(r.residual.closing)]] : []])}\n  <p class=\"note\">Inngående restsaldo + periodens bevegelsesdifferanse = utgående restsaldo. En inngående differanse oppsto før denne måneden.</p>\n  <h2>Kandidatforslag (${complete ? r.candidates.length : \"ikke vurdert\"})</h2>${table([\"Linje A\", \"Linje B\", \"Felles referanse\", \"Begrunnelse\"], r.candidates.map((c) => [c.a, c.b, c.reference, c.reason]))}<p class=\"note\">${complete ? `Ingen forslag er automatisk bekreftet. ${r.unresolved.length} linjer gjenstår uten kandidat.` : \"Kandidater og antall uavklarte linjer kan ikke vurderes før kontrollen er fullført.\"}</p>\n  <h2>${complete ? `Linjegrunnlag (${r.lines.length})` : \"Tilgjengelig linjegrunnlag – ufullstendig\"}</h2>${complete ? \"\" : '<p class=\"note\">Tabellen viser bare linjer som er validert før kontrollen stoppet. En tom tabell betyr ikke at perioden er uten transaksjoner.</p>'}${table(heads, lineRows)}\n  <h2>Kilder og innhenting</h2>${table([\"Kilde\", \"Hentet (UTC)\", \"HTTP\"], r.sources.map((source) => [source.key, source.retrievedAt, source.status]))}\n  <p class=\"note\">Saldoene hentes på nytt etter detaljene. Dette oppdager enkelte endringer underveis, men gir ikke et atomisk historisk øyeblikksbilde. Endringer som utligner hverandre kan forbli uoppdaget.</p><footer>Generert ${escapeHtml(r.generatedAt)} · versjon ${r.version} · Oppbevar rapport og kildegrunnlag privat etter virksomhetens rutiner.</footer></main></body></html>`;\n  return { html, csv: \"\\uFEFF\" + csvRows.map((row) => row.map(csvCell).join(\";\")).join(\"\\r\\n\") + \"\\r\\n\", json: JSON.stringify(report, null, 2) };\n}\n\n// <stdin>\nvar Checker = { initialize, dispatch, accept, compare, render, scopeFor };\n\nconst response = {...$input.first().json};\ntry { if ($input.first().binary?.raw) response.body = (await this.helpers.getBinaryDataBuffer(0, 'raw')).toString('utf8'); } catch { response.body = undefined; }\nreturn [{ json: Checker.accept($('Dispatch request').item.json, response, new Date()), pairedItem: { item: 0 } }];"
      },
      "position": [
        560,
        -100
      ]
    },
    {
      "id": "3827f9e829c840554447e7311a2456b6",
      "name": "Honor Retry-After",
      "type": "n8n-nodes-base.wait",
      "typeVersion": 1.1,
      "parameters": {
        "resume": "timeInterval",
        "amount": "={{ $json.waitSeconds }}",
        "unit": "seconds"
      },
      "position": [
        260,
        140
      ],
      "webhookId": "conta-intercompany-retry"
    },
    {
      "id": "4f36e11ca91522931bfddba9c7c2943a",
      "name": "Private reports",
      "type": "n8n-nodes-base.code",
      "typeVersion": 2,
      "parameters": {
        "mode": "runOnceForAllItems",
        "jsCode": "// src/money.ts\nfunction ore(value) {\n  if (typeof value !== \"string\" || !/^-?(0|[1-9]\\d*)(\\.\\d{1,2})?$/.test(value)) {\n    throw new Error(\"Ugyldig beløp: forventet desimaltall med høyst to desimaler.\");\n  }\n  const negative = value.startsWith(\"-\");\n  const [whole = \"\", fraction = \"\"] = (negative ? value.slice(1) : value).split(\".\");\n  return (BigInt(whole) * 100n + BigInt(fraction.padEnd(2, \"0\"))) * (negative ? -1n : 1n);\n}\nfunction decimal(value) {\n  const sign = value < 0n ? \"-\" : \"\";\n  const n = value < 0n ? -value : value;\n  return `${sign}${n / 100n}.${String(n % 100n).padStart(2, \"0\")}`;\n}\nfunction abs(n) {\n  return n < 0n ? -n : n;\n}\nfunction nok(value) {\n  const [whole = \"\", fraction = \"00\"] = value.split(\".\");\n  return `${whole.replace(/\\B(?=(\\d{3})+(?!\\d))/g, \" \")},${fraction.padEnd(2, \"0\")}`;\n}\n\n// src/config.ts\nfunction validDate(value) {\n  return typeof value === \"string\" && /^\\d{4}-\\d{2}-\\d{2}$/.test(value) && !Number.isNaN(Date.parse(value)) && new Date(value).toISOString().slice(0, 10) === value;\n}\nfunction scopeFor(input, now) {\n  if (!input || typeof input !== \"object\") throw new Error(\"Konfigurasjonen mangler.\");\n  const c = input;\n  if (![\"sandbox\", \"production\"].includes(c.environment)) throw new Error(\"Ugyldig Conta-miljø.\");\n  if (!Number.isFinite(now.getTime())) throw new Error(\"Ugyldig kjøringstidspunkt.\");\n  const oslo = new Intl.DateTimeFormat(\"en-CA\", { timeZone: \"Europe/Oslo\", year: \"numeric\", month: \"2-digit\", day: \"2-digit\" }).formatToParts(now);\n  const part = (name) => oslo.find((p) => p.type === name).value;\n  const currentMonth = `${part(\"year\")}-${part(\"month\")}`;\n  let month = c.period;\n  if (month === \"last-completed-month\") {\n    const date = /* @__PURE__ */ new Date(`${currentMonth}-01T12:00:00Z`);\n    date.setUTCMonth(date.getUTCMonth() - 1);\n    month = date.toISOString().slice(0, 7);\n  }\n  if (typeof month !== \"string\" || !/^20\\d{2}-(0[1-9]|1[0-2])$/.test(month) || month >= currentMonth) {\n    throw new Error(\"Velg én avsluttet kalendermåned (YYYY-MM).\");\n  }\n  const startDate = `${month}-01`;\n  const end = /* @__PURE__ */ new Date(`${startDate}T12:00:00Z`);\n  end.setUTCMonth(end.getUTCMonth() + 1, 0);\n  const tolerance = ore(c.tolerance);\n  if (tolerance < 0n || tolerance > 10000n) throw new Error(\"Toleranse må være mellom 0 og 100 NOK.\");\n  for (const side of [\"A\", \"B\"]) {\n    const company = c.companies?.[side];\n    if (!company || !/^[1-9]\\d{0,18}$/.test(company.organizationId) || typeof company.organizationId !== \"string\" || typeof company.name !== \"string\" || !company.name.trim() || company.name.length > 200) throw new Error(`Ugyldig selskap ${side}.`);\n    if (company.authorized !== true || company.currency !== \"NOK\") throw new Error(`Selskap ${side} må være autorisert og føre regnskap i NOK.`);\n    if (!Array.isArray(company.accounts) || !company.accounts.length) throw new Error(`Kontovalg mangler for ${side}.`);\n    const seen = /* @__PURE__ */ new Set();\n    for (const account of company.accounts) {\n      if (!account || typeof account.number !== \"string\" || !/^[12]\\d{3}$/.test(account.number) || seen.has(account.number)) throw new Error(`Ugyldig eller gjentatt balansekonto for ${side}.`);\n      if (account.dedicatedToOtherCompany !== true || account.positionsInNok !== true) throw new Error(`Konto ${account.number} må kun gjelde motparten og posisjoner i NOK.`);\n      seen.add(account.number);\n    }\n  }\n  if (c.companies.A.organizationId === c.companies.B.organizationId) throw new Error(\"Selskapene må ha ulike organisasjons-ID-er.\");\n  if (typeof c.sharedInvoiceNumbersConfirmed !== \"boolean\" || !Number.isInteger(c.candidateDateWindowDays) || c.candidateDateWindowDays < 0 || c.candidateDateWindowDays > 31) throw new Error(\"Ugyldige regler for kandidatforslag.\");\n  if (!c.liveValidation || typeof c.liveValidation.reference !== \"string\" || typeof c.liveValidation.signsDatesOpeningAndOmissionsVerified !== \"boolean\") throw new Error(\"Angi status for kontroll mot Conta-rapporter.\");\n  const clean = {\n    environment: c.environment,\n    period: month,\n    tolerance: c.tolerance,\n    companies: Object.fromEntries([\"A\", \"B\"].map((side) => [side, {\n      organizationId: c.companies[side].organizationId,\n      name: c.companies[side].name,\n      authorized: true,\n      currency: \"NOK\",\n      accounts: c.companies[side].accounts.map((a) => ({ number: a.number, dedicatedToOtherCompany: true, positionsInNok: true }))\n    }])),\n    sharedInvoiceNumbersConfirmed: c.sharedInvoiceNumbersConfirmed,\n    candidateDateWindowDays: c.candidateDateWindowDays,\n    liveValidation: { reference: c.liveValidation.reference, signsDatesOpeningAndOmissionsVerified: c.liveValidation.signsDatesOpeningAndOmissionsVerified }\n  };\n  return { config: clean, startDate, endDate: end.toISOString().slice(0, 10) };\n}\n\n// src/adapter.ts\nfunction record(value) {\n  if (!value || typeof value !== \"object\" || Array.isArray(value)) throw new Error(\"Uventet objektskjema fra Conta.\");\n  return value;\n}\nfunction list(value) {\n  if (!Array.isArray(value)) throw new Error(\"Uventet listeskjema fra Conta.\");\n  return value;\n}\nfunction id(value) {\n  if (typeof value !== \"string\" || !/^[1-9]\\d{0,18}$/.test(value)) throw new Error(\"Kilde-ID mangler eller er ugyldig.\");\n  return value;\n}\nfunction text(value, name) {\n  if (typeof value !== \"string\") throw new Error(`${name} mangler eller er ugyldig.`);\n  return value;\n}\nfunction validateAccounts(body, scope, side) {\n  const rows = list(body).map(record);\n  const ids = {};\n  for (const a of scope.config.companies[side].accounts) {\n    const found = rows.filter((r) => r.bookkeepingAccountNo === a.number);\n    if (found.length !== 1) throw new Error(`${side}: Konto ${a.number} mangler eller er tvetydig i kontoplanen.`);\n    ids[a.number] = id(found[0].id);\n    if (typeof found[0].isActive !== \"boolean\") throw new Error(`${side}: Kontostatus mangler for ${a.number}.`);\n  }\n  return ids;\n}\nfunction balances(body, scope, side) {\n  const rows = list(record(body).trialBalanceOutputSub).map(record);\n  return scope.config.companies[side].accounts.map((a) => {\n    const found = rows.filter((r) => r.bookkeepingAccountNo === a.number);\n    if (found.length !== 1) throw new Error(`${side}: Konto ${a.number} mangler eller er tvetydig i saldobalansen. Fravær er ikke null.`);\n    const row = found[0];\n    const opening = ore(row.sumIngoing), movement = ore(row.sumChanged), closing = ore(row.sumOutgoing);\n    if (opening + movement !== closing) throw new Error(`${side}: Inngående saldo + bevegelse stemmer ikke med utgående saldo på ${a.number}.`);\n    return {\n      side,\n      account: a.number,\n      opening: decimal(opening),\n      movement: decimal(movement),\n      closing: decimal(closing),\n      original: { sumIngoing: row.sumIngoing, sumChanged: row.sumChanged, sumOutgoing: row.sumOutgoing }\n    };\n  });\n}\nfunction ledger(receipt, scope) {\n  return list(receipt.body).map((value) => {\n    const row = record(value);\n    const account = text(row.bookkeepingAccountNo, \"Kontonummer\");\n    if (account !== receipt.request.account) throw new Error(\"Detaljsvaret inneholder en annen konto enn forespurt.\");\n    if (!validDate(row.date) || row.date < scope.startDate || row.date > scope.endDate) throw new Error(\"Detaljsvaret inneholder en ugyldig dato eller en dato utenfor perioden.\");\n    const lineId = id(row.id);\n    id(row.transactionId);\n    if (typeof row.isResetTransaction !== \"boolean\" || typeof row.isCorrection !== \"boolean\") throw new Error(\"Status for reversering eller korrigering mangler.\");\n    const original = {};\n    for (const key of [\"id\", \"transactionId\", \"transactionNo\", \"transactionNoYear\", \"date\", \"amount\", \"bookkeepingAccountNo\", \"description\", \"invoiceId\", \"invoiceNo\", \"isResetTransaction\", \"resetByTransactionId\", \"creditNoteId\", \"creditNoteNo\", \"isCorrection\"]) {\n      if (row[key] !== void 0) {\n        if (row[key] !== null && ![\"string\", \"boolean\"].includes(typeof row[key])) throw new Error(\"Uventet felt i kildebevis.\");\n        original[key] = row[key];\n      }\n    }\n    const invoiceNo = row.invoiceNo === void 0 || row.invoiceNo === null || row.invoiceNo === \"\" ? null : text(row.invoiceNo, \"Fakturanummer\");\n    return {\n      side: receipt.request.side,\n      account,\n      id: lineId,\n      date: row.date,\n      amount: decimal(ore(row.amount)),\n      invoiceNo,\n      description: row.description === void 0 ? \"\" : text(row.description, \"Beskrivelse\"),\n      correction: row.isResetTransaction || row.isCorrection || row.resetByTransactionId != null || row.creditNoteId != null,\n      original\n    };\n  });\n}\n\n// src/candidates.ts\nfunction suggest(lines, config) {\n  const candidates = [], unresolved = [];\n  const groups = /* @__PURE__ */ new Map();\n  for (const l of lines) if (l.invoiceNo) {\n    if (!groups.has(l.invoiceNo)) groups.set(l.invoiceNo, { A: [], B: [] });\n    groups.get(l.invoiceNo)[l.side].push(l);\n  }\n  for (const line of lines) {\n    let reason = \"\";\n    const group = line.invoiceNo ? groups.get(line.invoiceNo) : { A: [], B: [] };\n    const a = group.A, b = group.B;\n    if (!config.sharedInvoiceNumbersConfirmed) reason = \"Felles fakturareferanse er ikke bekreftet for relasjonen.\";\n    else if (!line.invoiceNo) reason = \"Mangler felles fakturareferanse.\";\n    else if (a.length > 1 || b.length > 1) reason = \"Tvetydig referanse: flere linjer eller delt oppgjør.\";\n    else if (!a.length || !b.length) reason = \"Ingen linje med samme fakturareferanse hos motparten.\";\n    else if (a[0].correction || b[0].correction) reason = \"Reversering eller korrigering krever manuell vurdering.\";\n    else if (ore(a[0].amount) === 0n || ore(a[0].amount) + ore(b[0].amount) !== 0n) reason = \"Referansen har ulike eller ikke motsatte beløp.\";\n    else if (Math.abs(Date.parse(a[0].date) - Date.parse(b[0].date)) / 864e5 > config.candidateDateWindowDays) reason = \"Referansen ligger utenfor valgt datovindu.\";\n    else {\n      if (line.side === \"A\") candidates.push({ a: a[0].id, b: b[0].id, reference: line.invoiceNo, reason: \"Samme bekreftede fakturareferanse, motsatte like beløp og dato innenfor datovindu. Kun forslag.\" });\n      continue;\n    }\n    unresolved.push({ side: line.side, lineId: line.id, reason });\n  }\n  return { candidates, unresolved };\n}\n\n// src/transport.ts\nfunction parseContaJson(text2) {\n  if (typeof text2 !== \"string\") throw new Error(\"Conta-svaret må hentes som tekst for å bevare beløp og ID-er.\");\n  try {\n    JSON.parse(text2);\n  } catch {\n    throw new Error(\"Conta returnerte ugyldig JSON.\");\n  }\n  const protectedNumbers = text2.replace(\n    /\"(?:\\\\.|[^\"\\\\])*\"|-?\\d+(?:\\.\\d+)?(?:[eE][+-]?\\d+)?/g,\n    (token) => token.startsWith('\"') ? token : JSON.stringify(token)\n  );\n  return JSON.parse(protectedNumbers);\n}\nfunction requestPlan(scope) {\n  const root = scope.config.environment === \"production\" ? \"https://api.gateway.conta.no\" : \"https://api.gateway.conta-sandbox.no\";\n  const requests = [];\n  const add = (side, kind, account) => {\n    const base = `${root}/accounting/organizations/${scope.config.companies[side].organizationId}`;\n    const dates = `startDate=${scope.startDate}&endDate=${scope.endDate}`;\n    const path = kind === \"accounts\" ? \"/bookkeeping-accounts?hits=0&page=0\" : kind === \"details\" ? `/reports/trial-balance/details?${dates}&bookkeepingAccountNo=${account}` : `/reports/trial-balance?${dates}`;\n    requests.push({ side, kind, ...account ? { account } : {}, key: `${side}:${kind}${account ? \":\" + account : \"\"}`, url: base + path });\n  };\n  for (const side of [\"A\", \"B\"]) {\n    add(side, \"accounts\");\n    add(side, \"before\");\n  }\n  for (const side of [\"A\", \"B\"]) for (const a of scope.config.companies[side].accounts) add(side, \"details\", a.number);\n  for (const side of [\"A\", \"B\"]) add(side, \"after\");\n  return requests;\n}\nfunction assertAllowed(request, scope) {\n  const expected = requestPlan(scope).find((r) => r.key === request.key);\n  if (!expected || JSON.stringify(expected) !== JSON.stringify(request)) throw new Error(\"Forespørselen er ikke tillatt.\");\n}\nfunction retryDelay(status, retryAfter, attempt, now) {\n  if (![0, 408, 429, 500, 502, 503, 504].includes(status) || attempt >= 3) return null;\n  let delay = 2 ** attempt;\n  if (retryAfter !== void 0) {\n    if (typeof retryAfter !== \"string\") return null;\n    if (/^\\d+$/.test(retryAfter)) delay = Math.max(delay, Number(retryAfter));\n    else {\n      const at = Date.parse(retryAfter);\n      if (!Number.isFinite(at)) return null;\n      delay = Math.max(delay, Math.ceil((at - now) / 1e3));\n    }\n  }\n  return delay <= 300 ? delay : null;\n}\n\n// src/compare.ts\nfunction emptyReport(scope, generatedAt, synthetic = false) {\n  return {\n    version: \"0.1.0\",\n    synthetic,\n    generatedAt,\n    completeness: \"INCOMPLETE\",\n    agreement: null,\n    scope,\n    issues: [],\n    accounts: [],\n    totals: null,\n    residual: null,\n    lines: [],\n    candidates: [],\n    unresolved: [],\n    sources: []\n  };\n}\nfunction compare(scope, receipts, now, synthetic = false) {\n  const report = emptyReport(scope, now.toISOString(), synthetic);\n  try {\n    const plan = requestPlan(scope);\n    if (receipts.length !== plan.length) throw new Error(\"Datainnhentingen er ufullstendig eller inneholder ekstra svar.\");\n    let lastTime = -Infinity;\n    for (let i = 0; i < plan.length; i++) {\n      const r = receipts[i];\n      if (JSON.stringify(r.request) !== JSON.stringify(plan[i])) throw new Error(\"Kildesvar har feil rekkefølge eller omfang.\");\n      const time = Date.parse(r.retrievedAt);\n      if (!Number.isFinite(time) || time < lastTime || time > now.getTime()) throw new Error(\"Ugyldig tidsstempel for datainnhenting.\");\n      lastTime = time;\n      report.sources.push({ key: r.request.key, url: r.request.url, retrievedAt: r.retrievedAt, status: r.status });\n      if (r.status !== 200 || r.failure || r.body === void 0) throw new Error(`${r.request.key}: Innhenting mislyktes (HTTP ${r.status || \"ukjent\"}).`);\n    }\n    for (const side of [\"A\", \"B\"]) {\n      const get = (kind) => receipts.find((r) => r.request.side === side && r.request.kind === kind);\n      const accountIds = validateAccounts(get(\"accounts\").body, scope, side);\n      const before = balances(get(\"before\").body, scope, side);\n      const after = balances(get(\"after\").body, scope, side);\n      const values = (list2) => list2.map(({ original: _original, ...balance }) => balance);\n      if (JSON.stringify(values(before)) !== JSON.stringify(values(after))) throw new Error(`${side}: Saldoene endret seg under innhenting. Kjør kontrollen på nytt.`);\n      const lines = receipts.filter((r) => r.request.side === side && r.request.kind === \"details\").flatMap((r) => ledger(r, scope));\n      const ids = /* @__PURE__ */ new Set();\n      for (const l of lines) {\n        if (ids.has(l.id)) throw new Error(`${side}: Gjentatt kildelinje-ID ${l.id}.`);\n        ids.add(l.id);\n      }\n      for (const balance of before) {\n        const sum = lines.filter((l) => l.account === balance.account).reduce((n, l) => n + ore(l.amount), 0n);\n        if (sum !== ore(balance.movement)) throw new Error(`${side}: Detaljlinjene stemmer ikke med periodens bevegelse på konto ${balance.account}.`);\n      }\n      report.accounts.push(...before.map((a) => ({ ...a, accountId: accountIds[a.account] })));\n      report.lines.push(...lines);\n    }\n    if (!synthetic && (!scope.config.liveValidation.signsDatesOpeningAndOmissionsVerified || !scope.config.liveValidation.reference.trim())) {\n      throw new Error(\"Fortegn, datoavgrensning, inngående saldo og utelatte kontoer er ikke verifisert mot autoriserte Conta-rapporter.\");\n    }\n    const total = (side) => Object.fromEntries([\"opening\", \"movement\", \"closing\"].map((k) => [\n      k,\n      decimal(report.accounts.filter((a) => a.side === side).reduce((n, a) => n + ore(a[k]), 0n))\n    ]));\n    report.totals = { A: total(\"A\"), B: total(\"B\") };\n    const residual = Object.fromEntries([\"opening\", \"movement\", \"closing\"].map((k) => [\n      k,\n      decimal(ore(report.totals.A[k]) + ore(report.totals.B[k]))\n    ]));\n    report.residual = { ...residual, magnitude: decimal(abs(ore(residual.closing))) };\n    Object.assign(report, suggest(report.lines, scope.config));\n    report.completeness = \"COMPLETE\";\n    report.agreement = abs(ore(residual.closing)) <= ore(scope.config.tolerance) ? \"BALANCES_AGREE\" : \"DIFFERENCE\";\n  } catch (error) {\n    report.issues.push(error instanceof Error ? error.message : \"Kontrollen kunne ikke fullføres.\");\n    report.accounts = [];\n    report.totals = null;\n    report.residual = null;\n    report.unresolved = report.lines.map((l) => ({ side: l.side, lineId: l.id, reason: \"Ufullstendig kontroll. Linjen er ikke vurdert.\" }));\n  }\n  return report;\n}\n\n// src/engine.ts\nfunction initialize(config, now) {\n  try {\n    const scope = scopeFor(config, now);\n    return { scope, plan: requestPlan(scope), index: 0, attempt: 1, receipts: [], route: \"A\" };\n  } catch (error) {\n    const report = emptyReport(null, now.toISOString());\n    report.issues.push(error instanceof TypeError ? \"Konfigurasjonen har ugyldig struktur.\" : error instanceof Error ? error.message : \"Ugyldig konfigurasjon.\");\n    return { scope: null, plan: [], index: 0, attempt: 1, receipts: [], route: \"DONE\", report };\n  }\n}\nfunction dispatch(state, now) {\n  if (state.report) return { ...state, route: \"DONE\" };\n  if (!state.scope) throw new Error(\"Mangler validert omfang.\");\n  if (state.waitUntil && state.waitUntil > now.getTime()) return { ...state, route: \"WAIT\", waitSeconds: Math.max(1, Math.ceil((state.waitUntil - now.getTime()) / 1e3)) };\n  if (state.index >= state.plan.length) return { ...state, route: \"DONE\", report: compare(state.scope, state.receipts, now) };\n  const request = state.plan[state.index];\n  assertAllowed(request, state.scope);\n  return { ...state, waitUntil: void 0, waitSeconds: void 0, route: request.side, request };\n}\nfunction accept(state, response, now) {\n  if (!state.scope || !state.request) throw new Error(\"Svar uten en validert forespørsel.\");\n  assertAllowed(state.request, state.scope);\n  const value = response && typeof response === \"object\" ? response : {};\n  const status = typeof value.statusCode === \"number\" && Number.isInteger(value.statusCode) ? value.statusCode : 0;\n  const headers = value.headers && typeof value.headers === \"object\" ? value.headers : {};\n  if (status !== 200) {\n    const retryAfter = Object.entries(headers).find(([key]) => key.toLowerCase() === \"retry-after\")?.[1];\n    const delay = retryDelay(status, retryAfter, state.attempt, now.getTime());\n    if (delay !== null) return { ...state, attempt: state.attempt + 1, waitUntil: now.getTime() + delay * 1e3, route: \"WAIT\", waitSeconds: delay };\n  }\n  const receipt = { request: state.request, retrievedAt: now.toISOString(), status };\n  try {\n    if (status !== 200) throw new Error(\"Forespørselen mislyktes.\");\n    receipt.body = parseContaJson(value.body);\n  } catch {\n    receipt.failure = \"Innhenting eller lesing av svaret mislyktes.\";\n  }\n  const next = { ...state, receipts: [...state.receipts, receipt], index: state.index + 1, attempt: 1, waitUntil: void 0 };\n  if (receipt.failure) {\n    const report = emptyReport(state.scope, now.toISOString());\n    report.issues.push(`${receipt.request.key}: ${receipt.failure} HTTP ${status || \"ukjent\"}.`);\n    report.sources = next.receipts.map((r) => ({ key: r.request.key, url: r.request.url, status: r.status, retrievedAt: r.retrievedAt }));\n    return { ...next, report, route: \"DONE\" };\n  }\n  return next;\n}\n\n// src/report.ts\nfunction escapeHtml(value) {\n  return String(value ?? \"\").replace(/[&<>\"']/g, (c) => ({ \"&\": \"&amp;\", \"<\": \"&lt;\", \">\": \"&gt;\", '\"': \"&quot;\", \"'\": \"&#39;\" })[c]);\n}\nfunction csvCell(value) {\n  let text2 = String(value ?? \"\");\n  if (/^[\\s\\u0000-\\u001f\\ufeff]*[=+@-]/.test(text2) || /^[\\t\\r\\n]/.test(text2)) text2 = \"'\" + text2;\n  return '\"' + text2.replace(/\"/g, '\"\"') + '\"';\n}\nvar table = (heads, rows) => `<div class=\"scroll\"><table><thead><tr>${heads.map((h) => `<th scope=\"col\">${escapeHtml(h)}</th>`).join(\"\")}</tr></thead><tbody>${rows.map((row) => `<tr>${row.map((cell) => `<td>${escapeHtml(cell)}</td>`).join(\"\")}</tr>`).join(\"\")}</tbody></table></div>`;\nfunction render(report) {\n  const r = report, s = r.scope;\n  const complete = r.completeness === \"COMPLETE\";\n  const status = r.completeness === \"INCOMPLETE\" ? \"Ufullstendig kontroll\" : r.agreement === \"BALANCES_AGREE\" ? \"Saldoene stemmer innenfor toleransen\" : \"Differanse i saldoene\";\n  const candidateIds = new Set(r.candidates.flatMap((c) => [`A:${c.a}`, `B:${c.b}`]));\n  const reasons = new Map(r.unresolved.map((u) => [`${u.side}:${u.lineId}`, u.reason]));\n  const lineRows = r.lines.map((l) => [\n    l.side,\n    l.account,\n    l.id,\n    l.original.transactionId,\n    l.original.transactionNoYear ?? l.original.transactionNo ?? \"\",\n    l.date,\n    l.amount,\n    l.invoiceNo ?? \"\",\n    l.correction ? \"Ja\" : \"Nei\",\n    l.description,\n    candidateIds.has(`${l.side}:${l.id}`) ? \"Kandidat – krever vurdering\" : reasons.get(`${l.side}:${l.id}`) ?? \"Ikke vurdert\"\n  ]);\n  const heads = [\"Side\", \"Konto\", \"Kildelinje-ID\", \"Transaksjons-ID\", \"Bilagsnummer\", \"Dato\", \"Beløp NOK\", \"Fakturareferanse\", \"Korreksjon/reversering\", \"Beskrivelse\", \"Vurdering\"];\n  const csvRows = [\n    [\"Mellomværendekontroll for Conta\", r.synthetic ? \"KUN SYNTETISKE DATA\" : \"Privat regnskapsmateriale\"],\n    [\"Datastatus\", r.completeness],\n    [\"Saldostatus\", r.agreement ?? \"IKKE VURDERT\"],\n    [\"Selskap A\", s?.config.companies.A.name ?? \"\", s?.config.companies.A.organizationId ?? \"\"],\n    [\"Selskap B\", s?.config.companies.B.name ?? \"\", s?.config.companies.B.organizationId ?? \"\"],\n    [\"Valgte kontoer A\", s?.config.companies.A.accounts.map((a) => a.number).join(\", \") ?? \"UKJENT\"],\n    [\"Valgte kontoer B\", s?.config.companies.B.accounts.map((a) => a.number).join(\", \") ?? \"UKJENT\"],\n    [\"Fra\", s?.startDate ?? \"\", \"Til\", s?.endDate ?? \"\"],\n    [\"Toleranse NOK\", s?.config.tolerance ?? \"\"],\n    [\"Side\", \"Konto\", \"Inngående NOK\", \"Bevegelse NOK\", \"Utgående NOK\"],\n    ...r.accounts.map((a) => [a.side, a.account, a.opening, a.movement, a.closing]),\n    [\"Restsaldo A + B\", \"\", r.residual?.opening ?? \"UKJENT\", r.residual?.movement ?? \"UKJENT\", r.residual?.closing ?? \"UKJENT\"],\n    [\"Absolutt utgående differanse NOK\", r.residual?.magnitude ?? \"UKJENT\"],\n    [\"Antall kandidatpar\", complete ? r.candidates.length : \"IKKE VURDERT\"],\n    [\"Antall linjer uten kandidat\", complete ? r.unresolved.length : \"IKKE VURDERT\"],\n    ...r.issues.map((i) => [\"Problem\", i]),\n    [\"Saldoenighet er ikke ferdig avstemming. Kandidater og uavklarte linjer krever regnskapsførers vurdering.\"],\n    [],\n    [\"Linje A\", \"Linje B\", \"Felles referanse\", \"Begrunnelse\"],\n    ...r.candidates.map((c) => [c.a, c.b, c.reference, c.reason]),\n    [],\n    heads,\n    ...lineRows,\n    [],\n    [\"Kilde\", \"URL\", \"Hentet\", \"HTTP\"],\n    ...r.sources.map((source) => [source.key, source.url, source.retrievedAt, source.status])\n  ];\n  const html = `<!doctype html><html lang=\"nb\"><head><meta charset=\"utf-8\"><meta name=\"viewport\" content=\"width=device-width,initial-scale=1\"><meta http-equiv=\"Content-Security-Policy\" content=\"default-src 'none'; style-src 'unsafe-inline'; base-uri 'none'; form-action 'none'\"><title>Mellomværendekontroll for Conta</title><style>\n  :root{font-family:system-ui,sans-serif;color:#172b36;background:#f2f5f4}body{margin:0}main{max-width:1120px;margin:32px auto;padding:32px;background:white;border-radius:12px}h1{font-size:30px;line-height:1.2}h2{margin-top:32px;font-size:21px}.eyebrow{font-size:13px;letter-spacing:.07em;text-transform:uppercase;color:#526b71}.status{padding:20px;background:#edf5f1;border-left:5px solid #1c6955}.amount{font-size:36px;font-weight:700;margin:10px 0}.note{color:#465961;line-height:1.6}.scroll{overflow-x:auto}table{border-collapse:collapse;width:100%;font-size:13px}th,td{padding:10px;text-align:left;border-bottom:1px solid #dce4e2;vertical-align:top;overflow-wrap:anywhere}th{background:#edf2f1}ul{line-height:1.7}footer{margin-top:32px;border-top:1px solid #dce4e2;padding-top:16px;font-size:12px;color:#465961}@media(max-width:600px){main{margin:0;padding:20px;border-radius:0}h1{font-size:25px}.amount{font-size:30px}}@media print{main{margin:0;padding:0;max-width:none}.scroll{overflow:visible}thead{display:table-header-group}tr{break-inside:avoid}}\n  </style></head><body><main><p class=\"eyebrow\">${r.synthetic ? \"Demonstrasjon · kun syntetiske data\" : \"Privat · grunnlag for regnskapsførers vurdering\"}</p><h1>Mellomværendekontroll for Conta</h1>\n  <p>${escapeHtml(s?.config.companies.A.name ?? \"Ukjent selskap\")} ↔ ${escapeHtml(s?.config.companies.B.name ?? \"Ukjent selskap\")}</p><p>${escapeHtml(s?.startDate)}–${escapeHtml(s?.endDate)} · NOK · toleranse ${escapeHtml(s ? nok(s.config.tolerance) : \"ukjent\")}</p>\n  <section class=\"status\"><strong>${escapeHtml(status)}</strong><p class=\"amount\">${r.residual ? escapeHtml(nok(r.residual.closing)) + \" NOK\" : \"Ikke beregnet\"}</p><p>Signert utgående restsaldo: A + B${r.residual ? \". Absolutt differanse: \" + escapeHtml(nok(r.residual.magnitude)) + \" NOK.\" : \".\"}</p><p>Datastatus: ${r.completeness} · Saldostatus: ${r.agreement ?? \"IKKE VURDERT\"}</p></section>\n  <p class=\"note\">Saldoenighet er ikke en ferdig avstemming eller bevis på at alle transaksjoner er bokført. Uavklarte linjer er ikke bekreftede bokføringsfeil. Regnskapsføreren godkjenner kontovalg og konklusjoner.</p>\n${r.issues.length ? \"<h2>Problemer som må løses</h2><ul>\" + r.issues.map((i) => \"<li>\" + escapeHtml(i) + \"</li>\").join(\"\") + \"</ul>\" : \"\"}\n  <h2>Omfang og kontovalg</h2>${table([\"Side\", \"Selskap\", \"Conta-ID\", \"Dedikerte kontoer\"], s ? [\"A\", \"B\"].map((side) => [side, s.config.companies[side].name, s.config.companies[side].organizationId, s.config.companies[side].accounts.map((a) => a.number).join(\", \")]) : [])}\n  <h2>Inngående saldo, bevegelse og utgående saldo</h2>${table([\"Side/konto\", \"Inngående NOK\", \"Bevegelse NOK\", \"Utgående NOK\"], [...r.accounts.map((a) => [a.side + \" / \" + a.account, nok(a.opening), nok(a.movement), nok(a.closing)]), ...r.residual ? [[\"Restsaldo A + B\", nok(r.residual.opening), nok(r.residual.movement), nok(r.residual.closing)]] : []])}\n  <p class=\"note\">Inngående restsaldo + periodens bevegelsesdifferanse = utgående restsaldo. En inngående differanse oppsto før denne måneden.</p>\n  <h2>Kandidatforslag (${complete ? r.candidates.length : \"ikke vurdert\"})</h2>${table([\"Linje A\", \"Linje B\", \"Felles referanse\", \"Begrunnelse\"], r.candidates.map((c) => [c.a, c.b, c.reference, c.reason]))}<p class=\"note\">${complete ? `Ingen forslag er automatisk bekreftet. ${r.unresolved.length} linjer gjenstår uten kandidat.` : \"Kandidater og antall uavklarte linjer kan ikke vurderes før kontrollen er fullført.\"}</p>\n  <h2>${complete ? `Linjegrunnlag (${r.lines.length})` : \"Tilgjengelig linjegrunnlag – ufullstendig\"}</h2>${complete ? \"\" : '<p class=\"note\">Tabellen viser bare linjer som er validert før kontrollen stoppet. En tom tabell betyr ikke at perioden er uten transaksjoner.</p>'}${table(heads, lineRows)}\n  <h2>Kilder og innhenting</h2>${table([\"Kilde\", \"Hentet (UTC)\", \"HTTP\"], r.sources.map((source) => [source.key, source.retrievedAt, source.status]))}\n  <p class=\"note\">Saldoene hentes på nytt etter detaljene. Dette oppdager enkelte endringer underveis, men gir ikke et atomisk historisk øyeblikksbilde. Endringer som utligner hverandre kan forbli uoppdaget.</p><footer>Generert ${escapeHtml(r.generatedAt)} · versjon ${r.version} · Oppbevar rapport og kildegrunnlag privat etter virksomhetens rutiner.</footer></main></body></html>`;\n  return { html, csv: \"\\uFEFF\" + csvRows.map((row) => row.map(csvCell).join(\";\")).join(\"\\r\\n\") + \"\\r\\n\", json: JSON.stringify(report, null, 2) };\n}\n\n// <stdin>\nvar Checker = { initialize, dispatch, accept, compare, render, scopeFor };\n\nconst report = $input.first().json.report;\nconst files = Checker.render(report);\nconst binary = {};\nfor (const [ext, content] of Object.entries(files)) binary[ext] = { data: Buffer.from(content, 'utf8').toString('base64'), mimeType: {html:'text/html',csv:'text/csv',json:'application/json'}[ext], fileName: 'mellomvaerendekontroll.' + ext };\nreturn [{ json: report, binary, pairedItem: { item: 0 } }];"
      },
      "position": [
        260,
        340
      ]
    },
    {
      "id": "4d740af9c9c049709697fcc27549d4ac",
      "name": "Read me",
      "type": "n8n-nodes-base.stickyNote",
      "typeVersion": 1,
      "parameters": {
        "content": "## Mellomværendekontroll for Conta\nGET-only. Two authorized companies; dedicated NOK accounts.\nRead README and docs/SETUP.md. Configure credentials in HTTP nodes only.\nINCOMPLETE is never zero. COMPLETE is not accountant approval.\nMonthly trigger is disabled. Live report semantics require documented validation.\nReports stay in this private execution. No email or publishing nodes.",
        "height": 270,
        "width": 430
      },
      "position": [
        -880,
        -430
      ]
    }
  ],
  "connections": {
    "Manual run": {
      "main": [
        [
          {
            "node": "Configuration",
            "type": "main",
            "index": 0
          }
        ]
      ]
    },
    "Monthly run (disabled)": {
      "main": [
        [
          {
            "node": "Configuration",
            "type": "main",
            "index": 0
          }
        ]
      ]
    },
    "Configuration": {
      "main": [
        [
          {
            "node": "Validate scope",
            "type": "main",
            "index": 0
          }
        ]
      ]
    },
    "Validate scope": {
      "main": [
        [
          {
            "node": "Dispatch request",
            "type": "main",
            "index": 0
          }
        ]
      ]
    },
    "Dispatch request": {
      "main": [
        [
          {
            "node": "Route",
            "type": "main",
            "index": 0
          }
        ]
      ]
    },
    "Route": {
      "main": [
        [
          {
            "node": "Conta GET A",
            "type": "main",
            "index": 0
          }
        ],
        [
          {
            "node": "Conta GET B",
            "type": "main",
            "index": 0
          }
        ],
        [
          {
            "node": "Honor Retry-After",
            "type": "main",
            "index": 0
          }
        ],
        [
          {
            "node": "Private reports",
            "type": "main",
            "index": 0
          }
        ]
      ]
    },
    "Conta GET A": {
      "main": [
        [
          {
            "node": "Accept response A",
            "type": "main",
            "index": 0
          }
        ]
      ]
    },
    "Accept response A": {
      "main": [
        [
          {
            "node": "Dispatch request",
            "type": "main",
            "index": 0
          }
        ]
      ]
    },
    "Conta GET B": {
      "main": [
        [
          {
            "node": "Accept response B",
            "type": "main",
            "index": 0
          }
        ]
      ]
    },
    "Accept response B": {
      "main": [
        [
          {
            "node": "Dispatch request",
            "type": "main",
            "index": 0
          }
        ]
      ]
    },
    "Honor Retry-After": {
      "main": [
        [
          {
            "node": "Dispatch request",
            "type": "main",
            "index": 0
          }
        ]
      ]
    }
  },
  "settings": {
    "executionOrder": "v1",
    "timezone": "Europe/Oslo",
    "saveDataErrorExecution": "none",
    "saveDataSuccessExecution": "all",
    "saveManualExecutions": false,
    "saveExecutionProgress": false,
    "executionTimeout": 1800
  },
  "pinData": {},
  "tags": []
}
