feat(invoices): disclose invoice-register coverage gaps + net-amount search (#2122)

* feat(invoices): disclose invoice-register coverage gaps + amount search

After a SIE migration or verifikat backfill, customer invoices exist only
as journal entries: the invoice list, kundreskontran, /api/invoices, v1
invoices.list, and MCP list_invoices all looked complete while silently
omitting everything before the register's first invoice (user report:
two invoiced fees nearly re-invoiced as "uninvoiced").

- lib/invoices/invoice-register-coverage.ts: coverage boundary = earliest
  register invoice; flags posted non-invoice-engine AR verifikat
  (1510/1513) before it. AR-keyed, not source_type='import'-keyed, so
  manual/API backfills are caught too.
- Invoice list page: one attn line disclosing the boundary (sv+en).
- Kundreskontra: register_coverage in the report payload, rendered in the
  summary card and as an explanation under "Ej avstamd".
- /api/invoices GET: invoice_register_coverage in the response.
- v1 invoices.list: meta.coverage + registry pitfall documenting it.
- MCP gnubok_list_invoices: invoice_register_coverage + coverage_note on
  the first page, pointing agents at gnubok_query_journal.
- Search: lib/invoices/invoice-search.ts matches net (subtotal) and gross
  amounts with sv-SE formatting, alongside number/customer matching; a
  known net amount like 14 000 now finds the 17 500 kr row.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01VcW5BU6mU1vNbWpkMKHbHF

* fix(invoices): harden register-coverage probe, period-gate reconciliation note, regen api skill

Skeptic + CI findings folded into one pass:

- Coverage probe: a failed AR lookup now degrades to UNKNOWN
  (NO_INVOICE_REGISTER_COVERAGE), never to a confident "complete".
- Probe driven from journal_entries (company-indexed) with the AR line
  condition as an inner embed, instead of the lines-table-with-embed-filters
  shape that lateral-scans every tenant (lib/bookkeeping/entry-lines.ts).
- DEBIT-only 1510/1513 lines; excludes every invoice-engine source type
  (invoice_created, invoice_paid, invoice_cash_payment, credit_note,
  reminder_fee, rot_rut_payout, storno, correction): an advance payment
  crediting 1510 or a re-dated rattelse of an engine entry no longer flags.
- covers_from ignores drafts so a backdated draft cannot move the boundary.
- Kundreskontra "Ej avstamd" explanation is now gated on pre-register AR
  debits existing IN the reconciled period (new
  ARReconciliationResult.pre_register_ar_in_period): prior-period migration
  history cannot explain this period's difference and must not excuse a
  real felbokning. Wording no longer says "snarare an felbokning".
- MCP coverage_note states the earliest register invoice date rather than
  claiming the register "covers" from it.
- Amount search compares magnitudes so credit notes (negative totals) are
  findable; "-17500" parses; null amounts never match "0".
- skills/accounted-api regenerated from the registry (apiskill:check).

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01VcW5BU6mU1vNbWpkMKHbHF

* chore(api-skill): regenerate accounted-api skill after merging origin/main

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01VcW5BU6mU1vNbWpkMKHbHF

* fix(invoices): round-2 review fixes for register-coverage disclosure

- covers_from now anchors on real invoices only (document_type='invoice',
  non-draft): proformas/delivery notes cannot move the boundary.
- INVOICE_ENGINE_SOURCE_TYPES exported + a test scans the engine writers
  (invoice-entries, reminder-fee, rot-rut, storno-service) so a future
  source_type cannot silently become false pre-register evidence.
- Kundreskontra guidance names both 1510 and 1513.
- MCP gnubok_list_invoices outputSchema declares invoice_register_coverage
  and coverage_note.
- v1 reports.ar-ledger documents data.register_coverage; invoices.list
  example made internally consistent; api skill regenerated.

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01VcW5BU6mU1vNbWpkMKHbHF

* fix(mcp): keep gnubok_list_invoices outputSchema minimal to hold the tools/list token budget

The expanded schema from the round-2 review pushed tools/list to 61 726
tokens against the held 61 600 ceiling (payload-size.bench.test.ts). The
ceiling is policy, not a baseline to bump: the description already tells
agents to read invoice_register_coverage/coverage_note, and paginatedSchema
has no additionalProperties:false, so the fields stay schema-valid.

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01VcW5BU6mU1vNbWpkMKHbHF

---------

