* fix(enable-banking): read BBAN from AccountIdentification.other and store it on the account Enable Banking has no top-level `bban` key on AccountIdentification: a Swedish BBAN (clearing + account number) arrives as `other.identification` with `other.scheme_name = 'BBAN'`, or in `all_account_ids`. The client typed `bban?: string` and read `.bban`, so the value was always undefined: no connected account ever carried its clearing + account number, and domestic counterparty accounts on transactions were dropped. Type the identifiers per the OpenAPI spec, add extractBban() and pickAccountIdentifier(), read counterparty identifiers through the scheme list (IBAN, then BBAN/BGNR/PGNR, then anything), and store `bban` on StoredAccount from the OAuth callback. The external_id dedup scope stays IBAN-then-uid and is untouched. Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01UaZTY21HVN57hJoPXKSLjV * feat(invoices): named payee accounts on cash_accounts with a default per currency A company had exactly one set of payment instructions per invoice currency (company_settings.invoice_payment_accounts), picked by currency alone. A second SEK bank account, or a second bankgiro number, had nowhere to live. cash_accounts is already the per-company bank-account entity. Migration 20260903150000 adds the payee fields (bankgiro, plusgiro, clearing + account number, BBAN, BIC, Swish, foreign routing) plus invoice_payee, a small invoice_payee_defaults table (one default account per currency; one account may be the default for several currencies, a SEK account with an IBAN is the usual EUR payee), and a SECURITY DEFINER mirror that rewrites the legacy map and the SEK bank columns from the default accounts. Every existing reader (PDF, email, reminders, v1, MCP) keeps working; the three writers that only touched legacy columns (PUT /api/settings, v1 settings, MCP update_company_settings) now write through to the default account, so what an agent sets is what the PDF prints. Peppol PaymentMeans is built from the resolver instead of the raw legacy column. bg_pg is dropped (never read or written; NULL on every prod and staging row). Backfill lands only on existing cash accounts (primary, IBAN match, or the only enabled account in the currency). Entries with no target stay in the map as the resolver fallback and get an attach action in settings. New: POST /api/cash-accounts (manual bank account on the next free 19xx), PATCH /api/cash-accounts/[id] payee fields (owner/admin), GET/PUT /api/cash-accounts/payee-defaults. Settings page rewritten as an account list with per-currency defaults. Behandlingshistorik and the full archive cover the new table and columns. Verified on staging: migration applied (11 defaults landed), mirror trigger observed rewriting company_settings from a payee edit. Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01UaZTY21HVN57hJoPXKSLjV * feat(invoices): choose which bank account an invoice is paid to, frozen at issue Migration 20260903160000 adds invoices.payment_cash_account_id (FK to cash_accounts, SET NULL) and invoices.payment_details, the payee fields frozen when the account is chosen and refreshed at issue. Resolver: resolveInvoicePaymentAccount / companyWithInvoicePaymentAccount / assertInvoicePaymentAccountForRender take an optional override, and hasRequiredInvoicePaymentAccount reads it from the invoice row, so every surface (PDF, Swish QR, email, reminders, payment confirmation, Peppol, recurring, staged MCP send) prints the frozen payee when one exists and the company default per currency otherwise. Invoices that never chose an account behave exactly as before. Issue paths (mark-sent, send, v1 send, v1 mark-sent, Peppol send, recurring, MCP send and mark-sent) refresh the snapshot from the account as it is at issue; a chosen account that is disabled, un-flagged or unusable for the currency blocks with INVOICE_SEND_PAYMENT_ACCOUNT_INVALID. Writers: dashboard POST/PATCH, v1 create/update and MCP create_invoice accept payment_cash_account_id and validate it against the company's payee accounts (INVOICE_PAYEE_ACCOUNT_INVALID). Credit notes inherit the original's payee; copies carry the choice; preview-pdf renders the chosen account. The editor shows "Betalas till" under the currency when the company has two or more usable payee accounts for that currency. Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01UaZTY21HVN57hJoPXKSLjV * feat(invoices): book manual payments on the invoice's chosen bank account Manual mark-paid (dashboard, v1, MCP gnubok_mark_invoice_as_paid) and the booking dialog's proposed lines debited 1930 regardless of which bank account the invoice asked to be paid to. They now resolve the chosen payee account's ledger account (resolveInvoiceSettlementAccount) and fall back to 1930 only when no account was chosen or the row is gone. Bank-transaction matching keeps debiting the account the money landed on and does not filter by the chosen account; between equal-confidence candidates it prefers the invoice that asked to be paid to the landing account. Scores are untouched, so nothing new auto-matches. Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01UaZTY21HVN57hJoPXKSLjV * chore(invoices): keep the payload-size and phantom-column ceilings after the payee work Shorten the new gnubok_create_invoice argument description (tools/list payload was 29 bytes over the 60 kB budget), inline the cash-account payee UPDATE/INSERT payloads and the settings select strings as literals so the phantom-column scanner can read their columns, and reuse ACCOUNT_NUMBER_RE instead of a hand-rolled copy. Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01UaZTY21HVN57hJoPXKSLjV * fix(invoices): harden the payee model after review (admin-only payee columns, separate payee IBAN, company-scoped FK) Review findings from CodeRabbit, Superagent, the Swedish accounting review and three skeptic passes, resolved in one batch: Schema (both migrations are unshipped and edited in place): - cash_accounts.payee_iban: the printed IBAN is its own column. iban stays the bank identity written by every sync and used to re-pair on reconnect, so a sync can no longer rewrite an invoice instruction or resurrect a cleared IBAN. The backfill copies each currency entry verbatim onto the target account (IBAN match first, then primary), so every invoice keeps printing exactly what it printed before; the bank IBAN is never pushed onto invoices that did not carry one. - Payee columns are owner/admin-only at the database (BEFORE trigger, service role exempt): cash_accounts is member-writable for bank sync, and the SECURITY DEFINER mirror would otherwise have let a member rewrite where customers pay. - Revoking an account as payee or disabling it drops its defaults; deleting a default drops that currency from the map and clears the legacy SEK columns (an admin saying "nothing to print" must not keep printing a closed account). The mirror leaves the legacy SEK columns alone when the map has no SEK entry, so legacy-only companies are never wiped by a mirror run for another currency. - Audit and mirror triggers fire on the same column set; anon and authenticated can no longer execute the trigger-only definer functions. - invoices.payment_cash_account_id is a composite same-company FK with SET NULL scoped to the account column. Code: - Only 19xx bank accounts can be payee: PATCH, the defaults PUT (which now also requires enabled, payee-flagged and usable for the currency), resolveInvoicePayeeChoice, and the mark-paid settlement resolver (which also refuses disabled rows and logs every fallback to 1930). - createManualBankAccount excludes every ledger slot any row already holds (findFreeLedgerAccount treats a manual holder as free; this path inserts). - The legacy settings writers (PUT /api/settings, v1, MCP) write through to the account BEFORE updating company_settings and fail the request on error; the account is written before it is adopted as default so the mirror never sees an empty payee. - snapshotInvoicePayee: dry runs no longer persist; a failed snapshot write blocks issue (INVOICE_PAYEE_SNAPSHOT_FAILED). v1 mark-sent/mark-paid projections carry the payee columns; v1 create validates the payee before the dry-run return and echoes it in the preview. - pickAccountIdentifier: supplementary IBAN wins over a primary BBAN, and non-account schemes (card PANs) are never persisted. - Editor shows the payee select for a single usable account with no default; the booking dialog waits for cash accounts before proposing lines; a failed default write no longer hides a created account. - Behandlingshistorik names the account on created/deleted defaults. - Regenerated skills/accounted-api; MCP argument description trimmed under the tools/list payload ceiling. Declined: clearing legacy columns via a forward migration (the mirror now does it on delete); Swedish review's "show the debit account in the mark-paid UI" (the booking dialog already proposes and lets the user edit the debit line); manual ledger collision (UNIQUE exists, and the create path now rejects it with a clear error); Peppol aligning to the PDF value for companies whose legacy column had drifted from the map (the PDF is the customer-facing document; both now agree). Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01UaZTY21HVN57hJoPXKSLjV * fix(invoices): read NEW.invoice_payee only on the cash_accounts branch of the mirror trigger trg_mirror_invoice_payee_defaults fires for both tables; plpgsql resolves record fields per expression, so the combined condition failed with "record new has no field invoice_payee" whenever a default row changed, which took down every pg-real case on the payee tables. The revoke/disable check now sits inside its own TG_TABLE_NAME branch. The MCP settings executor test mocks the payee write-through like the settings route test already does. Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01UaZTY21HVN57hJoPXKSLjV * fix(invoices): keep member disables from revoking payee defaults, gate payee on 1920-1999, fit the MCP payload Cycle 3 of /resolve-pr on #2233. Superagent P1: the SECURITY DEFINER mirror trigger deleted an admin's invoice_payee_defaults rows whenever cash_accounts.enabled flipped to false, and enabled is member-writable (the bank picker's "Synkas ej"), so a member could undo an admin's payee decision. The trigger now drops defaults only on the admin-only invoice_payee true -> false revoke; the mirror trigger's WHEN no longer lists enabled. Disabled accounts stay out of the pick lists and the send gate already refuses an invoice that chose one. Applied to staging as the same function + trigger definition and probed inside a rolled-back block: disable keeps the default and the mirrored bankgiro, revoke clears both. pg-real: the admin-guard test ran three expectations inside one withUserContext transaction; the first raise aborted it and the next statement failed with "current transaction is aborted". One transaction per expectation now, and the member case also flips enabled to prove the column stays member-level. Swedish review: payee eligibility was /^19\d\d$/, which admits 1910 Kassa and the 1911-1919 tills. A customer pays to a giro or bank account, so isBankCashAccount, CreateCashAccountSchema.ledger_account and the PATCH route now require BAS 1920-1999; tests cover 1910 and 1919. Unit tests (3/4): the tools/list payload guard read 60 025, then 60 014 tokens after main merged #2166 and #2163 alongside this branch. The ceiling is not bumped and no read on this surface is a demotion candidate, so gnubok_create_invoice drops payment_cash_account_id; agent-created invoices print the per-currency default and v1 REST plus the editor keep the field. Recorded in DECISIONS.md. Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01UaZTY21HVN57hJoPXKSLjV * chore(migrations): move invoices_payment_cash_account to 20260903183000 after colliding with main's KPI migration origin/main merged 20260903160000_kpi_monthly_include_reversed_originals while this branch held the same version; identical versions abort the Supabase apply. Staging's schema_migrations row was moved to the new version with the file. Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01UaZTY21HVN57hJoPXKSLjV * fix(invoices): gate invoice_payee on BAS 1920-1999 at the database, and unblock the typecheck ratchet Cycle 4 of /resolve-pr on #2233, on Emil's go. Swedish review: the 1920-1999 payee rule lived only in the routes. The cash_accounts_payee_admin_only trigger now also refuses invoice_payee on any other ledger (INVOICE_PAYEE_ACCOUNT_INVALID, 23514), whoever writes it, and the backfill only targets giro/bank rows, so a company whose single enabled cash_accounts row is a Stripe clearing account keeps its legacy bankgiro in company_settings instead of landing it on 1686. pg test covers insert and update on 1686 and 1910; the function was applied to staging and probed. Typecheck ratchet: main is red from two merges that landed with failing Checks, and every branch that syncs it inherits the errors. - #2242 added POST(req) calls to the fiscal-periods route test without the route params argument withRouteContext handlers take (25 errors in the file, baseline 23). All 25 calls now pass createMockRouteParams({}). - #2247 made SyncResult.requestedFromDate and historyNarrowed required; the 13 mockedSync results in the enable-banking accounts-route test lacked them. They now carry a fixed date and historyNarrowed: false. Both files' tests pass unchanged in behaviour. Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com> * chore(migrations): move invoices_payment_cash_account to 20260903193000 after colliding with main's party_promotion origin/main merged 20260903183000_party_promotion while this branch held the same version. Staging's schema_migrations row must follow (pending: the Supabase MCP was disconnected at the time of this commit). Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com> --------- Co-authored-by: Claude Fable 5.1 <noreply@anthropic.com>
4327 lines
139 KiB
TypeScript
4327 lines
139 KiB
TypeScript
// Entity types
|
||
export type EntityType = 'enskild_firma' | 'aktiebolag'
|
||
|
||
// Swedish accounting framework. K2 (BFNAR 2016:10) is the default simplified
|
||
// ruleset for smaller AB; K3 (BFNAR 2012:1) is the principles-based ruleset
|
||
// required for medium-to-large AB and permitted voluntarily for smaller ones.
|
||
// Only meaningful for entity_type='aktiebolag'.
|
||
export type AccountingFramework = 'k2' | 'k3'
|
||
|
||
// Company role for multi-tenant access
|
||
export type CompanyRole = 'owner' | 'admin' | 'member' | 'viewer'
|
||
|
||
// Team (consulting firm grouping). 'personal' teams are the implicit
|
||
// one-per-user grouping; 'byra' teams are ops-created accounting-firm
|
||
// tenants (WL-08) with invites, a brand, and cockpit access.
|
||
export interface Team {
|
||
id: string
|
||
name: string
|
||
kind: 'personal' | 'byra'
|
||
created_by: string
|
||
created_at: string
|
||
updated_at: string
|
||
}
|
||
|
||
// Company (multi-tenant identity)
|
||
export interface Company {
|
||
id: string
|
||
name: string
|
||
org_number: string | null
|
||
entity_type: EntityType
|
||
accounting_framework: AccountingFramework
|
||
created_by: string
|
||
team_id: string | null
|
||
archived_at: string | null
|
||
created_at: string
|
||
updated_at: string
|
||
// Denormalised from company_settings onto the active company in the
|
||
// dashboard layout so context consumers (e.g. the settings rail) can tell
|
||
// whether the company is a registered employer without an extra fetch.
|
||
// Optional because it isn't a column on `companies`. #782
|
||
pays_salaries?: boolean
|
||
}
|
||
|
||
// Company membership
|
||
export interface CompanyMember {
|
||
id: string
|
||
company_id: string
|
||
user_id: string
|
||
role: CompanyRole
|
||
invited_by: string | null
|
||
joined_at: string
|
||
created_at: string
|
||
updated_at: string
|
||
}
|
||
|
||
export const COMPANY_MIGRATION_RESET_COUNT_KEYS = [
|
||
'journal_entries',
|
||
'journal_entry_lines',
|
||
'committed_import_entries',
|
||
'transactions',
|
||
'fiscal_periods',
|
||
'documents',
|
||
'voucher_sequences',
|
||
'sie_imports',
|
||
'bank_file_imports',
|
||
'skattekonto_file_imports',
|
||
'customers',
|
||
'suppliers',
|
||
'invoices',
|
||
'supplier_invoices',
|
||
'bank_connections',
|
||
] as const
|
||
|
||
export type CompanyMigrationResetCountKey =
|
||
(typeof COMPANY_MIGRATION_RESET_COUNT_KEYS)[number]
|
||
|
||
export type CompanyMigrationResetBlockerCode =
|
||
| 'company_not_found'
|
||
| 'company_already_archived'
|
||
| 'migration_window_expired'
|
||
| 'sandbox_company'
|
||
| 'locked_or_closed_periods'
|
||
| 'authority_submission_detected'
|
||
| 'live_bank_connections'
|
||
| 'imports_in_progress'
|
||
| 'active_integrations_or_schedules'
|
||
| 'background_work_in_progress'
|
||
|
||
export interface CompanyMigrationResetBlocker {
|
||
code: CompanyMigrationResetBlockerCode
|
||
count: number
|
||
}
|
||
|
||
export interface CompanyMigrationResetEligibility {
|
||
eligible: boolean
|
||
display_name: string
|
||
created_at: string
|
||
window_ends_at: string
|
||
counts: Record<CompanyMigrationResetCountKey, number>
|
||
blockers: CompanyMigrationResetBlocker[]
|
||
}
|
||
|
||
export interface CompanyMigrationResetRpcResult {
|
||
ok: boolean
|
||
code?: string
|
||
details?: unknown
|
||
eligibility?: CompanyMigrationResetEligibility
|
||
reset_id?: string
|
||
source_company_id?: string
|
||
replacement_company_id?: string
|
||
archived_at?: string
|
||
counts?: CompanyMigrationResetEligibility['counts']
|
||
}
|
||
|
||
// Fiscal-year reset (issue #1883): guarded hard-delete of one OPEN fiscal
|
||
// year's vouchers. Mirrors the migration-reset envelope shapes above.
|
||
export type FiscalYearResetBlockerCode =
|
||
| 'period_closed'
|
||
| 'period_locked'
|
||
| 'company_lock_date'
|
||
| 'year_end_state'
|
||
| 'arsredovisning_state'
|
||
| 'next_year_dependency'
|
||
| 'vat_declared'
|
||
| 'agi_declared'
|
||
| 'rot_rut_state'
|
||
| 'cross_year_reference'
|
||
|
||
export interface FiscalYearResetBlocker {
|
||
code: FiscalYearResetBlockerCode
|
||
count?: number
|
||
date?: string
|
||
}
|
||
|
||
export interface FiscalYearResetEligibility {
|
||
eligible: boolean
|
||
blockers: FiscalYearResetBlocker[]
|
||
period: {
|
||
id: string
|
||
name: string
|
||
period_start: string
|
||
period_end: string
|
||
}
|
||
counts: {
|
||
vouchers: number
|
||
documents_to_detach: number
|
||
}
|
||
}
|
||
|
||
export interface FiscalYearResetRpcResult {
|
||
ok: boolean
|
||
code?: string
|
||
eligible?: boolean
|
||
blockers?: FiscalYearResetBlocker[]
|
||
period?: FiscalYearResetEligibility['period']
|
||
counts?: FiscalYearResetEligibility['counts']
|
||
deleted?: number
|
||
detached_documents?: number
|
||
period_name?: string
|
||
}
|
||
|
||
// Shape of user_preferences.ui_state. All fields optional: the bag grows
|
||
// as UI surfaces add preferences (UI migration plan PR 2/3).
|
||
export interface UserUiState {
|
||
nav_collapsed?: boolean
|
||
nav_folds?: {
|
||
register?: boolean
|
||
bokslut?: boolean
|
||
}
|
||
// Split-button last-used create modes, keyed per surface (plan PR 3/4),
|
||
// e.g. create_mode.bookkeeping = 'mall'.
|
||
create_mode?: Record<string, string>
|
||
// Assistant panel geometry (components/agent/AgentSheet): docked width,
|
||
// undocked floating rect, and which of the two modes is active. Client
|
||
// re-clamps to the current viewport on read, so stale sizes from another
|
||
// screen are safe.
|
||
agent_panel?: AgentPanelState
|
||
// One-time expired-trial dialog acknowledgement, keyed per company
|
||
// (companyId -> ISO timestamp of the ack). Lives on the user so each
|
||
// member of a company sees the notice once.
|
||
trial_expired_ack?: Record<string, string>
|
||
}
|
||
|
||
export type AgentPanelMode = 'docked' | 'floating'
|
||
|
||
// Viewport pixels of the undocked assistant window.
|
||
export interface AgentPanelFloatRect {
|
||
x: number
|
||
y: number
|
||
w: number
|
||
h: number
|
||
}
|
||
|
||
export interface AgentPanelState {
|
||
mode?: AgentPanelMode
|
||
dock_width?: number
|
||
float?: AgentPanelFloatRect
|
||
}
|
||
|
||
// Transaction categories
|
||
export type TransactionCategory =
|
||
| 'income_services'
|
||
| 'income_products'
|
||
| 'income_other'
|
||
| 'expense_equipment'
|
||
| 'expense_software'
|
||
| 'expense_travel'
|
||
| 'expense_office'
|
||
| 'expense_marketing'
|
||
| 'expense_professional_services'
|
||
| 'expense_education'
|
||
| 'expense_representation'
|
||
| 'expense_consumables'
|
||
| 'expense_vehicle'
|
||
| 'expense_telecom'
|
||
| 'expense_bank_fees'
|
||
| 'expense_card_fees'
|
||
| 'expense_currency_exchange'
|
||
| 'expense_other'
|
||
| 'private'
|
||
| 'uncategorized'
|
||
|
||
// Customer types for VAT handling
|
||
export type CustomerType =
|
||
| 'individual' // Swedish private person
|
||
| 'swedish_business' // Swedish company
|
||
| 'eu_business' // EU company (needs VAT validation)
|
||
| 'non_eu_business' // Non-EU company
|
||
|
||
// Invoice status
|
||
export type InvoiceStatus = 'draft' | 'sent' | 'paid' | 'partially_paid' | 'overdue' | 'cancelled' | 'credited'
|
||
|
||
// Invoice document type
|
||
export type InvoiceDocumentType = 'invoice' | 'proforma' | 'delivery_note' | 'quote'
|
||
|
||
// Offert decision. Lives in invoices.quote_status for document_type 'quote'
|
||
// only; the lifecycle column `status` keeps meaning draft / sent / cancelled.
|
||
// "expired" is never stored: derive it with isQuoteExpired() (lib/invoices/quote-status.ts).
|
||
export type QuoteStatus = 'open' | 'accepted' | 'declined'
|
||
|
||
// Supplier types
|
||
export type SupplierType = 'swedish_business' | 'eu_business' | 'non_eu_business'
|
||
|
||
// Supplier invoice status
|
||
// 'reversed' marks a credit note whose journal entry was storno-reversed via
|
||
// "Ångra kreditering". The row is preserved (BFL 7 kap) rather than hard-deleted.
|
||
export type SupplierInvoiceStatus = 'registered' | 'approved' | 'paid' | 'partially_paid' | 'overdue' | 'disputed' | 'credited' | 'reversed'
|
||
|
||
// VAT treatment
|
||
export type VatTreatment =
|
||
| 'standard_25' // 25% Swedish VAT
|
||
| 'reduced_12' // 12% reduced rate
|
||
| 'reduced_6' // 6% reduced rate
|
||
| 'reverse_charge' // EU reverse charge (0%)
|
||
| 'export' // Non-EU export (0%)
|
||
| 'exempt' // VAT exempt
|
||
|
||
// Accounting method (bokföringsmetod)
|
||
export type AccountingMethod = 'accrual' | 'cash'
|
||
|
||
// Moms reporting period
|
||
export type MomsPeriod = 'monthly' | 'quarterly' | 'yearly'
|
||
export type TaxFilingMethod = 'electronic' | 'paper'
|
||
|
||
// Reconciliation method
|
||
export type ReconciliationMethod = 'auto_exact' | 'auto_date_range' | 'auto_reference' | 'auto_fuzzy' | 'manual'
|
||
|
||
// Processing history (behandlingshistorik): event-driven audit trail per BFNAR 2013:2 kap 8
|
||
|
||
export type ProcessingHistoryActorType = 'user' | 'system' | 'llm' | 'cron' | 'api_key'
|
||
|
||
export interface ProcessingHistoryActor {
|
||
type: ProcessingHistoryActorType
|
||
id: string
|
||
label?: string
|
||
}
|
||
|
||
export type ProcessingHistoryAggregateType =
|
||
| 'Document'
|
||
| 'BankTransaction'
|
||
| 'MatchProposal'
|
||
| 'Verifikation'
|
||
| 'CounterpartyTemplate'
|
||
| 'Period'
|
||
| 'Migration'
|
||
| 'System'
|
||
|
||
// Bank connection status
|
||
// 'pending_selection' = PSD2 consent granted, awaiting user to pick which
|
||
// accounts to actually sync. No transactions are pulled in this state.
|
||
export type BankConnectionStatus = 'pending' | 'pending_selection' | 'active' | 'expired' | 'revoked' | 'error'
|
||
|
||
// Currency types
|
||
export type Currency = 'SEK' | 'EUR' | 'USD' | 'GBP' | 'NOK' | 'DKK'
|
||
|
||
export interface InvoicePaymentAccount {
|
||
bank_name: string | null
|
||
clearing_number: string | null
|
||
account_number: string | null
|
||
bankgiro: string | null
|
||
plusgiro: string | null
|
||
swish: string | null
|
||
iban: string | null
|
||
bic: string | null
|
||
/**
|
||
* Foreign non-IBAN routing: ABA routing number (USD), sort code (GBP),
|
||
* BSB (AUD) or a comparable national bank code. Only meaningful together
|
||
* with foreign_account_number + bic on a non-SEK account.
|
||
*/
|
||
bank_code?: string | null
|
||
/** Foreign account number for non-IBAN countries (US/UK/AU/CA style). */
|
||
foreign_account_number?: string | null
|
||
}
|
||
|
||
// Editable invoice email texts (standard invoices only; sv + en).
|
||
// Missing / whitespace-only fields fall back to the hardcoded defaults in
|
||
// lib/email/invoice-templates.ts. Supports the fixed placeholder set
|
||
// {fakturanummer} {kundnamn} {förnamn} {företag} {förfallodatum} {belopp}.
|
||
export interface InvoiceEmailTextOverrides {
|
||
subject?: string
|
||
greeting?: string
|
||
body?: string
|
||
signoff?: string
|
||
}
|
||
|
||
export interface InvoiceEmailTexts {
|
||
sv?: InvoiceEmailTextOverrides
|
||
en?: InvoiceEmailTextOverrides
|
||
}
|
||
|
||
// Editable reminder email texts per reminder level (Swedish only, matching
|
||
// the reminder templates). Missing / whitespace-only fields fall back to the
|
||
// defaults in lib/email/reminder-templates.ts (REMINDER_EMAIL_DEFAULT_TEXTS).
|
||
// Supports the fixed placeholder set {fakturanummer} {kundnamn} {förnamn}
|
||
// {företag} {fakturadatum} {förfallodatum} {belopp} {dagar}. TEXT only:
|
||
// reminder fee and interest math are unaffected (Lag 1981:739 caps the
|
||
// påminnelseavgift at 60 kr; the 450 kr förseningsersättning is out of scope).
|
||
export interface ReminderTextOverride {
|
||
subject?: string
|
||
body?: string
|
||
}
|
||
|
||
export interface ReminderTextOverrides {
|
||
level_1?: ReminderTextOverride
|
||
level_2?: ReminderTextOverride
|
||
level_3?: ReminderTextOverride
|
||
}
|
||
|
||
export type InvoiceFontFamily =
|
||
| 'Helvetica'
|
||
| 'Times-Roman'
|
||
| 'Courier'
|
||
| 'Source Sans 3'
|
||
| 'Source Serif 4'
|
||
| 'Custom'
|
||
|
||
// Company Settings
|
||
export interface CompanySettings {
|
||
id: string
|
||
user_id: string
|
||
company_id: string
|
||
|
||
// Entity info
|
||
entity_type: EntityType
|
||
company_name: string | null
|
||
org_number: string | null
|
||
|
||
// Address
|
||
address_line1: string | null
|
||
address_line2: string | null
|
||
postal_code: string | null
|
||
city: string | null
|
||
country: string
|
||
|
||
// Contact
|
||
phone: string | null
|
||
email: string | null
|
||
website: string | null
|
||
|
||
// Tax registration
|
||
pays_salaries: boolean
|
||
// null = never attested; deadline generation falls back to pays_salaries.
|
||
employer_registered?: boolean | null
|
||
employer_seasonal?: boolean
|
||
f_skatt: boolean
|
||
vat_registered: boolean
|
||
vat_number: string | null
|
||
moms_period: MomsPeriod | null
|
||
periodisk_sammanstallning_period: 'monthly' | 'quarterly'
|
||
vat_taxable_base_over_40m: boolean
|
||
vat_has_eu_trade: boolean
|
||
vat_filing_method: TaxFilingMethod
|
||
periodisk_sammanstallning_enabled: boolean
|
||
periodisk_sammanstallning_filing_method: TaxFilingMethod
|
||
// Annual kontrolluppgifter (KU10/KU20/KU31) reminder, due 31 January.
|
||
kontrolluppgifter_enabled: boolean
|
||
// ROT/RUT begäran om utbetalning reminder, due 31 January after the
|
||
// payment year (Lag 2009:194 8 §). Rows are only generated for years
|
||
// that actually have paid ROT/RUT invoices.
|
||
rot_rut_enabled: boolean
|
||
// Long-tail deadlines, explicit opt-in only ("Fler deadlines" in tax
|
||
// settings). OSS/IOSS are EU-law deadlines that never move to the next
|
||
// banking day.
|
||
oss_enabled: boolean
|
||
ioss_enabled: boolean
|
||
intrastat_enabled: boolean
|
||
punktskatt_enabled: boolean
|
||
fyllnadsinbetalning_enabled: boolean
|
||
|
||
// Tax contact (SKV-filings, periodisk sammanställning, AGI, etc.)
|
||
tax_contact_name: string | null
|
||
tax_contact_phone: string | null
|
||
tax_contact_email: string | null
|
||
|
||
// Fiscal year
|
||
fiscal_year_start_month: number // 1-12
|
||
// Transient first-year fields (used during onboarding, not persisted in DB)
|
||
is_first_fiscal_year?: boolean
|
||
first_year_start?: string
|
||
first_year_end?: string
|
||
|
||
// Preliminary tax
|
||
preliminary_tax_monthly: number | null
|
||
|
||
// Share capital per Bolagsverket (aktiekapital note in the annual report).
|
||
// Kvotvärde is derived as aktiekapital / antal_aktier, never stored.
|
||
aktiekapital?: number | null
|
||
antal_aktier?: number | null
|
||
|
||
// Bank details for invoices
|
||
bank_name: string | null
|
||
clearing_number: string | null
|
||
account_number: string | null
|
||
bankgiro: string | null
|
||
plusgiro: string | null
|
||
swish: string | null
|
||
iban: string | null
|
||
bic: string | null
|
||
// Foreign non-IBAN routing, only ever populated on the render-time copy
|
||
// produced by companyWithInvoicePaymentAccount (never a DB column).
|
||
bank_code?: string | null
|
||
foreign_account_number?: string | null
|
||
// Invoice payment instructions keyed by the currency they can receive.
|
||
// Legacy bank fields above remain the SEK fallback for older companies.
|
||
invoice_payment_accounts?: Partial<Record<Currency, InvoicePaymentAccount>>
|
||
|
||
// Accounting method
|
||
accounting_method: AccountingMethod
|
||
// #967: when true (accrual only), registering supplier invoices / sending
|
||
// customer invoices does NOT book them; booking is a separate explicit step.
|
||
defer_invoice_booking?: boolean
|
||
|
||
// Invoice settings
|
||
invoice_prefix: string | null
|
||
next_invoice_number: number
|
||
// Starting ankomstnummer for the supplier-invoice (leverantorsfaktura)
|
||
// series. Acts as a floor: get_next_arrival_number returns
|
||
// GREATEST(MAX(arrival_number)+1, next_arrival_number). Defaults to 1.
|
||
next_arrival_number: number
|
||
next_delivery_note_number: number
|
||
// Offert series (OF-nnn), allocated at insert by generate_quote_number.
|
||
next_quote_number: number
|
||
invoice_default_days: number
|
||
invoice_default_notes: string | null
|
||
// Default "Vår referens": pre-fills the per-invoice our_reference field.
|
||
default_our_reference: string | null
|
||
|
||
// Bookkeeping lock
|
||
bookkeeping_locked_through: string | null
|
||
auto_lock_period_days: number | null
|
||
|
||
// Voucher series
|
||
default_voucher_series: string
|
||
/**
|
||
* Per-source-type default voucher series map. Keys are
|
||
* JournalEntrySourceType values; values are single uppercase letters A-Z.
|
||
* Resolved by `lib/bookkeeping/voucher-series-resolver.ts`. Defaults to
|
||
* all "A" entries; users can override per source via the bookkeeping
|
||
* settings UI.
|
||
*/
|
||
default_voucher_series_per_source_type: Partial<Record<JournalEntrySourceType, string>>
|
||
|
||
// Most recently picked BAS account for supplier invoice payments: used to
|
||
// default the mark-paid dialog so repeat payments don't force re-picking.
|
||
last_supplier_payment_account: string | null
|
||
|
||
// Invoice PDF settings
|
||
ore_rounding: boolean
|
||
invoice_show_ocr: boolean
|
||
invoice_show_bankgiro: boolean
|
||
invoice_show_plusgiro: boolean
|
||
invoice_show_swish: boolean
|
||
invoice_show_logo: boolean
|
||
invoice_show_company_name: boolean
|
||
invoice_company_name_position: 'header' | 'footer'
|
||
invoice_late_fee_text: string | null
|
||
invoice_credit_terms_text: string | null
|
||
|
||
// Opt-in for the invoice payment-link feature (default false): shows the
|
||
// payment-link field in the invoice editor and enables automatic Stripe
|
||
// payment links on send. Enforced server-side in
|
||
// lib/extensions/payment-links.ts, not just in the UI.
|
||
invoice_payment_links_enabled: boolean
|
||
|
||
// Invoice branding (per-company colors, font, optional header/footer text).
|
||
// Defaults preserve the legacy hardcoded palette so unbranded companies
|
||
// render identically to the pre-branding template.
|
||
invoice_primary_color: string // hex #RRGGBB, default '#1a1a1a'
|
||
invoice_accent_color: string // hex #RRGGBB, default '#666666'
|
||
invoice_font_family: InvoiceFontFamily
|
||
invoice_custom_font_path: string | null
|
||
invoice_custom_font_name: string | null
|
||
invoice_header_text: string | null
|
||
invoice_footer_text: string | null
|
||
|
||
// Editable invoice email texts. null = all defaults.
|
||
invoice_email_texts: InvoiceEmailTexts | null
|
||
// Fixed invoice-email recipients. null means the company has not configured
|
||
// the setting yet and keeps the historical automatic CC fallback. [] is an
|
||
// explicit choice to send no copies.
|
||
invoice_email_cc_addresses?: string[] | null
|
||
invoice_email_bcc_addresses?: string[] | null
|
||
|
||
// Automation
|
||
send_invoice_reminders: boolean
|
||
reminder_days_level_1: number
|
||
reminder_days_level_2: number
|
||
reminder_days_level_3: number
|
||
// Editable reminder email texts per level. null = all defaults.
|
||
reminder_text_overrides: ReminderTextOverrides | null
|
||
|
||
// Reminder surcharges (dröjsmålsränta + lagstadgad påminnelseavgift)
|
||
reminder_fee_enabled: boolean
|
||
reminder_fee_amount: number
|
||
reminder_interest_rate_override: number | null
|
||
|
||
// Logo
|
||
logo_url: string | null
|
||
|
||
// Onboarding
|
||
onboarding_step: number
|
||
onboarding_complete: boolean
|
||
initial_setup_path?: InitialSetupPath | null
|
||
initial_setup_completed_at?: string | null
|
||
initial_setup_dismissed_at?: string | null
|
||
|
||
// Sector
|
||
sector_slug: string | null
|
||
|
||
// Dimensions (kostnadsställe/projekt): UI-visibility toggle only, never
|
||
// load-bearing for correctness. Free tier (founder decision 2026-07-02).
|
||
dimensions_enabled: boolean
|
||
|
||
// Körjournal (mileage log): UI-visibility toggle only, never load-bearing
|
||
// for correctness. The nav row also shows when mileage_trips rows exist.
|
||
mileage_enabled: boolean
|
||
|
||
// Kundorder (sales orders): UI-visibility toggle only, never load-bearing
|
||
// for correctness (the /sales-orders pages and APIs work regardless).
|
||
sales_orders_enabled: boolean
|
||
// Per-company counter behind generate_sales_order_number (OR-<n>).
|
||
next_sales_order_number?: number
|
||
|
||
// Data analysis consent (migration 20260828120000): when true, the
|
||
// company's bookkeeping outcomes may be read across companies to evaluate
|
||
// and improve automatic booking. Default false, enforced server-side
|
||
// (lib/company/data-analysis.ts); the UI only mirrors it.
|
||
data_analysis_opt_in: boolean
|
||
|
||
// Salary payments (migration 20260508120000 + 20260703190000).
|
||
// preferred_payment_format defaults to 'pain001' — Bankgirot Lön is
|
||
// retired by the banks during 2026.
|
||
preferred_payment_format: 'bg_lb' | 'pain001'
|
||
salary_pay_day: number
|
||
salary_default_bank: 'swedbank' | 'seb' | 'handelsbanken' | 'nordea' | 'other' | null
|
||
// Öresavrundning (migration 20260813143000): round each net payout up to
|
||
// whole kronor; the 0-99 öre diff books on 3740 via a derived line item.
|
||
salary_net_rounding: boolean
|
||
|
||
// Sandbox
|
||
is_sandbox: boolean
|
||
|
||
// Timestamps
|
||
created_at: string
|
||
updated_at: string
|
||
}
|
||
|
||
// Bank Connection
|
||
export interface BankConnection {
|
||
id: string
|
||
user_id: string
|
||
company_id: string
|
||
|
||
bank_name: string
|
||
provider: string
|
||
|
||
// Enable Banking specific
|
||
session_id: string | null
|
||
authorization_id: string | null
|
||
|
||
// Account info
|
||
accounts_data: BankAccount[]
|
||
|
||
// Status
|
||
status: BankConnectionStatus
|
||
|
||
// PSD2 PSU type chosen at authorization. Reused on reconnect so consent
|
||
// renewals keep the account type that actually worked. NULL on legacy rows.
|
||
psu_type: 'personal' | 'business' | null
|
||
|
||
// Consent
|
||
consent_expires: string | null
|
||
last_synced_at: string | null
|
||
error_message: string | null
|
||
|
||
// Initial-sync metadata. initial_sync_completed_at gates the cron's
|
||
// first-sync 90-day backfill path independently of last_synced_at, so
|
||
// a manual "Sync now" doesn't permanently lose the deep backfill window.
|
||
// The returned-date columns power the "we requested X but got Y" UI when
|
||
// an ASPSP truncates history below the requested window.
|
||
initial_sync_completed_at: string | null
|
||
initial_sync_requested_from: string | null
|
||
initial_sync_returned_min_date: string | null
|
||
initial_sync_returned_max_date: string | null
|
||
initial_sync_lookback_days: number | null
|
||
|
||
created_at: string
|
||
updated_at: string
|
||
}
|
||
|
||
export interface BankAccount {
|
||
uid: string // Enable Banking account UID
|
||
iban: string | null
|
||
name: string | null
|
||
currency: Currency
|
||
balance: number | null
|
||
balance_updated_at?: string | null
|
||
}
|
||
|
||
// Cash account: first-class entity for ledger-account routing decisions.
|
||
// Backed by the cash_accounts table; bank_connections.accounts_data remains
|
||
// the source for PSD2 sync metadata + UI display until a follow-up migration
|
||
// drops it 30 days after this PR.
|
||
export type CashAccountSource = 'enable_banking' | 'manual' | 'sie_import'
|
||
|
||
/**
|
||
* What a customer pays to. Lives on cash_accounts (migration 20260903150000)
|
||
* and is the single source for the payee printed on customer invoices; the
|
||
* per-currency map on company_settings is a trigger-maintained mirror of the
|
||
* default account per currency.
|
||
*/
|
||
export interface CashAccountPayeeFields {
|
||
bank_name: string | null
|
||
clearing_number: string | null
|
||
account_number: string | null
|
||
bankgiro: string | null
|
||
plusgiro: string | null
|
||
swish: string | null
|
||
iban: string | null
|
||
bic: string | null
|
||
bank_code: string | null
|
||
foreign_account_number: string | null
|
||
}
|
||
|
||
export interface CashAccount extends CashAccountPayeeFields {
|
||
id: string
|
||
company_id: string
|
||
bank_connection_id: string | null
|
||
external_uid: string | null // PSD2 StoredAccount.uid
|
||
// Raw BBAN from the bank connection (Swedish: clearing + account number,
|
||
// no separator). Prefill only; clearing_number/account_number print.
|
||
bban: string | null
|
||
// The IBAN printed on customer invoices. Separate from `iban` (the bank's
|
||
// identity of the account, written by every sync and used to re-pair on
|
||
// reconnect) so a sync never rewrites an invoice instruction.
|
||
payee_iban: string | null
|
||
// True when the account may be printed as the payee on customer invoices.
|
||
invoice_payee: boolean
|
||
name: string | null
|
||
currency: string // 3-char ISO; broader than Currency union to
|
||
// tolerate future currencies without DB-driven enum drift
|
||
ledger_account: string
|
||
balance: number | null
|
||
available_balance: number | null
|
||
balance_updated_at: string | null
|
||
enabled: boolean
|
||
is_primary: boolean
|
||
source: CashAccountSource
|
||
// Optional verifikationsserie (single letter) for entries booked from this
|
||
// account. null = follow company_settings.default_voucher_series_per_source_type.
|
||
// See 20260902121420_cash_accounts_voucher_series.sql.
|
||
voucher_series: string | null
|
||
created_at: string
|
||
updated_at: string
|
||
}
|
||
|
||
/**
|
||
* Which cash account an invoice in `currency` prints as payee when the
|
||
* invoice does not choose one itself. One account may be the default for
|
||
* several currencies (a SEK account with an IBAN is the usual EUR payee).
|
||
*/
|
||
export interface InvoicePayeeDefault {
|
||
id: string
|
||
company_id: string
|
||
currency: Currency
|
||
cash_account_id: string
|
||
created_at: string
|
||
updated_at: string
|
||
}
|
||
|
||
/**
|
||
* Closed vocabulary for HOW money moved (the payment rail), classified at
|
||
* ingest by classifyTransactionMethod() (lib/transactions/transaction-method.ts).
|
||
* Mirrored by the transactions_transaction_method_check DB constraint
|
||
* (migration 20260808090000): keep the three in sync when adding a value.
|
||
*/
|
||
export const TRANSACTION_METHODS = [
|
||
'card',
|
||
'transfer',
|
||
'bankgiro',
|
||
'plusgiro',
|
||
'swish',
|
||
'autogiro',
|
||
'e_invoice',
|
||
'international',
|
||
'deposit',
|
||
'withdrawal',
|
||
'salary',
|
||
'fee',
|
||
'interest',
|
||
'adjustment',
|
||
] as const
|
||
|
||
export type TransactionMethod = (typeof TRANSACTION_METHODS)[number]
|
||
|
||
// Transaction
|
||
export interface Transaction {
|
||
id: string
|
||
user_id: string
|
||
company_id: string
|
||
|
||
// Source
|
||
bank_connection_id: string | null
|
||
external_id: string | null // For deduplication
|
||
|
||
// The cash account (cash_accounts row) this transaction settled on. Drives
|
||
// per-account bank reconciliation isolation and the correct bank leg when
|
||
// booking. Null on legacy/unresolved rows: callers fall back to currency.
|
||
// See 20260606120000_transactions_cash_account_id.sql.
|
||
cash_account_id: string | null
|
||
|
||
// Details
|
||
date: string
|
||
description: string // Mutable working title: user-editable while unbooked (see PATCH /api/transactions/[id])
|
||
// Bank/PSD2 description captured at ingest, normalized (empty/whitespace and
|
||
// the legacy "Unknown" sentinel map to the Swedish neutral). Never overwritten
|
||
// by user title edits; source for the dedup bridge and the "restore original"
|
||
// action. Null only for rows predating the column.
|
||
original_description: string | null
|
||
// Set when the user has overridden the title; null = still the bank original.
|
||
title_edited_at: string | null
|
||
amount: number // Positive = income, negative = expense
|
||
currency: Currency
|
||
|
||
// For non-SEK transactions
|
||
amount_sek: number | null
|
||
exchange_rate: number | null
|
||
exchange_rate_date: string | null
|
||
|
||
// Categorization
|
||
category: TransactionCategory
|
||
is_business: boolean | null // null = uncategorized
|
||
|
||
// Linked invoice (for matching)
|
||
invoice_id: string | null
|
||
|
||
// Linked supplier invoice (for matching)
|
||
supplier_invoice_id: string | null
|
||
|
||
// Potential invoice match (suggested, not confirmed)
|
||
potential_invoice_id: string | null
|
||
|
||
// Potential supplier invoice match (suggested, not confirmed)
|
||
potential_supplier_invoice_id: string | null
|
||
|
||
// Potential journal-entry match (suggested by the reconciliation sweep, not
|
||
// confirmed). All three set together, or all null; cleared by DB triggers
|
||
// when the row is booked/ignored or the entry is consumed/reversed.
|
||
potential_journal_entry_id?: string | null
|
||
potential_match_method?: string | null
|
||
potential_match_confidence?: number | null
|
||
|
||
// Bookkeeping
|
||
journal_entry_id: string | null
|
||
mcc_code: number | null
|
||
merchant_name: string | null
|
||
|
||
// Payment rail classified at ingest (or by the 20260808090100 backfill);
|
||
// null = unclassifiable from the source data.
|
||
transaction_method: TransactionMethod | null
|
||
// Raw PSD2 transaction-type codes, verbatim provider evidence for the
|
||
// classification (previously dropped at insert). Null for non-PSD2 sources.
|
||
bank_transaction_code: string | null
|
||
proprietary_bank_transaction_code: string | null
|
||
|
||
// Receipt link
|
||
receipt_id: string | null
|
||
|
||
// Inbox/upload document pinned to this transaction (pre-categorization).
|
||
// Propagates to document_attachments.journal_entry_id on categorize.
|
||
document_id: string | null
|
||
|
||
// Reconciliation
|
||
reconciliation_method: ReconciliationMethod | null
|
||
|
||
// User has chosen to suppress this transaction from the bank reconciliation
|
||
// view without booking it. See migration
|
||
// 20260529140000_transactions_is_ignored.sql for the rationale.
|
||
is_ignored: boolean
|
||
|
||
// Import tracking
|
||
import_source: string | null
|
||
// The bank_file_imports batch that inserted this row (bank-file CSV/CAMT
|
||
// import paths only). NULL for PSD2/manual/MCP rows and rows imported
|
||
// before migration 20260820071500. Scope key for undo_bank_file_import.
|
||
// Optional like the other late-added columns: older fixtures/readers
|
||
// predate it.
|
||
bank_file_import_id?: string | null
|
||
reference: string | null // OCR number, Bankgiro reference
|
||
|
||
// Counterparty identification from PSD2 (creditor for outflows, debtor for
|
||
// inflows). The own-account transfer detector matches `counterparty_iban`
|
||
// against cash_accounts.iban for the same company. `counterparty_account`
|
||
// is the BG/PG/BBAN fallback for Swedish domestic transfers without IBAN.
|
||
counterparty_iban: string | null
|
||
counterparty_account: string | null
|
||
|
||
// Notes
|
||
notes: string | null
|
||
|
||
created_at: string
|
||
updated_at: string
|
||
}
|
||
|
||
// Bank File Import (tracking table for file-based imports)
|
||
// 'undone' = the batch's unbooked transactions were bulk-deleted via
|
||
// undo_bank_file_import; a re-import of the same file reuses the row
|
||
// (upsert on company_id + file_hash) and moves it back to 'processing'.
|
||
export type BankFileImportStatus = 'pending' | 'processing' | 'completed' | 'failed' | 'undone'
|
||
|
||
// Customer
|
||
export interface Customer {
|
||
id: string
|
||
user_id: string
|
||
company_id: string
|
||
|
||
// Basic info
|
||
name: string
|
||
customer_type: CustomerType
|
||
|
||
// User-assigned customer number (kundnummer) shown on invoices.
|
||
// Free text, no uniqueness enforced in v1.
|
||
customer_number: string | null
|
||
|
||
// Contact
|
||
contact_person: string | null
|
||
email: string | null
|
||
phone: string | null
|
||
invoice_email_cc_addresses: string[] | null
|
||
invoice_email_bcc_addresses: string[] | null
|
||
|
||
// Address
|
||
address_line1: string | null
|
||
address_line2: string | null
|
||
postal_code: string | null
|
||
city: string | null
|
||
/** ISO 3166-1 alpha-2 ('SE', 'DE'). Rows from before 2026-09 that the backfill could not map may still hold a name. */
|
||
country: string
|
||
|
||
// Tax info
|
||
org_number: string | null
|
||
vat_number: string | null
|
||
vat_number_validated: boolean
|
||
vat_number_validated_at: string | null
|
||
personal_number: string | null
|
||
|
||
// Language for customer-facing invoice PDF and email
|
||
language: 'sv' | 'en'
|
||
|
||
// Payment
|
||
default_payment_terms: number // Days
|
||
|
||
// Notes
|
||
notes: string | null
|
||
|
||
created_at: string
|
||
updated_at: string
|
||
}
|
||
|
||
// Supplier
|
||
export interface Supplier {
|
||
id: string
|
||
user_id: string
|
||
company_id: string
|
||
|
||
name: string
|
||
supplier_type: SupplierType
|
||
|
||
email: string | null
|
||
phone: string | null
|
||
|
||
address_line1: string | null
|
||
address_line2: string | null
|
||
postal_code: string | null
|
||
city: string | null
|
||
/** ISO 3166-1 alpha-2 ('SE', 'DE'). Rows from before 2026-09 that the backfill could not map may still hold a name. */
|
||
country: string
|
||
|
||
org_number: string | null
|
||
vat_number: string | null
|
||
|
||
bankgiro: string | null
|
||
plusgiro: string | null
|
||
bank_account: string | null
|
||
iban: string | null
|
||
bic: string | null
|
||
clearing_number: string | null
|
||
account_number: string | null
|
||
|
||
default_expense_account: string | null
|
||
default_payment_terms: number
|
||
default_currency: string
|
||
|
||
notes: string | null
|
||
|
||
created_at: string
|
||
updated_at: string
|
||
}
|
||
|
||
// Supplier payment batch (betalfil): an immutable snapshot of payment
|
||
// instructions handed to the bank as a file. Generating or downloading a
|
||
// batch books nothing; settlement stays in mark-paid / bank matching.
|
||
export type SupplierPaymentBatchFormat = 'pain001' | 'bg_lb'
|
||
export type SupplierPaymentBatchStatus = 'created' | 'cancelled'
|
||
|
||
export interface SupplierPaymentBatchDebtor {
|
||
name: string
|
||
org_number: string
|
||
iban: string
|
||
bic: string
|
||
/** Absent on batches created before the Swedbank MIG fixes (2026-08-10). */
|
||
bankgiro?: string | null
|
||
/** Company town for Dbtr/PstlAdr; absent on pre-TownName-fix batches. */
|
||
city?: string | null
|
||
}
|
||
|
||
export interface SupplierPaymentBatch {
|
||
id: string
|
||
company_id: string
|
||
user_id: string
|
||
format: SupplierPaymentBatchFormat
|
||
status: SupplierPaymentBatchStatus
|
||
currency: string
|
||
total_amount: number
|
||
item_count: number
|
||
/** pain.001 MsgId, fixed at creation; re-downloads reuse it verbatim. */
|
||
msg_id: string
|
||
debtor_snapshot: SupplierPaymentBatchDebtor
|
||
file_generated_at: string | null
|
||
download_count: number
|
||
cancelled_at: string | null
|
||
cancelled_by: string | null
|
||
created_at: string
|
||
updated_at: string
|
||
}
|
||
|
||
export type SupplierPaymentBatchPayeeType = 'bankgiro' | 'plusgiro' | 'bank_account'
|
||
export type SupplierPaymentBatchReferenceType = 'ocr' | 'invoice_number'
|
||
|
||
export interface SupplierPaymentBatchItem {
|
||
id: string
|
||
batch_id: string
|
||
company_id: string
|
||
supplier_invoice_id: string
|
||
amount: number
|
||
payment_date: string
|
||
payee_type: SupplierPaymentBatchPayeeType
|
||
payee_bankgiro: string | null
|
||
payee_plusgiro: string | null
|
||
payee_clearing: string | null
|
||
payee_account: string | null
|
||
payee_name: string
|
||
/** Supplier town at creation; feeds Cdtr/PstlAdr/TwnNm on IBAN-debited payments. */
|
||
payee_city: string | null
|
||
reference_type: SupplierPaymentBatchReferenceType
|
||
reference: string
|
||
created_at: string
|
||
}
|
||
|
||
// Kundorder (sales order): the non-ledger document between agreement and
|
||
// invoice. Never books. Four-state header machine; delivery and invoicing
|
||
// progress are derived per line (see SalesOrderItem.invoiced_qty).
|
||
export type SalesOrderStatus = 'draft' | 'confirmed' | 'completed' | 'cancelled'
|
||
|
||
/** Derived per-axis progress: none / partial / full. */
|
||
export type SalesOrderProgress = 'none' | 'partial' | 'full'
|
||
|
||
export interface SalesOrder {
|
||
id: string
|
||
company_id: string
|
||
user_id: string
|
||
customer_id: string | null
|
||
/** OR-<n>, allocated at creation by generate_sales_order_number. */
|
||
order_number: string | null
|
||
status: SalesOrderStatus
|
||
/** Proforma the order was converted from, if any. */
|
||
source_invoice_id: string | null
|
||
order_date: string
|
||
requested_delivery_date: string | null
|
||
/** Latest registered delivery date across all lines (display only; invoices use per-line dates). */
|
||
last_delivery_date: string | null
|
||
/** Customer facts the lines were VAT-validated under; invoicing refuses when they changed. */
|
||
customer_type_snapshot?: CustomerType | null
|
||
customer_vat_validated_snapshot?: boolean | null
|
||
currency: string
|
||
subtotal: number
|
||
vat_amount: number
|
||
total: number
|
||
your_reference: string | null
|
||
our_reference: string | null
|
||
notes: string | null
|
||
default_dimensions: Record<string, string>
|
||
confirmed_at: string | null
|
||
completed_at: string | null
|
||
cancelled_at: string | null
|
||
created_at: string
|
||
updated_at: string
|
||
|
||
// Embeds / derived (list + detail responses)
|
||
customer?: Customer | null
|
||
items?: SalesOrderItem[]
|
||
delivery_progress?: SalesOrderProgress
|
||
invoicing_progress?: SalesOrderProgress
|
||
}
|
||
|
||
export interface SalesOrderItem {
|
||
id: string
|
||
company_id: string
|
||
sales_order_id: string
|
||
sort_order: number
|
||
line_type: 'product' | 'text'
|
||
description: string
|
||
quantity: number
|
||
/** Stored: registered by the user via the deliver action. */
|
||
delivered_qty: number
|
||
/** Latest delivery date registered for this line (null until delivered). */
|
||
last_delivery_date?: string | null
|
||
unit: string
|
||
unit_price: number
|
||
discount_percent: number
|
||
vat_rate: number
|
||
/** NET of discount, order currency. */
|
||
line_total: number
|
||
article_id: string | null
|
||
revenue_account: string | null
|
||
dimensions: Record<string, string>
|
||
created_at: string
|
||
updated_at: string
|
||
|
||
/** Derived from linked invoice_items on non-cancelled, non-credited invoices. */
|
||
invoiced_qty?: number
|
||
/** quantity - invoiced_qty (never negative). */
|
||
remaining_qty?: number
|
||
}
|
||
|
||
export interface SalesOrderItemInput {
|
||
id?: string
|
||
line_type?: 'product' | 'text'
|
||
description: string
|
||
quantity: number
|
||
unit: string
|
||
unit_price: number
|
||
discount_percent?: number | null
|
||
vat_rate?: number
|
||
article_id?: string | null
|
||
revenue_account?: string | null
|
||
dimensions?: Record<string, string>
|
||
}
|
||
|
||
// Article (artikelregister): reusable invoice-line preset. NON-INVENTORY:
|
||
// no stock fields and no inventory postings, by deliberate design.
|
||
export type ArticleType = 'vara' | 'tjanst'
|
||
|
||
export interface Article {
|
||
id: string
|
||
company_id: string
|
||
user_id: string
|
||
|
||
/** Auto-numbered per company (generate_article_number RPC); user-overridable. */
|
||
article_number: string | null
|
||
name: string
|
||
/** English benämning for English-language invoices. */
|
||
name_en: string | null
|
||
type: ArticleType
|
||
unit: string
|
||
/** Always stored EXCLUDING VAT. */
|
||
price_excl_vat: number
|
||
/** Default line VAT rate as an integer percent: 25 | 12 | 6 | 0. */
|
||
vat_rate: number
|
||
/** Default price currency (ISO 4217 code from the currencies table);
|
||
* pre-fills the invoice currency when added. */
|
||
currency: string
|
||
/** Optional BAS class 1-3 posting account override. null = derive from VAT treatment. */
|
||
revenue_account: string | null
|
||
/** Margin/display only: never posted to the ledger. */
|
||
cost_price: number | null
|
||
ean: string | null
|
||
/** ROT/RUT arbetstypskod (tjänst only); pre-fills the invoice line. */
|
||
housework_type: string | null
|
||
notes: string | null
|
||
/** Soft-delete flag. Inactive articles are hidden from pickers but keep history. */
|
||
active: boolean
|
||
|
||
created_at: string
|
||
updated_at: string
|
||
}
|
||
|
||
export interface CreateArticleInput {
|
||
name: string
|
||
type?: ArticleType
|
||
unit?: string
|
||
price_excl_vat: number
|
||
vat_rate?: number
|
||
currency?: string
|
||
revenue_account?: string | null
|
||
cost_price?: number | null
|
||
ean?: string | null
|
||
housework_type?: string | null
|
||
name_en?: string | null
|
||
notes?: string | null
|
||
/** Optional manual article number; omit to auto-generate. */
|
||
article_number?: string | null
|
||
}
|
||
|
||
// Supplier Invoice
|
||
export interface SupplierInvoice {
|
||
id: string
|
||
user_id: string
|
||
company_id: string
|
||
supplier_id: string
|
||
|
||
arrival_number: number
|
||
supplier_invoice_number: string
|
||
|
||
invoice_date: string
|
||
due_date: string
|
||
received_date: string
|
||
delivery_date: string | null
|
||
|
||
status: SupplierInvoiceStatus
|
||
/**
|
||
* When the invoice was attested. The overdue cron collapses 'registered' and
|
||
* 'approved' into 'overdue', so this is the only durable attest marker: use
|
||
* it, not the status, to tell whether approval has happened.
|
||
*/
|
||
approved_at: string | null
|
||
|
||
currency: string
|
||
exchange_rate: number | null
|
||
exchange_rate_date: string | null
|
||
|
||
subtotal: number
|
||
subtotal_sek: number | null
|
||
vat_amount: number
|
||
vat_amount_sek: number | null
|
||
total: number
|
||
total_sek: number | null
|
||
|
||
/** Per-invoice öresavrundning override (display-only). null = off. */
|
||
ore_rounding: boolean | null
|
||
|
||
vat_treatment: VatTreatment
|
||
reverse_charge: boolean
|
||
|
||
payment_reference: string | null
|
||
paid_at: string | null
|
||
paid_amount: number
|
||
remaining_amount: number
|
||
|
||
is_credit_note: boolean
|
||
credited_invoice_id: string | null
|
||
|
||
registration_journal_entry_id: string | null
|
||
payment_journal_entry_id: string | null
|
||
|
||
transaction_id: string | null
|
||
document_id: string | null
|
||
|
||
// Owner paid out-of-pocket; AP step is bypassed and the expense is booked
|
||
// directly against 2893 (AB) or 2018 (EF). Status is set to 'paid' at
|
||
// creation and mark-paid is rejected by the existing status guard.
|
||
paid_with_private_funds: boolean
|
||
|
||
notes: string | null
|
||
|
||
// Default dimensions bag ({sie_dim_no: code}, e.g. {"1":"KS01","6":"P001"})
|
||
// applied to every generated journal line; item-level `dimensions` merge on
|
||
// top of it for the expense lines (dimensions PR7). Stored as jsonb
|
||
// DEFAULT '{}'. Optional in TS for pre-migration fixtures.
|
||
default_dimensions?: Record<string, string>
|
||
|
||
created_at: string
|
||
updated_at: string
|
||
|
||
// Relations (populated when fetched)
|
||
supplier?: Supplier
|
||
items?: SupplierInvoiceItem[]
|
||
payments?: SupplierInvoicePayment[]
|
||
}
|
||
|
||
// Supplier Invoice Item
|
||
export interface SupplierInvoiceItem {
|
||
id: string
|
||
supplier_invoice_id: string
|
||
|
||
sort_order: number
|
||
description: string
|
||
quantity: number
|
||
unit: string
|
||
unit_price: number
|
||
line_total: number
|
||
|
||
account_number: string
|
||
vat_code: string | null
|
||
vat_rate: number
|
||
vat_amount: number
|
||
// Self-assessed VAT rate for omvänd skattskyldighet (0.06/0.12/0.25), null
|
||
// for non-RC lines. The supplier charges no VAT so vat_rate stays 0; this
|
||
// rate drives the fiktiv-moms + basbelopp booking. See the booking engine.
|
||
reverse_charge_rate: number | null
|
||
|
||
// Periodisering (förutbetald kostnad): when set, the registration entry
|
||
// debits accrual_balance_account (17xx) instead of account_number, and an
|
||
// accrual_schedules row dissolves the net amount monthly over the period.
|
||
// VAT is never deferred. Both dates set together or not at all.
|
||
accrual_period_start?: string | null
|
||
accrual_period_end?: string | null
|
||
accrual_balance_account?: string | null
|
||
|
||
// Per-item dimensions bag, merged over the invoice's default_dimensions on
|
||
// the expense line this item books to (dimensions PR7). jsonb DEFAULT '{}'.
|
||
dimensions?: Record<string, string>
|
||
|
||
// Särskild löneskatt på pensionskostnader: when true the booking engine
|
||
// injects a self-balancing 7533 D / 2514 K pair at 24.26 % of line_total
|
||
// (lib/bookkeeping/slp-lines.ts). Only valid on 741x pension-premium
|
||
// accounts. Optional in TS for pre-migration fixtures.
|
||
apply_slp?: boolean
|
||
|
||
created_at: string
|
||
}
|
||
|
||
// Supplier Invoice Payment (partial payments)
|
||
export interface SupplierInvoicePayment {
|
||
id: string
|
||
supplier_invoice_id: string
|
||
|
||
payment_date: string
|
||
amount: number
|
||
currency: string
|
||
exchange_rate: number | null
|
||
exchange_rate_difference: number
|
||
|
||
journal_entry_id: string | null
|
||
transaction_id: string | null
|
||
notes: string | null
|
||
|
||
created_at: string
|
||
}
|
||
|
||
// Invoice Payment (partial payments)
|
||
export interface InvoicePayment {
|
||
id: string
|
||
user_id: string
|
||
company_id: string
|
||
invoice_id: string
|
||
|
||
payment_date: string
|
||
amount: number
|
||
currency: string
|
||
exchange_rate: number | null
|
||
exchange_rate_difference: number
|
||
|
||
journal_entry_id: string | null
|
||
transaction_id: string | null
|
||
notes: string | null
|
||
|
||
created_at: string
|
||
}
|
||
|
||
// Invoice
|
||
export interface Invoice {
|
||
id: string
|
||
user_id: string
|
||
company_id: string
|
||
customer_id: string
|
||
|
||
// Invoice number (auto-generated at first send; null while draft)
|
||
invoice_number: string | null
|
||
|
||
// Dates
|
||
invoice_date: string
|
||
due_date: string
|
||
delivery_date: string | null
|
||
|
||
// Status
|
||
status: InvoiceStatus
|
||
|
||
// Currency
|
||
currency: Currency
|
||
|
||
// Exchange rate (if non-SEK)
|
||
exchange_rate: number | null
|
||
exchange_rate_date: string | null
|
||
|
||
// Amounts
|
||
subtotal: number
|
||
subtotal_sek: number | null
|
||
|
||
vat_amount: number
|
||
vat_amount_sek: number | null
|
||
|
||
total: number
|
||
total_sek: number | null
|
||
|
||
/** Per-invoice öresavrundning override (display-only). null = inherit company_settings.ore_rounding. */
|
||
ore_rounding: boolean | null
|
||
|
||
// VAT
|
||
vat_treatment: VatTreatment
|
||
vat_rate: number
|
||
moms_ruta: string | null // For Swedish VAT reporting (05, 39, 40, etc.)
|
||
|
||
// Reference
|
||
your_reference: string | null
|
||
our_reference: string | null
|
||
// Fakturamärkning: buyer-required marking (cost center, project, PO label),
|
||
// separate from your_reference (Er referens = contact person). Printed on
|
||
// the PDF and mapped to Peppol BT-10 BuyerReference when set. Optional in
|
||
// TS for pre-migration fixtures.
|
||
invoice_marking?: string | null
|
||
|
||
// Optional online payment link (pasted by the user, e.g. a Stripe Payment
|
||
// Link). Rendered as a "Betala online" button in the invoice email and as a
|
||
// QR code + link on the PDF. Never copied to derived documents (credit
|
||
// notes, conversions, recurring invoices). Optional in TS for pre-migration
|
||
// fixtures.
|
||
payment_link_url?: string | null
|
||
// Stripe Payment Link id (plink_...) when the link above was auto-created by
|
||
// the Stripe extension; NULL for manually pasted links. Deterministic
|
||
// matching key for checkout.session.completed events and the handle used to
|
||
// deactivate the link on credit/paid.
|
||
stripe_payment_link_id?: string | null
|
||
// Per-invoice opt-out for automatic payment link creation on send.
|
||
payment_link_auto?: boolean
|
||
// Per-invoice payee (migration 20260903193000): the bank account this
|
||
// invoice asks the customer to pay to (null = the per-currency default),
|
||
// and its payee fields frozen when chosen and refreshed at issue. Issued
|
||
// invoices print from payment_details; the resolver falls back to the
|
||
// company default when it is null.
|
||
payment_cash_account_id?: string | null
|
||
payment_details?: InvoicePaymentAccount | null
|
||
|
||
// Notes
|
||
notes: string | null
|
||
|
||
// Reverse charge text (auto-added for EU B2B)
|
||
reverse_charge_text: string | null
|
||
|
||
// Credit note reference
|
||
credited_invoice_id: string | null
|
||
|
||
// Document type (invoice, proforma, delivery_note, quote)
|
||
document_type: InvoiceDocumentType
|
||
|
||
// Conversion tracking (proforma / quote -> invoice)
|
||
converted_from_id: string | null
|
||
|
||
// Quotes (offert) only. valid_until is the authoritative expiry date
|
||
// (due_date mirrors it because the column is NOT NULL); quote_status is
|
||
// NULL on every other document type. Optional in TS for pre-migration
|
||
// fixtures.
|
||
valid_until?: string | null
|
||
quote_status?: QuoteStatus | null
|
||
quote_decided_at?: string | null
|
||
|
||
// Kundorder this invoice was created from (sales_orders.id). Header-level
|
||
// provenance only; the per-line link is invoice_items.sales_order_item_id.
|
||
sales_order_id?: string | null
|
||
|
||
// Self-billing received (mottagen självfaktura, ML 17 kap 15§). When
|
||
// `is_self_billed` is true the customer issued the invoice on our behalf;
|
||
// for us it is a sale. The counterparty's number lives in
|
||
// `external_invoice_number` and our own `invoice_number` stays null so we
|
||
// never consume our löpnummerserie (BFL 5 kap 6§).
|
||
is_self_billed?: boolean
|
||
external_invoice_number?: string | null
|
||
self_billing_agreement_ref?: string | null
|
||
received_date?: string | null
|
||
|
||
// Verifikation produced when the invoice was booked (registration entry).
|
||
// Lets the payment flow detect an already-booked sale and clear 1510 rather
|
||
// than re-recognising revenue.
|
||
journal_entry_id?: string | null
|
||
|
||
// Payment tracking
|
||
paid_at: string | null
|
||
paid_amount: number | null
|
||
remaining_amount: number
|
||
|
||
// ROT/RUT-avdrag claim info. `deduction_total` is the sum of the per-item
|
||
// deduction_amount and equals the 1513 debit on the verifikation. The
|
||
// personnummer is stored only as AES-256-GCM ciphertext + the last four
|
||
// digits (PII isolation). All three fields are null/0 on invoices with
|
||
// no ROT/RUT lines. Optional in TypeScript to keep legacy fixtures
|
||
// (pre-migration) valid: treat undefined the same as 0/null.
|
||
deduction_total?: number
|
||
deduction_personnummer_encrypted?: string | null
|
||
deduction_personnummer_last4?: string | null
|
||
|
||
// Default dimensions bag ({sie_dim_no: code}) applied to every journal line
|
||
// generated from this invoice (issuance, payment, credit); item-level
|
||
// `dimensions` merge on top for the revenue lines (dimensions PR7).
|
||
// jsonb DEFAULT '{}'. Optional in TS for pre-migration fixtures.
|
||
default_dimensions?: Record<string, string>
|
||
|
||
created_at: string
|
||
updated_at: string
|
||
|
||
// Relations (populated when fetched)
|
||
customer?: Customer
|
||
items?: InvoiceItem[]
|
||
payments?: InvoicePayment[]
|
||
}
|
||
|
||
export type InvoiceDeliveryChannel = 'email' | 'manual'
|
||
export type InvoiceDeliveryStatus = 'preparing' | 'pending' | 'sent' | 'failed' | 'marked_sent'
|
||
|
||
/**
|
||
* Delivery outcome reported by the email provider after the send itself
|
||
* succeeded. The delivery keeps an aggregate outcome and, when the provider
|
||
* identifies affected recipients, outcomes keyed by stable To/CC positions.
|
||
* `null` means no report has arrived yet.
|
||
*/
|
||
export type InvoiceDeliveryProviderStatus =
|
||
| 'delayed'
|
||
| 'delivered'
|
||
| 'complained'
|
||
| 'bounced'
|
||
| 'failed'
|
||
| 'suppressed'
|
||
|
||
export interface InvoiceDeliveryRecipientStatus {
|
||
status: InvoiceDeliveryProviderStatus
|
||
status_at: string
|
||
}
|
||
|
||
/**
|
||
* PII-free recipient references. `to:1` is the first immutable To address and
|
||
* `cc:1` the first immutable CC address. BCC recipients are never exposed.
|
||
*/
|
||
export type InvoiceDeliveryRecipientStatuses = Partial<Record<
|
||
`to:${number}` | `cc:${number}`,
|
||
InvoiceDeliveryRecipientStatus
|
||
>>
|
||
|
||
export interface InvoiceDelivery {
|
||
id: string
|
||
company_id: string
|
||
user_id: string | null
|
||
invoice_id: string
|
||
channel: InvoiceDeliveryChannel
|
||
status: InvoiceDeliveryStatus
|
||
to_addresses: string[]
|
||
cc_addresses: string[]
|
||
bcc_addresses: string[]
|
||
reply_to: string | null
|
||
from_name: string | null
|
||
subject: string | null
|
||
body_text: string | null
|
||
body_html: string | null
|
||
provider: string | null
|
||
provider_message_id: string | null
|
||
provider_status: InvoiceDeliveryProviderStatus | null
|
||
provider_status_at: string | null
|
||
provider_status_detail: string | null
|
||
provider_recipient_statuses: InvoiceDeliveryRecipientStatuses
|
||
error_code: string | null
|
||
document_attachment_id: string | null
|
||
attachment_filename: string | null
|
||
attachment_content_type: string | null
|
||
attachment_sha256: string | null
|
||
sent_at: string | null
|
||
failed_at: string | null
|
||
retention_expires_at: string
|
||
pii_redacted_at: string | null
|
||
created_at: string
|
||
updated_at: string
|
||
}
|
||
|
||
// Invoice Item
|
||
export interface InvoiceItem {
|
||
id: string
|
||
invoice_id: string
|
||
|
||
// Order
|
||
sort_order: number
|
||
|
||
// Line kind. 'product' is a normal billable line; 'text' is a free-text or
|
||
// blank spacer row that carries only a description: no amounts, excluded from
|
||
// totals and bookkeeping. Optional in TS for legacy rows (defaults to
|
||
// 'product' in Postgres).
|
||
line_type?: 'product' | 'text'
|
||
|
||
// Description
|
||
description: string
|
||
|
||
// Quantity
|
||
quantity: number
|
||
unit: string // 'st', 'tim', 'dag', etc.
|
||
|
||
// Price
|
||
unit_price: number
|
||
|
||
// Percentage discount on the line (0-100). line_total and vat_amount are
|
||
// stored NET of this discount (lib/invoices/line-amounts.ts). Optional in
|
||
// TS for pre-migration fixtures; treat undefined the same as 0.
|
||
discount_percent?: number
|
||
|
||
// Calculated (net of discount_percent)
|
||
line_total: number
|
||
|
||
// Per-line VAT
|
||
vat_rate: number
|
||
vat_amount: number
|
||
|
||
// Article linkage. `article_id` is a soft back-reference to the source
|
||
// article (for the "Affärshändelser" history view); `revenue_account` is the
|
||
// BAS class 1-3 posting account frozen-copied from the article at line-create time.
|
||
// null `revenue_account` preserves the legacy "derive from VAT treatment"
|
||
// booking in generatePerRateLines().
|
||
article_id?: string | null
|
||
revenue_account?: string | null
|
||
|
||
// Kundorder line this invoice line was created from. The order line's
|
||
// invoiced quantity is DERIVED from these links (never stored), so an
|
||
// edit that drops the link would free the quantity for double invoicing:
|
||
// every write path round-trips it.
|
||
sales_order_item_id?: string | null
|
||
|
||
// Periodisering (förutbetald intäkt): when set, the revenue entry credits
|
||
// accrual_balance_account (29xx) instead of the line's revenue account, and
|
||
// an accrual_schedules row dissolves the net amount monthly over the
|
||
// period. Output VAT is never deferred. Both dates set together or not at
|
||
// all. Not combinable with ROT/RUT or text lines.
|
||
accrual_period_start?: string | null
|
||
accrual_period_end?: string | null
|
||
accrual_balance_account?: string | null
|
||
|
||
// ROT/RUT-avdrag (Sweden's tax deduction for household services / home
|
||
// renovation). When `deduction_type` is set, the system computes
|
||
// `deduction_amount` from the rules in lib/invoices/rot-rut-rules.ts
|
||
// and posts the receivable to BAS 1513 (Skatteverket). v1 deducts on
|
||
// the full line total; future work can use `labor_hours` to honour the
|
||
// labor-only restriction.
|
||
//
|
||
// All fields are optional in TypeScript even though Postgres has
|
||
// defaults: legacy rows pulled before the schema change carry
|
||
// `undefined` in JS land, and many existing test fixtures predate the
|
||
// ROT/RUT migration. Treat undefined the same as null/0 throughout.
|
||
deduction_type?: 'rot' | 'rut' | null
|
||
deduction_amount?: number
|
||
labor_hours?: number | null
|
||
/** Skatteverket arbetstypskod (e.g. 'BYGG', 'STAD'). See ROT_WORK_TYPES / RUT_WORK_TYPES. */
|
||
work_type?: string | null
|
||
/** Fastighetsbeteckning. Required for ROT, optional for RUT. */
|
||
housing_designation?: string | null
|
||
/** Lägenhetsnummer. Optional, used for ROT in flerbostadshus. */
|
||
apartment_number?: string | null
|
||
/** Bostadsrättsföreningens orgnr. ROT i bostadsrätt reports lägenhetsnummer
|
||
* + BRF orgnr instead of fastighetsbeteckning (Begaran.xsd: BrfOrgNr). */
|
||
brf_org_number?: string | null
|
||
|
||
// Per-item dimensions bag, merged over the invoice's default_dimensions on
|
||
// the revenue line this item books to (dimensions PR7). jsonb DEFAULT '{}'.
|
||
dimensions?: Record<string, string>
|
||
|
||
created_at: string
|
||
}
|
||
|
||
// Rot/rut payout request (begäran om utbetalning, Skatteverkets husavdragstjänst).
|
||
// One row per generated HUS XML file; items link the invoices whose 1513
|
||
// receivable the file requests. See lib/invoices/rot-rut-file.ts.
|
||
export type RotRutPayoutRequestStatus =
|
||
| 'generated'
|
||
| 'submitted'
|
||
| 'paid'
|
||
| 'partially_paid'
|
||
| 'rejected'
|
||
| 'cancelled'
|
||
|
||
// Recurring Invoice Schedule (template + monthly cadence)
|
||
export type RecurringInvoiceScheduleStatus = 'active' | 'paused'
|
||
|
||
export interface RecurringInvoiceSchedule {
|
||
id: string
|
||
company_id: string
|
||
user_id: string
|
||
customer_id: string
|
||
|
||
name: string
|
||
|
||
// Day-of-month anchor, 1-31. Clamped to last day of month in shorter
|
||
// months (handled by computeNextRunDate).
|
||
day_of_month: number
|
||
// Months between runs: 1 = monthly, 3 = quarterly, 6 = half-yearly,
|
||
// 12 = yearly. next_run_date is the month anchor the interval advances from.
|
||
interval_months: number
|
||
// Whole hour (0-23) in Europe/Stockholm time at which the schedule sends.
|
||
// The hourly cron only fires schedules matching the current Stockholm hour.
|
||
send_hour: number
|
||
payment_terms_days: number
|
||
|
||
currency: Currency
|
||
your_reference: string | null
|
||
our_reference: string | null
|
||
notes: string | null
|
||
|
||
// Dimension bag {sie_dim_no: code} copied onto every generated invoice's
|
||
// default_dimensions at spawn time.
|
||
default_dimensions?: Record<string, string>
|
||
|
||
auto_send: boolean
|
||
status: RecurringInvoiceScheduleStatus
|
||
|
||
next_run_date: string
|
||
last_run_at: string | null
|
||
last_invoice_id: string | null
|
||
last_run_warning: string | null
|
||
generated_count: number
|
||
|
||
created_at: string
|
||
updated_at: string
|
||
|
||
// Relations
|
||
customer?: Customer
|
||
items?: RecurringInvoiceScheduleItem[]
|
||
}
|
||
|
||
export interface RecurringInvoiceScheduleItem {
|
||
id: string
|
||
schedule_id: string
|
||
sort_order: number
|
||
description: string
|
||
quantity: number
|
||
unit: string
|
||
unit_price: number
|
||
// null = inherit customer's default VAT rate at spawn time
|
||
vat_rate: number | null
|
||
// Per-item bag copied onto the generated invoice_items.dimensions; merges
|
||
// over the schedule default on that item's revenue line.
|
||
dimensions?: Record<string, string>
|
||
created_at: string
|
||
}
|
||
|
||
// Form types for creating/updating
|
||
|
||
export interface CreateCustomerInput {
|
||
name: string
|
||
customer_type: CustomerType
|
||
customer_number?: string | null
|
||
contact_person?: string | null
|
||
email?: string
|
||
phone?: string
|
||
invoice_email_cc_addresses?: string[] | null
|
||
invoice_email_bcc_addresses?: string[] | null
|
||
address_line1?: string
|
||
address_line2?: string
|
||
postal_code?: string
|
||
city?: string
|
||
country?: string
|
||
org_number?: string
|
||
vat_number?: string
|
||
personal_number?: string | null
|
||
language?: 'sv' | 'en'
|
||
default_payment_terms?: number
|
||
notes?: string
|
||
}
|
||
|
||
export interface CreateSupplierInput {
|
||
name: string
|
||
supplier_type: SupplierType
|
||
email?: string
|
||
phone?: string
|
||
address_line1?: string
|
||
address_line2?: string
|
||
postal_code?: string
|
||
city?: string
|
||
country?: string
|
||
org_number?: string
|
||
vat_number?: string
|
||
bankgiro?: string
|
||
plusgiro?: string
|
||
bank_account?: string
|
||
iban?: string
|
||
bic?: string
|
||
clearing_number?: string
|
||
account_number?: string
|
||
default_expense_account?: string
|
||
default_payment_terms?: number
|
||
default_currency?: string
|
||
notes?: string
|
||
}
|
||
|
||
export interface CreateTransactionInput {
|
||
date: string
|
||
description: string
|
||
amount: number
|
||
currency: Currency
|
||
category?: TransactionCategory
|
||
is_business?: boolean
|
||
notes?: string
|
||
}
|
||
|
||
// API Response types
|
||
export interface ApiResponse<T> {
|
||
data?: T
|
||
error?: string
|
||
}
|
||
|
||
export interface ArchiveEstimate {
|
||
total_bytes: number
|
||
document_bytes: number
|
||
document_count: number
|
||
size_limit_bytes: number
|
||
within_limit: boolean
|
||
}
|
||
|
||
// VAT validation response
|
||
export interface VatValidationResult {
|
||
valid: boolean
|
||
name?: string
|
||
address?: string
|
||
country_code?: string
|
||
vat_number?: string
|
||
error?: string
|
||
}
|
||
|
||
// Exchange rate response
|
||
export interface ExchangeRate {
|
||
currency: Currency
|
||
rate: number
|
||
date: string
|
||
}
|
||
|
||
// ============================================================
|
||
// BAS Kontoplan & Bookkeeping Types
|
||
// ============================================================
|
||
|
||
// Risk levels for mapping rules
|
||
export type RiskLevel = 'NONE' | 'LOW' | 'MEDIUM' | 'HIGH' | 'VERY_HIGH'
|
||
|
||
// Account types
|
||
export type AccountType = 'asset' | 'equity' | 'liability' | 'revenue' | 'expense' | 'untaxed_reserves'
|
||
export type NormalBalance = 'debit' | 'credit'
|
||
export type PlanType = 'k1' | 'full_bas'
|
||
|
||
// Journal entry source
|
||
export type JournalEntrySourceType =
|
||
| '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'
|
||
|
||
// Journal entry status
|
||
export type JournalEntryStatus = 'draft' | 'posted' | 'reversed' | 'cancelled'
|
||
|
||
// Mapping rule type
|
||
export type MappingRuleType =
|
||
| 'mcc_code'
|
||
| 'merchant_name'
|
||
| 'description_pattern'
|
||
| 'amount_threshold'
|
||
| 'combined'
|
||
|
||
// BAS Account
|
||
export interface BASAccount {
|
||
id: string
|
||
user_id: string
|
||
company_id: string
|
||
account_number: string
|
||
account_name: string
|
||
account_class: number
|
||
account_group: string
|
||
account_type: AccountType
|
||
normal_balance: NormalBalance
|
||
plan_type: PlanType
|
||
is_active: boolean
|
||
is_system_account: boolean
|
||
default_vat_code: string | null
|
||
// Per-account default VAT rate for booking lines (0/0.06/0.12/0.25).
|
||
// null = no default (line keeps its own rate). Öresavrundning (3740) = 0.
|
||
default_vat_rate: number | null
|
||
default_vat_treatment: import('@/lib/vat/account-vat-treatment').AccountVatTreatment | null
|
||
description: string | null
|
||
sru_code: string | null
|
||
k2_excluded: boolean
|
||
sort_order: number
|
||
created_at: string
|
||
updated_at: string
|
||
}
|
||
|
||
// Fiscal Period (Räkenskapsår)
|
||
export interface FiscalPeriod {
|
||
id: string
|
||
user_id: string
|
||
company_id: string
|
||
name: string
|
||
period_start: string
|
||
period_end: string
|
||
is_closed: boolean
|
||
closed_at: string | null
|
||
// Closed via "klarmarkera": the bokslut was done in a previous bookkeeping
|
||
// system, so the period is closed here without a closing entry of its own.
|
||
// Optional: rows predate the column on some cached readers.
|
||
closed_externally?: boolean
|
||
locked_at: string | null
|
||
retention_expires_at: string | null
|
||
opening_balances_set: boolean
|
||
closing_entry_id: string | null
|
||
opening_balance_entry_id: string | null
|
||
previous_period_id: string | null
|
||
tax_depreciation_method?: 'rakenskapsenlig' | 'restvarde' | null
|
||
tax_depreciation_rule?: 'huvudregel_30' | 'kompletteringsregel_20' | null
|
||
tax_depreciation_opening_value?: number | null
|
||
tax_depreciation_base?: number | null
|
||
tax_depreciation_deduction?: number | null
|
||
tax_depreciation_closing_value?: number | null
|
||
tax_depreciation_calculation?: Record<string, unknown> | null
|
||
created_at: string
|
||
updated_at: string
|
||
}
|
||
|
||
// Journal Entry (Verifikation)
|
||
export interface JournalEntry {
|
||
id: string
|
||
user_id: string
|
||
company_id: string
|
||
fiscal_period_id: string
|
||
voucher_number: number
|
||
voucher_series: string
|
||
entry_date: string
|
||
description: string
|
||
source_type: JournalEntrySourceType
|
||
source_id: string | null
|
||
status: JournalEntryStatus
|
||
committed_at: string | null
|
||
reversed_by_id: string | null
|
||
reverses_id: string | null
|
||
correction_of_id: string | null
|
||
attachment_urls: string[] | null
|
||
notes: string | null
|
||
commit_method: string | null
|
||
// WHO relayed the commit; complements commit_method = HOW. Stamped at
|
||
// commit time since migration 20260619120000. actor_type is NULL or one of
|
||
// 'user' | 'api_key' | 'mcp_oauth' | 'cron' | 'system' | 'agent_chat'
|
||
// (the DB CHECK is the authority); actor_label is a credential snapshot
|
||
// (e.g. the API key name).
|
||
committed_actor_type: string | null
|
||
committed_actor_label: string | null
|
||
rubric_version: string | null
|
||
source_voucher_series: string | null
|
||
source_voucher_number: number | null
|
||
created_at: string
|
||
updated_at: string
|
||
// Relations
|
||
lines?: JournalEntryLine[]
|
||
// Set by list_fiscal_period_entries_with_related when the entry was
|
||
// returned as a follow-up from a different fiscal period than the one
|
||
// being viewed. Absent from plain PostgREST responses.
|
||
out_of_period?: boolean
|
||
}
|
||
|
||
// Journal Entry Line
|
||
export interface JournalEntryLine {
|
||
id: string
|
||
journal_entry_id: string
|
||
account_number: string
|
||
account_id: string | null
|
||
debit_amount: number
|
||
credit_amount: number
|
||
currency: string
|
||
amount_in_currency: number | null
|
||
exchange_rate: number | null
|
||
line_description: string | null
|
||
tax_code: string | null
|
||
// SIE dimension map {sie_dim_no: object_code}, e.g. {"1":"KS01","6":"P001"}.
|
||
// Source of truth; cost_center/project mirror keys '1'/'6'. Optional so
|
||
// pre-migration fixtures and partial selects stay type-valid.
|
||
dimensions?: Record<string, string>
|
||
cost_center: string | null
|
||
project: string | null
|
||
sort_order: number
|
||
created_at: string
|
||
}
|
||
|
||
// ── Periodisering (accrual schedules) ─────────────────────────
|
||
// One schedule per deferred invoice line: the net amount sits on a 17xx/29xx
|
||
// interim account and dissolves to the P&L account via monthly 'accrual'
|
||
// entries. See lib/bookkeeping/accruals/.
|
||
|
||
export type AccrualDirection = 'expense' | 'revenue'
|
||
export type AccrualScheduleStatus = 'active' | 'completed' | 'cancelled'
|
||
export type AccrualInstallmentStatus = 'pending' | 'posted' | 'cancelled'
|
||
|
||
export interface AccrualSchedule {
|
||
id: string
|
||
user_id: string
|
||
company_id: string
|
||
direction: AccrualDirection
|
||
supplier_invoice_id: string | null
|
||
supplier_invoice_item_id: string | null
|
||
invoice_id: string | null
|
||
invoice_item_id: string | null
|
||
// Interim balance account (17xx for expense, 29xx for revenue) and the
|
||
// P&L account each installment dissolves to. Strings, like all accounts.
|
||
balance_account: string
|
||
target_account: string
|
||
// Net SEK amount as booked (ex VAT). Always equals the sum of installments.
|
||
total_amount: number
|
||
period_start: string
|
||
period_end: string
|
||
months: number
|
||
origin_journal_entry_id: string | null
|
||
// Dissolution entries are never dated before this (= origin entry date).
|
||
posting_floor_date: string
|
||
status: AccrualScheduleStatus
|
||
description: string | null
|
||
// Dimensions bag ({sie_dim_no: object_code}) copied from the origin line
|
||
// (invoice default_dimensions merged with the item bag); carried onto both
|
||
// dissolution lines. jsonb DEFAULT '{}'. Optional in TS for pre-migration
|
||
// fixtures.
|
||
dimensions?: Record<string, string>
|
||
created_at: string
|
||
updated_at: string
|
||
// Relations
|
||
installments?: AccrualScheduleInstallment[]
|
||
}
|
||
|
||
export interface AccrualScheduleInstallment {
|
||
id: string
|
||
user_id: string
|
||
company_id: string
|
||
schedule_id: string
|
||
// First day of the calendar month the installment belongs to.
|
||
period_month: string
|
||
amount: number
|
||
status: AccrualInstallmentStatus
|
||
journal_entry_id: string | null
|
||
posted_at: string | null
|
||
last_error: string | null
|
||
created_at: string
|
||
updated_at: string
|
||
}
|
||
|
||
// Mapping Rule
|
||
export interface MappingRule {
|
||
id: string
|
||
user_id: string | null
|
||
company_id: string | null
|
||
rule_name: string
|
||
rule_type: MappingRuleType
|
||
priority: number
|
||
// Matching
|
||
mcc_codes: number[] | null
|
||
merchant_pattern: string | null
|
||
description_pattern: string | null
|
||
amount_min: number | null
|
||
amount_max: number | null
|
||
// Targets
|
||
debit_account: string | null
|
||
credit_account: string | null
|
||
vat_treatment: string | null
|
||
vat_debit_account: string | null
|
||
vat_credit_account: string | null
|
||
// Risk
|
||
risk_level: RiskLevel
|
||
default_private: boolean
|
||
requires_review: boolean
|
||
confidence_score: number
|
||
// Capitalization
|
||
capitalization_threshold: number | null
|
||
capitalized_debit_account: string | null
|
||
// Source tracking
|
||
source: 'auto' | 'user_description' | 'system'
|
||
user_description: string | null
|
||
template_id: string | null
|
||
// Meta
|
||
is_active: boolean
|
||
created_at: string
|
||
updated_at: string
|
||
}
|
||
|
||
// Mapping engine result
|
||
export interface MappingResult {
|
||
rule: MappingRule | null
|
||
template_id?: string
|
||
debit_account: string
|
||
credit_account: string
|
||
risk_level: RiskLevel
|
||
confidence: number
|
||
requires_review: boolean
|
||
default_private: boolean
|
||
vat_lines: VatJournalLine[]
|
||
all_lines_complete?: boolean // when true, vat_lines contains ALL non-settlement lines
|
||
description: string
|
||
// Set when a matched counterparty template's learned direction contradicts
|
||
// the transaction sign (e.g. an incoming refund matching an expense-learned
|
||
// template). The result is mirrored and review-gated, and must never be
|
||
// learned back into the template (it would flip the learned accounts).
|
||
direction_mismatch?: boolean
|
||
// Dimensions bag applied to the business (expense/revenue) lines of the
|
||
// generated entry: from a counterparty template's line pattern or an
|
||
// explicit categorize param (dimensions PR7). Bank/VAT lines stay untagged.
|
||
dimensions?: Record<string, string>
|
||
}
|
||
|
||
// VAT journal line (auto-generated)
|
||
export interface VatJournalLine {
|
||
account_number: string
|
||
debit_amount: number
|
||
credit_amount: number
|
||
description: string
|
||
// Set on business-type lines materialized from a LinePatternEntry that
|
||
// carries dimensions (dimensions PR7); VAT/tax lines stay untagged.
|
||
dimensions?: Record<string, string>
|
||
}
|
||
|
||
// Categorization template source
|
||
export type CategorizationTemplateSource = 'sie_import' | 'user_approved' | 'sni_default' | 'auto_learned' | 'ai_corrected'
|
||
|
||
// Multi-line booking pattern entry
|
||
export interface LinePatternEntry {
|
||
account: string
|
||
type: 'business' | 'vat' | 'tax'
|
||
side: 'debit' | 'credit'
|
||
ratio?: number // proportion of NON-VAT amount (business + tax ratios sum to ~1.0)
|
||
vat_rate?: number // applied to FULL amount via rate/(1+rate) (vat type only)
|
||
// Dimensions bag ({sie_dim_no: code}) learned from the source vouchers'
|
||
// lines; applied to the materialized line on booking (dimensions PR7).
|
||
// Only preserved by learning when every occurrence agrees.
|
||
dimensions?: Record<string, string>
|
||
}
|
||
|
||
// Per-tenant counterparty-based categorization template
|
||
export interface CategorizationTemplate {
|
||
id: string
|
||
// Pre-multi-tenant relic: nullable since 20260711100000 and never written
|
||
// by the learning path anymore. Scoping is company_id.
|
||
user_id: string | null
|
||
company_id: string
|
||
counterparty_name: string
|
||
counterparty_aliases: string[]
|
||
debit_account: string
|
||
credit_account: string
|
||
vat_treatment: VatTreatment | null
|
||
vat_account: string | null
|
||
category: TransactionCategory | null
|
||
line_pattern: LinePatternEntry[] | null
|
||
// Bag {sie_dim_no: code} learned from the latest tagged booking; applied to
|
||
// the business line on the legacy single-line template path (line_pattern
|
||
// entries carry their own bags on the multi-line path).
|
||
default_dimensions?: Record<string, string>
|
||
occurrence_count: number
|
||
confidence: number
|
||
last_seen_date: string | null
|
||
source: CategorizationTemplateSource
|
||
is_active: boolean
|
||
created_at: string
|
||
updated_at: string
|
||
}
|
||
|
||
// Booking template library categories
|
||
export type BookingTemplateCategory =
|
||
| 'eu_trade'
|
||
| 'tax_account'
|
||
| 'private_transfer'
|
||
| 'salary'
|
||
| 'representation'
|
||
| 'year_end'
|
||
| 'vat'
|
||
| 'financial'
|
||
| 'other'
|
||
|
||
// Booking template library line
|
||
export interface BookingTemplateLibraryLine {
|
||
account: string
|
||
label: string
|
||
side: 'debit' | 'credit'
|
||
type: 'business' | 'vat' | 'settlement'
|
||
ratio?: number
|
||
vat_rate?: number
|
||
}
|
||
|
||
// Booking template library entry (system, team, or company-scoped)
|
||
export interface BookingTemplateLibrary {
|
||
id: string
|
||
company_id: string | null
|
||
team_id: string | null
|
||
created_by: string | null
|
||
name: string
|
||
description: string
|
||
category: BookingTemplateCategory
|
||
entity_type: 'all' | EntityType
|
||
lines: BookingTemplateLibraryLine[]
|
||
is_system: boolean
|
||
is_active: boolean
|
||
created_at: string
|
||
updated_at: string
|
||
}
|
||
|
||
// Report types
|
||
export interface TrialBalanceRow {
|
||
account_number: string
|
||
account_name: string
|
||
account_class: number
|
||
opening_debit: number
|
||
opening_credit: number
|
||
period_debit: number
|
||
period_credit: number
|
||
closing_debit: number
|
||
closing_credit: number
|
||
}
|
||
|
||
export interface IncomeStatementSection {
|
||
title: string
|
||
rows: { account_number: string; account_name: string; amount: number }[]
|
||
subtotal: number
|
||
}
|
||
|
||
export interface IncomeStatementReport {
|
||
revenue_sections: IncomeStatementSection[]
|
||
total_revenue: number
|
||
expense_sections: IncomeStatementSection[]
|
||
total_expenses: number
|
||
financial_sections: IncomeStatementSection[]
|
||
total_financial: number
|
||
net_result: number
|
||
period: { start: string; end: string }
|
||
}
|
||
|
||
export interface BalanceSheetSection {
|
||
title: string
|
||
rows: { account_number: string; account_name: string; amount: number }[]
|
||
subtotal: number
|
||
}
|
||
|
||
/**
|
||
* A non-latest fiscal year whose P&L (class 3-8) does not net to zero —
|
||
* its result was never transferred to equity (omföring av årets resultat
|
||
* saknas). Every later period that derives its opening balance from prior
|
||
* class 1-2 lines inherits exactly this residual as a balance-sheet
|
||
* differens.
|
||
*/
|
||
export interface UntransferredResult {
|
||
fiscal_period_id: string
|
||
period_name: string
|
||
/** Class 3-8 net (credit-positive = profit), rounded to öre. */
|
||
pl_net: number
|
||
}
|
||
|
||
/**
|
||
* Server-built explanation for an unbalanced balance report. The message is
|
||
* Swedish (user-facing domain messages are Swedish) and names the exact
|
||
* fiscal years whose results were never moved to equity.
|
||
*/
|
||
export interface BalanceImbalanceDiagnosis {
|
||
differens: number
|
||
untransferred_results: UntransferredResult[]
|
||
message: string
|
||
}
|
||
|
||
export interface BalanceSheetReport {
|
||
asset_sections: BalanceSheetSection[]
|
||
total_assets: number
|
||
equity_liability_sections: BalanceSheetSection[]
|
||
total_equity_liabilities: number
|
||
period: { start: string; end: string }
|
||
/** Present only when the report does not balance. */
|
||
imbalance_diagnosis?: BalanceImbalanceDiagnosis
|
||
}
|
||
|
||
/**
|
||
* Highest POSTED voucher number per series inside a reported window.
|
||
*
|
||
* Reconciliation aid, not statutory (BFL does not require it). Deliberately the
|
||
* last posted number, not `voucher_sequences.last_number`: the sequence counter
|
||
* is an allocation high-water mark that can sit ahead of the books.
|
||
*/
|
||
export interface LatestVoucherPerSeries {
|
||
series: string
|
||
last_number: number
|
||
}
|
||
|
||
export interface ResultatrapportRow {
|
||
account_number: string
|
||
account_name: string
|
||
current_period: number
|
||
prior_period: number
|
||
}
|
||
|
||
export interface ResultatrapportGroup {
|
||
class: number
|
||
class_label: string
|
||
rows: ResultatrapportRow[]
|
||
subtotal_current: number
|
||
subtotal_prior: number
|
||
}
|
||
|
||
export interface ResultatrapportReport {
|
||
groups: ResultatrapportGroup[]
|
||
net_result_current: number
|
||
net_result_prior: number
|
||
period: { start: string; end: string }
|
||
prior_period: { start: string; end: string } | null
|
||
/** Omitted when the window holds no posted vouchers, or the report is dimension-filtered. */
|
||
latest_vouchers?: LatestVoucherPerSeries[]
|
||
}
|
||
|
||
// Resultat per projekt/kostnadsställe: value-as-column P&L matrix over one
|
||
// SIE dimension. `code: null` marks the "(Utan dimension)" residual bucket,
|
||
// which is computed as Totalt − tagged columns so every row sums exactly to
|
||
// its resultatrapport counterpart.
|
||
export interface DimensionPnlColumn {
|
||
code: string | null
|
||
name: string | null
|
||
}
|
||
|
||
export interface DimensionPnlRow {
|
||
account_number: string
|
||
account_name: string
|
||
values: number[]
|
||
total: number
|
||
}
|
||
|
||
export interface DimensionPnlGroup {
|
||
class: number
|
||
class_label: string
|
||
rows: DimensionPnlRow[]
|
||
subtotals: number[]
|
||
subtotal_total: number
|
||
}
|
||
|
||
export interface DimensionPnlReport {
|
||
dimension: { sie_dim_no: string; name: string }
|
||
columns: DimensionPnlColumn[]
|
||
groups: DimensionPnlGroup[]
|
||
net_per_column: number[]
|
||
net_total: number
|
||
period: { start: string; end: string }
|
||
}
|
||
|
||
export interface BalansrapportRow {
|
||
account_number: string
|
||
account_name: string
|
||
ib: number
|
||
ub: number
|
||
period_change: number
|
||
}
|
||
|
||
export interface BalansrapportGroup {
|
||
class: number
|
||
class_label: string
|
||
rows: BalansrapportRow[]
|
||
subtotal_ib: number
|
||
subtotal_ub: number
|
||
}
|
||
|
||
export interface BalansrapportReport {
|
||
groups: BalansrapportGroup[]
|
||
total_assets_ub: number
|
||
total_equity_liabilities_ub: number
|
||
beraknat_resultat: number
|
||
is_balanced: boolean
|
||
period: { start: string; end: string }
|
||
/** Present only when the underlying trial balance does not balance. */
|
||
imbalance_diagnosis?: BalanceImbalanceDiagnosis
|
||
/** Omitted when the window holds no posted vouchers. */
|
||
latest_vouchers?: LatestVoucherPerSeries[]
|
||
}
|
||
|
||
export interface SIEExportOptions {
|
||
fiscal_period_id: string
|
||
company_name: string
|
||
org_number: string | null
|
||
program_name?: string
|
||
/**
|
||
* When true, omit year-end closing verifikat (source_type = 'year_end')
|
||
* from #VER and from #RES/#UB calculations. Use when handing the file
|
||
* to systems (e.g. eDeklarera) that do their own closing: including
|
||
* our closing entry would zero out the P&L accounts.
|
||
*/
|
||
exclude_year_end_closing?: boolean
|
||
}
|
||
|
||
// Input types for creating entries
|
||
export interface CreateJournalEntryInput {
|
||
fiscal_period_id: string
|
||
entry_date: string
|
||
description: string
|
||
source_type: JournalEntrySourceType
|
||
source_id?: string
|
||
voucher_series?: string
|
||
notes?: string
|
||
lines: CreateJournalEntryLineInput[]
|
||
}
|
||
|
||
export interface CreateJournalEntryLineInput {
|
||
account_number: string
|
||
debit_amount: number
|
||
credit_amount: number
|
||
line_description?: string
|
||
currency?: string
|
||
amount_in_currency?: number
|
||
exchange_rate?: number
|
||
tax_code?: string
|
||
// SIE dimension map {sie_dim_no: object_code}. Wins per key over the
|
||
// deprecated cost_center/project aliases (normalizeLineDimensions).
|
||
dimensions?: Record<string, string>
|
||
/** @deprecated alias for dimensions['1']: kept for API/MCP compatibility */
|
||
cost_center?: string
|
||
/** @deprecated alias for dimensions['6']: kept for API/MCP compatibility */
|
||
project?: string
|
||
}
|
||
|
||
// ── Pending Operations ────────────────────────────────────────
|
||
|
||
export type PendingOperationType =
|
||
| 'categorize_transaction'
|
||
| 'create_customer'
|
||
| 'update_customer'
|
||
| 'update_company_settings'
|
||
| 'create_article'
|
||
| 'update_article'
|
||
// Kundorder (gnubok_create_sales_order / _transition_sales_order /
|
||
// _register_sales_order_delivery / _create_invoice_from_sales_order)
|
||
| 'create_sales_order'
|
||
| 'transition_sales_order'
|
||
| 'register_sales_order_delivery'
|
||
| 'create_invoice_from_sales_order'
|
||
// Kontoplan reference data (gnubok_create_account / gnubok_update_account)
|
||
| 'create_account'
|
||
| 'update_account'
|
||
| 'create_supplier'
|
||
| 'create_invoice'
|
||
| 'mark_invoice_paid'
|
||
| 'send_invoice'
|
||
| 'mark_invoice_sent'
|
||
| 'match_transaction_invoice'
|
||
// Stream 1 Phase 1: bookkeeping period operations
|
||
| 'close_period'
|
||
| 'lock_period'
|
||
| 'unlock_period'
|
||
| 'set_opening_balances'
|
||
| 'run_year_end'
|
||
| 'post_kontantmetod_cutoff'
|
||
| 'run_currency_revaluation'
|
||
// Stream 1 Phase 1: SIE import (export is read-only)
|
||
| 'import_sie'
|
||
// SIE undo: hard-deletes the import's journal entries and releases the
|
||
// (company_id, file_hash) slot. Recovery for botched imports.
|
||
| 'undo_sie_import'
|
||
// Stream 1 Phase 1: voucher gap explanations
|
||
| 'explain_voucher_gap'
|
||
// Stream 1 Phase 1: transaction reversal
|
||
| 'uncategorize_transaction'
|
||
// Document inbox: pin doc to bank transaction
|
||
| 'attach_document_to_transaction'
|
||
// Link a document directly to a journal entry (verifikation): for imported/
|
||
// manual vouchers that have no bank-transaction row.
|
||
| 'link_document_to_voucher'
|
||
// Bulk counterpart: N docs linked to N posted verifikationer in one staged
|
||
// op, addressed by voucher_series/voucher_number/fiscal_year instead of
|
||
// journal_entry_id UUIDs (resolved server-side).
|
||
| 'link_documents_to_vouchers'
|
||
// Manual transaction ingestion (uncategorized row, reversible by delete)
|
||
| 'create_transaction'
|
||
// Stream 1 Phase 1: supplier invoice lifecycle
|
||
| 'approve_supplier_invoice'
|
||
| 'credit_supplier_invoice'
|
||
// Phase 5: convert an OCR'd inbox item to a leverantörsfaktura + registration JE
|
||
| 'create_supplier_invoice_from_inbox'
|
||
// Stream 1 Phase 1: invoice operations beyond simple create/send
|
||
| 'credit_invoice'
|
||
| 'convert_invoice'
|
||
// Draft-only invoice removal: unnumbered drafts hard delete, numbered
|
||
// drafts are makulerade (number retained). Non-drafts are refused.
|
||
| 'delete_draft_invoice'
|
||
// Draft-only invoice edit (items full-replace); sent/booked stays immutable,
|
||
// correction is a kreditfaktura.
|
||
| 'update_invoice'
|
||
// Recurring invoice schedules (monthly templates; invoices spawn from the
|
||
// hourly cron, never at commit time). Update covers pause/resume via status.
|
||
| 'create_recurring_schedule'
|
||
| 'update_recurring_schedule'
|
||
// Phase 4: arbitrary-line bookkeeping primitives
|
||
| 'create_voucher'
|
||
| 'correct_entry'
|
||
// Pure makulering (storno) of a posted entry: agent-native API plan item 38
|
||
| 'reverse_entry'
|
||
// Notes-only annotation on a verifikat: the immutability trigger's carve-out
|
||
// (migration 20260608120000) makes this legal even on posted entries.
|
||
| 'set_voucher_note'
|
||
// Ignore / restore a bank transaction that is not an affärshändelse (PSD2
|
||
// ghost row, duplicate, never-executed transfer). Writes no verifikat, so a
|
||
// locked or closed period does not block it (issue #1661).
|
||
| 'ignore_transaction'
|
||
// Bokslut: planenlig avskrivning (one journal entry per asset)
|
||
| 'post_annual_depreciation'
|
||
// Payroll: salary run creation + AGI declaration
|
||
| 'create_salary_run'
|
||
| 'generate_agi'
|
||
// Körjournal: log a trip (pure travel documentation) + book the period's
|
||
// milersättning as one verifikat (7331 at schablon rate)
|
||
| 'log_mileage_trip'
|
||
| 'book_mileage_period'
|
||
// Mark invoice paid by linking an existing posted verifikat (no new JE)
|
||
| 'link_invoice_voucher'
|
||
// Supplier-side mirror: mark a leverantörsfaktura paid by linking an existing
|
||
// posted verifikat that debits 2440 (no new JE)
|
||
| 'link_supplier_invoice_voucher'
|
||
// PR #603/#607: allocate 1 bank tx across N customer or supplier invoices
|
||
| 'match_batch_allocate'
|
||
// PR #606/#610: bulk-book N bank txs into 1 combined verifikat
|
||
| 'bulk_book_transactions'
|
||
// Bulk-book N selected Underlag (Dokumentinkorgen) against their matched bank
|
||
// transactions: one verifikat per item, sharing a category + VAT treatment
|
||
| 'bulk_book_inbox_items'
|
||
// PR #614: link a single bank tx to an already-posted verifikat (no new JE)
|
||
| 'link_transaction_journal_entry'
|
||
// Account-keyed reconciliation (bank accounts + skattekonto): link outside
|
||
// rows to existing verifikat / clear such a link. No ledger writes.
|
||
| 'reconciliation_match'
|
||
| 'reconciliation_unmatch'
|
||
// Sign-off "avstämt t.o.m. <datum>" on one account (account_reconciliations row).
|
||
| 'reconciliation_signoff'
|
||
// Book the remainder of a bank selection as a fee/interest/rounding verifikat and link it.
|
||
| 'reconciliation_residual'
|
||
// Book synced skattekonto rows as posted verifikat (1630 + rule-matched
|
||
// counter account), same helper as the HTTP bokfor-batch route. The
|
||
// single-row op stores { transaction_id }; the batch op stores { ids }.
|
||
| 'book_skattekonto_row'
|
||
| 'book_skattekonto_rows'
|
||
// PR5: Skatteverket filing via MCP. Commit = "send for BankID signing"
|
||
// (returns a signing link); the user's signature in the browser files it.
|
||
| 'submit_vat_declaration'
|
||
| 'submit_agi'
|
||
// Dimensions PR3: stage a new dimension value (kostnadsställe/projekt object
|
||
// code, SIE #OBJEKT): agents never silently mint reporting values.
|
||
| 'create_dimension_value'
|
||
// Dimensions PR6: bulk retag of posted-line dimensions via the audited
|
||
// retag_line_dimensions RPC (gnubok_tag_journal_lines).
|
||
| 'retag_line_dimensions'
|
||
// Payroll gap-closure: payslip line edits + absence registration (1.7),
|
||
// employee master data (1.8; personnummer encrypted at staging), and
|
||
// cutover opening balances for mid-year migrations (2.4).
|
||
| 'update_payslip_line'
|
||
// Set THIS RUN's base salary for one employee (salary_run_employees.
|
||
// monthly_salary, draft only). The per-run column is what the engine reads;
|
||
// the employee master's fixed salary stays untouched (variable owner pay).
|
||
| 'set_run_salary'
|
||
// Draft-only salary-run header edit (payment_date / voucher_series /
|
||
// notes), same field set as the v1 PATCH; payment_date is the future
|
||
// booking entry date.
|
||
| 'update_salary_run'
|
||
| 'register_absence'
|
||
| 'create_employee'
|
||
| 'update_employee'
|
||
| 'set_employee_opening_balances'
|
||
// Payroll e2e parity with the v1 REST surface: book a calculated run
|
||
// (walks review → approved → paid → booked; the staged approval is the
|
||
// authorization act) and remove registered absence days. Employee
|
||
// archiving needs no own op: update_employee with is_active=false.
|
||
| 'book_salary_run'
|
||
| 'delete_absence'
|
||
// Semesterårsavslut: rolls vacation balances into the next year and may
|
||
// post a 2920/2940 drift-adjustment verifikation (Phase 3).
|
||
| 'vacation_year_close'
|
||
// 'failed_partial' (issue #842, DB CHECK widened in 20260722134114): terminal
|
||
// state for ops whose executor posted an irreversible side-effect (voucher,
|
||
// credit note) and then failed a later step. Not re-committable, not pending
|
||
// work; result_data.posted_ids carries the ids of what WAS posted.
|
||
export type PendingOperationStatus = 'pending' | 'committing' | 'committed' | 'rejected' | 'failed_partial'
|
||
|
||
// 'agent_chat' = the in-app AI chat (DB CHECK widened in migration
|
||
// 20260519090000_actor_type_agent_chat).
|
||
export type PendingOperationActorType = 'user' | 'api_key' | 'mcp_oauth' | 'cron' | 'agent_chat'
|
||
export type PendingOperationRiskLevel = 'low' | 'medium' | 'high'
|
||
|
||
export interface PendingOperationAgentMetadata {
|
||
conversation_id?: string
|
||
intent_id?: string
|
||
model?: string
|
||
model_version?: string
|
||
prompt_hash?: string
|
||
atoms_loaded?: string[]
|
||
approved_by_user_id?: string
|
||
}
|
||
|
||
export type PendingOperationRejectionCategory =
|
||
| 'wrong_category'
|
||
| 'wrong_amount'
|
||
| 'duplicate'
|
||
| 'wrong_period'
|
||
| 'other'
|
||
|
||
export interface PendingOperation {
|
||
id: string
|
||
user_id: string
|
||
company_id: string
|
||
operation_type: PendingOperationType
|
||
status: PendingOperationStatus
|
||
title: string
|
||
params: Record<string, unknown>
|
||
preview_data: Record<string, unknown>
|
||
result_data: Record<string, unknown> | null
|
||
// Stream 2 Phase 1: actor model
|
||
actor_type: PendingOperationActorType
|
||
actor_id: string | null
|
||
actor_label: string | null
|
||
risk_level: PendingOperationRiskLevel
|
||
// Stream 2 Phase 3: agent provenance (populated by chat loop, NULL for user-staged)
|
||
agent_metadata: PendingOperationAgentMetadata | null
|
||
// Stream 2 Phase 4: structured rejection so the agent can learn from "no"
|
||
rejection_category: PendingOperationRejectionCategory | null
|
||
rejection_reason: string | null
|
||
created_at: string
|
||
resolved_at: string | null
|
||
updated_at: string
|
||
}
|
||
|
||
// Onboarding progress for new user checklist
|
||
export interface OnboardingProgress {
|
||
hasCustomers: boolean
|
||
hasInvoices: boolean
|
||
hasBankConnected: boolean
|
||
hasSIEImport: boolean
|
||
/** True when the active user has a stored Skatteverket OAuth token. */
|
||
hasSkatteverketConnected: boolean
|
||
/** True when the company has ever received an item in the document inbox. */
|
||
hasInboxItems: boolean
|
||
}
|
||
|
||
export type InitialSetupPath = 'migration' | 'bank' | 'fresh'
|
||
|
||
export interface InitialSetupState {
|
||
path: InitialSetupPath | null
|
||
completedAt: string | null
|
||
dismissedAt: string | null
|
||
}
|
||
|
||
// ============================================================
|
||
// Calendar & Deadline Types
|
||
// ============================================================
|
||
|
||
// Calendar view mode
|
||
export type CalendarViewMode = 'month' | 'week' | 'day'
|
||
|
||
// Payment calendar day (for invoice due date tracking)
|
||
export interface PaymentCalendarDay {
|
||
date: string
|
||
invoices: Invoice[]
|
||
totalExpected: number
|
||
overdueCount: number
|
||
}
|
||
|
||
// Tax deadline types (Swedish Skatteverket)
|
||
export type TaxDeadlineType =
|
||
| 'moms_monthly'
|
||
| 'moms_quarterly'
|
||
| 'moms_yearly'
|
||
| 'f_skatt'
|
||
| 'arbetsgivardeklaration'
|
||
| 'skatteinbetalning'
|
||
| 'inkomstdeklaration_ef'
|
||
| 'inkomstdeklaration_ab'
|
||
| 'arsredovisning'
|
||
| 'arsstamma'
|
||
| 'periodisk_sammanstallning'
|
||
| 'kontrolluppgifter'
|
||
| 'rot_rut_begaran'
|
||
| 'oss_quarterly'
|
||
| 'ioss_monthly'
|
||
| 'intrastat_monthly'
|
||
| 'punktskatt_monthly'
|
||
| 'fyllnadsinbetalning'
|
||
| 'kvarskatt'
|
||
|
||
export type TaxAssessmentDecisionType = 'final' | 'reassessment'
|
||
|
||
export interface TaxAssessmentNotice {
|
||
id: string
|
||
company_id: string
|
||
user_id: string | null
|
||
fiscal_period_id: string
|
||
decision_type: TaxAssessmentDecisionType
|
||
decision_date: string
|
||
payment_due_date: string
|
||
archived_at: string | null
|
||
created_at: string
|
||
updated_at: string
|
||
fiscal_period?: Pick<FiscalPeriod, 'id' | 'name' | 'period_start' | 'period_end'>
|
||
}
|
||
|
||
// Deadline status workflow
|
||
export type DeadlineStatus =
|
||
| 'upcoming' // More than 14 days away
|
||
| 'action_needed' // Within 14 days, needs attention
|
||
| 'in_progress' // User is working on it
|
||
| 'submitted' // Submitted to Skatteverket
|
||
| 'confirmed' // Confirmed/acknowledged
|
||
| 'overdue' // Past due date without submission
|
||
|
||
// Deadline source
|
||
export type DeadlineSource = 'system' | 'user'
|
||
|
||
// Deadline types
|
||
export type DeadlineType = 'delivery' | 'invoicing' | 'report' | 'tax' | 'other'
|
||
export type DeadlinePriority = 'critical' | 'important' | 'normal'
|
||
|
||
// Deadline record
|
||
export interface Deadline {
|
||
id: string
|
||
user_id: string
|
||
company_id: string
|
||
title: string
|
||
due_date: string
|
||
due_time: string | null
|
||
deadline_type: DeadlineType
|
||
priority: DeadlinePriority
|
||
is_completed: boolean
|
||
completed_at: string | null
|
||
customer_id: string | null
|
||
is_auto_generated: boolean
|
||
notes: string | null
|
||
created_at: string
|
||
updated_at: string
|
||
|
||
// Tax deadline fields
|
||
tax_deadline_type: TaxDeadlineType | null
|
||
tax_period: string | null
|
||
source: DeadlineSource
|
||
reminder_offsets: number[] | null
|
||
status: DeadlineStatus
|
||
status_changed_at: string
|
||
// Durable opt-out for system deadlines: hidden everywhere, never
|
||
// recreated by the generator or the backfill cron.
|
||
dismissed_at: string | null
|
||
linked_report_type: string | null
|
||
linked_report_period: Record<string, unknown> | null
|
||
tax_assessment_notice_id: string | null
|
||
|
||
// Relations
|
||
customer?: Customer
|
||
}
|
||
|
||
// ============================================================
|
||
// Push Notification Types
|
||
// ============================================================
|
||
|
||
// Notification settings per user
|
||
export interface NotificationSettings {
|
||
id: string
|
||
user_id: string
|
||
tax_deadlines_enabled: boolean
|
||
invoice_reminders_enabled: boolean
|
||
quiet_start: string // time format "HH:MM"
|
||
quiet_end: string // time format "HH:MM"
|
||
email_enabled: boolean
|
||
push_enabled: boolean
|
||
period_locked_enabled: boolean
|
||
period_year_closed_enabled: boolean
|
||
invoice_sent_enabled: boolean
|
||
receipt_extracted_enabled: boolean
|
||
receipt_matched_enabled: boolean
|
||
missing_underlag_enabled: boolean
|
||
email_digest_enabled: boolean
|
||
created_at: string
|
||
updated_at: string
|
||
}
|
||
|
||
// Notification type for logging
|
||
export type NotificationType =
|
||
| 'tax_deadline'
|
||
| 'invoice_due'
|
||
| 'invoice_overdue'
|
||
| 'period_locked'
|
||
| 'period_year_closed'
|
||
| 'receipt_extracted'
|
||
| 'receipt_matched'
|
||
| 'invoice_sent'
|
||
| 'missing_underlag'
|
||
| 'skv_kvittens'
|
||
| 'skv_connection_expired'
|
||
| 'bookkeeping_digest'
|
||
|
||
// ============================================================
|
||
// Calendar Feed Types (ICS)
|
||
// ============================================================
|
||
|
||
// Calendar feed for Apple Calendar / Google Calendar sync
|
||
export interface CalendarFeed {
|
||
id: string
|
||
user_id: string
|
||
company_id: string
|
||
feed_token: string
|
||
is_active: boolean
|
||
include_tax_deadlines: boolean
|
||
include_invoices: boolean
|
||
last_accessed_at: string | null
|
||
access_count: number
|
||
created_at: string
|
||
updated_at: string
|
||
}
|
||
|
||
// ============================================================
|
||
// Invoice Inbox Types
|
||
// ============================================================
|
||
|
||
// 'processing' is the staged-upload in-flight state: the row exists (instant
|
||
// receipt ack) but AI extraction has not landed yet; extracted_data is NULL
|
||
// until the deferred worker (or the sweep cron) flips it to 'received'.
|
||
export type InboxItemStatus = 'received' | 'processing' | 'error'
|
||
export type InboxItemSource = 'email' | 'upload' | 'whatsapp' | 'mail_hunt' | 'peppol'
|
||
|
||
export type CompanyInboxStatus = 'active' | 'deprecated' | 'blocked'
|
||
|
||
export interface CompanyInbox {
|
||
id: string
|
||
company_id: string
|
||
local_part: string
|
||
status: CompanyInboxStatus
|
||
slug_seed: string
|
||
created_at: string
|
||
updated_at: string
|
||
deprecated_at: string | null
|
||
}
|
||
|
||
export type CompanyInboundDomainStatus = 'pending' | 'verified' | 'failed'
|
||
|
||
// A DNS record the user must publish to verify their custom inbound domain
|
||
// (verbatim from the Resend domains API).
|
||
export interface InboundDomainDnsRecord {
|
||
record: string
|
||
name: string
|
||
value: string
|
||
type: string
|
||
ttl: string
|
||
status: string
|
||
priority?: number
|
||
}
|
||
|
||
export interface CompanyInboundDomain {
|
||
id: string
|
||
company_id: string
|
||
domain: string
|
||
status: CompanyInboundDomainStatus
|
||
resend_domain_id: string | null
|
||
dns_records: InboundDomainDnsRecord[] | null
|
||
verified_at: string | null
|
||
last_checked_at: string | null
|
||
created_at: string
|
||
updated_at: string
|
||
}
|
||
|
||
export type CompanySendingDomainStatus = 'pending' | 'verified' | 'failed'
|
||
|
||
// A DNS record the user must publish to verify their custom sending domain
|
||
// (verbatim from the Resend domains API; same shape as the inbound records).
|
||
export type SendingDomainDnsRecord = InboundDomainDnsRecord
|
||
|
||
// Opt-in per-company sender identity for invoice email. Only a row with
|
||
// status = 'verified' AND enabled = true changes the From header; everything
|
||
// else falls back to the platform sender.
|
||
export interface CompanySendingDomain {
|
||
id: string
|
||
company_id: string
|
||
domain: string
|
||
status: CompanySendingDomainStatus
|
||
sender_local_part: string
|
||
sender_name: string | null
|
||
enabled: boolean
|
||
resend_domain_id: string | null
|
||
dns_records: SendingDomainDnsRecord[] | null
|
||
verified_at: string | null
|
||
last_checked_at: string | null
|
||
created_at: string
|
||
updated_at: string
|
||
}
|
||
|
||
export interface InvoiceInboxItem {
|
||
id: string
|
||
user_id: string
|
||
company_id: string
|
||
status: InboxItemStatus
|
||
source: InboxItemSource
|
||
email_from: string | null
|
||
email_subject: string | null
|
||
email_received_at: string | null
|
||
email_body_text: string | null
|
||
resend_email_id: string | null
|
||
resend_attachment_id: string | null
|
||
// Sender-declared kind from the +lev / +ver plus-address tag (migration
|
||
// 20260901210000). Wins over extracted_data.documentKind in the inbox UI.
|
||
kind_hint?: 'supplier_invoice' | 'receipt' | null
|
||
document_id: string | null
|
||
extracted_data: Record<string, unknown> | null
|
||
matched_supplier_id: string | null
|
||
created_supplier_invoice_id: string | null
|
||
matched_transaction_id: string | null
|
||
created_journal_entry_id: string | null
|
||
error_message: string | null
|
||
raw_email_payload: Record<string, unknown> | null
|
||
|
||
// WhatsApp channel (migration 20260802092000). whatsapp_message_id links
|
||
// back to the delivering chat message; channel_context holds verified
|
||
// human answers from the chat (kept OUT of extracted_data on purpose:
|
||
// retry-extraction overwrites that container wholesale).
|
||
whatsapp_message_id?: string | null
|
||
channel_context?: InboxChannelContext | null
|
||
|
||
// Audit chain (processing_history correlation)
|
||
correlation_id: string | null
|
||
|
||
created_at: string
|
||
updated_at: string
|
||
|
||
// Relations (populated when fetched)
|
||
document?: DocumentAttachment
|
||
supplier?: Supplier
|
||
supplier_invoice?: SupplierInvoice
|
||
}
|
||
|
||
// Chat-sourced context attached to an inbox item. `raw_answer` + timestamps
|
||
// double as the Skatteverket representation documentation trail.
|
||
export interface InboxChannelContext {
|
||
/**
|
||
* Which intake wrote this. 'mail_hunt' rows carry the mail_* fields below;
|
||
* everything else on this type belongs to the WhatsApp branch and is absent
|
||
* on them.
|
||
*/
|
||
channel: 'whatsapp' | 'mail_hunt' | 'peppol'
|
||
/** Set by lib/invoices/peppol-inbox-delivery.ts: provenance of a received e-invoice. */
|
||
peppol_provider?: string | null
|
||
/** The provider's id for the received document (Qvalia integrationId). */
|
||
peppol_document_id?: string | null
|
||
peppol_document_type?: 'Invoice' | 'CreditNote' | null
|
||
peppol_sender_endpoint?: string | null
|
||
/** Archived exact UBL XML, when the inbox document is a rendering (embedded PDF) instead. */
|
||
peppol_xml_document_id?: string | null
|
||
/** Set by lib/receipt-hunt/ingest.ts: which mailbox the receipt came out of. */
|
||
mail_mailbox?: string | null
|
||
mail_provider?: 'gmail' | 'microsoft' | null
|
||
mail_subject?: string | null
|
||
mail_from?: string | null
|
||
mail_received_at?: string | null
|
||
caption?: string | null
|
||
company_selected_via?: 'button' | 'list' | 'numbered' | 'pin' | 'default' | 'single'
|
||
representation?: {
|
||
participants: { name: string; company: string | null }[]
|
||
purpose: string | null
|
||
event_date: string | null
|
||
raw_answer: string
|
||
answered_at: string
|
||
/** True when the user answered `nej` (or the LLM read a denial): the
|
||
* receipt is NOT representation and the question is settled. */
|
||
denied?: boolean
|
||
}
|
||
user_note?: string | null
|
||
/** What the user actually typed when answering a context question, kept
|
||
* next to the LLM paraphrase in user_note. The paraphrase is what renders;
|
||
* this is the durable human answer, mirroring the representation branch
|
||
* (whatsapp_messages.body_text is purged at 90 days, so it is no trail). */
|
||
context_answer?: {
|
||
raw_answer: string
|
||
answered_at: string
|
||
}
|
||
quality?: {
|
||
resend_requested_at: string
|
||
resent?: boolean
|
||
/** Set on the OLD item when a re-sent, sharper file created a fresh item
|
||
* (WORM archive + anchored-doc invariant forbid swapping the document
|
||
* out from under the original). */
|
||
superseded?: boolean
|
||
}
|
||
pending_question?: {
|
||
type: 'representation' | 'context' | 'resend'
|
||
asked_at: string
|
||
status: 'open' | 'answered' | 'moved_to_app'
|
||
}
|
||
}
|
||
|
||
// ============================================================
|
||
// WhatsApp Channel Types (migrations 20260802090000/091000)
|
||
// ============================================================
|
||
|
||
export interface WhatsAppPhoneLink {
|
||
id: string
|
||
user_id: string
|
||
phone_hash: string
|
||
phone_enc: string
|
||
phone_masked: string
|
||
wa_profile_name: string | null
|
||
default_company_id: string | null
|
||
last_company_id: string | null
|
||
verified_at: string
|
||
revoked_at: string | null
|
||
muted_at: string | null
|
||
last_message_at: string | null
|
||
created_at: string
|
||
updated_at: string
|
||
}
|
||
|
||
export type WhatsAppConversationState =
|
||
| 'idle'
|
||
| 'awaiting_company'
|
||
| 'awaiting_representation'
|
||
| 'awaiting_context'
|
||
| 'awaiting_resend'
|
||
|
||
export interface WhatsAppConversation {
|
||
id: string
|
||
phone_link_id: string
|
||
state: WhatsAppConversationState
|
||
context: Record<string, unknown>
|
||
company_id: string | null
|
||
service_window_expires_at: string | null
|
||
debounce_until: string | null
|
||
pending_ack: boolean
|
||
last_inbound_at: string | null
|
||
last_outbound_at: string | null
|
||
created_at: string
|
||
updated_at: string
|
||
}
|
||
|
||
export type WhatsAppMessageProcessingStatus =
|
||
| 'received'
|
||
| 'processing'
|
||
| 'done'
|
||
| 'skipped'
|
||
| 'error'
|
||
|
||
export interface WhatsAppMessage {
|
||
id: string
|
||
direction: 'inbound' | 'outbound'
|
||
wamid: string | null
|
||
sender_phone_hash: string | null
|
||
phone_link_id: string | null
|
||
conversation_id: string | null
|
||
message_type: string
|
||
body_text: string | null
|
||
media_id: string | null
|
||
media_mime: string | null
|
||
media_sha256: string | null
|
||
media_filename: string | null
|
||
raw_payload: Record<string, unknown> | null
|
||
processing_status: WhatsAppMessageProcessingStatus
|
||
attempts: number
|
||
error_message: string | null
|
||
inbox_item_id: string | null
|
||
delivery_status: string | null
|
||
correlation_id: string | null
|
||
/** When a combined burst ack (M4/M5) covered this ingested row.
|
||
* NULL = not yet acked (the burst winner's work queue). */
|
||
acked_at: string | null
|
||
created_at: string
|
||
updated_at: string
|
||
}
|
||
|
||
// ============================================================
|
||
// Receipt Types
|
||
// ============================================================
|
||
|
||
// Receipt extraction status
|
||
export type ReceiptStatus = 'pending' | 'processing' | 'extracted' | 'confirmed' | 'error'
|
||
|
||
// Receipt record
|
||
export interface Receipt {
|
||
id: string
|
||
user_id: string
|
||
company_id: string
|
||
|
||
// Image storage
|
||
image_url: string
|
||
image_thumbnail_url: string | null
|
||
|
||
// Extraction status
|
||
status: ReceiptStatus
|
||
extraction_confidence: number | null
|
||
|
||
// Extracted header data
|
||
merchant_name: string | null
|
||
merchant_org_number: string | null
|
||
merchant_vat_number: string | null
|
||
receipt_date: string | null
|
||
receipt_time: string | null
|
||
total_amount: number | null
|
||
currency: string
|
||
vat_amount: number | null
|
||
|
||
// Special flags
|
||
is_restaurant: boolean
|
||
is_systembolaget: boolean
|
||
is_foreign_merchant: boolean
|
||
|
||
// Restaurant representation data
|
||
representation_persons: number | null
|
||
representation_purpose: string | null
|
||
representation_business_connection: string | null
|
||
|
||
// Source tracking (for email-originated receipts)
|
||
source: 'upload' | 'camera' | 'email'
|
||
email_from: string | null
|
||
|
||
// Transaction matching
|
||
matched_transaction_id: string | null
|
||
match_confidence: number | null
|
||
|
||
// Raw extraction data
|
||
raw_extraction: ReceiptExtractionResult | null
|
||
|
||
created_at: string
|
||
updated_at: string
|
||
|
||
// Relations (populated when fetched)
|
||
line_items?: ReceiptLineItem[]
|
||
matched_transaction?: Transaction
|
||
}
|
||
|
||
// Receipt line item record
|
||
export interface ReceiptLineItem {
|
||
id: string
|
||
receipt_id: string
|
||
|
||
// Extracted data
|
||
description: string
|
||
quantity: number
|
||
unit_price: number | null
|
||
line_total: number
|
||
vat_rate: number | null
|
||
vat_amount: number | null
|
||
|
||
// Classification
|
||
is_business: boolean | null
|
||
category: TransactionCategory | null
|
||
bas_account: string | null
|
||
|
||
// Confidence
|
||
extraction_confidence: number | null
|
||
suggested_category: string | null
|
||
|
||
sort_order: number
|
||
created_at: string
|
||
}
|
||
|
||
// AI extraction result from Claude Vision
|
||
export interface ReceiptExtractionResult {
|
||
merchant: {
|
||
name: string | null
|
||
orgNumber: string | null
|
||
vatNumber: string | null
|
||
isForeign: boolean
|
||
}
|
||
receipt: {
|
||
date: string | null
|
||
time: string | null
|
||
currency: string
|
||
}
|
||
lineItems: ExtractedLineItem[]
|
||
totals: {
|
||
subtotal: number | null
|
||
vatAmount: number | null
|
||
total: number | null
|
||
}
|
||
flags: {
|
||
isRestaurant: boolean
|
||
isSystembolaget: boolean
|
||
isForeignMerchant: boolean
|
||
}
|
||
confidence: number
|
||
suggestedTemplateId?: string
|
||
}
|
||
|
||
// Extracted line item from AI
|
||
export interface ExtractedLineItem {
|
||
description: string
|
||
quantity: number
|
||
unitPrice: number | null
|
||
lineTotal: number
|
||
vatRate: number | null
|
||
suggestedCategory: string | null
|
||
suggestedTemplateId?: string
|
||
confidence?: number
|
||
}
|
||
|
||
// ============================================================
|
||
// VAT Declaration Types (Momsdeklaration)
|
||
// ============================================================
|
||
|
||
// VAT period type
|
||
export type VatPeriodType = MomsPeriod
|
||
|
||
// VAT declaration rutor (boxes) according to SKV 4700
|
||
// Complete set of all 30 boxes in the momsdeklaration form.
|
||
export interface VatDeclarationRutor {
|
||
// Momspliktig försäljning (taxable sales basis, all rates combined)
|
||
ruta05: number // Momspliktig försäljning (excl. ruta 06, 07, 08)
|
||
ruta06: number // Momspliktiga uttag (always 0 for most users)
|
||
ruta07: number // Vinstmarginalbeskattning (always 0 for most users)
|
||
ruta08: number // Hyresinkomster frivillig beskattning (always 0 for most users)
|
||
|
||
// Utgående moms (Output VAT per rate)
|
||
ruta10: number // Utgående moms 25%
|
||
ruta11: number // Utgående moms 12%
|
||
ruta12: number // Utgående moms 6%
|
||
|
||
// Inköp vid omvänd skattskyldighet (reverse charge purchase bases)
|
||
ruta20: number // Inköp av varor från annat EU-land
|
||
ruta21: number // Inköp av tjänster från annat EU-land
|
||
ruta22: number // Inköp av tjänster från land utanför EU
|
||
ruta23: number // Inköp av varor i Sverige (construction reverse charge goods)
|
||
ruta24: number // Övriga inköp av tjänster i Sverige (domestic reverse charge)
|
||
|
||
// Utgående moms omvänd skattskyldighet (self-assessed output VAT on reverse charge)
|
||
ruta30: number // Utgående moms 25% omvänd skattskyldighet
|
||
ruta31: number // Utgående moms 12% omvänd skattskyldighet
|
||
ruta32: number // Utgående moms 6% omvänd skattskyldighet
|
||
|
||
// EU och export försäljning
|
||
ruta35: number // Varuförsäljning till annat EU-land
|
||
ruta36: number // Varuförsäljning utanför EU (export)
|
||
ruta37: number // Mellanmans inköp vid trepartshandel
|
||
ruta38: number // Mellanmans försäljning vid trepartshandel
|
||
ruta39: number // Försäljning av tjänster till annat EU-land (reverse charge)
|
||
ruta40: number // Övrig försäljning av tjänster utomlands
|
||
ruta41: number // Försäljning med omvänd skattskyldighet (Sverige)
|
||
ruta42: number // Övrig momsfri försäljning m.m.
|
||
|
||
// Ingående moms (Input VAT)
|
||
ruta48: number // Ingående moms att dra av
|
||
|
||
// Moms att betala eller få tillbaka
|
||
ruta49: number // Moms att betala (positive) eller återfå (negative)
|
||
|
||
// Import (via Tullverket)
|
||
ruta50: number // Beskattningsunderlag vid import
|
||
ruta60: number // Utgående moms 25% import
|
||
ruta61: number // Utgående moms 12% import
|
||
ruta62: number // Utgående moms 6% import
|
||
}
|
||
|
||
// VAT declaration response
|
||
export interface VatDeclaration {
|
||
period: {
|
||
type: VatPeriodType
|
||
year: number
|
||
period: number // 1-12 for monthly, 1-4 for quarterly, 1 for yearly
|
||
start: string // YYYY-MM-DD
|
||
end: string // YYYY-MM-DD
|
||
}
|
||
rutor: VatDeclarationRutor
|
||
/**
|
||
* Period debit/credit totals for the two reverse-charge INPUT VAT accounts,
|
||
* keyed by account number: 2645 (beräknad ingående moms på förvärv från
|
||
* utlandet) and 2647 (ingående moms, omvänd betalningsskyldighet i Sverige).
|
||
*
|
||
* Carried so a caller that reads the declaration over HTTP can hand
|
||
* `runVatDeclarationChecks` its optional per-account totals and get the sharp
|
||
* RC_INPUT_VAT_MISMATCH comparison (rutor 30-32 against 2645/2647) instead of
|
||
* the ruta 48 fallback, which ordinary debiterad ingående moms on 2641 masks.
|
||
* Only this pair travels, not the whole totals map: the check reads nothing
|
||
* else, and the response stays small rather than publishing every VAT account
|
||
* balance in the period.
|
||
*
|
||
* Optional because it crosses a JSON boundary. A client parsing a response
|
||
* from an older deploy must fall back to the ruta 48 form instead of reading
|
||
* absent accounts as zero, which would invert the check into a false alarm.
|
||
* Rebuild the map with `rcInputTotalsFromDeclaration()`
|
||
* (lib/reports/vat-declaration.ts), never by hand.
|
||
*/
|
||
rcInputAccountTotals?: Record<string, { debit: number; credit: number }>
|
||
/**
|
||
* Net debit balance of the reverse-charge BASIS accounts (44xx/45xx),
|
||
* grouped per momssats: r25/r12/r6. Carried so a caller that reads the
|
||
* declaration over HTTP can hand `withRcBasisGapFindings` its downgrade
|
||
* evidence (lib/reports/vat-filing-gate.ts): rutor 20-24 are partitioned by
|
||
* purchase type, not rate, so the per-rate identity against rutor 30-32 is
|
||
* only computable from these account-level figures.
|
||
*
|
||
* Optional because it crosses a JSON boundary: a client parsing a response
|
||
* from an older deploy must keep the blocking per-voucher behavior rather
|
||
* than fabricate zeros, which would read as "no basis booked at any rate"
|
||
* and block correct periods. Produced by `rcBasisTotalsByRate()`, never by
|
||
* hand.
|
||
*/
|
||
rcBasisByRate?: { r25: number; r12: number; r6: number }
|
||
// Supporting data
|
||
invoiceCount: number
|
||
transactionCount: number
|
||
// Breakdown by source
|
||
breakdown: {
|
||
invoices: {
|
||
ruta05: number
|
||
ruta06: number
|
||
ruta07: number
|
||
ruta10: number
|
||
ruta11: number
|
||
ruta12: number
|
||
ruta39: number
|
||
ruta40: number
|
||
// Per-rate base amounts for UI display
|
||
base25: number
|
||
base12: number
|
||
base6: number
|
||
}
|
||
transactions: {
|
||
ruta48: number // Ingående moms from categorized expenses
|
||
}
|
||
receipts: {
|
||
ruta48: number // Ingående moms from receipts
|
||
}
|
||
reverseCharge: {
|
||
ruta20: number
|
||
ruta21: number
|
||
ruta22: number
|
||
ruta23: number
|
||
ruta24: number
|
||
ruta30: number
|
||
ruta31: number
|
||
ruta32: number
|
||
}
|
||
}
|
||
}
|
||
|
||
// Labels for VAT rutor
|
||
export const VAT_RUTA_LABELS: Record<keyof VatDeclarationRutor, string> = {
|
||
ruta05: 'Momspliktig försäljning',
|
||
ruta06: 'Momspliktiga uttag',
|
||
ruta07: 'Vinstmarginalbeskattning',
|
||
ruta08: 'Hyresinkomster (frivillig beskattning)',
|
||
ruta10: 'Utgående moms 25%',
|
||
ruta11: 'Utgående moms 12%',
|
||
ruta12: 'Utgående moms 6%',
|
||
ruta20: 'Inköp av varor från annat EU-land',
|
||
ruta21: 'Inköp av tjänster från annat EU-land',
|
||
ruta22: 'Inköp av tjänster från land utanför EU',
|
||
ruta23: 'Inköp av varor i Sverige',
|
||
ruta24: 'Övriga inköp av tjänster i Sverige',
|
||
ruta30: 'Utgående moms 25% (omvänd skattskyldighet)',
|
||
ruta31: 'Utgående moms 12% (omvänd skattskyldighet)',
|
||
ruta32: 'Utgående moms 6% (omvänd skattskyldighet)',
|
||
ruta35: 'Varuförsäljning till annat EU-land',
|
||
ruta36: 'Varuförsäljning utanför EU (export)',
|
||
ruta37: 'Mellanmans inköp vid trepartshandel',
|
||
ruta38: 'Mellanmans försäljning vid trepartshandel',
|
||
ruta39: 'Försäljning av tjänster till EU-land',
|
||
ruta40: 'Övrig försäljning av tjänster utomlands',
|
||
ruta41: 'Försäljning med omvänd skattskyldighet (Sverige)',
|
||
ruta42: 'Övrig momsfri försäljning m.m.',
|
||
ruta48: 'Ingående moms att dra av',
|
||
ruta49: 'Moms att betala/återfå',
|
||
ruta50: 'Beskattningsunderlag vid import',
|
||
ruta60: 'Utgående moms 25% import',
|
||
ruta61: 'Utgående moms 12% import',
|
||
ruta62: 'Utgående moms 6% import',
|
||
}
|
||
|
||
// ============================================================
|
||
// Event Payload Placeholder Types
|
||
// ============================================================
|
||
|
||
/** Credit note is an invoice with a credited_invoice_id */
|
||
export interface CreditNote extends Invoice {
|
||
credited_invoice_id: string
|
||
}
|
||
|
||
// ============================================================
|
||
// Document Archive Types
|
||
// ============================================================
|
||
|
||
export type DocumentUploadSource =
|
||
| 'camera'
|
||
| 'file_upload'
|
||
| 'email'
|
||
| 'e_invoice'
|
||
| 'scan'
|
||
| 'api'
|
||
| 'system'
|
||
| 'whatsapp'
|
||
/** Fetched by the receipt hunt out of a connected mailbox. */
|
||
| 'mail_hunt'
|
||
|
||
export interface DocumentAttachment {
|
||
id: string
|
||
user_id: string
|
||
company_id: string
|
||
storage_path: string
|
||
file_name: string
|
||
file_size_bytes: number | null
|
||
mime_type: string | null
|
||
sha256_hash: string
|
||
version: number
|
||
original_id: string | null
|
||
superseded_by_id: string | null
|
||
is_current_version: boolean
|
||
uploaded_by: string | null
|
||
upload_source: DocumentUploadSource | null
|
||
digitization_date: string | null
|
||
journal_entry_id: string | null
|
||
journal_entry_line_id: string | null
|
||
prev_version_hash: string | null
|
||
last_integrity_check_at: string | null
|
||
created_at: string
|
||
updated_at: string
|
||
}
|
||
|
||
// ============================================================
|
||
// Audit Log Types
|
||
// ============================================================
|
||
|
||
export type AuditAction =
|
||
| 'INSERT'
|
||
| 'UPDATE'
|
||
| 'DELETE'
|
||
| 'COMMIT'
|
||
| 'REVERSE'
|
||
| 'CORRECT'
|
||
| 'LOCK_PERIOD'
|
||
| 'CLOSE_PERIOD'
|
||
| 'DOCUMENT_DELETE_BLOCKED'
|
||
| 'RETENTION_BLOCK'
|
||
| 'SECURITY_EVENT'
|
||
| 'INTEGRITY_FAILURE'
|
||
| 'COMMITTED_AT_OVERRIDE'
|
||
| 'RESET_SNAPSHOT'
|
||
|
||
export interface AuditLogEntry {
|
||
id: string
|
||
// Nullable in the database and genuinely null in practice: write_audit_log()
|
||
// falls back to auth.uid(), which is NULL for a service-role or global write
|
||
// (the company-less salary_payroll_config rows are the standing example).
|
||
user_id: string | null
|
||
company_id: string | null
|
||
action: AuditAction
|
||
table_name: string | null
|
||
record_id: string | null
|
||
actor_id: string | null
|
||
actor_type: 'user' | 'api_key' | 'mcp_oauth' | 'cron' | 'agent_chat' | 'system' | null
|
||
actor_label: string | null
|
||
old_state: Record<string, unknown> | null
|
||
new_state: Record<string, unknown> | null
|
||
description: string | null
|
||
created_at: string
|
||
}
|
||
|
||
// ============================================================
|
||
// Voucher Gap Detection
|
||
// ============================================================
|
||
|
||
export interface VoucherGap {
|
||
gap_start: number
|
||
gap_end: number
|
||
series: string
|
||
}
|
||
|
||
export interface SequenceMismatch {
|
||
series: string
|
||
sequenceCounter: number
|
||
actualMax: number
|
||
}
|
||
|
||
// ============================================================
|
||
// Year-End Closing Types (Årsbokslut)
|
||
// ============================================================
|
||
|
||
/**
|
||
* Stable machine codes for year-end readiness blockers. One code per
|
||
* blockers.push site in validateYearEndReadiness: the wizard matches on
|
||
* these to attach remediation links, so codes must never be renamed once
|
||
* shipped. The Swedish message stays the display text.
|
||
*/
|
||
export type YearEndBlockerCode =
|
||
| 'PERIOD_NOT_FOUND'
|
||
| 'PERIOD_NOT_ENDED'
|
||
| 'PERIOD_ALREADY_CLOSED'
|
||
| 'CLOSING_ENTRY_EXISTS'
|
||
| 'DRAFT_ENTRIES'
|
||
| 'UNEXPLAINED_VOUCHER_GAP'
|
||
| 'SEQUENCE_COUNTER_BEHIND'
|
||
| 'TRIAL_BALANCE_UNBALANCED'
|
||
| 'CONTINUITY_MISMATCH'
|
||
| 'NEXT_PERIOD_HAS_IB'
|
||
| 'KONTANTMETOD_CUTOFF_REQUIRED'
|
||
| 'KONTANTMETOD_CUTOFF_CHECK_FAILED'
|
||
| 'UNBOOKED_TRANSACTIONS'
|
||
| 'UNBOOKED_CHECK_FAILED'
|
||
|
||
export interface YearEndBlocker {
|
||
code: YearEndBlockerCode
|
||
/** Swedish, user-facing: bokslut is a stays-Swedish surface. */
|
||
message: string
|
||
}
|
||
|
||
export interface YearEndValidation {
|
||
ready: boolean
|
||
/** Blocking errors with stable machine codes. */
|
||
blockers: YearEndBlocker[]
|
||
/** Blocker messages only; mirrors `blockers`. Kept so existing consumers
|
||
* of the string list (v1 compliance check, MCP tool) stay unchanged. */
|
||
errors: string[]
|
||
warnings: string[]
|
||
draftCount: number
|
||
voucherGaps: VoucherGap[]
|
||
unexplainedGaps: VoucherGap[]
|
||
sequenceMismatches: SequenceMismatch[]
|
||
trialBalanceBalanced: boolean
|
||
/**
|
||
* Bank transactions in the period with no verifikat (untriaged +
|
||
* business-confirmed-but-unbooked). Blocking: lockPeriod refuses to lock
|
||
* over them, so surfacing the count here stops executeYearEndClosing from
|
||
* aborting mid-flow at the lock step. Optional: absent on the early
|
||
* period-not-found return.
|
||
*/
|
||
unbookedTransactionCount?: number
|
||
}
|
||
|
||
export interface YearEndPreview {
|
||
netResult: number
|
||
closingAccount: string
|
||
closingAccountName: string
|
||
closingLines: CreateJournalEntryLineInput[]
|
||
resultAccountSummary: { account_number: string; account_name: string; amount: number }[]
|
||
currencyRevaluation: CurrencyRevaluationPreview | null
|
||
/**
|
||
* True when an aktiebolag is about to close a profit year with no tax
|
||
* account (89xx except 8999) among the accounts being closed. Advisory
|
||
* only, never a blocker: zero tax is legitimate with underskottsavdrag.
|
||
*/
|
||
bolagsskattMissing: boolean
|
||
}
|
||
|
||
export interface YearEndResult {
|
||
closingEntry: JournalEntry
|
||
nextPeriod: FiscalPeriod
|
||
openingBalanceEntry: JournalEntry
|
||
revaluationEntry: JournalEntry | null
|
||
/**
|
||
* Year-open omföring av föregående års resultat (Dr 2099 / Cr 2098) posted
|
||
* into the new period so 2099 "Årets resultat" starts the year at zero.
|
||
* Aktiebolag only; null for enskild firma or when 2099 carried no balance.
|
||
* The further disposition 2098 → 2091/2898 is the stämma's decision and is
|
||
* intentionally left to a separate step.
|
||
*/
|
||
resultAppropriationEntry: JournalEntry | null
|
||
/**
|
||
* True when the year-open omföring (2099 → 2098) was attempted but threw.
|
||
* The close + IB are already valid and immutable, so the failure is
|
||
* non-fatal to the year-end itself, but it leaves 2099 carrying the prior
|
||
* result into the new period, which is non-compliant. Surfaced so the UI can
|
||
* alert the user (and an alertable log line fires server-side); the
|
||
* retroactive catch-up script (scripts/repair-result-appropriation.ts) then
|
||
* posts the missing omföring. False on success or when there was nothing to do.
|
||
*/
|
||
resultAppropriationFailed: boolean
|
||
/**
|
||
* IB/UB reconciliation per balance sheet account, computed after the
|
||
* opening balances are posted. Surfaced to the UI's ResultStep so the
|
||
* user can verify continuity before navigating away. Always within
|
||
* ORE_TOLERANCE, otherwise executeYearEndClosing would have thrown.
|
||
*/
|
||
continuity?: ContinuityCheckResult
|
||
}
|
||
|
||
// ============================================================
|
||
// Asset Register Types (Anläggningsregister)
|
||
// ============================================================
|
||
|
||
export type AssetCategory =
|
||
| 'immaterial'
|
||
| 'building'
|
||
| 'land_improvement'
|
||
| 'machinery'
|
||
| 'equipment'
|
||
| 'vehicle'
|
||
| 'computer'
|
||
| 'other_tangible'
|
||
|
||
/** Read type includes historical per-asset tax-method values retained on
|
||
* disposed rows. New and active assets may only be written as linear. */
|
||
export type DepreciationMethod =
|
||
| 'linear'
|
||
| 'declining_balance_30'
|
||
| 'declining_balance_20'
|
||
| 'restvardesavskrivning_25'
|
||
|
||
export type WritableDepreciationMethod = 'linear'
|
||
export type AssetDisposalType = 'sale' | 'scrap' | 'business_transfer'
|
||
export type AssetJamkningDirection = 'increase' | 'decrease' | 'none' | 'transferred'
|
||
|
||
/**
|
||
* K3 component (BFNAR 2012:1 ch 17.4: komponentavskrivning). When a
|
||
* substantial asset (typically real estate) has significant components with
|
||
* materially different useful lives, K3 reporting requires each component to
|
||
* be depreciated on its own life rather than treating the asset as a single
|
||
* unit. Components are stored as an array on `Asset.k3_components`; when
|
||
* non-null, the depreciation engine routes through `computeComponentDepreciation`
|
||
* and sums per-component linear depreciation (with the same pro-ration logic
|
||
* as the asset-level linear method).
|
||
*
|
||
* Validation (enforced in `lib/bokslut/assets/k3-components.ts`):
|
||
* - sum(components.cost) === asset.acquisition_cost (±1 kr tolerance)
|
||
* - every component: cost > 0, useful_life_months > 0
|
||
* - salvage_value (if present) ≤ component cost
|
||
* - non-empty array when set to non-null
|
||
*
|
||
* Salvage_value defaults to 0 when omitted.
|
||
*/
|
||
export interface K3Component {
|
||
name: string
|
||
cost: number
|
||
useful_life_months: number
|
||
salvage_value?: number
|
||
}
|
||
|
||
export interface Asset {
|
||
id: string
|
||
user_id: string
|
||
company_id: string
|
||
name: string
|
||
category: AssetCategory
|
||
acquisition_date: string
|
||
acquisition_cost: number
|
||
salvage_value: number
|
||
useful_life_months: number
|
||
depreciation_method: DepreciationMethod
|
||
bas_asset_account: string
|
||
bas_accumulated_account: string
|
||
bas_expense_account: string
|
||
/** Deprecated legacy field. New tax depreciation is pooled per fiscal
|
||
* period and ordinary per-asset depreciation is linear. */
|
||
restvarde_target: number | null
|
||
disposed_at: string | null
|
||
disposed_proceeds: number | null
|
||
/** How the asset left the register. Null for legacy disposal records. */
|
||
disposal_type?: AssetDisposalType | null
|
||
/** Posted voucher that atomically completed the disposal. */
|
||
disposal_journal_entry_id?: string | null
|
||
/** Output VAT on disposal proceeds (ML 3 kap 3 § / 7 kap 3 §). Defaults to
|
||
* 0: only nonzero when the sale was momspliktig. The VAT account
|
||
* (2611/2621/2631) is derived from disposed_vat_treatment. */
|
||
disposed_proceeds_vat: number
|
||
/** VAT treatment applied to disposal proceeds. Null for legacy disposals
|
||
* without VAT data. Constrained by DB CHECK to the same enum as
|
||
* VatTreatment. */
|
||
disposed_vat_treatment: VatTreatment | null
|
||
/** Absolute input VAT adjustment under ML (2023:200), chapter 15. */
|
||
jamkning_amount: number
|
||
/** Remaining months in the korrigeringstid at disposal date. Audit
|
||
* metadata only: the booking sits on the journal entry. */
|
||
jamkning_remaining_months: number | null
|
||
/** Total korrigeringstid in months: 60 (lös egendom) or 120 (fastighet /
|
||
* markanläggning). Audit metadata. */
|
||
jamkning_total_months: number | null
|
||
/** Original input VAT that was deducted at acquisition. Audit metadata
|
||
* the user supplies (or the system derives from the supplier invoice). */
|
||
jamkning_original_input_vat: number | null
|
||
/** Current-law adjustment metadata. Old month fields remain for legacy rows. */
|
||
jamkning_direction?: AssetJamkningDirection | null
|
||
jamkning_remaining_years?: number | null
|
||
jamkning_total_years?: number | null
|
||
jamkning_original_deduction_percent?: number | null
|
||
jamkning_new_deduction_percent?: number | null
|
||
/** K3 component depreciation (BFNAR 2012:1 ch.17.4). When non-null, the
|
||
* depreciation engine sums per-component linear depreciation instead of
|
||
* applying `depreciation_method` to the asset as a whole. Null for K2
|
||
* companies (the API rejects writes for accounting_framework='k2'). */
|
||
k3_components: K3Component[] | null
|
||
notes: string | null
|
||
created_at: string
|
||
updated_at: string
|
||
}
|
||
|
||
// ============================================================
|
||
// IB/UB Continuity Check Types (Avstämning ingående/utgående balans)
|
||
// ============================================================
|
||
|
||
export interface ContinuityDiscrepancy {
|
||
account_number: string
|
||
account_name: string
|
||
previous_ub_net: number
|
||
current_ib_net: number
|
||
difference: number
|
||
}
|
||
|
||
export interface ContinuityCheckResult {
|
||
valid: boolean
|
||
period_name: string
|
||
previous_period_name: string | null
|
||
discrepancies: ContinuityDiscrepancy[]
|
||
checked_accounts: number
|
||
}
|
||
|
||
// ============================================================
|
||
// Currency Revaluation Types (Omvärdering utländsk valuta)
|
||
// ============================================================
|
||
|
||
export interface RevaluationItem {
|
||
type: 'receivable' | 'payable'
|
||
source_id: string
|
||
reference: string
|
||
currency: Currency
|
||
amount_in_currency: number
|
||
original_rate: number
|
||
closing_rate: number
|
||
original_sek: number
|
||
closing_sek: number
|
||
difference_sek: number
|
||
}
|
||
|
||
export interface CurrencyRevaluationPreview {
|
||
items: RevaluationItem[]
|
||
lines: CreateJournalEntryLineInput[]
|
||
closingRates: Record<string, number>
|
||
totalGain: number
|
||
totalLoss: number
|
||
netEffect: number
|
||
}
|
||
|
||
export interface CurrencyRevaluationResult {
|
||
entry: JournalEntry
|
||
preview: CurrencyRevaluationPreview
|
||
}
|
||
|
||
// ============================================================
|
||
// Invoice Reminder Types (Betalningspåminnelser)
|
||
// ============================================================
|
||
|
||
// Response type from customer action
|
||
export type ReminderResponseType = 'marked_paid' | 'disputed'
|
||
|
||
// Invoice reminder record
|
||
export interface InvoiceReminder {
|
||
id: string
|
||
invoice_id: string
|
||
user_id: string
|
||
company_id: string
|
||
reminder_level: 1 | 2 | 3
|
||
sent_at: string
|
||
email_to: string
|
||
response_type: ReminderResponseType | null
|
||
response_at: string | null
|
||
action_token: string
|
||
action_token_used: boolean
|
||
created_at: string
|
||
// Dröjsmålsränta + lagstadgad påminnelseavgift (Räntelagen §6, Lag 1981:739)
|
||
interest_amount: number
|
||
interest_rate: number | null
|
||
interest_from_date: string | null
|
||
interest_days: number | null
|
||
reminder_fee: number
|
||
fee_journal_entry_id: string | null
|
||
}
|
||
|
||
// ============================================================
|
||
// Transaction Ingestion Types (re-exported for extension use)
|
||
// ============================================================
|
||
|
||
/** Normalized transaction input for the generic ingestion pipeline */
|
||
export interface RawTransaction {
|
||
date: string
|
||
description: string
|
||
amount: number
|
||
currency: string
|
||
external_id: string
|
||
mcc_code?: number | null
|
||
merchant_name?: string | null
|
||
reference?: string | null
|
||
bank_connection_id?: string | null
|
||
import_source?: string
|
||
/**
|
||
* Counterparty IBAN from PSD2 (creditor for outflows, debtor for inflows).
|
||
* Used by the own-account transfer detector: when this matches another
|
||
* cash_accounts row for the same company, both legs auto-book as a transfer.
|
||
*/
|
||
counterparty_iban?: string | null
|
||
/**
|
||
* Bankgiro / Plusgiro / BBAN fallback when no IBAN is available (typical
|
||
* for Swedish domestic transfers). Kept distinct from IBAN so matching
|
||
* doesn't accidentally collide BG numbers with IBAN strings.
|
||
*/
|
||
counterparty_account?: string | null
|
||
/**
|
||
* Payment rail the source already knows structurally (e.g. the Stripe feed's
|
||
* balance-transaction type). Beats every ingest-side heuristic; leave unset
|
||
* to let classifyTransactionMethod() derive it from codes/description/MCC.
|
||
*/
|
||
transaction_method?: TransactionMethod | null
|
||
/** ISO 20022 bank transaction code from PSD2, verbatim (e.g. PMNT-CCRD-POSD). */
|
||
bank_transaction_code?: string | null
|
||
/** ASPSP-proprietary transaction code from PSD2, verbatim. */
|
||
proprietary_bank_transaction_code?: string | null
|
||
}
|
||
|
||
/** Options for the transaction ingestion pipeline */
|
||
export interface IngestOptions {
|
||
/** Skip auto-categorization (mapping engine + journal entry creation).
|
||
* Reconciliation and invoice matching still run.
|
||
* Used when SIE-imported entries overlap the sync date range
|
||
* to prevent double-booking. */
|
||
skipAutoCategorization?: boolean
|
||
/** Override the default settlement account (1930) for bank transactions.
|
||
* Used when importing to a secondary bank account (e.g., 1931). */
|
||
settlementAccount?: string
|
||
/** Only INSERT transactions + dedup. Skip reconciliation, invoice matching,
|
||
* supplier matching, and auto-categorization. For viewer imports. */
|
||
rawInsertOnly?: boolean
|
||
/** The bank_file_imports batch id to stamp on every inserted row
|
||
* (transactions.bank_file_import_id). Set by the bank-file import paths
|
||
* so "undo this import" can scope its bulk delete to exactly this batch.
|
||
* Omitted by every other caller (PSD2 sync, MCP): those rows stay NULL. */
|
||
bankFileImportId?: string
|
||
}
|
||
|
||
/** Result of the transaction ingestion pipeline */
|
||
export interface IngestResult {
|
||
imported: number
|
||
duplicates: number
|
||
reconciled: number
|
||
auto_categorized: number
|
||
auto_matched_invoices: number
|
||
errors: number
|
||
transaction_ids: string[]
|
||
/** First insert error encountered, surfaced for debugging. Optional. */
|
||
first_error?: { message: string; code?: string | null; details?: string | null; hint?: string | null }
|
||
/**
|
||
* SHADOW-MODE counter: rows that an enforcing same-feed scope-drift dedup rule
|
||
* WOULD have treated as re-imports (IBAN-drift re-imports the external_id
|
||
* check misses). These are still imported: the field only measures how often
|
||
* the rule would fire, so it can be validated on real data before enforcement.
|
||
*/
|
||
shadow_scope_drift_candidates?: number
|
||
/**
|
||
* SHADOW-MODE counter: rows that an enforcing date-drift dedup rule WOULD have
|
||
* treated as re-imports: a twin with the same öre and an account-compatible,
|
||
* bridging (or cross-channel count-symmetric) match one day away, which the
|
||
* exact-date content bridge misses. Still imported; the field only measures
|
||
* how often the rule would fire, for validation before any enforcement.
|
||
*/
|
||
shadow_date_drift_candidates?: number
|
||
}
|
||
|
||
// ── Webshop orders (Orders page; synced by the woocommerce/shopify extensions) ──
|
||
|
||
export type WebshopPlatform = 'woocommerce' | 'shopify'
|
||
export type WebshopOrderRowType = 'order' | 'refund'
|
||
|
||
/** One VAT rate bucket of an order, in the order's currency. */
|
||
export interface WebshopVatBreakdownLine {
|
||
/** Percent as a number (25, 12, 6, 0). */
|
||
rate: number
|
||
net: number
|
||
tax: number
|
||
}
|
||
|
||
/** One order line, in the order's currency. */
|
||
export interface WebshopOrderLineItem {
|
||
name: string
|
||
quantity: number
|
||
total: number
|
||
total_tax: number
|
||
/** Percent; null when the rate could not be resolved from tax_lines. */
|
||
vat_rate: number | null
|
||
}
|
||
|
||
/** Row shape of public.webshop_orders. */
|
||
export interface WebshopOrder {
|
||
id: string
|
||
company_id: string
|
||
user_id: string
|
||
platform: WebshopPlatform
|
||
/** Normalized store host(+path); the identity frozen into external_id. */
|
||
store_scope: string
|
||
store_label: string | null
|
||
/** Soft pointer to the platform's *_connections row (no FK). */
|
||
connection_id: string | null
|
||
row_type: WebshopOrderRowType
|
||
parent_order_id: string | null
|
||
/** Frozen feed scheme: woo_{scope}_order_{id} / woo_{scope}_refund_{id}. */
|
||
external_id: string
|
||
platform_order_id: string
|
||
order_number: string
|
||
/** Raw platform status (pending/processing/completed/refunded/...). */
|
||
status: string
|
||
is_paid: boolean
|
||
order_date: string
|
||
paid_date: string | null
|
||
currency: string
|
||
/** Gross incl. tax and shipping; negative on refund rows. */
|
||
total: number
|
||
total_tax: number
|
||
/** Null until the FX rate resolves; booking is blocked while null. */
|
||
total_sek: number | null
|
||
exchange_rate: number | null
|
||
vat_breakdown: WebshopVatBreakdownLine[]
|
||
line_items: WebshopOrderLineItem[]
|
||
customer_name: string | null
|
||
customer_company: string | null
|
||
customer_email: string | null
|
||
/** Best effort; must be user-confirmed before use in legal fields. */
|
||
customer_orgnr: string | null
|
||
/** Billing country, ISO 3166-1 alpha-2; drives the export/EU 0%-sale hint. */
|
||
customer_country: string | null
|
||
payment_method: string | null
|
||
payment_method_title: string | null
|
||
gateway_reference: string | null
|
||
/** Order rows: informational sum of refunds seen so far. */
|
||
refunded_total: number
|
||
journal_entry_id: string | null
|
||
invoice_id: string | null
|
||
/** Same money event already imported by the legacy transactions feed. */
|
||
legacy_transaction_id: string | null
|
||
/** Financial delta arrived from the store after booking froze this row. */
|
||
remote_changed_after_freeze: boolean
|
||
/** User marked the row as booked/handled outside the integration. */
|
||
manually_booked_at: string | null
|
||
manually_booked_by: string | null
|
||
/** Optional informational reference to the existing verifikat. */
|
||
manually_booked_journal_entry_id: string | null
|
||
created_at: string
|
||
updated_at: string
|
||
}
|
||
|
||
/** Per-payment-method booking policy in webshop_store_settings. */
|
||
export type WebshopPaymentMethodPolicy =
|
||
| { mode: 'book'; account: string }
|
||
| { mode: 'invoice' }
|
||
|
||
/** Row shape of public.webshop_store_settings. */
|
||
export interface WebshopStoreSettings {
|
||
id: string
|
||
company_id: string
|
||
user_id: string
|
||
platform: WebshopPlatform
|
||
store_scope: string
|
||
payment_method_account_map: Record<string, WebshopPaymentMethodPolicy>
|
||
created_at: string
|
||
updated_at: string
|
||
}
|
||
|
||
// ── Invoice extraction (used by invoice-inbox extension and core utils) ──
|
||
|
||
export type ExtractedDocumentKind =
|
||
| 'receipt'
|
||
| 'supplier_invoice'
|
||
| 'government_letter'
|
||
| 'other'
|
||
export type ExtractedPaymentMethod = 'card' | 'swish' | 'cash' | 'invoice' | 'other'
|
||
export type ExtractedMerchantCategory =
|
||
| 'restaurant'
|
||
| 'cafe'
|
||
| 'taxi'
|
||
| 'parking'
|
||
| 'fuel'
|
||
| 'grocery'
|
||
| 'hotel'
|
||
| 'other'
|
||
export type ExtractedLegibility = 'good' | 'partial' | 'unreadable'
|
||
|
||
export interface InvoiceExtractionResult {
|
||
// Classification fields (2026-08): optional because extractions stored
|
||
// before they existed lack them. They route UI emphasis and clarifying
|
||
// questions only: never bookings.
|
||
documentKind?: ExtractedDocumentKind | null
|
||
merchantCategory?: ExtractedMerchantCategory | null
|
||
legibility?: ExtractedLegibility | null
|
||
purchaseTime?: string | null
|
||
payment?: { method: ExtractedPaymentMethod | null; cardLast4: string | null } | null
|
||
supplier: {
|
||
name: string | null
|
||
orgNumber: string | null
|
||
vatNumber: string | null
|
||
address: string | null
|
||
bankgiro: string | null
|
||
plusgiro: string | null
|
||
}
|
||
invoice: {
|
||
invoiceNumber: string | null
|
||
invoiceDate: string | null
|
||
dueDate: string | null
|
||
paymentReference: string | null
|
||
currency: string
|
||
// Service/coverage window the invoice charges for: drives the
|
||
// periodisering prefill. Optional: extractions from before the field
|
||
// existed lack it.
|
||
servicePeriodStart?: string | null
|
||
servicePeriodEnd?: string | null
|
||
}
|
||
lineItems: ExtractedInvoiceLineItem[]
|
||
totals: {
|
||
subtotal: number | null
|
||
vatAmount: number | null
|
||
total: number | null
|
||
// Öresavrundning line on Swedish receipts; negative when rounded down.
|
||
roundingAmount?: number | null
|
||
}
|
||
vatBreakdown: VatBreakdownItem[]
|
||
// Amounts visible on non-invoice documents (bankintyg, avtal, contracts)
|
||
// with no invoice-style total. Matching hint only, never booked. Optional:
|
||
// extractions from before the field existed lack it.
|
||
prominentAmounts?: ProminentAmount[]
|
||
// 'prominent' = totals.total was promoted from the document's single
|
||
// prominent amount (promoteSingleProminentAmount), not read off an invoice.
|
||
// Matching treats such a total as fallback-grade; cleared when a user edits
|
||
// totals.total.
|
||
totalSource?: 'prominent' | null
|
||
confidence: number
|
||
suggestedTemplateId?: string
|
||
// Set by the caller (not the model) when a long PDF was sliced before
|
||
// extraction: fields were read from `analyzed` of `total` pages (the first
|
||
// pages plus the last, where totals usually sit).
|
||
pages?: { total: number; analyzed: number }
|
||
}
|
||
|
||
export interface ExtractedInvoiceLineItem {
|
||
description: string
|
||
quantity: number
|
||
unitPrice: number | null
|
||
lineTotal: number
|
||
vatRate: number | null
|
||
accountSuggestion: string | null
|
||
suggestedTemplateId?: string
|
||
}
|
||
|
||
export interface VatBreakdownItem {
|
||
rate: number
|
||
base: number
|
||
amount: number
|
||
}
|
||
|
||
/** One amount printed on a non-invoice document, with the document's own label. */
|
||
export interface ProminentAmount {
|
||
amount: number
|
||
label: string | null
|
||
}
|
||
|
||
// KPI Report
|
||
export interface KPIReport {
|
||
netResult: number // SEK
|
||
cashPosition: number // SEK (sum of 19xx account balances)
|
||
outstandingReceivables: number // SEK
|
||
overdueReceivables: number // SEK
|
||
vatLiability: number // SEK, ruta 49 (positive = owe, negative = refund)
|
||
totalRevenue: number // SEK
|
||
totalExpenses: number // SEK
|
||
grossMargin: number | null // percentage, null if no revenue
|
||
expenseRatio: number | null // percentage, null if no revenue
|
||
avgPaymentDays: number | null // days, null if fewer than 5 paid invoices
|
||
periodComplete: boolean // whether selected period is closed/complete
|
||
months: { label: string; income: number; expenses: number; net: number }[]
|
||
period: { start: string; end: string }
|
||
expenseComposition: {
|
||
class4: number
|
||
class5: number
|
||
class6: number
|
||
class7: number
|
||
}
|
||
/** Top expense accounts (BAS classes 4-7) for the period, largest first. */
|
||
topExpenseAccounts: { account_number: string; account_name: string; total: number }[]
|
||
topSuppliers: { supplier_id: string; supplier_name: string; total: number }[]
|
||
/**
|
||
* Foreign-currency supplier invoices excluded from `topSuppliers` because
|
||
* they had neither a SEK total nor an exchange rate. Same contract as
|
||
* `unconverted_fx_count` on the supplier ledger: excluded rows are counted,
|
||
* not silently dropped.
|
||
*/
|
||
topSuppliersUnconvertedFxCount: number
|
||
}
|
||
|
||
export interface KPIPreferences {
|
||
visibleKpis: string[]
|
||
kpiOrder: string[]
|
||
accountOverrides: Record<string, string[]>
|
||
}
|
||
|
||
// ============================================================
|
||
// Salary Module Types (Lönehantering)
|
||
// ============================================================
|
||
|
||
export type EmploymentType = 'employee' | 'company_owner' | 'board_member'
|
||
export type SalaryType = 'monthly' | 'hourly'
|
||
export type FSkattStatus = 'a_skatt' | 'f_skatt' | 'fa_skatt' | 'not_verified'
|
||
export type VacationRule = 'procentregeln' | 'sammaloneregeln' | 'none' | 'semesterersattning'
|
||
export type SalaryRunStatus = 'draft' | 'review' | 'approved' | 'paid' | 'booked' | 'corrected'
|
||
|
||
export type SalaryLineItemType =
|
||
| 'monthly_salary' | 'hourly_salary'
|
||
| 'overtime' | 'overtime_50' | 'overtime_100'
|
||
| 'ob_weekday_evening' | 'ob_weekend' | 'ob_night' | 'ob_holiday'
|
||
| 'bonus' | 'commission'
|
||
| 'gross_deduction_pension' | 'gross_deduction_other'
|
||
| 'benefit_car' | 'benefit_housing' | 'benefit_meals' | 'benefit_wellness' | 'benefit_bike' | 'benefit_other'
|
||
| 'sick_karens' | 'sick_day2_14' | 'sick_day15_plus'
|
||
| 'vab' | 'parental_leave' | 'unpaid_leave' | 'vacation' | 'semesterersattning'
|
||
| 'traktamente_taxfree' | 'traktamente_taxable'
|
||
| 'mileage_taxfree' | 'mileage_taxable'
|
||
| 'net_deduction_advance' | 'net_deduction_union' | 'net_deduction_benefit_payment'
|
||
| 'net_deduction_other'
|
||
| 'oresavrundning'
|
||
| 'correction' | 'other'
|
||
|
||
export type ShiftPremiumItemType =
|
||
| 'overtime_50' | 'overtime_100'
|
||
| 'ob_weekday_evening' | 'ob_weekend' | 'ob_night' | 'ob_holiday'
|
||
|
||
export interface ShiftPremiumRule {
|
||
id: string
|
||
company_id: string
|
||
name: string
|
||
applies_to_all_employees: boolean
|
||
applies_to_employee_ids: string[]
|
||
/** ISO weekday array: 1 = Monday … 7 = Sunday. */
|
||
day_of_week: number[]
|
||
/** 'HH:MM' or 'HH:MM:SS' (PostgreSQL TIME). */
|
||
start_time: string
|
||
/** 'HH:MM' or 'HH:MM:SS'. End values <= start mean the window wraps midnight. */
|
||
end_time: string
|
||
premium_percent: number
|
||
item_type: ShiftPremiumItemType
|
||
priority: number
|
||
is_active: boolean
|
||
created_at: string
|
||
updated_at: string
|
||
created_by: string | null
|
||
}
|
||
|
||
export interface Employee {
|
||
id: string
|
||
company_id: string
|
||
user_id: string
|
||
first_name: string
|
||
last_name: string
|
||
personnummer: string
|
||
personnummer_last4: string
|
||
employment_type: EmploymentType
|
||
employment_start: string
|
||
employment_end: string | null
|
||
employment_degree: number
|
||
salary_type: SalaryType
|
||
monthly_salary: number | null
|
||
hourly_rate: number | null
|
||
tax_table_number: number | null
|
||
tax_column: number
|
||
tax_municipality: string | null
|
||
jamkning_percentage: number | null
|
||
jamkning_valid_from: string | null
|
||
jamkning_valid_to: string | null
|
||
is_sidoinkomst: boolean
|
||
f_skatt_status: FSkattStatus
|
||
f_skatt_verified_at: string | null
|
||
clearing_number: string | null
|
||
bank_account_number: string | null
|
||
vacation_rule: VacationRule
|
||
vacation_days_per_year: number
|
||
vacation_days_saved: number
|
||
semestertillagg_rate: number
|
||
// Arbetsschema-lite: weekly schedule driving the hourly/daily divisors
|
||
// (173/21 at the defaults). employment_degree keeps prorating base salary;
|
||
// these ONLY drive divisors.
|
||
hours_per_week: number
|
||
workdays_per_week: number
|
||
email: string | null
|
||
phone: string | null
|
||
address_line1: string | null
|
||
postal_code: string | null
|
||
city: string | null
|
||
specification_number: number | null
|
||
vaxa_stod_eligible: boolean
|
||
vaxa_stod_start: string | null
|
||
vaxa_stod_end: string | null
|
||
// Dimensions PR8: bag ({sie_dim_no: code}) applied to this employee's P&L
|
||
// cost lines when a salary run is booked. jsonb DEFAULT '{}'. Optional in
|
||
// TS for pre-migration fixtures.
|
||
default_dimensions?: Record<string, string>
|
||
is_active: boolean
|
||
created_at: string
|
||
updated_at: string
|
||
}
|
||
|
||
/**
|
||
* An employee as returned by the read surfaces (`/api/salary/employees`,
|
||
* `/api/salary/employees/{id}`, `/api/salary/runs/{id}`,
|
||
* `/api/salary/runs/{id}/employees/{employeeId}`) and by the v1 REST write
|
||
* responses.
|
||
*
|
||
* `personnummer` is deliberately ABSENT: the column holds AES-256-GCM
|
||
* ciphertext, and the display form is carried under the separate, read-only
|
||
* `personnummer_masked` key. Returning the mask under the writable key name
|
||
* would let a client that reads an object and writes it back post the mask
|
||
* into the encrypt path, so the two names never collide by construction.
|
||
*/
|
||
export type EmployeeMasked = Omit<Employee, 'personnummer'> & {
|
||
personnummer_masked: string
|
||
}
|
||
|
||
export interface SalaryRun {
|
||
id: string
|
||
company_id: string
|
||
user_id: string
|
||
period_year: number
|
||
period_month: number
|
||
payment_date: string
|
||
status: SalaryRunStatus
|
||
voucher_series: string
|
||
total_gross: number
|
||
total_tax: number
|
||
total_net: number
|
||
total_avgifter: number
|
||
total_vacation_accrual: number
|
||
total_employer_cost: number
|
||
salary_entry_id: string | null
|
||
avgifter_entry_id: string | null
|
||
vacation_entry_id: string | null
|
||
agi_generated_at: string | null
|
||
agi_submitted_at: string | null
|
||
payment_file_format: 'bg_lb' | 'pain001' | null
|
||
payment_file_generated_at: string | null
|
||
calculation_params: Record<string, unknown> | null
|
||
approved_by: string | null
|
||
approved_at: string | null
|
||
paid_at: string | null
|
||
booked_at: string | null
|
||
booked_by: string | null
|
||
notes: string | null
|
||
is_correction: boolean
|
||
corrects_run_id: string | null
|
||
created_at: string
|
||
updated_at: string
|
||
// Relations
|
||
employees?: SalaryRunEmployee[]
|
||
}
|
||
|
||
export interface SalaryRunEmployee {
|
||
id: string
|
||
salary_run_id: string
|
||
employee_id: string
|
||
company_id: string
|
||
employment_degree: number
|
||
monthly_salary: number
|
||
salary_type: string
|
||
hours_worked: number | null
|
||
gross_salary: number
|
||
gross_deductions: number
|
||
benefit_values: number
|
||
taxable_income: number
|
||
tax_withheld: number
|
||
tax_withheld_override: number | null
|
||
net_deductions: number
|
||
net_salary: number
|
||
avgifter_rate: number
|
||
avgifter_amount: number
|
||
avgifter_amount_override: number | null
|
||
avgifter_basis: number
|
||
avgifter_basis_override: number | null
|
||
override_reason: string | null
|
||
vacation_accrual: number
|
||
vacation_accrual_avgifter: number
|
||
tax_table_number: number | null
|
||
tax_column: number | null
|
||
tax_table_year: number | null
|
||
sick_days: number
|
||
vab_days: number
|
||
parental_days: number
|
||
vacation_days_taken: number
|
||
calculation_breakdown: Record<string, unknown> | null
|
||
ytd_gross: number
|
||
ytd_tax: number
|
||
ytd_net: number
|
||
created_at: string
|
||
updated_at: string
|
||
// Relations
|
||
employee?: Employee
|
||
line_items?: SalaryLineItem[]
|
||
}
|
||
|
||
export interface SalaryLineItem {
|
||
id: string
|
||
salary_run_employee_id: string
|
||
company_id: string
|
||
item_type: SalaryLineItemType
|
||
description: string
|
||
quantity: number | null
|
||
unit_price: number | null
|
||
amount: number
|
||
is_taxable: boolean
|
||
is_avgift_basis: boolean
|
||
is_vacation_basis: boolean
|
||
is_gross_deduction: boolean
|
||
is_net_deduction: boolean
|
||
account_number: string | null
|
||
sort_order: number
|
||
created_at: string
|
||
updated_at: string
|
||
}
|
||
|
||
/**
|
||
* A `pending_operations` row a chat conversation staged and nobody has answered
|
||
* yet, as returned by GET /api/agent/conversations/[id] and by the /chat/[id]
|
||
* server page.
|
||
*
|
||
* Approval cards ride on streamed events that are never persisted, so this is
|
||
* what lets a resumed thread show its still-open proposal instead of silently
|
||
* dropping it. `operation_type` is the bare action name as stored
|
||
* ('categorize_transaction'), not the prefixed MCP tool name.
|
||
*/
|
||
export interface StoredStagedOperation {
|
||
id: string
|
||
operation_type: string
|
||
title?: string | null
|
||
risk_level?: string | null
|
||
preview_data?: unknown
|
||
params?: Record<string, unknown> | null
|
||
}
|
||
|
||
// ============================================================
|
||
// Körjournal (mileage trips)
|
||
// ============================================================
|
||
|
||
export type MileageVehicleType = 'own_car' | 'company_car_fossil' | 'company_car_electric'
|
||
|
||
export type MileageTripStatus = 'draft' | 'booked'
|
||
|
||
/** A `mileage_trips` row: one business trip in the körjournal. */
|
||
export interface MileageTrip {
|
||
id: string
|
||
company_id: string
|
||
user_id: string
|
||
employee_id: string | null
|
||
trip_date: string
|
||
vehicle_type: MileageVehicleType
|
||
vehicle_registration: string | null
|
||
odometer_start: number | null
|
||
odometer_end: number | null
|
||
distance_km: number
|
||
from_location: string
|
||
to_location: string
|
||
purpose: string
|
||
visited: string | null
|
||
is_round_trip: boolean
|
||
status: MileageTripStatus
|
||
journal_entry_id: string | null
|
||
salary_run_id: string | null
|
||
notes: string | null
|
||
created_via: 'manual' | 'mcp' | 'import'
|
||
created_at: string
|
||
updated_at: string
|
||
}
|
||
|
||
export interface CreateMileageTripInput {
|
||
trip_date: string
|
||
vehicle_type?: MileageVehicleType
|
||
vehicle_registration?: string | null
|
||
odometer_start?: number | null
|
||
odometer_end?: number | null
|
||
distance_km: number
|
||
from_location: string
|
||
to_location: string
|
||
purpose: string
|
||
visited?: string | null
|
||
is_round_trip?: boolean
|
||
employee_id?: string | null
|
||
notes?: string | null
|
||
created_via?: 'manual' | 'mcp' | 'import'
|
||
}
|
||
|
||
/** Per-vehicle-type aggregation of draft trips for a period. */
|
||
export interface MileagePeriodSummary {
|
||
vehicle_type: MileageVehicleType
|
||
trip_count: number
|
||
total_km: number
|
||
total_mil: number
|
||
rate_per_mil: number
|
||
amount: number
|
||
}
|