* 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>
663 lines
23 KiB
Markdown
663 lines
23 KiB
Markdown
<!-- GENERATED FILE, do not edit. Source: lib/api/v1 registry + scripts/api-skill/overlays. Regenerate with `npm run apiskill:generate`. -->
|
|
|
|
# Journal entries endpoints
|
|
|
|
The ledger itself: journal entries follow draft -> commit -> immutable. There is no edit or delete after commit; undo via reverse (storno) or correct. Voucher numbers are server-assigned and gapless; explain unavoidable gaps via voucher-gap-explanations.
|
|
|
|
Conventions (auth, envelope, pagination, dry-run, idempotency, standard errors)
|
|
are in SKILL.md and are not repeated per endpoint.
|
|
|
|
### `GET /api/v1/companies/{companyId}/journal-entries`
|
|
|
|
**List journal entries (verifikationer).**
|
|
`scope:reports:read · risk:low · idempotent`
|
|
|
|
Cursor-paginated list of journal entries ordered by created_at DESC, id ASC (newest-booked first; the `entry_date` column is the verifikationsdatum and is filterable via ?date_from / ?date_to but is not the sort key). Filters: fiscal_period_id, status, date_from, date_to. Excludes status=cancelled by default; pass status=cancelled to inspect storno-cancelled drafts.
|
|
|
|
**Use when:** You need to walk the verifikationsserie for a period (audit, SIE export, gap detection) or list recent activity for a UI.
|
|
**Do not use for:** Reading a single verifikation (use GET /{id}). Reading lines without the header (no separate endpoint: they ride in /{id}).
|
|
|
|
**Pitfalls:**
|
|
- Cancelled drafts are hidden by default. They are NOT a löpnummer gap (no voucher_number is allocated for drafts); the filter is for noise reduction.
|
|
- voucher_number=0 indicates a draft that has not been committed. Posted entries always have voucher_number > 0.
|
|
- Ordering is by created_at (when the verifikat was booked), not entry_date. A backdated verifikat appears where it was booked: filter on ?date_from / ?date_to when you need entry_date ranges, and walk the whole cursor chain when you need a full period.
|
|
- Cursor pagination: pass ?cursor=<next_cursor> from the previous response. A stale or tampered cursor is ignored and the first page is returned again.
|
|
|
|
| Parameter | In | Type | Required | Notes |
|
|
|---|---|---|---|---|
|
|
| `companyId` | path | `string` | yes | |
|
|
|
|
Response `200`:
|
|
```ts
|
|
{
|
|
data: { id: string, fiscal_period_id: string, voucher_series: string, voucher_number: number, entry_date: string, description: string, status: "draft" | "posted" | "cancelled", source_type: string, created_at: string }[],
|
|
meta: {
|
|
request_id: string,
|
|
api_version: string,
|
|
next_cursor?: string,
|
|
audit?: { voucher_number?: string, voucher_url?: string, audit_trail_url?: string, immutable_at?: string },
|
|
partial_expansions?: string[],
|
|
coverage?: Record<string, unknown>
|
|
}
|
|
}
|
|
```
|
|
|
|
Example response `200`:
|
|
```json
|
|
{
|
|
"data": [
|
|
{
|
|
"id": "0e9c…",
|
|
"fiscal_period_id": "a8f1…",
|
|
"voucher_series": "A",
|
|
"voucher_number": 142,
|
|
"entry_date": "2026-05-12",
|
|
"description": "Levfaktura 2026-1234, Office Depot AB (ankomstnr 42)",
|
|
"status": "posted",
|
|
"source_type": "supplier_invoice_registered",
|
|
"created_at": "2026-05-13T15:00:00Z"
|
|
}
|
|
],
|
|
"meta": {
|
|
"request_id": "req_…",
|
|
"api_version": "2026-05-12",
|
|
"next_cursor": null
|
|
}
|
|
}
|
|
```
|
|
|
|
---
|
|
|
|
### `POST /api/v1/companies/{companyId}/journal-entries`
|
|
|
|
**Create a draft journal entry (verifikation).**
|
|
`scope:bookkeeping:write · risk:high · idempotent · dry-run · reversible`
|
|
|
|
Creates a draft journal entry via the engine's createDraftEntry(). The draft has no voucher_number until /commit is called. Idempotent (mandatory Idempotency-Key). Dry-runnable: a dry-run validates balance + account-chart membership + period date constraints without inserting any row.
|
|
|
|
**Use when:** You're posting an arbitrary verifikation (manual journal entries, accrual reversals, period closing adjustments) outside the invoicing / supplier-invoice / transaction flows.
|
|
**Do not use for:** Bookkeeping flows that have a dedicated endpoint (invoices, supplier-invoices, transactions). Editing an existing posted entry: use /correct instead.
|
|
|
|
**Pitfalls:**
|
|
- Idempotency-Key is mandatory.
|
|
- Lines must sum to zero (Σ debit = Σ credit). Engine rejects with JOURNAL_ENTRY_NOT_BALANCED on imbalance.
|
|
- entry_date must fall within fiscal_period_id's [period_start, period_end]; otherwise ENTRY_DATE_OUTSIDE_FISCAL_PERIOD.
|
|
- All account_numbers must be active in the chart_of_accounts; otherwise ACCOUNTS_NOT_IN_CHART.
|
|
- voucher_series defaults to "A" if omitted. Must be a single uppercase letter.
|
|
- This creates a DRAFT only: call POST /{id}/commit to assign the voucher_number and post atomically.
|
|
|
|
| Parameter | In | Type | Required | Notes |
|
|
|---|---|---|---|---|
|
|
| `companyId` | path | `string` | yes | |
|
|
|
|
Request body:
|
|
```ts
|
|
{
|
|
fiscal_period_id: string,
|
|
entry_date: string,
|
|
description: string,
|
|
source_type?: "manual" | "bank_transaction" | "invoice_created" | "invoice_paid" | "invoice_cash_payment" | "credit_note" | "salary_payment" | "opening_balance" | "year_end" | "storno" | "correction" | "import" | "system" | "inbox_item" | "supplier_invoice_registered" | "supplier_invoice_paid" | "supplier_invoice_cash_payment" | "supplier_invoice_privately_paid" | "supplier_credit_note" | "currency_revaluation" | "reminder_fee" | "accrual" | "result_appropriation" | "rot_rut_payout" | "vat_settlement" | "stripe_payout" | "webshop_order",
|
|
source_id?: string,
|
|
voucher_series?: string,
|
|
notes?: string,
|
|
lines: { account_number: string, debit_amount?: number, credit_amount?: number, line_description?: string, currency?: string, amount_in_currency?: number, exchange_rate?: number, tax_code?: string, dimensions?: Record<string, string>, cost_center?: string, project?: string }[]
|
|
}
|
|
```
|
|
|
|
Example request:
|
|
```json
|
|
{
|
|
"fiscal_period_id": "a8f1…",
|
|
"entry_date": "2026-05-12",
|
|
"description": "Bankavgift maj 2026",
|
|
"lines": [
|
|
{
|
|
"account_number": "6570",
|
|
"debit_amount": 50,
|
|
"credit_amount": 0,
|
|
"line_description": "Bankavgift"
|
|
},
|
|
{
|
|
"account_number": "1930",
|
|
"debit_amount": 0,
|
|
"credit_amount": 50,
|
|
"line_description": "Företagskonto"
|
|
}
|
|
]
|
|
}
|
|
```
|
|
|
|
Response `200`:
|
|
```ts
|
|
{
|
|
data: {
|
|
id: string,
|
|
fiscal_period_id: string,
|
|
voucher_series: string,
|
|
voucher_number: number,
|
|
entry_date: string,
|
|
description: string,
|
|
status: "draft" | "posted" | "cancelled",
|
|
source_type: string,
|
|
created_at: string,
|
|
notes: string,
|
|
reverses_id: string,
|
|
reversed_by_id: string,
|
|
correction_of_id: string,
|
|
lines: { id: string, account_number: string, debit_amount: number, credit_amount: number, line_description: string, currency: string, amount_in_currency: number, exchange_rate: number, tax_code: string, cost_center: string, project: string }[]
|
|
},
|
|
meta: {
|
|
request_id: string,
|
|
api_version: string,
|
|
next_cursor?: string,
|
|
audit?: { voucher_number?: string, voucher_url?: string, audit_trail_url?: string, immutable_at?: string },
|
|
partial_expansions?: string[],
|
|
coverage?: Record<string, unknown>
|
|
}
|
|
}
|
|
```
|
|
|
|
Example response `200`:
|
|
```json
|
|
{
|
|
"data": {
|
|
"id": "0e9c…",
|
|
"status": "draft",
|
|
"voucher_series": "A",
|
|
"voucher_number": 0
|
|
},
|
|
"meta": {
|
|
"request_id": "req_…",
|
|
"api_version": "2026-05-12"
|
|
}
|
|
}
|
|
```
|
|
|
|
---
|
|
|
|
### `GET /api/v1/companies/{companyId}/journal-entries/{id}`
|
|
|
|
**Retrieve a single verifikation by id.**
|
|
`scope:reports:read · risk:low · idempotent`
|
|
|
|
Returns the full journal entry including all lines, dimensions, and the storno chain (reverses_id, reversed_by_id, correction_of_id).
|
|
|
|
**Use when:** You need the full verifikation for audit / reconciliation, or to display the line-by-line breakdown.
|
|
**Do not use for:** Listing entries (use the list endpoint with filters).
|
|
|
|
**Pitfalls:**
|
|
- Cancelled drafts are returned (no filter on status here); inspect status before assuming the entry is posted.
|
|
- Lines are sorted by sort_order; the order matters for display but not for accounting (the sum across lines is the meaningful quantity).
|
|
|
|
| Parameter | In | Type | Required | Notes |
|
|
|---|---|---|---|---|
|
|
| `companyId` | path | `string` | yes | |
|
|
| `id` | path | `string` | yes | |
|
|
|
|
Response `200`:
|
|
```ts
|
|
{
|
|
data: {
|
|
id: string,
|
|
fiscal_period_id: string,
|
|
voucher_series: string,
|
|
voucher_number: number,
|
|
entry_date: string,
|
|
description: string,
|
|
status: "draft" | "posted" | "cancelled",
|
|
source_type: string,
|
|
source_id: string,
|
|
notes: string,
|
|
reverses_id: string,
|
|
reversed_by_id: string,
|
|
correction_of_id: string,
|
|
lines: { id: string, account_number: string, debit_amount: number, credit_amount: number, line_description: string, currency: string, amount_in_currency: number, exchange_rate: number, tax_code: string, cost_center: string, project: string, sort_order: number }[],
|
|
created_at: string,
|
|
updated_at: string
|
|
},
|
|
meta: {
|
|
request_id: string,
|
|
api_version: string,
|
|
next_cursor?: string,
|
|
audit?: { voucher_number?: string, voucher_url?: string, audit_trail_url?: string, immutable_at?: string },
|
|
partial_expansions?: string[],
|
|
coverage?: Record<string, unknown>
|
|
}
|
|
}
|
|
```
|
|
|
|
Example response `200`:
|
|
```json
|
|
{
|
|
"data": {
|
|
"id": "0e9c…",
|
|
"voucher_series": "A",
|
|
"voucher_number": 142,
|
|
"entry_date": "2026-05-12",
|
|
"status": "posted",
|
|
"lines": [
|
|
{
|
|
"account_number": "6570",
|
|
"debit_amount": 50,
|
|
"credit_amount": 0,
|
|
"sort_order": 0
|
|
},
|
|
{
|
|
"account_number": "1930",
|
|
"debit_amount": 0,
|
|
"credit_amount": 50,
|
|
"sort_order": 1
|
|
}
|
|
]
|
|
},
|
|
"meta": {
|
|
"request_id": "req_…",
|
|
"api_version": "2026-05-12"
|
|
}
|
|
}
|
|
```
|
|
|
|
---
|
|
|
|
### `POST /api/v1/companies/{companyId}/journal-entries/{id}/commit`
|
|
|
|
**Commit a draft journal entry.**
|
|
`scope:bookkeeping:write · risk:high · idempotent · dry-run · reversible`
|
|
|
|
Atomically advances the voucher series and flips the draft to posted. The voucher_number is the smallest integer not yet used in (fiscal_period_id, voucher_series); a failed commit does NOT burn the number.
|
|
|
|
**Use when:** You created a draft via POST /journal-entries and now want to post it to the books. After commit the entry is immutable per BFL 5 kap 2 §; corrections require /reverse or /correct.
|
|
**Do not use for:** Re-committing an already-posted entry (returns 409). Committing across companies: the URL companyId must match the draft's company.
|
|
|
|
**Pitfalls:**
|
|
- Idempotency-Key is mandatory.
|
|
- Posted entries cannot be edited. Plan the lines carefully or call /correct after commit if you need to change them.
|
|
- Voucher numbers are sequential within (fiscal_period_id, voucher_series). A commit failure (e.g. period locked between draft creation and commit) does not advance the sequence.
|
|
- If the key has an unattended commit limit, an entry above it returns 403 UNATTENDED_COMMIT_LIMIT_EXCEEDED and stays a draft for a human to commit. Do not split it into smaller entries: one affärshändelse is one verifikat (BFL 5 kap. 6 §).
|
|
|
|
| Parameter | In | Type | Required | Notes |
|
|
|---|---|---|---|---|
|
|
| `companyId` | path | `string` | yes | |
|
|
| `id` | path | `string` | yes | |
|
|
|
|
Response `200`:
|
|
```ts
|
|
{
|
|
data: { id: string, voucher_series: string, voucher_number: number, status: "posted", entry_date: string },
|
|
meta: {
|
|
request_id: string,
|
|
api_version: string,
|
|
next_cursor?: string,
|
|
audit?: { voucher_number?: string, voucher_url?: string, audit_trail_url?: string, immutable_at?: string },
|
|
partial_expansions?: string[],
|
|
coverage?: Record<string, unknown>
|
|
}
|
|
}
|
|
```
|
|
|
|
Example response `200`:
|
|
```json
|
|
{
|
|
"data": {
|
|
"id": "0e9c…",
|
|
"voucher_series": "A",
|
|
"voucher_number": 143,
|
|
"status": "posted",
|
|
"entry_date": "2026-05-12"
|
|
},
|
|
"meta": {
|
|
"request_id": "req_…",
|
|
"api_version": "2026-05-12"
|
|
}
|
|
}
|
|
```
|
|
|
|
---
|
|
|
|
### `POST /api/v1/companies/{companyId}/journal-entries/{id}/correct`
|
|
|
|
**Correct a posted journal entry (BFL 5:5 storno-then-replace).**
|
|
`scope:bookkeeping:write · risk:high · idempotent · dry-run`
|
|
|
|
Per Bokföringslagen 5 kap 5 §, posted entries cannot be modified. This endpoint creates the canonical correction trail: a storno reversing the original, then a new entry with the corrected lines. All three are visible in the verifikationsserie and linked via reverses_id / reversed_by_id / correction_of_id. Idempotent. Dry-runnable.
|
|
|
|
**Use when:** You need to amend a posted verifikation. Use this rather than /reverse when the entry is being REPLACED with new lines: /reverse just nullifies.
|
|
**Do not use for:** Drafts (no voucher_number: cancel via dashboard). Already-corrected entries (the chain only supports one correction; correct the latest in the chain).
|
|
|
|
**Pitfalls:**
|
|
- Idempotency-Key is mandatory.
|
|
- The new lines must balance. JOURNAL_ENTRY_NOT_BALANCED if not.
|
|
- The original's entry_date and fiscal_period_id are inherited. If the original's period has been locked since posting, the call returns PERIOD_LOCKED.
|
|
- Three voucher numbers are advanced in this call: the original (already burned), the reversal, and the corrected. The series stays unbroken.
|
|
- A chain 3+ corrections deep returns CORRECTION_CHAIN_TOO_DEEP (409). Compute the net effect of the whole chain and book ONE correction, or pass allow_deep_chain=true to override.
|
|
|
|
| Parameter | In | Type | Required | Notes |
|
|
|---|---|---|---|---|
|
|
| `companyId` | path | `string` | yes | |
|
|
| `id` | path | `string` | yes | |
|
|
|
|
Request body:
|
|
```ts
|
|
{
|
|
description?: string,
|
|
lines: { account_number: string, debit_amount?: number, credit_amount?: number, line_description?: string, currency?: string, amount_in_currency?: number, exchange_rate?: number, tax_code?: string, dimensions?: Record<string, string>, cost_center?: string, project?: string }[],
|
|
allow_deep_chain?: boolean
|
|
}
|
|
```
|
|
|
|
Example request:
|
|
```json
|
|
{
|
|
"lines": [
|
|
{
|
|
"account_number": "6570",
|
|
"debit_amount": 75,
|
|
"credit_amount": 0,
|
|
"line_description": "Bankavgift (rättad)"
|
|
},
|
|
{
|
|
"account_number": "1930",
|
|
"debit_amount": 0,
|
|
"credit_amount": 75,
|
|
"line_description": "Företagskonto"
|
|
}
|
|
]
|
|
}
|
|
```
|
|
|
|
Response `200`:
|
|
```ts
|
|
{
|
|
data: {
|
|
reversal_id: string,
|
|
corrected_id: string,
|
|
original_id: string,
|
|
voucher_series: string,
|
|
reversal_voucher_number: number,
|
|
corrected_voucher_number: number
|
|
},
|
|
meta: {
|
|
request_id: string,
|
|
api_version: string,
|
|
next_cursor?: string,
|
|
audit?: { voucher_number?: string, voucher_url?: string, audit_trail_url?: string, immutable_at?: string },
|
|
partial_expansions?: string[],
|
|
coverage?: Record<string, unknown>
|
|
}
|
|
}
|
|
```
|
|
|
|
Example response `200`:
|
|
```json
|
|
{
|
|
"data": {
|
|
"reversal_id": "4d2a…",
|
|
"corrected_id": "7b3a…",
|
|
"original_id": "0e9c…",
|
|
"voucher_series": "A",
|
|
"reversal_voucher_number": 144,
|
|
"corrected_voucher_number": 145
|
|
},
|
|
"meta": {
|
|
"request_id": "req_…",
|
|
"api_version": "2026-05-12"
|
|
}
|
|
}
|
|
```
|
|
|
|
---
|
|
|
|
### `POST /api/v1/companies/{companyId}/journal-entries/{id}/reverse`
|
|
|
|
**Storno a posted journal entry.**
|
|
`scope:bookkeeping:write · risk:high · idempotent · dry-run`
|
|
|
|
Creates a reversing journal entry that nullifies the original. The original remains posted and visible: the reversal links via reverses_id and the original is annotated reversed_by_id. The reversal carries its own voucher_number in the same series so the löpnummer chain stays unbroken (BFL 5 kap 5-7 §§).
|
|
|
|
**Use when:** A posted entry needs to be cancelled and there is no replacement coming: e.g. a duplicate booking, an entry posted to the wrong period. Use /correct instead when you need to replace the entry with corrected lines.
|
|
**Do not use for:** Cancelling a draft (drafts have no voucher_number; cancel via the dashboard). Reversing an already-reversed entry (returns ENTRY_ALREADY_REVERSED).
|
|
|
|
**Pitfalls:**
|
|
- Idempotency-Key is mandatory.
|
|
- reversal_date defaults to today; the reversal is posted in the fiscal period covering that date. If today's period is locked the call returns PERIOD_LOCKED.
|
|
- You cannot reverse a draft (status must be posted). Use /correct after commit if the original needs replacing.
|
|
- Reversing an entry 3+ links deep in a correction chain returns CORRECTION_CHAIN_TOO_DEEP (409). Book ONE net-effect correction instead, or pass allow_deep_chain=true to override.
|
|
|
|
| Parameter | In | Type | Required | Notes |
|
|
|---|---|---|---|---|
|
|
| `companyId` | path | `string` | yes | |
|
|
| `id` | path | `string` | yes | |
|
|
|
|
Request body:
|
|
```ts
|
|
{ reversal_date?: string, allow_deep_chain?: boolean }
|
|
```
|
|
|
|
Example request:
|
|
```json
|
|
{
|
|
"reversal_date": "2026-05-13"
|
|
}
|
|
```
|
|
|
|
Response `200`:
|
|
```ts
|
|
{
|
|
data: {
|
|
reversal_id: string,
|
|
original_id: string,
|
|
voucher_series: string,
|
|
voucher_number: number,
|
|
entry_date: string,
|
|
status: "posted"
|
|
},
|
|
meta: {
|
|
request_id: string,
|
|
api_version: string,
|
|
next_cursor?: string,
|
|
audit?: { voucher_number?: string, voucher_url?: string, audit_trail_url?: string, immutable_at?: string },
|
|
partial_expansions?: string[],
|
|
coverage?: Record<string, unknown>
|
|
}
|
|
}
|
|
```
|
|
|
|
Example response `200`:
|
|
```json
|
|
{
|
|
"data": {
|
|
"reversal_id": "4d2a…",
|
|
"original_id": "0e9c…",
|
|
"voucher_series": "A",
|
|
"voucher_number": 144,
|
|
"entry_date": "2026-05-13",
|
|
"status": "posted"
|
|
},
|
|
"meta": {
|
|
"request_id": "req_…",
|
|
"api_version": "2026-05-12"
|
|
}
|
|
}
|
|
```
|
|
|
|
---
|
|
|
|
### `POST /api/v1/companies/{companyId}/journal-entries/batch-create`
|
|
|
|
**Create up to 50 draft journal entries (partial-success).**
|
|
`scope:bookkeeping:write · risk:high · idempotent · dry-run · reversible`
|
|
|
|
Bulk-create endpoint mirroring /invoices/bulk-create and /suppliers/bulk-create. Each entry is validated and inserted independently: per-item failures do not roll back items that succeeded. Returns DRAFTS only; commit each separately. Idempotent over the whole batch. Dry-runnable.
|
|
|
|
**Use when:** You're replaying historical bookkeeping from another system, or batching a set of manual verifikationer from a spreadsheet. Use dry-run first to validate the batch.
|
|
**Do not use for:** Committing posted entries: use POST /{id}/commit per entry. Transactional all-or-nothing imports: passing all_or_nothing: true returns 501 NOT_IMPLEMENTED.
|
|
|
|
**Pitfalls:**
|
|
- Idempotency-Key is mandatory and covers the WHOLE batch.
|
|
- all_or_nothing: true returns 501 NOT_IMPLEMENTED. Today only partial-success batches exist.
|
|
- Each entry must balance independently. Per-item JOURNAL_ENTRY_NOT_BALANCED appears in the results array.
|
|
|
|
| Parameter | In | Type | Required | Notes |
|
|
|---|---|---|---|---|
|
|
| `companyId` | path | `string` | yes | |
|
|
|
|
Request body:
|
|
```ts
|
|
{
|
|
journal_entries: { fiscal_period_id: string, entry_date: string, description: string, source_type?: "manual" | "bank_transaction" | "invoice_created" | "invoice_paid" | "invoice_cash_payment" | "credit_note" | "salary_payment" | "opening_balance" | "year_end" | "storno" | "correction" | "import" | "system" | "inbox_item" | "supplier_invoice_registered" | "supplier_invoice_paid" | "supplier_invoice_cash_payment" | "supplier_invoice_privately_paid" | "supplier_credit_note" | "currency_revaluation" | "reminder_fee" | "accrual" | "result_appropriation" | "rot_rut_payout" | "vat_settlement" | "stripe_payout" | "webshop_order", source_id?: string, voucher_series?: string, notes?: string, lines: { account_number: string, debit_amount?: number, credit_amount?: number, line_description?: string, currency?: string, amount_in_currency?: number, exchange_rate?: number, tax_code?: string, dimensions?: Record<string, string>, cost_center?: string, project?: string }[] }[],
|
|
all_or_nothing?: boolean
|
|
}
|
|
```
|
|
|
|
Example request:
|
|
```json
|
|
{
|
|
"journal_entries": [
|
|
{
|
|
"fiscal_period_id": "a8f1…",
|
|
"entry_date": "2026-05-12",
|
|
"description": "Bankavgift",
|
|
"lines": [
|
|
{
|
|
"account_number": "6570",
|
|
"debit_amount": 50,
|
|
"credit_amount": 0
|
|
},
|
|
{
|
|
"account_number": "1930",
|
|
"debit_amount": 0,
|
|
"credit_amount": 50
|
|
}
|
|
]
|
|
}
|
|
]
|
|
}
|
|
```
|
|
|
|
Response `200`:
|
|
```ts
|
|
{
|
|
data: {
|
|
results: { ok: boolean, request_index: number, data?: unknown, error?: { code: string, message: string, details?: unknown } }[],
|
|
summary: { total: number, succeeded: number, failed: number }
|
|
},
|
|
meta: {
|
|
request_id: string,
|
|
api_version: string,
|
|
next_cursor?: string,
|
|
audit?: { voucher_number?: string, voucher_url?: string, audit_trail_url?: string, immutable_at?: string },
|
|
partial_expansions?: string[],
|
|
coverage?: Record<string, unknown>
|
|
}
|
|
}
|
|
```
|
|
|
|
Example response `200`:
|
|
```json
|
|
{
|
|
"data": {
|
|
"results": [
|
|
{
|
|
"ok": true,
|
|
"request_index": 0,
|
|
"data": {
|
|
"id": "0e9c…",
|
|
"status": "draft"
|
|
}
|
|
}
|
|
],
|
|
"summary": {
|
|
"total": 1,
|
|
"succeeded": 1,
|
|
"failed": 0
|
|
}
|
|
},
|
|
"meta": {
|
|
"request_id": "req_…",
|
|
"api_version": "2026-05-12"
|
|
}
|
|
}
|
|
```
|
|
|
|
---
|
|
|
|
### `POST /api/v1/companies/{companyId}/voucher-gap-explanations`
|
|
|
|
**Document a gap in the verifikationsserie (BFL 5 kap 6-7 §§).**
|
|
`scope:bookkeeping:write · risk:low · idempotent · dry-run`
|
|
|
|
Records an explanation for one or more missing voucher numbers in a series. Required when a number is unaccounted for during audit. Statutory basis: BFL 5 kap 6-7 §§ (verifikationsnummer i löpande följd utan luckor); BFNAR 2013:2 kap 8 § governs the systemdokumentation that surfaces the gap. Idempotent. Dry-runnable.
|
|
|
|
**Use when:** You're responding to a voucher-gap audit finding and need to document the cause. Also used by migration flows that claim numbers without filling them.
|
|
**Do not use for:** Falsifying a series: every gap MUST have a genuine explanation. The dashboard surfaces these for auditor review.
|
|
|
|
**Pitfalls:**
|
|
- Idempotency-Key is mandatory.
|
|
- gap_end must be >= gap_start; a single-number gap has gap_start = gap_end.
|
|
- voucher_series is a single uppercase letter (A-Z); the same series + period + numeric range must not already exist.
|
|
|
|
| Parameter | In | Type | Required | Notes |
|
|
|---|---|---|---|---|
|
|
| `companyId` | path | `string` | yes | |
|
|
|
|
Request body:
|
|
```ts
|
|
{
|
|
fiscal_period_id: string,
|
|
voucher_series: string,
|
|
gap_start: number,
|
|
gap_end: number,
|
|
explanation: string
|
|
}
|
|
```
|
|
|
|
Example request:
|
|
```json
|
|
{
|
|
"fiscal_period_id": "a8f1…",
|
|
"voucher_series": "A",
|
|
"gap_start": 142,
|
|
"gap_end": 145,
|
|
"explanation": "Migration from previous bookkeeping system on 2026-05-12: series A148-onwards corresponds to the new Accounted numbering; numbers A142-A145 were assigned in the legacy system to manual paper vouchers archived offline (BFL 7 kap retention applies). Paper vouchers are stored in the company archive under reference 2026-PAPER-Q2."
|
|
}
|
|
```
|
|
|
|
Response `200`:
|
|
```ts
|
|
{
|
|
data: {
|
|
id: string,
|
|
fiscal_period_id: string,
|
|
voucher_series: string,
|
|
gap_start: number,
|
|
gap_end: number,
|
|
explanation: string,
|
|
created_at: string
|
|
},
|
|
meta: {
|
|
request_id: string,
|
|
api_version: string,
|
|
next_cursor?: string,
|
|
audit?: { voucher_number?: string, voucher_url?: string, audit_trail_url?: string, immutable_at?: string },
|
|
partial_expansions?: string[],
|
|
coverage?: Record<string, unknown>
|
|
}
|
|
}
|
|
```
|
|
|
|
Example response `200`:
|
|
```json
|
|
{
|
|
"data": {
|
|
"id": "0e9c…",
|
|
"voucher_series": "A",
|
|
"gap_start": 142,
|
|
"gap_end": 145
|
|
},
|
|
"meta": {
|
|
"request_id": "req_…",
|
|
"api_version": "2026-05-12"
|
|
}
|
|
}
|
|
```
|