Co-authored-by: Claude Fable 5 <noreply@anthropic.com>
Co-authored-by: Jakob Wennberg <311770904+jakobwennberg-oss@users.noreply.github.com>
This commit is contained in:
Mattsson
2026-09-04 09:39:14 +02:00
committed by GitHub
parent 67e878fb23
commit 8265b5d166
30 changed files with 967 additions and 150 deletions
+1
View File
@@ -1454,6 +1454,7 @@ One line per decision: `[YYYY-MM-DD] <decision>: <why>`. Appended by agents and
[2026-09-01] Skeptic BLOCK on woo failed-order removal fixed by: freeze guards repeated on the DELETE statement (TOCTOU), is_paid=false + legacy_transaction_id null guards, orderRemoves gated on !orderIsPaid. No BEFORE DELETE trigger/RPC: the is_paid guard makes the cascade race unreachable (refund children only exist under paid parents).
[2026-09-01] EB claim guard, skeptic round (PR #2116): active-company standing state (enabled cash_accounts + enabled accounts on its live-ish rows) outranks sibling claims COMPANY-wide, not row-wide: a bank-list renewal arrives on a fresh row and must not switch a working feed off. pending_selection rows neither claim nor remember deselections (unconfirmed callback output; also stops fail-closed writes from poisoning later connects). Guard-disabled accounts are never mirrored from the callback (mirroring enabled:false can promote the seeded primary 1930 manual row and disable it under a foreign identity) and the selection save skips allocation+mirror for disabled never-mirrored accounts, so the no-slot-burned invariant holds end to end. Deselection carry got a picker note; enabling an account clears the guard flags. Legacy both-companies-enabled overlaps stay untouched (Swedish review advisory: prod sweep is a follow-up, not this PR).
[2026-09-01] EB claim guard round 2 (skeptic re-verify): pending_selection rows are asymmetric, not excluded: their ENABLED accounts still claim (attach-created rows hold offered accounts with no cash rows until saved; excluding them reopened the attach-window double-booking), while their disabled flags stay out of deselection memory (unconfirmed callback output). Both fetchAllRows claim queries order('id'): unordered .range() pagination can silently skip rows at page boundaries, and a skipped row is a missed claim (fail-open).
[2026-09-01] Invoice-register coverage detection keys on pre-boundary AR lines (1510/1513), not source_type='import': the reporter's 2026 invoice history was backfilled as source_type='manual' verifikat, which an import-keyed marker (fetchMigrationCoverageEnd) would miss; widening to class-3 revenue would false-flag cash/webshop sales.
[2026-09-01] F2 bank-data staleness: ship freshness reads only (last_synced_at/consent_expires/error_message on gnubok_connect_bank + new GET /api/v1/.../bank-connections, scope companies:read mirroring the MCP mapping): the daily cron already syncs server-side, so visibility is what the incident lacked; an agent-triggerable sync is a product bet (EB call cost, runaway agents) and was deferred by Emil.
[2026-09-01] Verifikationsserie in the Ny verifikation modal is a closed dropdown instead of a one-letter free-text field: a typo there silently opens a brand-new series with its own number sequence, and the letters only mean anything if everyone uses the same ones. The letters are NOT prescribed by law (BFL 5 kap. 7 § requires only unbroken systematic numbering within each series), and the incumbents disagree: Björn Lundén uses A Huvudserie, F Kundfakturor, I Inbetalningar, L Leverantörsfakturor, N Löner, U Utbetalningar, J Bokslut. We ship FORTNOX's table verbatim (A Redovisning, B Kundfakturor, C Inbetalningar från kunder, D Leverantörsfakturor, E Utbetalningar till leverantörer, F Kassa, G Avskrivning, H Periodisering, I Bokslut, J Revisor, K Lön, L Kontantfaktura, M Momsrapport), from their own Systemdokumentation, because Fortnox is the system most companies migrate here from and an imported ledger should keep its meaning. REJECTED an earlier draft that labelled A as Kundfakturor: A is the general series manual entries land in (the one point Fortnox and BL agree on, and Fortnox allows manuell kontering ONLY in A), and migration 20260526120700 ships every source_type defaulting to 'A', so every existing company's A series already holds everything. Calling it Kundfakturor would mislabel their entire history and the modal's own default. The list is closed but any letter the company already configured, or that a draft was saved with, is appended so no existing value can fall out of the picker. Also: tabbing or clicking into an untouched amount field now proposes the outstanding difference (pre-selected, so typing replaces it) when the row already has an account and the difference belongs on that side. This deliberately reverses part of the note in updateLine that said a balancing amount must never auto-fill: that note was about filling on ACCOUNT selection, which stole the amount before the user had a chance to split it. Filling on focus keeps the split case intact because the proposal is selected text, and it fixes the common moms case where the last line is just the remainder.
[2026-09-01] Settings PUT cross-field VAT validations scoped to touched field groups (vat-completeness, 40m-monthly, periodisk sammanstallning), not fixed at onboarding: partial saves from surfaces without VAT fields (invoice bank-details dialog) were hard-blocked by pre-existing vat_registered-without-number state (Marketio Lab case). The invariant still holds on every save that touches its group; explicit null now counts as a clear instead of falling back to the stored value during validation. Onboarding-side VAT number collection left as follow-up.
+40 -8
View File
@@ -31,6 +31,12 @@ import { cn } from '@/lib/utils'
import { invoiceDisplayNumber } from '@/lib/invoices/display'
import { getDisplayTotal } from '@/lib/invoices/rounding'
import { effectiveQuoteStatus } from '@/lib/invoices/quote-status'
import { matchesInvoiceSearch } from '@/lib/invoices/invoice-search'
import {
fetchInvoiceRegisterCoverage,
NO_INVOICE_REGISTER_COVERAGE,
type InvoiceRegisterCoverage,
} from '@/lib/invoices/invoice-register-coverage'
import {
sortInvoiceList,
type InvoiceListSort,
@@ -315,6 +321,30 @@ export default function InvoicesPage() {
const showRotRutAction =
rotRutEnabled || invoices.some((invoice) => (invoice.deduction_total ?? 0) > 0)
// Invoice-register coverage (see lib/invoices/invoice-register-coverage.ts):
// a migrated or backfilled company has invoices that live only as verifikat,
// so this list looks complete for periods it doesn't cover. One quiet attn
// line discloses the boundary; without it the user's next step is "those
// invoices were never sent" (the 2026-09-01 report: nearly double-invoiced).
const [registerCoverage, setRegisterCoverage] = useState<InvoiceRegisterCoverage>(
NO_INVOICE_REGISTER_COVERAGE,
)
useEffect(() => {
if (!company) {
setRegisterCoverage(NO_INVOICE_REGISTER_COVERAGE)
return
}
let cancelled = false
;(async () => {
const coverage = await fetchInvoiceRegisterCoverage(supabase, company.id)
if (!cancelled) setRegisterCoverage(coverage)
})()
return () => {
cancelled = true
}
// eslint-disable-next-line react-hooks/exhaustive-deps
}, [company?.id])
async function fetchInvoices() {
if (!company) return
// Skeleton takeover only while nothing is on screen: refetches after an
@@ -360,14 +390,7 @@ export default function InvoicesPage() {
const scopedInvoices = useMemo(
() =>
invoices.filter((invoice) => {
const matchesSearch =
(invoice.invoice_number ?? '').toLocaleLowerCase('sv-SE').includes(normalizedSearch) ||
(invoice.external_invoice_number ?? '')
.toLocaleLowerCase('sv-SE')
.includes(normalizedSearch) ||
(invoice.customer as { name: string })?.name
?.toLocaleLowerCase('sv-SE')
.includes(normalizedSearch)
const matchesSearch = matchesInvoiceSearch(invoice, normalizedSearch)
const matchesFy =
!fyPeriod ||
@@ -692,6 +715,15 @@ export default function InvoicesPage() {
</div>
</div>
{/* Coverage boundary (convention 6: one page-domain attn line). Shown
only when posted AR verifikat predate the register's first invoice:
the list is then silently incomplete for that period. */}
{registerCoverage.has_pre_register_invoices && registerCoverage.covers_from && (
<p className="attn text-[12.5px]">
{t('coverage_notice', { date: formatDate(registerCoverage.covers_from) })}
</p>
)}
{/* Bulkbar: appears once anything is selected (supplier-invoices shape). */}
{selectedIds.size > 0 && (
<div className="flex flex-wrap items-center gap-x-5 gap-y-2 border-b border-border px-1 py-2 text-[12.5px] animate-fade-in">
+38
View File
@@ -118,6 +118,44 @@ describe('GET /api/invoices', () => {
expect(body.data[0].customer).toBeNull()
})
it('reports invoice-register coverage alongside the list', async () => {
// List, then the coverage helper's two lookups: earliest register
// invoice + a posted AR verifikat predating it (migrated/backfilled
// invoice history living only as journal entries).
enqueue({ data: [makeInvoice()], error: null, count: 1 })
enqueue({ data: { invoice_date: '2026-07-19' }, error: null })
enqueue({ data: { id: 'line-1' }, error: null })
const request = createMockRequest('/api/invoices')
const response = await GET(request, { params: Promise.resolve({}) })
const { status, body } = await parseJsonResponse<{
invoice_register_coverage: { covers_from: string | null; has_pre_register_invoices: boolean }
}>(response)
expect(status).toBe(200)
expect(body.invoice_register_coverage).toEqual({
covers_from: '2026-07-19',
has_pre_register_invoices: true,
})
})
it('degrades to no coverage instead of failing the list', async () => {
enqueue({ data: [makeInvoice()], error: null, count: 1 })
// Coverage lookups resolve to nothing (empty queue → null data).
const request = createMockRequest('/api/invoices')
const response = await GET(request, { params: Promise.resolve({}) })
const { status, body } = await parseJsonResponse<{
invoice_register_coverage: { covers_from: string | null; has_pre_register_invoices: boolean }
}>(response)
expect(status).toBe(200)
expect(body.invoice_register_coverage).toEqual({
covers_from: null,
has_pre_register_invoices: false,
})
})
it('applies status filter', async () => {
enqueue({ data: [], error: null, count: 0 })
+20 -1
View File
@@ -13,6 +13,10 @@ import { errorResponse, errorResponseFromCode } from '@/lib/errors/get-structure
import type { Logger } from '@/lib/logger'
import { getErrorMessage as getUserErrorMessage } from '@/lib/errors/get-error-message'
import { maskEmbeddedCustomer } from '@/lib/customers/protect-personal-number'
import {
fetchInvoiceRegisterCoverage,
NO_INVOICE_REGISTER_COVERAGE,
} from '@/lib/invoices/invoice-register-coverage'
ensureInitialized()
@@ -49,9 +53,24 @@ export const GET = withRouteContext(
return errorResponse(error, log, { requestId })
}
// Coverage disclosure: the register only holds invoices created in
// Accounted, so for a migrated/backfilled company this list is silently
// incomplete before its first invoice. Non-fatal: a failed lookup
// degrades to "no marker", never to a failed list.
let coverage = NO_INVOICE_REGISTER_COVERAGE
try {
coverage = await fetchInvoiceRegisterCoverage(supabase, companyId)
} catch {
// keep NO_INVOICE_REGISTER_COVERAGE
}
// Mask the embedded customer's personnummer: the customers(*) join
// carries the stored ciphertext, which has no business reaching a client.
return NextResponse.json({ data: (data ?? []).map(maskEmbeddedCustomer), count })
return NextResponse.json({
data: (data ?? []).map(maskEmbeddedCustomer),
count,
invoice_register_coverage: coverage,
})
},
)
@@ -39,6 +39,10 @@ import {
type SelfBilledSaleFailure,
} from '@/lib/invoices/self-billed-sale'
import { eventBus } from '@/lib/events'
import {
fetchInvoiceRegisterCoverage,
NO_INVOICE_REGISTER_COVERAGE,
} from '@/lib/invoices/invoice-register-coverage'
import type { Customer, Invoice, InvoiceDocumentType } from '@/types'
// Map a self-billed-sale service failure onto the v1 invoice error envelope.
@@ -160,6 +164,7 @@ registerEndpoint({
'Ordering is by created_at (registration time), not invoice_date. Backdated invoices therefore appear where they were created, not where their date falls: filter on ?date_from / ?date_to when you care about the business date.',
'Cursor pagination: pass ?cursor=<next_cursor> from the previous response. A stale or tampered cursor is ignored and the first page is returned again.',
'Quotes (document_type=quote, offert) carry valid_until and quote_status (open | accepted | declined | expired). "expired" is derived: an open quote past valid_until; filter with ?quote_status=expired. Quotes never book and are never payable: convert an accepted quote to an invoice in the dashboard first.',
'The register only contains invoices created in Accounted. A company migrated or backfilled mid-year has real customer invoices that exist only as journal entries and are NOT in this list. Check meta.coverage: when has_pre_register_invoices is true, treat periods before covers_from as not answered by this endpoint (query journal entries instead).',
],
example: {
response: {
@@ -182,7 +187,12 @@ registerEndpoint({
created_at: '2026-05-01T09:14:33Z',
},
],
meta: { request_id: 'req_…', api_version: '2026-05-12', next_cursor: null },
meta: {
request_id: 'req_…',
api_version: '2026-05-12',
next_cursor: null,
coverage: { covers_from: '2026-05-01', has_pre_register_invoices: true },
},
},
},
scope: 'invoices:read',
@@ -366,9 +376,20 @@ export const GET = withApiV1<{ params: Promise<{ companyId: string }> }>(
? encodeDefaultCursor({ id: last.id, created_at: last.created_at })
: null
// Register-coverage disclosure (meta.coverage): without it, an agent
// reading this list for a migrated company gets a confidently
// incomplete answer to "which invoices exist". Non-fatal on failure.
let coverage = NO_INVOICE_REGISTER_COVERAGE
try {
coverage = await fetchInvoiceRegisterCoverage(ctx.supabase, ctx.companyId!)
} catch {
// keep NO_INVOICE_REGISTER_COVERAGE
}
return paginated(invoices, {
requestId: ctx.requestId,
nextCursor: nextCursor ?? undefined,
coverage: { ...coverage },
})
},
)
@@ -27,6 +27,7 @@ registerEndpoint({
pitfalls: [
'`as_of_date` is optional; format `YYYY-MM-DD`. Defaults to today (UTC).',
'Only invoices in `sent`/`overdue`/`partially_paid` status appear. Drafts and credited invoices are excluded.',
'The ledger is built from the invoice register only. `data.register_coverage` ({ covers_from, has_pre_register_invoices }) discloses when posted AR verifikat predate the register\'s earliest invoice (migrated or backfilled invoice history): those receivables are NOT in this ledger. When has_pre_register_invoices is true, treat periods before covers_from as unanswered here and query journal entries on 1510/1513 instead.',
],
example: {
response: {
+21
View File
@@ -3107,6 +3107,10 @@ interface ARLedgerData {
total_overdue: number
unpaid_count: number
unconverted_fx_count: number
register_coverage?: {
covers_from: string | null
has_pre_register_invoices: boolean
}
}
reconciliation: {
ar_ledger_total: number
@@ -3114,6 +3118,7 @@ interface ARLedgerData {
difference: number
is_reconciled: boolean
unconverted_fx_count: number
pre_register_ar_in_period?: boolean
} | null
}
@@ -3297,6 +3302,11 @@ export function ARLedgerView({ periodId }: { periodId: string }) {
{ledger.unconverted_fx_count} faktura i utländsk valuta utan växelkurs är inte med i totalen.
</p>
)}
{ledger.register_coverage?.has_pre_register_invoices && ledger.register_coverage.covers_from && (
<p className="mt-1 text-xs text-muted-foreground">
Fakturor före {formatDate(ledger.register_coverage.covers_from)} kan ligga som bokförda verifikat och ingår inte i reskontran.
</p>
)}
</CardContent>
</Card>
<Card>
@@ -3421,6 +3431,17 @@ export function ARLedgerView({ periodId }: { periodId: string }) {
{reconciliation.unconverted_fx_count} kundfaktura i utländsk valuta saknar växelkurs: differensen kan bero saknade kursuppgifter snarare än felbokning.
</p>
)}
{/* Only when pre-register AR debits exist IN the reconciled
period: prior-period migration history contributes nothing
to this period's balance, and offering it as an explanation
there would cushion a genuine felbokning. */}
{!reconciliation.is_reconciled &&
reconciliation.pre_register_ar_in_period &&
ledger.register_coverage?.covers_from && (
<p className="text-xs text-muted-foreground">
Perioden innehåller verifikat med kundfordringar före {formatDate(ledger.register_coverage.covers_from)} som inte ligger i fakturaregistret (t.ex. efter en migrering): differensen kan bero det. Kontrollera huvudboken <AccountNumber number="1510" /> och <AccountNumber number="1513" /> innan du letar felbokning.
</p>
)}
</div>
</div>
</CardContent>
+28 -1
View File
@@ -120,6 +120,10 @@ import {
import { resolveDefaultPaymentTerms } from '@/lib/customers/default-payment-terms'
import { fetchEntryLines, fetchLinesByEntryIds, type EntryLinesQuery } from '@/lib/bookkeeping/entry-lines'
import { generateARLedger } from '@/lib/reports/ar-ledger'
import {
fetchInvoiceRegisterCoverage,
NO_INVOICE_REGISTER_COVERAGE,
} from '@/lib/invoices/invoice-register-coverage'
import { generateMonthlyBreakdown } from '@/lib/reports/monthly-breakdown'
import { uiWidgets, findUiWidget, WIDGET_MIME_TYPE } from './widgets'
import { dataResources, findResource, parseResourceQuery } from './resources'
@@ -6550,7 +6554,7 @@ export const tools: McpTool[] = [
name: 'gnubok_list_invoices',
keywords: ['faktura', 'kundfaktura', 'fakturor', 'obetalda', 'förfallna', 'påminnelse', 'offert', 'offerter'],
title: 'List Customer Invoices',
description: 'List invoices and quotes (offerter) for the active company, newest first. Optional status, document_type and quote_status filters.',
description: 'List invoices and quotes (offerter) for the active company, newest first. Optional status, document_type and quote_status filters. Heed invoice_register_coverage/coverage_note: after a migration or backfill, older invoices may exist only as journal entries and NOT appear here.',
inputSchema: {
type: 'object',
additionalProperties: false,
@@ -6638,12 +6642,35 @@ export const tools: McpTool[] = [
: offset + invoices.length < count
const total = count ?? offset + invoices.length + (hasMore ? 1 : 0)
// Register-coverage disclosure: the register only holds invoices
// created in Accounted. Migrated/backfilled invoice history lives as
// journal entries, so without this field an agent reads a silently
// incomplete list as complete. First page only: continuation pages of
// the same listing don't need the disclosure re-queried. Non-fatal:
// lookup failure degrades to "no note", never a failed list.
let coverage = NO_INVOICE_REGISTER_COVERAGE
if (offset === 0) {
try {
coverage = await fetchInvoiceRegisterCoverage(supabase, companyId)
} catch {
// keep NO_INVOICE_REGISTER_COVERAGE
}
}
return {
invoices,
count: invoices.length,
total_count: total,
has_more: hasMore,
...(hasMore ? { next_offset: offset + invoices.length } : {}),
// Omitted (not nulled) on continuation pages: a null covers_from on
// page 2 would read as "no coverage limit" when it just wasn't queried.
...(offset === 0 ? { invoice_register_coverage: coverage } : {}),
...(coverage.has_pre_register_invoices && coverage.covers_from
? {
coverage_note: `Äldsta fakturan i fakturaregistret är daterad ${coverage.covers_from}. Det finns bokförda verifikat med kundfordringar (1510/1513) före det datumet: äldre kundfakturor kan ligga som verifikat utanför registret (t.ex. efter en migrering) och syns inte i detta svar. Sök i journalen (gnubok_query_journal) för perioden före ${coverage.covers_from}.`,
}
: {}),
}
},
},
+3
View File
@@ -46,6 +46,9 @@ export const ResponseMetaSchema = z.object({
next_cursor: z.string().nullable().optional(),
audit: ResponseAuditSchema.optional(),
partial_expansions: z.array(z.string()).optional(),
// Endpoint-specific register-coverage disclosure (e.g. invoices.list:
// { covers_from, has_pre_register_invoices }). Documented per endpoint.
coverage: z.record(z.string(), z.unknown()).optional(),
})
/**
+11
View File
@@ -34,6 +34,14 @@ export interface ResponseMeta {
* detect a degraded response without parsing the body.
*/
partial_expansions?: string[]
/**
* Registry-coverage disclosure for list endpoints whose backing register
* may not span all of the company's bookkeeping (e.g. the invoice
* register after a mid-year migration). Shape is endpoint-specific and
* documented in the endpoint's registry entry. Present only when the
* endpoint computes it.
*/
coverage?: Record<string, unknown>
}
interface ResponseOptions {
@@ -45,6 +53,8 @@ interface ResponseOptions {
nextCursor?: string
/** Names of `?expand=` keys whose data fetch failed (soft-degrade). */
partialExpansions?: string[]
/** Endpoint-specific register-coverage disclosure (see ResponseMeta.coverage). */
coverage?: Record<string, unknown>
/** Marks the response as a replay of a previously-cached idempotent call. */
idempotentReplay?: boolean
/** Marks the response as a dry-run preview rather than a committed write. */
@@ -80,6 +90,7 @@ function buildMeta(opts: ResponseOptions): ResponseMeta {
}
if (opts.nextCursor) meta.next_cursor = opts.nextCursor
if (opts.audit) meta.audit = opts.audit
if (opts.coverage) meta.coverage = opts.coverage
if (opts.partialExpansions && opts.partialExpansions.length > 0) {
meta.partial_expansions = opts.partialExpansions
}
@@ -0,0 +1,161 @@
import { describe, it, expect, beforeEach } from 'vitest'
import type { SupabaseClient } from '@supabase/supabase-js'
import { createQueuedMockSupabase } from '@/tests/helpers'
import { readFileSync } from 'node:fs'
import { join } from 'node:path'
import {
fetchInvoiceRegisterCoverage,
hasPreRegisterArInPeriod,
INVOICE_ENGINE_SOURCE_TYPES,
NO_INVOICE_REGISTER_COVERAGE,
} from '../invoice-register-coverage'
const { supabase, enqueue, reset, findCall, findCalls } = createQueuedMockSupabase()
const client = supabase as unknown as SupabaseClient
describe('fetchInvoiceRegisterCoverage', () => {
beforeEach(() => {
reset()
})
it('returns no coverage when the register is empty, without probing the journal', async () => {
enqueue({ data: null, error: null })
const coverage = await fetchInvoiceRegisterCoverage(client, 'company-1')
expect(coverage).toEqual(NO_INVOICE_REGISTER_COVERAGE)
expect(findCall('journal_entries', 'select')).toBeUndefined()
// Drafts must not anchor the boundary: a backdated draft would suppress
// the disclosure for exactly the period it exists to cover.
expect(findCall('invoices', 'neq')).toEqual(['status', 'draft'])
expect(findCalls('invoices', 'eq')).toContainEqual(['document_type', 'invoice'])
})
it('flags pre-register invoices when a posted non-engine AR debit predates the first invoice', async () => {
enqueue({ data: { invoice_date: '2026-07-19' }, error: null })
enqueue({ data: { id: 'entry-1' }, error: null })
const coverage = await fetchInvoiceRegisterCoverage(client, 'company-1')
expect(coverage).toEqual({
covers_from: '2026-07-19',
has_pre_register_invoices: true,
})
// Driven from journal_entries (company-indexed), never from the lines
// table with entry filters on an embed (the lateral-scan shape
// lib/bookkeeping/entry-lines.ts exists to prevent).
const eqCalls = findCalls('journal_entries', 'eq')
expect(eqCalls).toContainEqual(['company_id', 'company-1'])
expect(eqCalls).toContainEqual(['status', 'posted'])
expect(findCall('journal_entries', 'lt')).toEqual(['entry_date', '2026-07-19'])
// Every invoice-engine source type is excluded, storno/correction
// included (a rättelse can be re-dated before the boundary).
expect(findCall('journal_entries', 'not')).toEqual([
'source_type',
'in',
'("invoice_created","invoice_paid","invoice_cash_payment","credit_note","reminder_fee","rot_rut_payout","storno","correction")',
])
// AR-scoped and DEBIT-only: an advance payment crediting 1510 before the
// first invoice is not evidence of register-external invoices.
expect(findCall('journal_entries', 'in')).toEqual([
'journal_entry_lines.account_number',
['1510', '1513'],
])
expect(findCall('journal_entries', 'gt')).toEqual([
'journal_entry_lines.debit_amount',
0,
])
})
it('reports full coverage when no AR debit predates the first invoice', async () => {
enqueue({ data: { invoice_date: '2026-01-02' }, error: null })
enqueue({ data: null, error: null })
const coverage = await fetchInvoiceRegisterCoverage(client, 'company-1')
expect(coverage).toEqual({
covers_from: '2026-01-02',
has_pre_register_invoices: false,
})
})
it('degrades to UNKNOWN when the AR probe errors, never to a confident "complete"', async () => {
enqueue({ data: { invoice_date: '2026-07-19' }, error: null })
enqueue({ data: null, error: { message: 'statement timeout' } })
const coverage = await fetchInvoiceRegisterCoverage(client, 'company-1')
// { covers_from: '2026-07-19', has_pre_register_invoices: false } here
// would tell an agent the register is complete on the strength of a
// failed query: the exact double-invoicing incident this module exists
// to prevent.
expect(coverage).toEqual(NO_INVOICE_REGISTER_COVERAGE)
})
it('degrades to UNKNOWN when the register lookup errors', async () => {
enqueue({ data: null, error: { message: 'connection reset' } })
const coverage = await fetchInvoiceRegisterCoverage(client, 'company-1')
expect(coverage).toEqual(NO_INVOICE_REGISTER_COVERAGE)
})
})
describe('hasPreRegisterArInPeriod', () => {
beforeEach(() => {
reset()
})
it('requires pre-register AR activity inside the given period', async () => {
// Coverage lookups: first invoice + global probe (flagged) ...
enqueue({ data: { invoice_date: '2026-07-19' }, error: null })
enqueue({ data: { id: 'entry-1' }, error: null })
// ... then the period-scoped probe finds nothing in THIS period.
enqueue({ data: null, error: null })
expect(await hasPreRegisterArInPeriod(client, 'company-1', 'period-1')).toBe(false)
const eqCalls = findCalls('journal_entries', 'eq')
expect(eqCalls).toContainEqual(['fiscal_period_id', 'period-1'])
})
it('is true when flagged activity exists inside the period', async () => {
enqueue({ data: { invoice_date: '2026-07-19' }, error: null })
enqueue({ data: { id: 'entry-1' }, error: null })
enqueue({ data: { id: 'entry-1' }, error: null })
expect(await hasPreRegisterArInPeriod(client, 'company-1', 'period-1')).toBe(true)
})
it('skips the period probe entirely when the company is not flagged', async () => {
enqueue({ data: { invoice_date: '2026-01-02' }, error: null })
enqueue({ data: null, error: null })
expect(await hasPreRegisterArInPeriod(client, 'company-1', 'period-1')).toBe(false)
})
})
describe('INVOICE_ENGINE_SOURCE_TYPES', () => {
// Maintenance guard (Swedish compliance review, PR #2122): every source_type
// the invoice engine and its correction paths write must be excluded from
// the pre-register probe, or the engine's own AR debits would be read as
// evidence of register-external invoices. Scans the writers instead of
// trusting the list.
it('covers every source_type the invoice engine writes', () => {
const writers = [
'lib/bookkeeping/invoice-entries.ts',
'lib/bookkeeping/reminder-fee-entries.ts',
'lib/bookkeeping/rot-rut-entries.ts',
'lib/core/bookkeeping/storno-service.ts',
]
const written = new Set<string>()
for (const rel of writers) {
const src = readFileSync(join(process.cwd(), rel), 'utf8')
for (const m of src.matchAll(/source_type: '([a-z_]+)'/g)) written.add(m[1])
}
expect(written.size).toBeGreaterThan(0)
const missing = [...written].filter(
(t) => !(INVOICE_ENGINE_SOURCE_TYPES as readonly string[]).includes(t),
)
expect(missing).toEqual([])
})
})
@@ -0,0 +1,73 @@
import { describe, it, expect } from 'vitest'
import { matchesInvoiceSearch, parseAmountTerm } from '../invoice-search'
const invoice = {
invoice_number: '2627',
external_invoice_number: null,
customer: { name: 'Testbrand AB' },
subtotal: 14000,
total: 17500,
}
describe('parseAmountTerm', () => {
it('parses plain, spaced, and decimal amounts', () => {
expect(parseAmountTerm('14000')).toBe(14000)
expect(parseAmountTerm('14 000')).toBe(14000)
expect(parseAmountTerm('17 500')).toBe(17500) // NBSP thousand separator
expect(parseAmountTerm('17 500,50')).toBe(17500.5)
expect(parseAmountTerm('17500.50')).toBe(17500.5)
})
it('rejects non-amounts', () => {
expect(parseAmountTerm('Testbrand')).toBeNull()
expect(parseAmountTerm('26-27')).toBeNull()
expect(parseAmountTerm('14000,123')).toBeNull()
expect(parseAmountTerm('')).toBeNull()
})
})
describe('matchesInvoiceSearch', () => {
it('matches invoice number, external number, and customer name (case-insensitive)', () => {
expect(matchesInvoiceSearch(invoice, '2627')).toBe(true)
expect(matchesInvoiceSearch(invoice, 'testbrand')).toBe(true)
expect(
matchesInvoiceSearch({ ...invoice, external_invoice_number: 'SB-17' }, 'sb-17'),
).toBe(true)
expect(matchesInvoiceSearch(invoice, 'annat bolag')).toBe(false)
})
it('matches the net amount, not only the gross (the user-report case)', () => {
// The list displays 17 500 kr; the user knows the avtalad avgift is 14 000.
expect(matchesInvoiceSearch(invoice, '14 000')).toBe(true)
expect(matchesInvoiceSearch(invoice, '14000')).toBe(true)
expect(matchesInvoiceSearch(invoice, '17500')).toBe(true)
})
it('uses exact-amount semantics with an öre tolerance', () => {
expect(matchesInvoiceSearch(invoice, '1400')).toBe(false)
expect(matchesInvoiceSearch({ ...invoice, total: 17500.004 }, '17500')).toBe(true)
expect(matchesInvoiceSearch({ ...invoice, total: 17500.5 }, '17500')).toBe(false)
})
it('still string-matches digits against numbers before falling back to amounts', () => {
// '262' is a substring of invoice_number 2627: prefix typing keeps working.
expect(matchesInvoiceSearch(invoice, '262')).toBe(true)
})
it('finds credit notes by magnitude (stored totals are negative)', () => {
const creditNote = { ...invoice, subtotal: -14000, total: -17500 }
expect(matchesInvoiceSearch(creditNote, '17500')).toBe(true)
expect(matchesInvoiceSearch(creditNote, '14 000')).toBe(true)
expect(matchesInvoiceSearch(creditNote, '-17500')).toBe(true)
})
it('does not let a zero term match rows with missing amounts', () => {
expect(matchesInvoiceSearch({ subtotal: null, total: null }, '0')).toBe(false)
})
it('handles empty terms and missing fields', () => {
expect(matchesInvoiceSearch(invoice, '')).toBe(true)
expect(matchesInvoiceSearch({}, 'x')).toBe(false)
expect(matchesInvoiceSearch({ subtotal: null, total: null }, '14000')).toBe(false)
})
})
+161
View File
@@ -0,0 +1,161 @@
import type { SupabaseClient } from '@supabase/supabase-js'
/**
* Coverage of the invoice register (the `invoices` table) relative to the
* company's bookkeeping. The register only holds invoices created in
* Accounted: a company migrated mid-year (SIE import) or backfilled through
* manual/API verifikat has real customer invoices that exist ONLY as journal
* entries. Every surface that answers "which invoices do we have" from the
* register alone looks complete while silently omitting that period; this
* helper computes the disclosure those surfaces show.
*
* Detection is AR-based, not import-based: `fetchMigrationCoverageEnd()`
* keys on source_type='import', which misses invoice history recreated as
* plain manual verifikat (the 2026-09-01 user report: a full spring of
* "Kundfakturor" with source_type='manual'). An invoice booked as a
* verifikat DEBITS the receivable on 1510/1513, so posted pre-boundary AR
* debit lines from outside the invoice engine are the signal. Credits are
* deliberately not counted (a manually booked advance payment before the
* first invoice credits 1510 and says nothing about register coverage), and
* every invoice-engine source type is excluded, storno/correction included:
* a rättelse of an engine entry may be re-dated before the boundary
* (storno-service newEntryDate) and must not flag its own register invoice
* as external. Kontantmetod invoice history booked straight against 1930
* stays undetected; accepted, because widening to all class-3 revenue would
* flag cash sales (webshop orders, kassa) that were never register material.
*
* The probe is driven from journal_entries (company-indexed) with the line
* condition as an inner embed, never from journal_entry_lines with entry
* filters on the embed: that inverted shape compiles to a lateral scan of
* the whole lines table across tenants (see lib/bookkeeping/entry-lines.ts).
*/
export interface InvoiceRegisterCoverage {
/**
* Earliest invoice_date of a real, non-draft invoice in the register, i.e.
* where register-backed answers start being complete. Drafts, proformas
* and delivery notes are excluded: a backdated one would move the boundary
* and suppress the disclosure for exactly the period it exists to cover.
* Null when the register is empty (the empty state already routes the
* user to migration) OR when the lookup failed: null always means
* "unknown", never "complete".
*/
covers_from: string | null
/**
* True when posted non-invoice-engine verifikat carry AR (1510/1513)
* debit lines dated before covers_from: invoices likely exist outside the
* register for that period. False only when the probe RAN and found
* nothing; a failed probe returns NO_INVOICE_REGISTER_COVERAGE instead,
* so a DB error can never assert completeness.
*/
has_pre_register_invoices: boolean
}
export const NO_INVOICE_REGISTER_COVERAGE: InvoiceRegisterCoverage = {
covers_from: null,
has_pre_register_invoices: false,
}
/**
* Every source_type the invoice engine (and its correction paths) writes.
* Pre-boundary AR debits from these are the register's own bookkeeping, not
* evidence of register-external invoices.
*/
export const INVOICE_ENGINE_SOURCE_TYPES = [
'invoice_created',
'invoice_paid',
'invoice_cash_payment',
'credit_note',
'reminder_fee',
'rot_rut_payout',
'storno',
'correction',
] as const
/** PostgREST `in` literal for the NOT-IN filter. */
const INVOICE_ENGINE_SOURCE_TYPES_FILTER =
'(' + INVOICE_ENGINE_SOURCE_TYPES.map((t) => `"${t}"`).join(',') + ')'
const AR_ACCOUNTS = ['1510', '1513']
/**
* True when a posted, non-engine verifikat with an AR debit line exists
* before `coversFrom`. Optionally scoped to one fiscal period (used by the
* 1510 reconciliation, which compares period-scoped balances: a settled
* pre-boundary residual in an EARLIER period cannot explain this period's
* difference and must not be offered as an explanation).
* Returns null when the probe itself failed (unknown, not false).
*/
async function probePreRegisterArDebits(
supabase: SupabaseClient,
companyId: string,
coversFrom: string,
fiscalPeriodId?: string,
): Promise<boolean | null> {
let query = supabase
.from('journal_entries')
.select('id, journal_entry_lines!inner(id)')
.eq('company_id', companyId)
.eq('status', 'posted')
.lt('entry_date', coversFrom)
.not('source_type', 'in', INVOICE_ENGINE_SOURCE_TYPES_FILTER)
.in('journal_entry_lines.account_number', AR_ACCOUNTS)
.gt('journal_entry_lines.debit_amount', 0)
if (fiscalPeriodId) query = query.eq('fiscal_period_id', fiscalPeriodId)
const { data, error } = await query.limit(1).maybeSingle()
if (error) return null
return data != null
}
export async function fetchInvoiceRegisterCoverage(
supabase: SupabaseClient,
companyId: string,
): Promise<InvoiceRegisterCoverage> {
const { data: firstInvoice, error: firstError } = await supabase
.from('invoices')
.select('invoice_date')
.eq('company_id', companyId)
.neq('status', 'draft')
// Proformas and delivery notes are not register invoices (no AR posting);
// an early one would move the boundary without covering anything.
.eq('document_type', 'invoice')
.order('invoice_date', { ascending: true })
.limit(1)
.maybeSingle()
if (firstError) return NO_INVOICE_REGISTER_COVERAGE
const coversFrom = (firstInvoice as { invoice_date?: string } | null)?.invoice_date ?? null
if (!coversFrom) return NO_INVOICE_REGISTER_COVERAGE
const hasPreRegister = await probePreRegisterArDebits(supabase, companyId, coversFrom)
// Probe failure means UNKNOWN: never report a confident "complete".
if (hasPreRegister === null) return NO_INVOICE_REGISTER_COVERAGE
return {
covers_from: coversFrom,
has_pre_register_invoices: hasPreRegister,
}
}
/**
* Period-scoped variant for the 1510/1513 reconciliation: does pre-boundary
* non-engine AR debit activity exist INSIDE the given fiscal period? Only
* then may the reconciliation offer "migrated/backfilled invoices" as an
* explanation for its difference. Returns false on any lookup failure (no
* explanation is the safe degrade: the red badge stands unqualified).
*/
export async function hasPreRegisterArInPeriod(
supabase: SupabaseClient,
companyId: string,
fiscalPeriodId: string,
): Promise<boolean> {
const coverage = await fetchInvoiceRegisterCoverage(supabase, companyId)
if (!coverage.has_pre_register_invoices || !coverage.covers_from) return false
const inPeriod = await probePreRegisterArDebits(
supabase,
companyId,
coverage.covers_from,
fiscalPeriodId,
)
return inPeriod === true
}
+59
View File
@@ -0,0 +1,59 @@
/**
* Invoice-list search predicate. Extracted from the page filter so the
* matching rules are testable and documented in one place.
*
* Matches, in order of what users actually paste into the box:
* - invoice number / external (self-billed) number, substring
* - customer name, substring
* - an amount, against BOTH the net (subtotal) and gross (total): a user
* checking an avtalad avgift knows the net ("14 000"); the list shows
* the gross ("17 500 kr"). Amount terms accept Swedish formatting:
* spaces (incl. NBSP/thin NBSP) as thousand separators, comma or dot
* decimals. Exact-amount semantics with an öre tolerance; substring
* digit matching would drown "1400" in false hits.
*/
export interface SearchableInvoice {
invoice_number?: string | null
external_invoice_number?: string | null
customer?: { name?: string | null } | null
subtotal?: number | string | null
total?: number | string | null
}
/**
* Parse "14 000", "17 500,50", "17500.50", "-17500" → magnitude (absolute
* value); null when not an amount. \s covers NBSP/thin-NBSP thousand
* separators from sv-SE formatting. Sign is discarded: credit notes store
* negative totals, and a user searching a belopp thinks in magnitudes.
*/
export function parseAmountTerm(term: string): number | null {
const compact = term.replace(/\s/g, '').replace(',', '.')
if (!/^-?\d+(\.\d{1,2})?$/.test(compact)) return null
const value = Number(compact)
return Number.isFinite(value) ? Math.abs(value) : null
}
const amountEquals = (candidate: number | string | null | undefined, target: number): boolean => {
if (candidate == null || candidate === '') return false
const value = Number(candidate)
// Magnitude comparison so 17500 finds the -17500 kreditfaktura row too.
return Number.isFinite(value) && Math.abs(Math.abs(value) - target) < 0.005
}
export function matchesInvoiceSearch(invoice: SearchableInvoice, rawTerm: string): boolean {
const term = rawTerm.trim().toLocaleLowerCase('sv-SE')
if (!term) return true
if (
(invoice.invoice_number ?? '').toLocaleLowerCase('sv-SE').includes(term) ||
(invoice.external_invoice_number ?? '').toLocaleLowerCase('sv-SE').includes(term) ||
(invoice.customer?.name ?? '').toLocaleLowerCase('sv-SE').includes(term)
) {
return true
}
const amount = parseAmountTerm(rawTerm)
if (amount === null) return false
return amountEquals(invoice.subtotal, amount) || amountEquals(invoice.total, amount)
}
+24
View File
@@ -3,6 +3,11 @@ import { fetchAllRows } from '@/lib/supabase/fetch-all'
import { resolveSekAmount } from '@/lib/bookkeeping/currency-utils'
import { roundOre } from '@/lib/money'
import { fetchPaymentsAsOf, outstandingAsOf, todayIsoDate, type PaymentsAsOf } from './reskontra-payments'
import {
fetchInvoiceRegisterCoverage,
NO_INVOICE_REGISTER_COVERAGE,
type InvoiceRegisterCoverage,
} from '@/lib/invoices/invoice-register-coverage'
export interface ARInvoiceDetail {
invoice_id: string
@@ -48,6 +53,14 @@ export interface ARLedgerReport {
* outstanding_sek = null) so the user can see them.
*/
unconverted_fx_count: number
/**
* Coverage of the invoice register this ledger is built from. A migrated
* or backfilled company has invoices that exist only as journal entries;
* this ledger cannot list them, and their 1510 balance is why the 1510
* reconciliation shows a residual. Renderers show the boundary when
* register_coverage.has_pre_register_invoices is true.
*/
register_coverage: InvoiceRegisterCoverage
}
/**
@@ -105,9 +118,19 @@ export async function generateARLedger(
total_overdue: 0,
unpaid_count: 0,
unconverted_fx_count: 0,
register_coverage: NO_INVOICE_REGISTER_COVERAGE,
}
}
// Coverage disclosure. Non-fatal: the ledger's own numbers do not depend
// on it, so a failed lookup degrades to "no marker", never to an error.
let registerCoverage: InvoiceRegisterCoverage = NO_INVOICE_REGISTER_COVERAGE
try {
registerCoverage = await fetchInvoiceRegisterCoverage(supabase, companyId)
} catch {
// keep NO_INVOICE_REGISTER_COVERAGE
}
// Group by customer and calculate aging
const byCustomer = new Map<string, ARLedgerEntry>()
let unconvertedFxCount = 0
@@ -238,5 +261,6 @@ export async function generateARLedger(
total_overdue: Math.round(total_overdue * 100) / 100,
unpaid_count,
unconverted_fx_count: unconvertedFxCount,
register_coverage: registerCoverage,
}
}
+23
View File
@@ -2,6 +2,7 @@ import type { SupabaseClient } from '@supabase/supabase-js'
import { resolveSekAmount } from '@/lib/bookkeeping/currency-utils'
import { fetchAllRows } from '@/lib/supabase/fetch-all'
import { fetchEntryLines, type EntryLinesQuery } from '@/lib/bookkeeping/entry-lines'
import { hasPreRegisterArInPeriod } from '@/lib/invoices/invoice-register-coverage'
export interface ARReconciliationResult {
ar_ledger_total: number
@@ -21,6 +22,17 @@ export interface ARReconciliationResult {
* than a true reconciliation break.
*/
unconverted_fx_count: number
/**
* True when posted non-invoice-engine AR debit verifikat dated before the
* register's first invoice exist IN THIS PERIOD (migrated/backfilled
* invoice history). Only then may a renderer offer "migration" as an
* explanation for the difference: pre-boundary activity settled in an
* earlier period contributes nothing to this period's balance, and
* offering it anyway would cushion a genuine felbokning.
* Optional so report fixtures elsewhere stay valid; generateARReconciliation
* always sets it.
*/
pre_register_ar_in_period?: boolean
}
/**
@@ -115,6 +127,16 @@ export async function generateARReconciliation(
const difference = Math.round((arLedgerTotal - account1510Balance) * 100) / 100
// Coverage context for the difference. Non-fatal: a failed lookup degrades
// to "no explanation offered" (the helper returns false on failure), which
// leaves the red badge standing unqualified rather than excused.
let preRegisterArInPeriod = false
try {
preRegisterArInPeriod = await hasPreRegisterArInPeriod(supabase, companyId, periodId)
} catch {
// keep false
}
return {
ar_ledger_total: Math.round(arLedgerTotal * 100) / 100,
account_1510_balance: Math.round(account1510Balance * 100) / 100,
@@ -125,5 +147,6 @@ export async function generateARReconciliation(
// Avstämd: the user must fix the underlying data first.
is_reconciled: Math.abs(difference) < 0.01 && unconvertedFxCount === 0,
unconverted_fx_count: unconvertedFxCount,
pre_register_ar_in_period: preRegisterArInPeriod,
}
}
+3 -2
View File
@@ -6458,7 +6458,7 @@
"summary_unpaid": "{count} unpaid",
"summary_to_collect": "{amount} outstanding",
"summary_overdue": "{count} overdue",
"search_placeholder": "Search invoices",
"search_placeholder": "Search invoice number, customer or amount",
"tab_all": "All",
"tab_unpaid": "Unpaid",
"tab_paid": "Paid",
@@ -6527,7 +6527,8 @@
"bulk_book_failed_title": "Could not book",
"status_overdue_days": "Overdue {days} d",
"status_paid_date": "Paid {date}",
"status_picker_aria": "Filter by status"
"status_picker_aria": "Filter by status",
"coverage_notice": "Invoices before {date} may exist only as posted journal entries and are not shown in this list."
},
"notices": {
"bank_broken_one": "The {bank} bank connection has stopped working and no longer fetches transactions.",
+3 -2
View File
@@ -6458,7 +6458,7 @@
"summary_unpaid": "{count} obetalda",
"summary_to_collect": "{amount} att få in",
"summary_overdue": "{count} förfallna",
"search_placeholder": "Sök fakturor",
"search_placeholder": "Sök fakturanummer, kund eller belopp",
"tab_all": "Alla",
"tab_unpaid": "Obetalda",
"tab_paid": "Betalda",
@@ -6527,7 +6527,8 @@
"bulk_book_failed_title": "Kunde inte bokföra",
"status_overdue_days": "Förfallen {days} dgr",
"status_paid_date": "Betald {date}",
"status_picker_aria": "Filtrera på status"
"status_picker_aria": "Filtrera på status",
"coverage_notice": "Fakturor före {date} kan ligga som bokförda verifikat och visas inte i den här listan."
},
"notices": {
"bank_broken_one": "Bankkopplingen till {bank} har slutat fungera och hämtar inte längre transaktioner.",
+54 -27
View File
@@ -38,7 +38,8 @@ Response `200`:
api_version: string,
next_cursor?: string,
audit?: { voucher_number?: string, voucher_url?: string, audit_trail_url?: string, immutable_at?: string },
partial_expansions?: string[]
partial_expansions?: string[],
coverage?: Record<string, unknown>
}
}
```
@@ -107,7 +108,8 @@ Response `200`:
api_version: string,
next_cursor?: string,
audit?: { voucher_number?: string, voucher_url?: string, audit_trail_url?: string, immutable_at?: string },
partial_expansions?: string[]
partial_expansions?: string[],
coverage?: Record<string, unknown>
}
}
```
@@ -163,7 +165,8 @@ Response `200`:
api_version: string,
next_cursor?: string,
audit?: { voucher_number?: string, voucher_url?: string, audit_trail_url?: string, immutable_at?: string },
partial_expansions?: string[]
partial_expansions?: string[],
coverage?: Record<string, unknown>
}
}
```
@@ -228,7 +231,8 @@ Response `200`:
api_version: string,
next_cursor?: string,
audit?: { voucher_number?: string, voucher_url?: string, audit_trail_url?: string, immutable_at?: string },
partial_expansions?: string[]
partial_expansions?: string[],
coverage?: Record<string, unknown>
}
}
```
@@ -283,7 +287,8 @@ Response `200`:
api_version: string,
next_cursor?: string,
audit?: { voucher_number?: string, voucher_url?: string, audit_trail_url?: string, immutable_at?: string },
partial_expansions?: string[]
partial_expansions?: string[],
coverage?: Record<string, unknown>
}
}
```
@@ -338,7 +343,8 @@ Response `200`:
api_version: string,
next_cursor?: string,
audit?: { voucher_number?: string, voucher_url?: string, audit_trail_url?: string, immutable_at?: string },
partial_expansions?: string[]
partial_expansions?: string[],
coverage?: Record<string, unknown>
}
}
```
@@ -456,7 +462,8 @@ Response `200`:
api_version: string,
next_cursor?: string,
audit?: { voucher_number?: string, voucher_url?: string, audit_trail_url?: string, immutable_at?: string },
partial_expansions?: string[]
partial_expansions?: string[],
coverage?: Record<string, unknown>
}
}
```
@@ -578,7 +585,8 @@ Response `200`:
api_version: string,
next_cursor?: string,
audit?: { voucher_number?: string, voucher_url?: string, audit_trail_url?: string, immutable_at?: string },
partial_expansions?: string[]
partial_expansions?: string[],
coverage?: Record<string, unknown>
}
}
```
@@ -673,7 +681,8 @@ Response `200`:
api_version: string,
next_cursor?: string,
audit?: { voucher_number?: string, voucher_url?: string, audit_trail_url?: string, immutable_at?: string },
partial_expansions?: string[]
partial_expansions?: string[],
coverage?: Record<string, unknown>
}
}
```
@@ -746,7 +755,8 @@ Response `200`:
api_version: string,
next_cursor?: string,
audit?: { voucher_number?: string, voucher_url?: string, audit_trail_url?: string, immutable_at?: string },
partial_expansions?: string[]
partial_expansions?: string[],
coverage?: Record<string, unknown>
}
}
```
@@ -817,7 +827,8 @@ Response `200`:
api_version: string,
next_cursor?: string,
audit?: { voucher_number?: string, voucher_url?: string, audit_trail_url?: string, immutable_at?: string },
partial_expansions?: string[]
partial_expansions?: string[],
coverage?: Record<string, unknown>
}
}
```
@@ -896,7 +907,8 @@ Response `200`:
api_version: string,
next_cursor?: string,
audit?: { voucher_number?: string, voucher_url?: string, audit_trail_url?: string, immutable_at?: string },
partial_expansions?: string[]
partial_expansions?: string[],
coverage?: Record<string, unknown>
}
}
```
@@ -954,7 +966,8 @@ Response `200`:
api_version: string,
next_cursor?: string,
audit?: { voucher_number?: string, voucher_url?: string, audit_trail_url?: string, immutable_at?: string },
partial_expansions?: string[]
partial_expansions?: string[],
coverage?: Record<string, unknown>
}
}
```
@@ -1034,7 +1047,8 @@ Response `200`:
api_version: string,
next_cursor?: string,
audit?: { voucher_number?: string, voucher_url?: string, audit_trail_url?: string, immutable_at?: string },
partial_expansions?: string[]
partial_expansions?: string[],
coverage?: Record<string, unknown>
}
}
```
@@ -1111,7 +1125,8 @@ Response `200`:
api_version: string,
next_cursor?: string,
audit?: { voucher_number?: string, voucher_url?: string, audit_trail_url?: string, immutable_at?: string },
partial_expansions?: string[]
partial_expansions?: string[],
coverage?: Record<string, unknown>
}
}
```
@@ -1193,7 +1208,8 @@ Response `200`:
api_version: string,
next_cursor?: string,
audit?: { voucher_number?: string, voucher_url?: string, audit_trail_url?: string, immutable_at?: string },
partial_expansions?: string[]
partial_expansions?: string[],
coverage?: Record<string, unknown>
}
}
```
@@ -1262,7 +1278,8 @@ Response `200`:
api_version: string,
next_cursor?: string,
audit?: { voucher_number?: string, voucher_url?: string, audit_trail_url?: string, immutable_at?: string },
partial_expansions?: string[]
partial_expansions?: string[],
coverage?: Record<string, unknown>
}
}
```
@@ -1324,7 +1341,8 @@ Response `200`:
api_version: string,
next_cursor?: string,
audit?: { voucher_number?: string, voucher_url?: string, audit_trail_url?: string, immutable_at?: string },
partial_expansions?: string[]
partial_expansions?: string[],
coverage?: Record<string, unknown>
}
}
```
@@ -1407,7 +1425,8 @@ Response `200`:
api_version: string,
next_cursor?: string,
audit?: { voucher_number?: string, voucher_url?: string, audit_trail_url?: string, immutable_at?: string },
partial_expansions?: string[]
partial_expansions?: string[],
coverage?: Record<string, unknown>
}
}
```
@@ -1496,7 +1515,8 @@ Response `200`:
api_version: string,
next_cursor?: string,
audit?: { voucher_number?: string, voucher_url?: string, audit_trail_url?: string, immutable_at?: string },
partial_expansions?: string[]
partial_expansions?: string[],
coverage?: Record<string, unknown>
}
}
```
@@ -1548,7 +1568,8 @@ Response `200`:
api_version: string,
next_cursor?: string,
audit?: { voucher_number?: string, voucher_url?: string, audit_trail_url?: string, immutable_at?: string },
partial_expansions?: string[]
partial_expansions?: string[],
coverage?: Record<string, unknown>
}
}
```
@@ -1598,7 +1619,8 @@ Response `200`:
api_version: string,
next_cursor?: string,
audit?: { voucher_number?: string, voucher_url?: string, audit_trail_url?: string, immutable_at?: string },
partial_expansions?: string[]
partial_expansions?: string[],
coverage?: Record<string, unknown>
}
}
```
@@ -1677,7 +1699,8 @@ Response `200`:
api_version: string,
next_cursor?: string,
audit?: { voucher_number?: string, voucher_url?: string, audit_trail_url?: string, immutable_at?: string },
partial_expansions?: string[]
partial_expansions?: string[],
coverage?: Record<string, unknown>
}
}
```
@@ -1753,7 +1776,8 @@ Response `200`:
api_version: string,
next_cursor?: string,
audit?: { voucher_number?: string, voucher_url?: string, audit_trail_url?: string, immutable_at?: string },
partial_expansions?: string[]
partial_expansions?: string[],
coverage?: Record<string, unknown>
}
}
```
@@ -1806,7 +1830,8 @@ Response `200`:
api_version: string,
next_cursor?: string,
audit?: { voucher_number?: string, voucher_url?: string, audit_trail_url?: string, immutable_at?: string },
partial_expansions?: string[]
partial_expansions?: string[],
coverage?: Record<string, unknown>
}
}
```
@@ -1881,7 +1906,8 @@ Response `200`:
api_version: string,
next_cursor?: string,
audit?: { voucher_number?: string, voucher_url?: string, audit_trail_url?: string, immutable_at?: string },
partial_expansions?: string[]
partial_expansions?: string[],
coverage?: Record<string, unknown>
}
}
```
@@ -1979,7 +2005,8 @@ Response `200`:
api_version: string,
next_cursor?: string,
audit?: { voucher_number?: string, voucher_url?: string, audit_trail_url?: string, immutable_at?: string },
partial_expansions?: string[]
partial_expansions?: string[],
coverage?: Record<string, unknown>
}
}
```
+10 -5
View File
@@ -30,7 +30,8 @@ Response `200`:
api_version: string,
next_cursor?: string,
audit?: { voucher_number?: string, voucher_url?: string, audit_trail_url?: string, immutable_at?: string },
partial_expansions?: string[]
partial_expansions?: string[],
coverage?: Record<string, unknown>
}
}
```
@@ -128,7 +129,8 @@ Response `200`:
api_version: string,
next_cursor?: string,
audit?: { voucher_number?: string, voucher_url?: string, audit_trail_url?: string, immutable_at?: string },
partial_expansions?: string[]
partial_expansions?: string[],
coverage?: Record<string, unknown>
}
}
```
@@ -234,7 +236,8 @@ Response `200`:
api_version: string,
next_cursor?: string,
audit?: { voucher_number?: string, voucher_url?: string, audit_trail_url?: string, immutable_at?: string },
partial_expansions?: string[]
partial_expansions?: string[],
coverage?: Record<string, unknown>
}
}
```
@@ -289,7 +292,8 @@ Response `200`:
api_version: string,
next_cursor?: string,
audit?: { voucher_number?: string, voucher_url?: string, audit_trail_url?: string, immutable_at?: string },
partial_expansions?: string[]
partial_expansions?: string[],
coverage?: Record<string, unknown>
}
}
```
@@ -351,7 +355,8 @@ Response `200`:
api_version: string,
next_cursor?: string,
audit?: { voucher_number?: string, voucher_url?: string, audit_trail_url?: string, immutable_at?: string },
partial_expansions?: string[]
partial_expansions?: string[],
coverage?: Record<string, unknown>
}
}
```
+12 -6
View File
@@ -39,7 +39,8 @@ Response `200`:
api_version: string,
next_cursor?: string,
audit?: { voucher_number?: string, voucher_url?: string, audit_trail_url?: string, immutable_at?: string },
partial_expansions?: string[]
partial_expansions?: string[],
coverage?: Record<string, unknown>
}
}
```
@@ -106,7 +107,8 @@ Response `200`:
api_version: string,
next_cursor?: string,
audit?: { voucher_number?: string, voucher_url?: string, audit_trail_url?: string, immutable_at?: string },
partial_expansions?: string[]
partial_expansions?: string[],
coverage?: Record<string, unknown>
}
}
```
@@ -229,7 +231,8 @@ Response `200`:
api_version: string,
next_cursor?: string,
audit?: { voucher_number?: string, voucher_url?: string, audit_trail_url?: string, immutable_at?: string },
partial_expansions?: string[]
partial_expansions?: string[],
coverage?: Record<string, unknown>
}
}
```
@@ -311,7 +314,8 @@ Response `200`:
api_version: string,
next_cursor?: string,
audit?: { voucher_number?: string, voucher_url?: string, audit_trail_url?: string, immutable_at?: string },
partial_expansions?: string[]
partial_expansions?: string[],
coverage?: Record<string, unknown>
}
}
```
@@ -430,7 +434,8 @@ Response `200`:
api_version: string,
next_cursor?: string,
audit?: { voucher_number?: string, voucher_url?: string, audit_trail_url?: string, immutable_at?: string },
partial_expansions?: string[]
partial_expansions?: string[],
coverage?: Record<string, unknown>
}
}
```
@@ -535,7 +540,8 @@ Response `200`:
api_version: string,
next_cursor?: string,
audit?: { voucher_number?: string, voucher_url?: string, audit_trail_url?: string, immutable_at?: string },
partial_expansions?: string[]
partial_expansions?: string[],
coverage?: Record<string, unknown>
}
}
```
+8 -4
View File
@@ -59,7 +59,8 @@ Response `200`:
api_version: string,
next_cursor?: string,
audit?: { voucher_number?: string, voucher_url?: string, audit_trail_url?: string, immutable_at?: string },
partial_expansions?: string[]
partial_expansions?: string[],
coverage?: Record<string, unknown>
}
}
```
@@ -123,7 +124,8 @@ Response `200`:
api_version: string,
next_cursor?: string,
audit?: { voucher_number?: string, voucher_url?: string, audit_trail_url?: string, immutable_at?: string },
partial_expansions?: string[]
partial_expansions?: string[],
coverage?: Record<string, unknown>
}
}
```
@@ -190,7 +192,8 @@ Response `200`:
api_version: string,
next_cursor?: string,
audit?: { voucher_number?: string, voucher_url?: string, audit_trail_url?: string, immutable_at?: string },
partial_expansions?: string[]
partial_expansions?: string[],
coverage?: Record<string, unknown>
}
}
```
@@ -254,7 +257,8 @@ Response `200`:
api_version: string,
next_cursor?: string,
audit?: { voucher_number?: string, voucher_url?: string, audit_trail_url?: string, immutable_at?: string },
partial_expansions?: string[]
partial_expansions?: string[],
coverage?: Record<string, unknown>
}
}
```
+24 -12
View File
@@ -35,7 +35,8 @@ Response `200`:
api_version: string,
next_cursor?: string,
audit?: { voucher_number?: string, voucher_url?: string, audit_trail_url?: string, immutable_at?: string },
partial_expansions?: string[]
partial_expansions?: string[],
coverage?: Record<string, unknown>
}
}
```
@@ -178,7 +179,8 @@ Response `200`:
api_version: string,
next_cursor?: string,
audit?: { voucher_number?: string, voucher_url?: string, audit_trail_url?: string, immutable_at?: string },
partial_expansions?: string[]
partial_expansions?: string[],
coverage?: Record<string, unknown>
}
}
```
@@ -284,7 +286,8 @@ Response `200`:
api_version: string,
next_cursor?: string,
audit?: { voucher_number?: string, voucher_url?: string, audit_trail_url?: string, immutable_at?: string },
partial_expansions?: string[]
partial_expansions?: string[],
coverage?: Record<string, unknown>
}
}
```
@@ -430,7 +433,8 @@ Response `200`:
api_version: string,
next_cursor?: string,
audit?: { voucher_number?: string, voucher_url?: string, audit_trail_url?: string, immutable_at?: string },
partial_expansions?: string[]
partial_expansions?: string[],
coverage?: Record<string, unknown>
}
}
```
@@ -500,7 +504,8 @@ Response `200`:
api_version: string,
next_cursor?: string,
audit?: { voucher_number?: string, voucher_url?: string, audit_trail_url?: string, immutable_at?: string },
partial_expansions?: string[]
partial_expansions?: string[],
coverage?: Record<string, unknown>
}
}
```
@@ -580,7 +585,8 @@ Response `200`:
api_version: string,
next_cursor?: string,
audit?: { voucher_number?: string, voucher_url?: string, audit_trail_url?: string, immutable_at?: string },
partial_expansions?: string[]
partial_expansions?: string[],
coverage?: Record<string, unknown>
}
}
```
@@ -635,7 +641,8 @@ Response `200`:
api_version: string,
next_cursor?: string,
audit?: { voucher_number?: string, voucher_url?: string, audit_trail_url?: string, immutable_at?: string },
partial_expansions?: string[]
partial_expansions?: string[],
coverage?: Record<string, unknown>
}
}
```
@@ -698,7 +705,8 @@ Response `200`:
api_version: string,
next_cursor?: string,
audit?: { voucher_number?: string, voucher_url?: string, audit_trail_url?: string, immutable_at?: string },
partial_expansions?: string[]
partial_expansions?: string[],
coverage?: Record<string, unknown>
}
}
```
@@ -800,7 +808,8 @@ Response `200`:
api_version: string,
next_cursor?: string,
audit?: { voucher_number?: string, voucher_url?: string, audit_trail_url?: string, immutable_at?: string },
partial_expansions?: string[]
partial_expansions?: string[],
coverage?: Record<string, unknown>
}
}
```
@@ -863,7 +872,8 @@ Response `200`:
api_version: string,
next_cursor?: string,
audit?: { voucher_number?: string, voucher_url?: string, audit_trail_url?: string, immutable_at?: string },
partial_expansions?: string[]
partial_expansions?: string[],
coverage?: Record<string, unknown>
}
}
```
@@ -945,7 +955,8 @@ Response `200`:
api_version: string,
next_cursor?: string,
audit?: { voucher_number?: string, voucher_url?: string, audit_trail_url?: string, immutable_at?: string },
partial_expansions?: string[]
partial_expansions?: string[],
coverage?: Record<string, unknown>
}
}
```
@@ -1013,7 +1024,8 @@ Response `200`:
api_version: string,
next_cursor?: string,
audit?: { voucher_number?: string, voucher_url?: string, audit_trail_url?: string, immutable_at?: string },
partial_expansions?: string[]
partial_expansions?: string[],
coverage?: Record<string, unknown>
}
}
```
+28 -12
View File
@@ -24,6 +24,7 @@ Cursor-paginated invoice list ordered by created_at DESC, id ASC (newest-registe
- Ordering is by created_at (registration time), not invoice_date. Backdated invoices therefore appear where they were created, not where their date falls: filter on ?date_from / ?date_to when you care about the business date.
- Cursor pagination: pass ?cursor=<next_cursor> from the previous response. A stale or tampered cursor is ignored and the first page is returned again.
- Quotes (document_type=quote, offert) carry valid_until and quote_status (open | accepted | declined | expired). "expired" is derived: an open quote past valid_until; filter with ?quote_status=expired. Quotes never book and are never payable: convert an accepted quote to an invoice in the dashboard first.
- The register only contains invoices created in Accounted. A company migrated or backfilled mid-year has real customer invoices that exist only as journal entries and are NOT in this list. Check meta.coverage: when has_pre_register_invoices is true, treat periods before covers_from as not answered by this endpoint (query journal entries instead).
| Parameter | In | Type | Required | Notes |
|---|---|---|---|---|
@@ -38,7 +39,8 @@ Response `200`:
api_version: string,
next_cursor?: string,
audit?: { voucher_number?: string, voucher_url?: string, audit_trail_url?: string, immutable_at?: string },
partial_expansions?: string[]
partial_expansions?: string[],
coverage?: Record<string, unknown>
}
}
```
@@ -68,7 +70,11 @@ Example response `200`:
"meta": {
"request_id": "req_…",
"api_version": "2026-05-12",
"next_cursor": null
"next_cursor": null,
"coverage": {
"covers_from": "2026-05-01",
"has_pre_register_invoices": true
}
}
}
```
@@ -176,7 +182,8 @@ Response `200`:
api_version: string,
next_cursor?: string,
audit?: { voucher_number?: string, voucher_url?: string, audit_trail_url?: string, immutable_at?: string },
partial_expansions?: string[]
partial_expansions?: string[],
coverage?: Record<string, unknown>
}
}
```
@@ -250,7 +257,8 @@ Response `200`:
api_version: string,
next_cursor?: string,
audit?: { voucher_number?: string, voucher_url?: string, audit_trail_url?: string, immutable_at?: string },
partial_expansions?: string[]
partial_expansions?: string[],
coverage?: Record<string, unknown>
}
}
```
@@ -353,7 +361,8 @@ Response `200`:
api_version: string,
next_cursor?: string,
audit?: { voucher_number?: string, voucher_url?: string, audit_trail_url?: string, immutable_at?: string },
partial_expansions?: string[]
partial_expansions?: string[],
coverage?: Record<string, unknown>
}
}
```
@@ -406,7 +415,8 @@ Response `200`:
api_version: string,
next_cursor?: string,
audit?: { voucher_number?: string, voucher_url?: string, audit_trail_url?: string, immutable_at?: string },
partial_expansions?: string[]
partial_expansions?: string[],
coverage?: Record<string, unknown>
}
}
```
@@ -477,7 +487,8 @@ Response `200`:
api_version: string,
next_cursor?: string,
audit?: { voucher_number?: string, voucher_url?: string, audit_trail_url?: string, immutable_at?: string },
partial_expansions?: string[]
partial_expansions?: string[],
coverage?: Record<string, unknown>
}
}
```
@@ -562,7 +573,8 @@ Response `200`:
api_version: string,
next_cursor?: string,
audit?: { voucher_number?: string, voucher_url?: string, audit_trail_url?: string, immutable_at?: string },
partial_expansions?: string[]
partial_expansions?: string[],
coverage?: Record<string, unknown>
}
}
```
@@ -626,7 +638,8 @@ Response `200`:
api_version: string,
next_cursor?: string,
audit?: { voucher_number?: string, voucher_url?: string, audit_trail_url?: string, immutable_at?: string },
partial_expansions?: string[]
partial_expansions?: string[],
coverage?: Record<string, unknown>
}
}
```
@@ -726,7 +739,8 @@ Response `200`:
api_version: string,
next_cursor?: string,
audit?: { voucher_number?: string, voucher_url?: string, audit_trail_url?: string, immutable_at?: string },
partial_expansions?: string[]
partial_expansions?: string[],
coverage?: Record<string, unknown>
}
}
```
@@ -816,7 +830,8 @@ Response `200`:
api_version: string,
next_cursor?: string,
audit?: { voucher_number?: string, voucher_url?: string, audit_trail_url?: string, immutable_at?: string },
partial_expansions?: string[]
partial_expansions?: string[],
coverage?: Record<string, unknown>
}
}
```
@@ -909,7 +924,8 @@ Response `200`:
api_version: string,
next_cursor?: string,
audit?: { voucher_number?: string, voucher_url?: string, audit_trail_url?: string, immutable_at?: string },
partial_expansions?: string[]
partial_expansions?: string[],
coverage?: Record<string, unknown>
}
}
```
@@ -36,7 +36,8 @@ Response `200`:
api_version: string,
next_cursor?: string,
audit?: { voucher_number?: string, voucher_url?: string, audit_trail_url?: string, immutable_at?: string },
partial_expansions?: string[]
partial_expansions?: string[],
coverage?: Record<string, unknown>
}
}
```
@@ -150,7 +151,8 @@ Response `200`:
api_version: string,
next_cursor?: string,
audit?: { voucher_number?: string, voucher_url?: string, audit_trail_url?: string, immutable_at?: string },
partial_expansions?: string[]
partial_expansions?: string[],
coverage?: Record<string, unknown>
}
}
```
@@ -218,7 +220,8 @@ Response `200`:
api_version: string,
next_cursor?: string,
audit?: { voucher_number?: string, voucher_url?: string, audit_trail_url?: string, immutable_at?: string },
partial_expansions?: string[]
partial_expansions?: string[],
coverage?: Record<string, unknown>
}
}
```
@@ -286,7 +289,8 @@ Response `200`:
api_version: string,
next_cursor?: string,
audit?: { voucher_number?: string, voucher_url?: string, audit_trail_url?: string, immutable_at?: string },
partial_expansions?: string[]
partial_expansions?: string[],
coverage?: Record<string, unknown>
}
}
```
@@ -377,7 +381,8 @@ Response `200`:
api_version: string,
next_cursor?: string,
audit?: { voucher_number?: string, voucher_url?: string, audit_trail_url?: string, immutable_at?: string },
partial_expansions?: string[]
partial_expansions?: string[],
coverage?: Record<string, unknown>
}
}
```
@@ -451,7 +456,8 @@ Response `200`:
api_version: string,
next_cursor?: string,
audit?: { voucher_number?: string, voucher_url?: string, audit_trail_url?: string, immutable_at?: string },
partial_expansions?: string[]
partial_expansions?: string[],
coverage?: Record<string, unknown>
}
}
```
@@ -540,7 +546,8 @@ Response `200`:
api_version: string,
next_cursor?: string,
audit?: { voucher_number?: string, voucher_url?: string, audit_trail_url?: string, immutable_at?: string },
partial_expansions?: string[]
partial_expansions?: string[],
coverage?: Record<string, unknown>
}
}
```
@@ -632,7 +639,8 @@ Response `200`:
api_version: string,
next_cursor?: string,
audit?: { voucher_number?: string, voucher_url?: string, audit_trail_url?: string, immutable_at?: string },
partial_expansions?: string[]
partial_expansions?: string[],
coverage?: Record<string, unknown>
}
}
```
+26 -13
View File
@@ -37,7 +37,8 @@ Response `200`:
api_version: string,
next_cursor?: string,
audit?: { voucher_number?: string, voucher_url?: string, audit_trail_url?: string, immutable_at?: string },
partial_expansions?: string[]
partial_expansions?: string[],
coverage?: Record<string, unknown>
}
}
```
@@ -102,7 +103,8 @@ Response `200`:
api_version: string,
next_cursor?: string,
audit?: { voucher_number?: string, voucher_url?: string, audit_trail_url?: string, immutable_at?: string },
partial_expansions?: string[]
partial_expansions?: string[],
coverage?: Record<string, unknown>
}
}
```
@@ -169,7 +171,8 @@ Response `200`:
api_version: string,
next_cursor?: string,
audit?: { voucher_number?: string, voucher_url?: string, audit_trail_url?: string, immutable_at?: string },
partial_expansions?: string[]
partial_expansions?: string[],
coverage?: Record<string, unknown>
}
}
```
@@ -261,7 +264,8 @@ Response `200`:
api_version: string,
next_cursor?: string,
audit?: { voucher_number?: string, voucher_url?: string, audit_trail_url?: string, immutable_at?: string },
partial_expansions?: string[]
partial_expansions?: string[],
coverage?: Record<string, unknown>
}
}
```
@@ -340,7 +344,8 @@ Response `200`:
api_version: string,
next_cursor?: string,
audit?: { voucher_number?: string, voucher_url?: string, audit_trail_url?: string, immutable_at?: string },
partial_expansions?: string[]
partial_expansions?: string[],
coverage?: Record<string, unknown>
}
}
```
@@ -396,7 +401,8 @@ Response `200`:
api_version: string,
next_cursor?: string,
audit?: { voucher_number?: string, voucher_url?: string, audit_trail_url?: string, immutable_at?: string },
partial_expansions?: string[]
partial_expansions?: string[],
coverage?: Record<string, unknown>
}
}
```
@@ -447,7 +453,8 @@ Response `200`:
api_version: string,
next_cursor?: string,
audit?: { voucher_number?: string, voucher_url?: string, audit_trail_url?: string, immutable_at?: string },
partial_expansions?: string[]
partial_expansions?: string[],
coverage?: Record<string, unknown>
}
}
```
@@ -505,7 +512,8 @@ Response `200`:
api_version: string,
next_cursor?: string,
audit?: { voucher_number?: string, voucher_url?: string, audit_trail_url?: string, immutable_at?: string },
partial_expansions?: string[]
partial_expansions?: string[],
coverage?: Record<string, unknown>
}
}
```
@@ -574,7 +582,8 @@ Response `200`:
api_version: string,
next_cursor?: string,
audit?: { voucher_number?: string, voucher_url?: string, audit_trail_url?: string, immutable_at?: string },
partial_expansions?: string[]
partial_expansions?: string[],
coverage?: Record<string, unknown>
}
}
```
@@ -627,7 +636,8 @@ Response `200`:
api_version: string,
next_cursor?: string,
audit?: { voucher_number?: string, voucher_url?: string, audit_trail_url?: string, immutable_at?: string },
partial_expansions?: string[]
partial_expansions?: string[],
coverage?: Record<string, unknown>
}
}
```
@@ -690,7 +700,8 @@ Response `200`:
api_version: string,
next_cursor?: string,
audit?: { voucher_number?: string, voucher_url?: string, audit_trail_url?: string, immutable_at?: string },
partial_expansions?: string[]
partial_expansions?: string[],
coverage?: Record<string, unknown>
}
}
```
@@ -748,7 +759,8 @@ Response `200`:
api_version: string,
next_cursor?: string,
audit?: { voucher_number?: string, voucher_url?: string, audit_trail_url?: string, immutable_at?: string },
partial_expansions?: string[]
partial_expansions?: string[],
coverage?: Record<string, unknown>
}
}
```
@@ -801,7 +813,8 @@ Response `200`:
api_version: string,
next_cursor?: string,
audit?: { voucher_number?: string, voucher_url?: string, audit_trail_url?: string, immutable_at?: string },
partial_expansions?: string[]
partial_expansions?: string[],
coverage?: Record<string, unknown>
}
}
```
+27 -13
View File
@@ -20,6 +20,7 @@ Returns the customer-receivable ledger as of `as_of_date` (defaults to today). E
**Pitfalls:**
- `as_of_date` is optional; format `YYYY-MM-DD`. Defaults to today (UTC).
- Only invoices in `sent`/`overdue`/`partially_paid` status appear. Drafts and credited invoices are excluded.
- The ledger is built from the invoice register only. `data.register_coverage` ({ covers_from, has_pre_register_invoices }) discloses when posted AR verifikat predate the register's earliest invoice (migrated or backfilled invoice history): those receivables are NOT in this ledger. When has_pre_register_invoices is true, treat periods before covers_from as unanswered here and query journal entries on 1510/1513 instead.
| Parameter | In | Type | Required | Notes |
|---|---|---|---|---|
@@ -34,7 +35,8 @@ Response `200`:
api_version: string,
next_cursor?: string,
audit?: { voucher_number?: string, voucher_url?: string, audit_trail_url?: string, immutable_at?: string },
partial_expansions?: string[]
partial_expansions?: string[],
coverage?: Record<string, unknown>
}
}
```
@@ -83,7 +85,8 @@ Response `200`:
api_version: string,
next_cursor?: string,
audit?: { voucher_number?: string, voucher_url?: string, audit_trail_url?: string, immutable_at?: string },
partial_expansions?: string[]
partial_expansions?: string[],
coverage?: Record<string, unknown>
}
}
```
@@ -133,7 +136,8 @@ Response `200`:
api_version: string,
next_cursor?: string,
audit?: { voucher_number?: string, voucher_url?: string, audit_trail_url?: string, immutable_at?: string },
partial_expansions?: string[]
partial_expansions?: string[],
coverage?: Record<string, unknown>
}
}
```
@@ -209,7 +213,8 @@ Response `200`:
api_version: string,
next_cursor?: string,
audit?: { voucher_number?: string, voucher_url?: string, audit_trail_url?: string, immutable_at?: string },
partial_expansions?: string[]
partial_expansions?: string[],
coverage?: Record<string, unknown>
}
}
```
@@ -258,7 +263,8 @@ Response `200`:
api_version: string,
next_cursor?: string,
audit?: { voucher_number?: string, voucher_url?: string, audit_trail_url?: string, immutable_at?: string },
partial_expansions?: string[]
partial_expansions?: string[],
coverage?: Record<string, unknown>
}
}
```
@@ -307,7 +313,8 @@ Response `200`:
api_version: string,
next_cursor?: string,
audit?: { voucher_number?: string, voucher_url?: string, audit_trail_url?: string, immutable_at?: string },
partial_expansions?: string[]
partial_expansions?: string[],
coverage?: Record<string, unknown>
}
}
```
@@ -384,7 +391,8 @@ Response `200`:
api_version: string,
next_cursor?: string,
audit?: { voucher_number?: string, voucher_url?: string, audit_trail_url?: string, immutable_at?: string },
partial_expansions?: string[]
partial_expansions?: string[],
coverage?: Record<string, unknown>
}
}
```
@@ -431,7 +439,8 @@ Response `200`:
api_version: string,
next_cursor?: string,
audit?: { voucher_number?: string, voucher_url?: string, audit_trail_url?: string, immutable_at?: string },
partial_expansions?: string[]
partial_expansions?: string[],
coverage?: Record<string, unknown>
}
}
```
@@ -481,7 +490,8 @@ Response `200`:
api_version: string,
next_cursor?: string,
audit?: { voucher_number?: string, voucher_url?: string, audit_trail_url?: string, immutable_at?: string },
partial_expansions?: string[]
partial_expansions?: string[],
coverage?: Record<string, unknown>
}
}
```
@@ -554,7 +564,8 @@ Response `200`:
api_version: string,
next_cursor?: string,
audit?: { voucher_number?: string, voucher_url?: string, audit_trail_url?: string, immutable_at?: string },
partial_expansions?: string[]
partial_expansions?: string[],
coverage?: Record<string, unknown>
}
}
```
@@ -609,7 +620,8 @@ Response `200`:
api_version: string,
next_cursor?: string,
audit?: { voucher_number?: string, voucher_url?: string, audit_trail_url?: string, immutable_at?: string },
partial_expansions?: string[]
partial_expansions?: string[],
coverage?: Record<string, unknown>
}
}
```
@@ -668,7 +680,8 @@ Response `200`:
api_version: string,
next_cursor?: string,
audit?: { voucher_number?: string, voucher_url?: string, audit_trail_url?: string, immutable_at?: string },
partial_expansions?: string[]
partial_expansions?: string[],
coverage?: Record<string, unknown>
}
}
```
@@ -719,7 +732,8 @@ Response `200`:
api_version: string,
next_cursor?: string,
audit?: { voucher_number?: string, voucher_url?: string, audit_trail_url?: string, immutable_at?: string },
partial_expansions?: string[]
partial_expansions?: string[],
coverage?: Record<string, unknown>
}
}
```
+30 -15
View File
@@ -35,7 +35,8 @@ Response `200`:
api_version: string,
next_cursor?: string,
audit?: { voucher_number?: string, voucher_url?: string, audit_trail_url?: string, immutable_at?: string },
partial_expansions?: string[]
partial_expansions?: string[],
coverage?: Record<string, unknown>
}
}
```
@@ -140,7 +141,8 @@ Response `200`:
api_version: string,
next_cursor?: string,
audit?: { voucher_number?: string, voucher_url?: string, audit_trail_url?: string, immutable_at?: string },
partial_expansions?: string[]
partial_expansions?: string[],
coverage?: Record<string, unknown>
}
}
```
@@ -220,7 +222,8 @@ Response `200`:
api_version: string,
next_cursor?: string,
audit?: { voucher_number?: string, voucher_url?: string, audit_trail_url?: string, immutable_at?: string },
partial_expansions?: string[]
partial_expansions?: string[],
coverage?: Record<string, unknown>
}
}
```
@@ -318,7 +321,8 @@ Response `200`:
api_version: string,
next_cursor?: string,
audit?: { voucher_number?: string, voucher_url?: string, audit_trail_url?: string, immutable_at?: string },
partial_expansions?: string[]
partial_expansions?: string[],
coverage?: Record<string, unknown>
}
}
```
@@ -391,7 +395,8 @@ Response `200`:
api_version: string,
next_cursor?: string,
audit?: { voucher_number?: string, voucher_url?: string, audit_trail_url?: string, immutable_at?: string },
partial_expansions?: string[]
partial_expansions?: string[],
coverage?: Record<string, unknown>
}
}
```
@@ -458,7 +463,8 @@ Response `200`:
api_version: string,
next_cursor?: string,
audit?: { voucher_number?: string, voucher_url?: string, audit_trail_url?: string, immutable_at?: string },
partial_expansions?: string[]
partial_expansions?: string[],
coverage?: Record<string, unknown>
}
}
```
@@ -537,7 +543,8 @@ Response `200`:
api_version: string,
next_cursor?: string,
audit?: { voucher_number?: string, voucher_url?: string, audit_trail_url?: string, immutable_at?: string },
partial_expansions?: string[]
partial_expansions?: string[],
coverage?: Record<string, unknown>
}
}
```
@@ -597,7 +604,8 @@ Response `200`:
api_version: string,
next_cursor?: string,
audit?: { voucher_number?: string, voucher_url?: string, audit_trail_url?: string, immutable_at?: string },
partial_expansions?: string[]
partial_expansions?: string[],
coverage?: Record<string, unknown>
}
}
```
@@ -680,7 +688,8 @@ Response `200`:
api_version: string,
next_cursor?: string,
audit?: { voucher_number?: string, voucher_url?: string, audit_trail_url?: string, immutable_at?: string },
partial_expansions?: string[]
partial_expansions?: string[],
coverage?: Record<string, unknown>
}
}
```
@@ -776,7 +785,8 @@ Response `200`:
api_version: string,
next_cursor?: string,
audit?: { voucher_number?: string, voucher_url?: string, audit_trail_url?: string, immutable_at?: string },
partial_expansions?: string[]
partial_expansions?: string[],
coverage?: Record<string, unknown>
}
}
```
@@ -861,7 +871,8 @@ Response `200`:
api_version: string,
next_cursor?: string,
audit?: { voucher_number?: string, voucher_url?: string, audit_trail_url?: string, immutable_at?: string },
partial_expansions?: string[]
partial_expansions?: string[],
coverage?: Record<string, unknown>
}
}
```
@@ -983,7 +994,8 @@ Response `200`:
api_version: string,
next_cursor?: string,
audit?: { voucher_number?: string, voucher_url?: string, audit_trail_url?: string, immutable_at?: string },
partial_expansions?: string[]
partial_expansions?: string[],
coverage?: Record<string, unknown>
}
}
```
@@ -1047,7 +1059,8 @@ Response `200`:
api_version: string,
next_cursor?: string,
audit?: { voucher_number?: string, voucher_url?: string, audit_trail_url?: string, immutable_at?: string },
partial_expansions?: string[]
partial_expansions?: string[],
coverage?: Record<string, unknown>
}
}
```
@@ -1155,7 +1168,8 @@ Response `200`:
api_version: string,
next_cursor?: string,
audit?: { voucher_number?: string, voucher_url?: string, audit_trail_url?: string, immutable_at?: string },
partial_expansions?: string[]
partial_expansions?: string[],
coverage?: Record<string, unknown>
}
}
```
@@ -1228,7 +1242,8 @@ Response `200`:
api_version: string,
next_cursor?: string,
audit?: { voucher_number?: string, voucher_url?: string, audit_trail_url?: string, immutable_at?: string },
partial_expansions?: string[]
partial_expansions?: string[],
coverage?: Record<string, unknown>
}
}
```
+24 -12
View File
@@ -37,7 +37,8 @@ Response `200`:
api_version: string,
next_cursor?: string,
audit?: { voucher_number?: string, voucher_url?: string, audit_trail_url?: string, immutable_at?: string },
partial_expansions?: string[]
partial_expansions?: string[],
coverage?: Record<string, unknown>
}
}
```
@@ -170,7 +171,8 @@ Response `200`:
api_version: string,
next_cursor?: string,
audit?: { voucher_number?: string, voucher_url?: string, audit_trail_url?: string, immutable_at?: string },
partial_expansions?: string[]
partial_expansions?: string[],
coverage?: Record<string, unknown>
}
}
```
@@ -250,7 +252,8 @@ Response `200`:
api_version: string,
next_cursor?: string,
audit?: { voucher_number?: string, voucher_url?: string, audit_trail_url?: string, immutable_at?: string },
partial_expansions?: string[]
partial_expansions?: string[],
coverage?: Record<string, unknown>
}
}
```
@@ -354,7 +357,8 @@ Response `200`:
api_version: string,
next_cursor?: string,
audit?: { voucher_number?: string, voucher_url?: string, audit_trail_url?: string, immutable_at?: string },
partial_expansions?: string[]
partial_expansions?: string[],
coverage?: Record<string, unknown>
}
}
```
@@ -409,7 +413,8 @@ Response `200`:
api_version: string,
next_cursor?: string,
audit?: { voucher_number?: string, voucher_url?: string, audit_trail_url?: string, immutable_at?: string },
partial_expansions?: string[]
partial_expansions?: string[],
coverage?: Record<string, unknown>
}
}
```
@@ -468,7 +473,8 @@ Response `200`:
api_version: string,
next_cursor?: string,
audit?: { voucher_number?: string, voucher_url?: string, audit_trail_url?: string, immutable_at?: string },
partial_expansions?: string[]
partial_expansions?: string[],
coverage?: Record<string, unknown>
}
}
```
@@ -551,7 +557,8 @@ Response `200`:
api_version: string,
next_cursor?: string,
audit?: { voucher_number?: string, voucher_url?: string, audit_trail_url?: string, immutable_at?: string },
partial_expansions?: string[]
partial_expansions?: string[],
coverage?: Record<string, unknown>
}
}
```
@@ -605,7 +612,8 @@ Response `200`:
api_version: string,
next_cursor?: string,
audit?: { voucher_number?: string, voucher_url?: string, audit_trail_url?: string, immutable_at?: string },
partial_expansions?: string[]
partial_expansions?: string[],
coverage?: Record<string, unknown>
}
}
```
@@ -733,7 +741,8 @@ Response `200`:
api_version: string,
next_cursor?: string,
audit?: { voucher_number?: string, voucher_url?: string, audit_trail_url?: string, immutable_at?: string },
partial_expansions?: string[]
partial_expansions?: string[],
coverage?: Record<string, unknown>
}
}
```
@@ -817,7 +826,8 @@ Response `200`:
api_version: string,
next_cursor?: string,
audit?: { voucher_number?: string, voucher_url?: string, audit_trail_url?: string, immutable_at?: string },
partial_expansions?: string[]
partial_expansions?: string[],
coverage?: Record<string, unknown>
}
}
```
@@ -938,7 +948,8 @@ Response `200`:
api_version: string,
next_cursor?: string,
audit?: { voucher_number?: string, voucher_url?: string, audit_trail_url?: string, immutable_at?: string },
partial_expansions?: string[]
partial_expansions?: string[],
coverage?: Record<string, unknown>
}
}
```
@@ -1043,7 +1054,8 @@ Response `200`:
api_version: string,
next_cursor?: string,
audit?: { voucher_number?: string, voucher_url?: string, audit_trail_url?: string, immutable_at?: string },
partial_expansions?: string[]
partial_expansions?: string[],
coverage?: Record<string, unknown>
}
}
```
+16 -8
View File
@@ -35,7 +35,8 @@ Response `200`:
api_version: string,
next_cursor?: string,
audit?: { voucher_number?: string, voucher_url?: string, audit_trail_url?: string, immutable_at?: string },
partial_expansions?: string[]
partial_expansions?: string[],
coverage?: Record<string, unknown>
}
}
```
@@ -126,7 +127,8 @@ Response `200`:
api_version: string,
next_cursor?: string,
audit?: { voucher_number?: string, voucher_url?: string, audit_trail_url?: string, immutable_at?: string },
partial_expansions?: string[]
partial_expansions?: string[],
coverage?: Record<string, unknown>
}
}
```
@@ -192,7 +194,8 @@ Response `200`:
api_version: string,
next_cursor?: string,
audit?: { voucher_number?: string, voucher_url?: string, audit_trail_url?: string, immutable_at?: string },
partial_expansions?: string[]
partial_expansions?: string[],
coverage?: Record<string, unknown>
}
}
```
@@ -273,7 +276,8 @@ Response `200`:
api_version: string,
next_cursor?: string,
audit?: { voucher_number?: string, voucher_url?: string, audit_trail_url?: string, immutable_at?: string },
partial_expansions?: string[]
partial_expansions?: string[],
coverage?: Record<string, unknown>
}
}
```
@@ -353,7 +357,8 @@ Response `200`:
api_version: string,
next_cursor?: string,
audit?: { voucher_number?: string, voucher_url?: string, audit_trail_url?: string, immutable_at?: string },
partial_expansions?: string[]
partial_expansions?: string[],
coverage?: Record<string, unknown>
}
}
```
@@ -415,7 +420,8 @@ Response `200`:
api_version: string,
next_cursor?: string,
audit?: { voucher_number?: string, voucher_url?: string, audit_trail_url?: string, immutable_at?: string },
partial_expansions?: string[]
partial_expansions?: string[],
coverage?: Record<string, unknown>
}
}
```
@@ -464,7 +470,8 @@ Response `200`:
api_version: string,
next_cursor?: string,
audit?: { voucher_number?: string, voucher_url?: string, audit_trail_url?: string, immutable_at?: string },
partial_expansions?: string[]
partial_expansions?: string[],
coverage?: Record<string, unknown>
}
}
```
@@ -511,7 +518,8 @@ Response `200`:
api_version: string,
next_cursor?: string,
audit?: { voucher_number?: string, voucher_url?: string, audit_trail_url?: string, immutable_at?: string },
partial_expansions?: string[]
partial_expansions?: string[],
coverage?: Record<string, unknown>
}
}
```