diff --git a/DECISIONS.md b/DECISIONS.md index 4aca68bb..e67c2e15 100644 --- a/DECISIONS.md +++ b/DECISIONS.md @@ -1454,6 +1454,7 @@ One line per decision: `[YYYY-MM-DD] : `. 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. diff --git a/app/(dashboard)/invoices/page.tsx b/app/(dashboard)/invoices/page.tsx index 4357fbbf..dd8a959a 100644 --- a/app/(dashboard)/invoices/page.tsx +++ b/app/(dashboard)/invoices/page.tsx @@ -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( + 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() { + {/* 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 && ( +

+ {t('coverage_notice', { date: formatDate(registerCoverage.covers_from) })} +

+ )} + {/* Bulkbar: appears once anything is selected (supplier-invoices shape). */} {selectedIds.size > 0 && (
diff --git a/app/api/invoices/__tests__/route.test.ts b/app/api/invoices/__tests__/route.test.ts index 8d2b4167..e78b46d7 100644 --- a/app/api/invoices/__tests__/route.test.ts +++ b/app/api/invoices/__tests__/route.test.ts @@ -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 }) diff --git a/app/api/invoices/route.ts b/app/api/invoices/route.ts index a055ae19..a49ede4c 100644 --- a/app/api/invoices/route.ts +++ b/app/api/invoices/route.ts @@ -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, + }) }, ) diff --git a/app/api/v1/companies/[companyId]/invoices/route.ts b/app/api/v1/companies/[companyId]/invoices/route.ts index 7bbbdc34..72a6bdd3 100644 --- a/app/api/v1/companies/[companyId]/invoices/route.ts +++ b/app/api/v1/companies/[companyId]/invoices/route.ts @@ -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= 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 }, }) }, ) diff --git a/app/api/v1/companies/[companyId]/reports/ar-ledger/route.ts b/app/api/v1/companies/[companyId]/reports/ar-ledger/route.ts index c6f47261..a00c8cca 100644 --- a/app/api/v1/companies/[companyId]/reports/ar-ledger/route.ts +++ b/app/api/v1/companies/[companyId]/reports/ar-ledger/route.ts @@ -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: { diff --git a/components/reports/views/index.tsx b/components/reports/views/index.tsx index b8c5cd5d..20c6d613 100644 --- a/components/reports/views/index.tsx +++ b/components/reports/views/index.tsx @@ -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.

)} + {ledger.register_coverage?.has_pre_register_invoices && ledger.register_coverage.covers_from && ( +

+ Fakturor före {formatDate(ledger.register_coverage.covers_from)} kan ligga som bokförda verifikat och ingår inte i reskontran. +

+ )} @@ -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 på saknade kursuppgifter snarare än felbokning.

)} + {/* 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 && ( +

+ 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 på det. Kontrollera huvudboken på och innan du letar felbokning. +

+ )}
diff --git a/extensions/general/mcp-server/server.ts b/extensions/general/mcp-server/server.ts index 9094cab3..cdcba08a 100644 --- a/extensions/general/mcp-server/server.ts +++ b/extensions/general/mcp-server/server.ts @@ -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}.`, + } + : {}), } }, }, diff --git a/lib/api/v1/registry.ts b/lib/api/v1/registry.ts index 5f4e1c09..0ac8e42d 100644 --- a/lib/api/v1/registry.ts +++ b/lib/api/v1/registry.ts @@ -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(), }) /** diff --git a/lib/api/v1/response.ts b/lib/api/v1/response.ts index 7f30bd19..9bbac931 100644 --- a/lib/api/v1/response.ts +++ b/lib/api/v1/response.ts @@ -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 } 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 /** 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 } diff --git a/lib/invoices/__tests__/invoice-register-coverage.test.ts b/lib/invoices/__tests__/invoice-register-coverage.test.ts new file mode 100644 index 00000000..2fefe9cc --- /dev/null +++ b/lib/invoices/__tests__/invoice-register-coverage.test.ts @@ -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() + 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([]) + }) +}) diff --git a/lib/invoices/__tests__/invoice-search.test.ts b/lib/invoices/__tests__/invoice-search.test.ts new file mode 100644 index 00000000..dfc78283 --- /dev/null +++ b/lib/invoices/__tests__/invoice-search.test.ts @@ -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) + }) +}) diff --git a/lib/invoices/invoice-register-coverage.ts b/lib/invoices/invoice-register-coverage.ts new file mode 100644 index 00000000..78d454bd --- /dev/null +++ b/lib/invoices/invoice-register-coverage.ts @@ -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 { + 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 { + 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 { + 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 +} diff --git a/lib/invoices/invoice-search.ts b/lib/invoices/invoice-search.ts new file mode 100644 index 00000000..391d2308 --- /dev/null +++ b/lib/invoices/invoice-search.ts @@ -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) +} diff --git a/lib/reports/ar-ledger.ts b/lib/reports/ar-ledger.ts index abd5852e..e304116b 100644 --- a/lib/reports/ar-ledger.ts +++ b/lib/reports/ar-ledger.ts @@ -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() 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, } } diff --git a/lib/reports/ar-reconciliation.ts b/lib/reports/ar-reconciliation.ts index 2f34ddb7..0e65da13 100644 --- a/lib/reports/ar-reconciliation.ts +++ b/lib/reports/ar-reconciliation.ts @@ -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, } } diff --git a/messages/en.json b/messages/en.json index fcd31940..63e9f84b 100644 --- a/messages/en.json +++ b/messages/en.json @@ -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.", diff --git a/messages/sv.json b/messages/sv.json index 99c940c7..be429e14 100644 --- a/messages/sv.json +++ b/messages/sv.json @@ -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.", diff --git a/skills/accounted-api/references/banking.md b/skills/accounted-api/references/banking.md index eb38ee31..7198f439 100644 --- a/skills/accounted-api/references/banking.md +++ b/skills/accounted-api/references/banking.md @@ -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 } } ``` @@ -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 } } ``` @@ -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 } } ``` @@ -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 } } ``` @@ -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 } } ``` @@ -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 } } ``` @@ -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 } } ``` @@ -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 } } ``` @@ -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 } } ``` @@ -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 } } ``` @@ -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 } } ``` @@ -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 } } ``` @@ -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 } } ``` @@ -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 } } ``` @@ -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 } } ``` @@ -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 } } ``` @@ -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 } } ``` @@ -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 } } ``` @@ -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 } } ``` @@ -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 } } ``` @@ -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 } } ``` @@ -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 } } ``` @@ -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 } } ``` @@ -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 } } ``` @@ -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 } } ``` @@ -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 } } ``` @@ -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 } } ``` diff --git a/skills/accounted-api/references/core.md b/skills/accounted-api/references/core.md index 05486c80..74faf739 100644 --- a/skills/accounted-api/references/core.md +++ b/skills/accounted-api/references/core.md @@ -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 } } ``` @@ -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 } } ``` @@ -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 } } ``` @@ -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 } } ``` @@ -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 } } ``` diff --git a/skills/accounted-api/references/customers.md b/skills/accounted-api/references/customers.md index edef6aee..68208ca6 100644 --- a/skills/accounted-api/references/customers.md +++ b/skills/accounted-api/references/customers.md @@ -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 } } ``` @@ -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 } } ``` @@ -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 } } ``` @@ -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 } } ``` @@ -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 } } ``` @@ -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 } } ``` diff --git a/skills/accounted-api/references/documents.md b/skills/accounted-api/references/documents.md index 6347f33f..666deb91 100644 --- a/skills/accounted-api/references/documents.md +++ b/skills/accounted-api/references/documents.md @@ -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 } } ``` @@ -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 } } ``` @@ -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 } } ``` @@ -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 } } ``` diff --git a/skills/accounted-api/references/employees.md b/skills/accounted-api/references/employees.md index c2e0396f..ccd24f77 100644 --- a/skills/accounted-api/references/employees.md +++ b/skills/accounted-api/references/employees.md @@ -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 } } ``` @@ -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 } } ``` @@ -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 } } ``` @@ -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 } } ``` @@ -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 } } ``` @@ -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 } } ``` @@ -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 } } ``` @@ -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 } } ``` @@ -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 } } ``` @@ -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 } } ``` @@ -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 } } ``` @@ -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 } } ``` diff --git a/skills/accounted-api/references/invoices.md b/skills/accounted-api/references/invoices.md index 28a9f91d..d7f93da7 100644 --- a/skills/accounted-api/references/invoices.md +++ b/skills/accounted-api/references/invoices.md @@ -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= 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 } } ``` @@ -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 } } ``` @@ -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 } } ``` @@ -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 } } ``` @@ -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 } } ``` @@ -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 } } ``` @@ -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 } } ``` @@ -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 } } ``` @@ -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 } } ``` @@ -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 } } ``` @@ -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 } } ``` diff --git a/skills/accounted-api/references/journal-entries.md b/skills/accounted-api/references/journal-entries.md index fc074ccc..b3c1609d 100644 --- a/skills/accounted-api/references/journal-entries.md +++ b/skills/accounted-api/references/journal-entries.md @@ -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 } } ``` @@ -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 } } ``` @@ -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 } } ``` @@ -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 } } ``` @@ -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 } } ``` @@ -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 } } ``` @@ -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 } } ``` @@ -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 } } ``` diff --git a/skills/accounted-api/references/periods.md b/skills/accounted-api/references/periods.md index 21604e99..aff08979 100644 --- a/skills/accounted-api/references/periods.md +++ b/skills/accounted-api/references/periods.md @@ -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 } } ``` @@ -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 } } ``` @@ -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 } } ``` @@ -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 } } ``` @@ -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 } } ``` @@ -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 } } ``` @@ -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 } } ``` @@ -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 } } ``` @@ -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 } } ``` @@ -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 } } ``` @@ -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 } } ``` @@ -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 } } ``` @@ -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 } } ``` diff --git a/skills/accounted-api/references/reports.md b/skills/accounted-api/references/reports.md index 883991b4..09491eaa 100644 --- a/skills/accounted-api/references/reports.md +++ b/skills/accounted-api/references/reports.md @@ -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 } } ``` @@ -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 } } ``` @@ -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 } } ``` @@ -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 } } ``` @@ -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 } } ``` @@ -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 } } ``` @@ -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 } } ``` @@ -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 } } ``` @@ -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 } } ``` @@ -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 } } ``` @@ -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 } } ``` @@ -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 } } ``` @@ -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 } } ``` diff --git a/skills/accounted-api/references/salary-runs.md b/skills/accounted-api/references/salary-runs.md index 9f489ee7..a1742567 100644 --- a/skills/accounted-api/references/salary-runs.md +++ b/skills/accounted-api/references/salary-runs.md @@ -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 } } ``` @@ -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 } } ``` @@ -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 } } ``` @@ -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 } } ``` @@ -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 } } ``` @@ -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 } } ``` @@ -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 } } ``` @@ -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 } } ``` @@ -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 } } ``` @@ -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 } } ``` @@ -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 } } ``` @@ -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 } } ``` @@ -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 } } ``` @@ -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 } } ``` @@ -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 } } ``` diff --git a/skills/accounted-api/references/suppliers.md b/skills/accounted-api/references/suppliers.md index c96b5255..96874fa5 100644 --- a/skills/accounted-api/references/suppliers.md +++ b/skills/accounted-api/references/suppliers.md @@ -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 } } ``` @@ -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 } } ``` @@ -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 } } ``` @@ -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 } } ``` @@ -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 } } ``` @@ -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 } } ``` @@ -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 } } ``` @@ -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 } } ``` @@ -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 } } ``` @@ -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 } } ``` @@ -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 } } ``` @@ -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 } } ``` diff --git a/skills/accounted-api/references/webhooks.md b/skills/accounted-api/references/webhooks.md index c7eac34d..051bd5c9 100644 --- a/skills/accounted-api/references/webhooks.md +++ b/skills/accounted-api/references/webhooks.md @@ -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 } } ``` @@ -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 } } ``` @@ -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 } } ``` @@ -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 } } ``` @@ -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 } } ``` @@ -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 } } ``` @@ -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 } } ``` @@ -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 } } ```