{
  "name": "Conta intercompany — synthetic demo",
  "active": false,
  "nodes": [
    {
      "id": "e5b177e58a2b674fbb5f171524b57657",
      "name": "Manual demo",
      "type": "n8n-nodes-base.manualTrigger",
      "typeVersion": 1,
      "parameters": {},
      "position": [
        0,
        0
      ]
    },
    {
      "id": "23fc880d4944d4a449c4c3e21d85d529",
      "name": "Synthetic comparison",
      "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 fixture = {\"config\":{\"environment\":\"sandbox\",\"period\":\"2026-08\",\"tolerance\":\"0.01\",\"companies\":{\"A\":{\"organizationId\":\"900001\",\"name\":\"Eksempel Fjord AS (syntetisk)\",\"authorized\":true,\"currency\":\"NOK\",\"accounts\":[{\"number\":\"1560\",\"dedicatedToOtherCompany\":true,\"positionsInNok\":true}]},\"B\":{\"organizationId\":\"900002\",\"name\":\"Eksempel Ås AS (syntetisk)\",\"authorized\":true,\"currency\":\"NOK\",\"accounts\":[{\"number\":\"2960\",\"dedicatedToOtherCompany\":true,\"positionsInNok\":true}]}},\"sharedInvoiceNumbersConfirmed\":true,\"candidateDateWindowDays\":3,\"liveValidation\":{\"reference\":\"\",\"signsDatesOpeningAndOmissionsVerified\":false}},\"receipts\":[{\"request\":{\"side\":\"A\",\"kind\":\"accounts\",\"key\":\"A:accounts\",\"url\":\"https://api.gateway.conta-sandbox.no/accounting/organizations/900001/bookkeeping-accounts?hits=0&page=0\"},\"body\":[{\"id\":\"10\",\"bookkeepingAccountNo\":\"1560\",\"name\":\"Mellomværende – dedikert konto\",\"isActive\":true}],\"retrievedAt\":\"2026-09-16T11:59:40.000Z\",\"status\":200},{\"request\":{\"side\":\"A\",\"kind\":\"before\",\"key\":\"A:before\",\"url\":\"https://api.gateway.conta-sandbox.no/accounting/organizations/900001/reports/trial-balance?startDate=2026-08-01&endDate=2026-08-31\"},\"body\":{\"trialBalanceOutputSub\":[{\"bookkeepingAccountNo\":\"1560\",\"bookkeepingAccountName\":\"Mellomværende\",\"sumIngoing\":\"100000.00\",\"sumChanged\":\"150000.00\",\"sumOutgoing\":\"250000.00\"}],\"sumIngoing\":\"0.00\",\"sumChanged\":\"0.00\",\"sumOutgoing\":\"0.00\"},\"retrievedAt\":\"2026-09-16T11:59:41.000Z\",\"status\":200},{\"request\":{\"side\":\"B\",\"kind\":\"accounts\",\"key\":\"B:accounts\",\"url\":\"https://api.gateway.conta-sandbox.no/accounting/organizations/900002/bookkeeping-accounts?hits=0&page=0\"},\"body\":[{\"id\":\"20\",\"bookkeepingAccountNo\":\"2960\",\"name\":\"Mellomværende – dedikert konto\",\"isActive\":true}],\"retrievedAt\":\"2026-09-16T11:59:42.000Z\",\"status\":200},{\"request\":{\"side\":\"B\",\"kind\":\"before\",\"key\":\"B:before\",\"url\":\"https://api.gateway.conta-sandbox.no/accounting/organizations/900002/reports/trial-balance?startDate=2026-08-01&endDate=2026-08-31\"},\"body\":{\"trialBalanceOutputSub\":[{\"bookkeepingAccountNo\":\"2960\",\"bookkeepingAccountName\":\"Mellomværende\",\"sumIngoing\":\"-85000.00\",\"sumChanged\":\"-140000.00\",\"sumOutgoing\":\"-225000.00\"}],\"sumIngoing\":\"0.00\",\"sumChanged\":\"0.00\",\"sumOutgoing\":\"0.00\"},\"retrievedAt\":\"2026-09-16T11:59:43.000Z\",\"status\":200},{\"request\":{\"side\":\"A\",\"kind\":\"details\",\"account\":\"1560\",\"key\":\"A:details:1560\",\"url\":\"https://api.gateway.conta-sandbox.no/accounting/organizations/900001/reports/trial-balance/details?startDate=2026-08-01&endDate=2026-08-31&bookkeepingAccountNo=1560\"},\"body\":[{\"id\":\"1000\",\"transactionId\":\"10000\",\"transactionNoYear\":\"1-2026\",\"date\":\"2026-08-01\",\"bookkeepingAccountNo\":\"1560\",\"amount\":\"15000.00\",\"invoiceNo\":\"DEMO-1\",\"description\":\"Syntetisk mellomværende 1\",\"isResetTransaction\":false,\"isCorrection\":false},{\"id\":\"1001\",\"transactionId\":\"10001\",\"transactionNoYear\":\"2-2026\",\"date\":\"2026-08-02\",\"bookkeepingAccountNo\":\"1560\",\"amount\":\"15000.00\",\"invoiceNo\":\"DEMO-2\",\"description\":\"Syntetisk mellomværende 2\",\"isResetTransaction\":false,\"isCorrection\":false},{\"id\":\"1002\",\"transactionId\":\"10002\",\"transactionNoYear\":\"3-2026\",\"date\":\"2026-08-03\",\"bookkeepingAccountNo\":\"1560\",\"amount\":\"15000.00\",\"invoiceNo\":\"DEMO-3\",\"description\":\"Syntetisk mellomværende 3\",\"isResetTransaction\":false,\"isCorrection\":false},{\"id\":\"1003\",\"transactionId\":\"10003\",\"transactionNoYear\":\"4-2026\",\"date\":\"2026-08-04\",\"bookkeepingAccountNo\":\"1560\",\"amount\":\"15000.00\",\"invoiceNo\":\"DEMO-4\",\"description\":\"Syntetisk mellomværende 4\",\"isResetTransaction\":false,\"isCorrection\":false},{\"id\":\"1004\",\"transactionId\":\"10004\",\"transactionNoYear\":\"5-2026\",\"date\":\"2026-08-05\",\"bookkeepingAccountNo\":\"1560\",\"amount\":\"15000.00\",\"invoiceNo\":\"DEMO-5\",\"description\":\"Syntetisk mellomværende 5\",\"isResetTransaction\":false,\"isCorrection\":false},{\"id\":\"1005\",\"transactionId\":\"10005\",\"transactionNoYear\":\"6-2026\",\"date\":\"2026-08-06\",\"bookkeepingAccountNo\":\"1560\",\"amount\":\"15000.00\",\"invoiceNo\":\"DEMO-6\",\"description\":\"Syntetisk mellomværende 6\",\"isResetTransaction\":false,\"isCorrection\":false},{\"id\":\"1006\",\"transactionId\":\"10006\",\"transactionNoYear\":\"7-2026\",\"date\":\"2026-08-07\",\"bookkeepingAccountNo\":\"1560\",\"amount\":\"15000.00\",\"invoiceNo\":\"DEMO-7\",\"description\":\"Syntetisk mellomværende 7\",\"isResetTransaction\":false,\"isCorrection\":false},{\"id\":\"1007\",\"transactionId\":\"10007\",\"transactionNoYear\":\"8-2026\",\"date\":\"2026-08-08\",\"bookkeepingAccountNo\":\"1560\",\"amount\":\"15000.00\",\"invoiceNo\":\"DEMO-8\",\"description\":\"Syntetisk mellomværende 8\",\"isResetTransaction\":false,\"isCorrection\":false},{\"id\":\"1008\",\"transactionId\":\"10008\",\"transactionNoYear\":\"9-2026\",\"date\":\"2026-08-09\",\"bookkeepingAccountNo\":\"1560\",\"amount\":\"15000.00\",\"invoiceNo\":\"DEMO-9\",\"description\":\"Syntetisk mellomværende 9\",\"isResetTransaction\":false,\"isCorrection\":false},{\"id\":\"1009\",\"transactionId\":\"10009\",\"transactionNoYear\":\"10-2026\",\"date\":\"2026-08-31\",\"bookkeepingAccountNo\":\"1560\",\"amount\":\"15000.00\",\"invoiceNo\":\"A-ONLY\",\"description\":\"Syntetisk mellomværende 10\",\"isResetTransaction\":false,\"isCorrection\":false}],\"retrievedAt\":\"2026-09-16T11:59:44.000Z\",\"status\":200},{\"request\":{\"side\":\"B\",\"kind\":\"details\",\"account\":\"2960\",\"key\":\"B:details:2960\",\"url\":\"https://api.gateway.conta-sandbox.no/accounting/organizations/900002/reports/trial-balance/details?startDate=2026-08-01&endDate=2026-08-31&bookkeepingAccountNo=2960\"},\"body\":[{\"id\":\"2000\",\"transactionId\":\"20000\",\"transactionNoYear\":\"1-2026\",\"date\":\"2026-08-01\",\"bookkeepingAccountNo\":\"2960\",\"amount\":\"-15000.00\",\"invoiceNo\":\"DEMO-1\",\"description\":\"Syntetisk mellomværende 1\",\"isResetTransaction\":false,\"isCorrection\":false},{\"id\":\"2001\",\"transactionId\":\"20001\",\"transactionNoYear\":\"2-2026\",\"date\":\"2026-08-02\",\"bookkeepingAccountNo\":\"2960\",\"amount\":\"-15000.00\",\"invoiceNo\":\"DEMO-2\",\"description\":\"Syntetisk mellomværende 2\",\"isResetTransaction\":false,\"isCorrection\":false},{\"id\":\"2002\",\"transactionId\":\"20002\",\"transactionNoYear\":\"3-2026\",\"date\":\"2026-08-03\",\"bookkeepingAccountNo\":\"2960\",\"amount\":\"-15000.00\",\"invoiceNo\":\"DEMO-3\",\"description\":\"Syntetisk mellomværende 3\",\"isResetTransaction\":false,\"isCorrection\":false},{\"id\":\"2003\",\"transactionId\":\"20003\",\"transactionNoYear\":\"4-2026\",\"date\":\"2026-08-04\",\"bookkeepingAccountNo\":\"2960\",\"amount\":\"-15000.00\",\"invoiceNo\":\"DEMO-4\",\"description\":\"Syntetisk mellomværende 4\",\"isResetTransaction\":false,\"isCorrection\":false},{\"id\":\"2004\",\"transactionId\":\"20004\",\"transactionNoYear\":\"5-2026\",\"date\":\"2026-08-05\",\"bookkeepingAccountNo\":\"2960\",\"amount\":\"-15000.00\",\"invoiceNo\":\"DEMO-5\",\"description\":\"Syntetisk mellomværende 5\",\"isResetTransaction\":false,\"isCorrection\":false},{\"id\":\"2005\",\"transactionId\":\"20005\",\"transactionNoYear\":\"6-2026\",\"date\":\"2026-08-06\",\"bookkeepingAccountNo\":\"2960\",\"amount\":\"-15000.00\",\"invoiceNo\":\"DEMO-6\",\"description\":\"Syntetisk mellomværende 6\",\"isResetTransaction\":false,\"isCorrection\":false},{\"id\":\"2006\",\"transactionId\":\"20006\",\"transactionNoYear\":\"7-2026\",\"date\":\"2026-08-07\",\"bookkeepingAccountNo\":\"2960\",\"amount\":\"-15000.00\",\"invoiceNo\":\"DEMO-7\",\"description\":\"Syntetisk mellomværende 7\",\"isResetTransaction\":false,\"isCorrection\":false},{\"id\":\"2007\",\"transactionId\":\"20007\",\"transactionNoYear\":\"8-2026\",\"date\":\"2026-08-08\",\"bookkeepingAccountNo\":\"2960\",\"amount\":\"-15000.00\",\"invoiceNo\":\"DEMO-8\",\"description\":\"Syntetisk mellomværende 8\",\"isResetTransaction\":false,\"isCorrection\":false},{\"id\":\"2008\",\"transactionId\":\"20008\",\"transactionNoYear\":\"9-2026\",\"date\":\"2026-08-09\",\"bookkeepingAccountNo\":\"2960\",\"amount\":\"-15000.00\",\"invoiceNo\":\"DEMO-9\",\"description\":\"Syntetisk mellomværende 9\",\"isResetTransaction\":false,\"isCorrection\":false},{\"id\":\"2009\",\"transactionId\":\"20009\",\"transactionNoYear\":\"10-2026\",\"date\":\"2026-08-31\",\"bookkeepingAccountNo\":\"2960\",\"amount\":\"-5000.00\",\"invoiceNo\":\"B-ONLY\",\"description\":\"Syntetisk mellomværende 10\",\"isResetTransaction\":false,\"isCorrection\":false}],\"retrievedAt\":\"2026-09-16T11:59:45.000Z\",\"status\":200},{\"request\":{\"side\":\"A\",\"kind\":\"after\",\"key\":\"A:after\",\"url\":\"https://api.gateway.conta-sandbox.no/accounting/organizations/900001/reports/trial-balance?startDate=2026-08-01&endDate=2026-08-31\"},\"body\":{\"trialBalanceOutputSub\":[{\"bookkeepingAccountNo\":\"1560\",\"bookkeepingAccountName\":\"Mellomværende\",\"sumIngoing\":\"100000.00\",\"sumChanged\":\"150000.00\",\"sumOutgoing\":\"250000.00\"}],\"sumIngoing\":\"0.00\",\"sumChanged\":\"0.00\",\"sumOutgoing\":\"0.00\"},\"retrievedAt\":\"2026-09-16T11:59:46.000Z\",\"status\":200},{\"request\":{\"side\":\"B\",\"kind\":\"after\",\"key\":\"B:after\",\"url\":\"https://api.gateway.conta-sandbox.no/accounting/organizations/900002/reports/trial-balance?startDate=2026-08-01&endDate=2026-08-31\"},\"body\":{\"trialBalanceOutputSub\":[{\"bookkeepingAccountNo\":\"2960\",\"bookkeepingAccountName\":\"Mellomværende\",\"sumIngoing\":\"-85000.00\",\"sumChanged\":\"-140000.00\",\"sumOutgoing\":\"-225000.00\"}],\"sumIngoing\":\"0.00\",\"sumChanged\":\"0.00\",\"sumOutgoing\":\"0.00\"},\"retrievedAt\":\"2026-09-16T11:59:47.000Z\",\"status\":200}]};\nconst now = new Date('2026-09-16T12:00:00Z');\nreturn [{ json: ({report:Checker.compare(Checker.scopeFor(fixture.config, now), fixture.receipts, now, true)}), pairedItem: { item: 0 } }];"
      },
      "position": [
        240,
        0
      ]
    },
    {
      "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": [
        480,
        0
      ]
    }
  ],
  "connections": {
    "Manual demo": {
      "main": [
        [
          {
            "node": "Synthetic comparison",
            "type": "main",
            "index": 0
          }
        ]
      ]
    },
    "Synthetic comparison": {
      "main": [
        [
          {
            "node": "Private reports",
            "type": "main",
            "index": 0
          }
        ]
      ]
    }
  },
  "settings": {
    "executionOrder": "v1",
    "timezone": "Europe/Oslo",
    "saveDataErrorExecution": "none",
    "saveDataSuccessExecution": "all",
    "saveManualExecutions": false,
    "saveExecutionProgress": false,
    "executionTimeout": 1800
  },
  "pinData": {},
  "tags": []
}
