* feat(company): ideell förening as a third legal form, behind a flag (#2072 step 1) Why the problem occurred: the legal form was modelled as a binary flag in ~300 files. `EntityType` was a two-member union, but nothing dispatched on it exhaustively: 28 sites defaulted `?? 'enskild_firma'` (invoice, categorize, match, stripe, invoice-inbox) or `?? 'aktiebolag'` (year-end, bokslut, MCP), and every form-dependent choice was an `=== 'aktiebolag' ? A : B` ternary. Widening the union compiled everywhere and changed nothing, so a förening would have booked as an enskild firma in the app and as an aktiebolag in bokslut and MCP, with no error anywhere. The lookup refused föreningar at the door (mapEntityType returned null), which is what the tester hit. What was removed or simplified: the silent defaults. One module, lib/company/entity-type.ts, now holds the list (ENTITY_TYPES), the parser (never defaults), the resolver (settings hint, then companies.entity_type, then throw) and `byEntityType`, whose Record arms make the compiler refuse the next widening until each site has an answer. The form-dependent facts (closing account, owner settlement account, calendar-year lock, default method, K1/K2 label, personnummer vs 16-prefix) live there once instead of in the ternaries. On the SQL side supported_entity_types() replaces four copies of the literal list in the create RPCs. Why this shape and not the proposed one: the tracker asked for the enum widening plus a chart; that alone was the dangerous version (compiles, books wrong). Bundling stiftelse was considered and dropped: identical plumbing but no chart block. Creation sits behind NEXT_PUBLIC_IDEELL_FORENING_ENABLED so the CHECK, RPCs and seed can ship now and the first partner is switched on without a migration; the flag goes when Phase 2 (packs, INK3, årsbokslut, Swish) lands on the tracker. Domain choices (DECISIONS.md 2026-09-08, verify with an accountant before Phase 2): result closes to 2069 with 2068 as prior-year carry; no owner accounts, member settlement on 2890; accrual default; brutet räkenskapsår allowed; K1 label for the 5 000 kr accrual threshold (BFNAR 2010:1); org number gets the 16 prefix. Migration 20260908110835 widens the three CHECK constraints, adds supported_entity_types(), re-creates the three create RPCs with the widened guard and adds the förening block to seed_chart_of_accounts. Applied to staging and covered by ideell-forening-entity-type.pg.test.ts. Part of #2072 Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01PdGafpUA7jVV1oYjkwfQCh * fix(company): close the förening paths the skeptic refuted (#2072) Five refutations from the /skeptic pass on 7a05c54d2, each fixed at the shared definition rather than the reported site: 1. Privately paid supplier invoices and the utlägg dialog resolved the owner account in lib/expenses/payer.ts with its own AB/EF ternary, so a förening member's invoice was built on 2893 and then refused by the expense-claim service (which already said 2890), burning an ankomstnummer. The helper now uses ownerSettlementAccount. 2. Booking templates substitute their `_ab` accounts only for an aktiebolag; the `private_expense` template kept its base 2013 for a förening. Template accounts now resolve through templateAccountForForm: EF base, AB override, förening base with owner accounts translated to 2890 (booking-templates.ts and proposal-lines.ts share it). 3. A VAT-registered förening with helårsmoms got no momsdeklaration deadline: the annual VAT rule bailed on anything but AB/EF. A förening is a juridisk person and follows the räkenskapsår schedule (SFL 26 kap 33 §), so the rule now keys on fiscalYearLockedToCalendar instead of the two literals; same in the MCP VAT report. 4. 2069 would have accumulated across years: the year-open omföring was AB-only with 2099/2098 hard-coded. planResultAppropriation now takes the pair from resultClosingAccounts (AB 2099 -> 2098, förening 2069 -> 2068) and skips forms with no carry (EF). 5. With the flag off, a registry lookup that returned "Ideell förening" was prefilled into the onboarding journey, the form picker was skipped and the create step answered "Ogiltig företagsform" with no way back. The journey, the BankID picker, the onboarding page and the MCP lookup now use mapSetupEntityType, which maps only creatable forms, so a flagged-off form falls through to the picker as before. Also: form picker keeps its AB-first order; tests for each fix. Part of #2072 Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01PdGafpUA7jVV1oYjkwfQCh * chore(migrations): move ideell förening migration after main's latest version (20260908143051) Two migrations landed on main after the branch forked; a lower version would be skipped by the merge-time apply. Staging history row renamed to match. Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01PdGafpUA7jVV1oYjkwfQCh * chore(skills): regenerate accounted-api reference for the widened entity_type enum Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01PdGafpUA7jVV1oYjkwfQCh --------- Co-authored-by: Claude Fable 5.1 <noreply@anthropic.com>
449 lines
17 KiB
TypeScript
449 lines
17 KiB
TypeScript
import type { TransactionCategory, MappingResult, VatJournalLine, Transaction, EntityType, VatTreatment } from '@/types'
|
||
import { getVatRate, generateReverseChargeLines } from './vat-entries'
|
||
import { resolveSekAmount } from './currency-utils'
|
||
import { roundOre } from '@/lib/money'
|
||
import { byEntityType, ownerSettlementAccount } from '@/lib/company/entity-type'
|
||
|
||
/**
|
||
* Maps TransactionCategory to BAS accounts for journal entry creation
|
||
*
|
||
* Account mapping follows Swedish BAS Kontoplan:
|
||
* - 1xxx: Assets
|
||
* - 2xxx: Equity & Liabilities
|
||
* - 3xxx: Revenue
|
||
* - 4xxx: Cost of goods sold
|
||
* - 5xxx: External expenses
|
||
* - 6xxx: Other external expenses
|
||
* - 7xxx: Personnel costs
|
||
* - 8xxx: Financial items
|
||
*
|
||
* Key differences between entity types:
|
||
* - Enskild Firma: Uses 2013 (Eget uttag) for private withdrawals
|
||
* - Aktiebolag: Uses 2893 (Skuld till aktieägare) for owner transactions
|
||
*/
|
||
|
||
interface CategoryAccountMapping {
|
||
debitAccount: string
|
||
creditAccount: string
|
||
vatTreatment: string | null
|
||
vatDebitAccount: string | null
|
||
vatCreditAccount: string | null
|
||
}
|
||
|
||
// Default bank account - typically 1930 (Företagskonto/checkkonto)
|
||
const BANK_ACCOUNT = '1930'
|
||
|
||
// Private/owner transaction accounts live in lib/company/entity-type.ts
|
||
// (ownerSettlementAccount): EF 2013/2018, AB 2893, ideell förening 2890.
|
||
|
||
// Single source of truth for category -> expense account mapping
|
||
const EXPENSE_ACCOUNTS: Record<string, string> = {
|
||
expense_equipment: '5410', // Förbrukningsinventarier
|
||
expense_software: '5420', // Programvaror
|
||
expense_travel: '5890', // Övriga resekostnader (5800 är gruppkonto)
|
||
expense_office: '6110', // Kontorsförbrukning
|
||
expense_marketing: '5910', // Annonsering
|
||
expense_professional_services: '6530', // Redovisningstjänster
|
||
expense_representation: '6071', // Representation, avdragsgill
|
||
expense_consumables: '5460', // Förbrukningsvaror
|
||
expense_vehicle: '5611', // Drivmedel bil
|
||
expense_telecom: '6230', // Datakommunikation (6200 är gruppkonto)
|
||
expense_bank_fees: '6570', // Bankavgifter
|
||
expense_card_fees: '6570', // Kortavgifter
|
||
expense_currency_exchange: '7960', // Valutakursförluster
|
||
expense_other: '6991', // Övriga avdragsgilla kostnader
|
||
}
|
||
|
||
// Income account mapping
|
||
const INCOME_ACCOUNTS: Record<string, string> = {
|
||
income_services: '3001', // Försäljning tjänster 25%
|
||
income_products: '3001', // Försäljning varor 25% moms
|
||
income_other: '3999', // Övriga rörelseintäkter (3900 är gruppkonto)
|
||
}
|
||
|
||
/**
|
||
* Get the expense account for a category, with entity-specific overrides.
|
||
* Education (expense_education) differs: AB uses 7610, EF uses 6991.
|
||
*/
|
||
function getExpenseAccount(category: string, entityType: EntityType): string {
|
||
if (category === 'expense_education') {
|
||
// 7610 is a personnel cost: only a form with employees by default books
|
||
// education there; the others take the general external-cost account.
|
||
return byEntityType(entityType, {
|
||
aktiebolag: '7610',
|
||
enskild_firma: '6991',
|
||
ideell_forening: '6991',
|
||
})
|
||
}
|
||
return EXPENSE_ACCOUNTS[category] || '6991'
|
||
}
|
||
|
||
/**
|
||
* Get the income account for a category, resolving by VAT treatment.
|
||
* BAS mandates revenue account segregation by VAT rate:
|
||
* 3001=25%, 3002=12%, 3003=6%, 3305=Export, 3308=EU services, 3004=Exempt.
|
||
*/
|
||
function getIncomeAccount(category: string, vatTreatment?: VatTreatment): string {
|
||
// income_other always maps to 3999 regardless of VAT treatment (3900 är gruppkonto)
|
||
if (category === 'income_other') return '3999'
|
||
|
||
if (vatTreatment) {
|
||
switch (vatTreatment) {
|
||
case 'standard_25': return '3001'
|
||
case 'reduced_12': return '3002'
|
||
case 'reduced_6': return '3003'
|
||
case 'export': return '3305'
|
||
case 'reverse_charge': return '3308'
|
||
case 'exempt': return '3004'
|
||
}
|
||
}
|
||
|
||
// No vatTreatment provided: fall back to static mapping
|
||
return INCOME_ACCOUNTS[category] || '3999'
|
||
}
|
||
|
||
/**
|
||
* Get account mapping for a transaction category
|
||
*
|
||
* For expenses: Debit expense account, Credit bank (or private for non-business)
|
||
* For income: Debit bank, Credit revenue account
|
||
*/
|
||
export function getCategoryAccountMapping(
|
||
category: TransactionCategory,
|
||
amount: number,
|
||
isBusiness: boolean,
|
||
entityType: EntityType,
|
||
vatTreatment?: VatTreatment
|
||
): CategoryAccountMapping {
|
||
// Private/owner transactions use entity-specific accounts
|
||
// EF: 2013 for withdrawals (uttag), 2018 for deposits (insättningar)
|
||
// AB: 2893 for both directions; ideell förening: 2890 (no owner)
|
||
if (!isBusiness) {
|
||
const privateAccount = ownerSettlementAccount(entityType, amount < 0 ? 'withdrawal' : 'contribution')
|
||
return {
|
||
debitAccount: amount < 0 ? privateAccount : BANK_ACCOUNT,
|
||
creditAccount: amount < 0 ? BANK_ACCOUNT : privateAccount,
|
||
vatTreatment: null,
|
||
vatDebitAccount: null,
|
||
vatCreditAccount: null,
|
||
}
|
||
}
|
||
|
||
// Check if it's an expense category
|
||
if (category.startsWith('expense_')) {
|
||
const expenseAccount = getExpenseAccount(category, entityType)
|
||
|
||
// Bank fees, card fees, and currency exchange are VAT-exempt in Sweden
|
||
const vatExemptCategories = ['expense_bank_fees', 'expense_card_fees', 'expense_currency_exchange']
|
||
const isVatExempt = vatExemptCategories.includes(category)
|
||
|
||
// Representation defaults to reduced_12 (ML 13 kap 24-25 §§, max 300 SEK/person).
|
||
// Note: income tax deduction was abolished 2017 (IL 16 kap 2 §), but VAT deduction remains.
|
||
const resolvedVat = vatTreatment ?? (isVatExempt ? null : category === 'expense_representation' ? 'reduced_12' : 'standard_25')
|
||
|
||
if (amount > 0) {
|
||
// Incoming refund: bank receives money, expense account is reduced (credited).
|
||
// Ingående moms is reversed: credit 2641 instead of debit.
|
||
return {
|
||
debitAccount: BANK_ACCOUNT,
|
||
creditAccount: expenseAccount,
|
||
vatTreatment: resolvedVat,
|
||
vatDebitAccount: null,
|
||
vatCreditAccount: resolvedVat ? '2641' : null,
|
||
}
|
||
}
|
||
|
||
return {
|
||
debitAccount: expenseAccount,
|
||
creditAccount: BANK_ACCOUNT,
|
||
vatTreatment: resolvedVat,
|
||
vatDebitAccount: resolvedVat ? '2641' : null, // Debiterad ingående moms
|
||
vatCreditAccount: null,
|
||
}
|
||
}
|
||
|
||
// Check if it's an income category
|
||
if (category.startsWith('income_')) {
|
||
const incomeAccount = getIncomeAccount(category, vatTreatment)
|
||
|
||
// Use provided vatTreatment, or default to standard_25
|
||
const resolvedVat = vatTreatment ?? 'standard_25'
|
||
|
||
// Determine output VAT account based on rate
|
||
let outputVatAccount: string | null = null
|
||
switch (resolvedVat) {
|
||
case 'standard_25':
|
||
outputVatAccount = '2611' // Utgående moms försäljning 25%
|
||
break
|
||
case 'reduced_12':
|
||
outputVatAccount = '2621' // Utgående moms försäljning 12%
|
||
break
|
||
case 'reduced_6':
|
||
outputVatAccount = '2631' // Utgående moms försäljning 6%
|
||
break
|
||
default:
|
||
outputVatAccount = null
|
||
break
|
||
}
|
||
|
||
return {
|
||
debitAccount: BANK_ACCOUNT,
|
||
creditAccount: incomeAccount,
|
||
vatTreatment: resolvedVat,
|
||
vatDebitAccount: null,
|
||
vatCreditAccount: outputVatAccount,
|
||
}
|
||
}
|
||
|
||
// Uncategorized - default to misc expense/income based on amount
|
||
if (amount < 0) {
|
||
return {
|
||
debitAccount: '6991',
|
||
creditAccount: BANK_ACCOUNT,
|
||
vatTreatment: null,
|
||
vatDebitAccount: null,
|
||
vatCreditAccount: null,
|
||
}
|
||
} else {
|
||
return {
|
||
debitAccount: BANK_ACCOUNT,
|
||
creditAccount: '3999',
|
||
vatTreatment: null,
|
||
vatDebitAccount: null,
|
||
vatCreditAccount: null,
|
||
}
|
||
}
|
||
}
|
||
|
||
/**
|
||
* Build a MappingResult from a category selection
|
||
* Used by the categorization API to create journal entries
|
||
*
|
||
* `vatAmountOverride` is the underlag's actual VAT when it differs from the
|
||
* rate-derived amount: e.g. a restaurant receipt where dricks carries no
|
||
* moms, so the document's VAT is lower than rate × gross. It can only replace
|
||
* a rate-based VAT line (standard_25/reduced_12/reduced_6); it never applies
|
||
* to fictive reverse-charge VAT and never conjures a line for treatments
|
||
* without VAT. Zero is rejected: a document with no moms is an exempt supply
|
||
* and must be booked with vat_treatment "exempt" so the momsdeklaration sees
|
||
* the correct classification, not a rate-bearing treatment minus its VAT line.
|
||
*
|
||
* Currency: `vatAmountOverride` is denominated in the TRANSACTION's currency,
|
||
* exactly like `transaction.amount` (it is the figure printed on the underlag).
|
||
* All journal entry lines are SEK, so every VAT figure here goes through the
|
||
* same SEK resolution buildTransactionEntryLines applies to the gross. Before
|
||
* this, a 15.87 USD override validated against the USD gross but posted as
|
||
* 15.87 kr: the entry balanced (the net line absorbed the difference), so
|
||
* nothing downstream could detect the wrong 26xx figure.
|
||
*/
|
||
export function buildMappingResultFromCategory(
|
||
category: TransactionCategory,
|
||
transaction: Transaction,
|
||
isBusiness: boolean,
|
||
entityType: EntityType,
|
||
vatTreatment?: VatTreatment,
|
||
vatAmountOverride?: number | null
|
||
): MappingResult {
|
||
const mapping = getCategoryAccountMapping(category, transaction.amount, isBusiness, entityType, vatTreatment)
|
||
|
||
const vatLines: VatJournalLine[] = []
|
||
|
||
// Calculate VAT if applicable using the resolved treatment from mapping
|
||
const treatment = mapping.vatTreatment as VatTreatment | null
|
||
const hasVatOverride = vatAmountOverride !== undefined && vatAmountOverride !== null
|
||
|
||
// Journal entry lines are SEK; transaction.amount is in transaction.currency.
|
||
// The lenient resolver (not the OrNull sibling) is deliberate: it must agree
|
||
// with buildTransactionEntryLines, which nets these VAT lines against the
|
||
// same resolution of the gross. Disagreeing resolvers would unbalance the
|
||
// net line; agreeing ones keep legacy rateless rows exactly as before.
|
||
const absAmount = Math.abs(transaction.amount)
|
||
const absSekAmount = Math.abs(resolveSekAmount(
|
||
transaction.amount, transaction.amount_sek, transaction.currency, transaction.exchange_rate
|
||
))
|
||
|
||
if (hasVatOverride) {
|
||
// Treatment compatibility first: an invalid override on reverse_charge is
|
||
// a treatment problem, not an amount problem: the agent should get the
|
||
// correction hint that matches the actual mistake.
|
||
if (!isBusiness || !treatment || treatment === 'reverse_charge' || getVatRate(treatment) <= 0) {
|
||
throw new Error(
|
||
`vat_amount cannot be combined with vat_treatment "${treatment ?? 'none'}": ` +
|
||
'it only overrides a rate-based VAT line (standard_25, reduced_12, reduced_6).'
|
||
)
|
||
}
|
||
// typeof re-check is deliberate: at commit time the override comes from
|
||
// jsonb params, so the TS signature doesn't guarantee a number at runtime.
|
||
if (typeof vatAmountOverride !== 'number' || !Number.isFinite(vatAmountOverride) || vatAmountOverride <= 0) {
|
||
throw new Error(
|
||
`vat_amount must be a positive number, got ${vatAmountOverride}. ` +
|
||
'For a document with no moms, use vat_treatment "exempt" instead of vat_amount 0.'
|
||
)
|
||
}
|
||
// 25% is the highest Swedish VAT rate, so rate-extraction at 25% bounds
|
||
// any legitimate document VAT: even on mixed-rate receipts. The bound and
|
||
// the override share the transaction's currency (both come off the
|
||
// underlag), so the comparison stays in that currency.
|
||
const maxVat = roundOre(absAmount * 0.25 / 1.25)
|
||
if (vatAmountOverride > maxVat) {
|
||
const currencyTag = transaction.currency && transaction.currency !== 'SEK'
|
||
? ` ${transaction.currency}` : ''
|
||
throw new Error(
|
||
`vat_amount ${vatAmountOverride} exceeds the maximum possible Swedish VAT on ${absAmount}${currencyTag} ` +
|
||
`(${maxVat}${currencyTag} at 25%). vat_amount is denominated in the transaction's currency, like belopp. ` +
|
||
`Check the underlag: the override must be the document's actual moms.`
|
||
)
|
||
}
|
||
}
|
||
|
||
if (isBusiness && treatment) {
|
||
const vatRate = getVatRate(treatment)
|
||
if (treatment === 'reverse_charge' && transaction.amount < 0) {
|
||
// EU reverse charge: fiktiv moms (offsetting entries), 25% of the SEK
|
||
// value: an EUR invoice's fiktiv moms posted off the EUR figure would
|
||
// understate 2614/2645 by the exchange rate.
|
||
const rcLines = generateReverseChargeLines(absSekAmount)
|
||
for (const rcl of rcLines) {
|
||
vatLines.push({
|
||
account_number: rcl.account_number,
|
||
debit_amount: rcl.debit_amount,
|
||
credit_amount: rcl.credit_amount,
|
||
description: rcl.line_description || '',
|
||
})
|
||
}
|
||
} else if (vatRate > 0) {
|
||
// Override: scale from transaction currency to SEK by the same ratio
|
||
// the gross resolved at (amount_sek embeds the bank's actual settlement
|
||
// rate, so a plain exchange_rate multiply could disagree with the gross
|
||
// line). absAmount > 0 is guaranteed here: a positive override on a
|
||
// zero-amount transaction already failed the maxVat bound above.
|
||
const vatAmount = hasVatOverride
|
||
? roundOre((vatAmountOverride as number) * (absSekAmount / absAmount))
|
||
: roundOre(absSekAmount * vatRate / (1 + vatRate))
|
||
|
||
if (vatAmount > 0 && transaction.amount < 0 && mapping.vatDebitAccount) {
|
||
// Expense: Ingående moms (deductible VAT)
|
||
vatLines.push({
|
||
account_number: mapping.vatDebitAccount,
|
||
debit_amount: vatAmount,
|
||
credit_amount: 0,
|
||
description: hasVatOverride ? 'Ingående moms (enligt underlag)' : `Ingående moms ${vatRate * 100}%`,
|
||
})
|
||
} else if (vatAmount > 0 && transaction.amount > 0 && mapping.vatCreditAccount) {
|
||
// Income output VAT, or expense refund reversing ingående moms (both credit vatCreditAccount).
|
||
// Distinguish by account: 2641 = reversed ingående moms, 2611/2621/2631 = utgående moms.
|
||
const isExpenseRefund = mapping.vatCreditAccount === '2641'
|
||
vatLines.push({
|
||
account_number: mapping.vatCreditAccount,
|
||
debit_amount: 0,
|
||
credit_amount: vatAmount,
|
||
description: isExpenseRefund
|
||
? (hasVatOverride ? 'Återföring ingående moms (enligt underlag)' : `Återföring ingående moms ${vatRate * 100}%`)
|
||
: (hasVatOverride ? 'Utgående moms (enligt underlag)' : `Utgående moms ${vatRate * 100}%`),
|
||
})
|
||
}
|
||
}
|
||
}
|
||
|
||
// Generate description
|
||
const categoryLabels: Record<TransactionCategory, string> = {
|
||
income_services: 'Tjänsteförsäljning',
|
||
income_products: 'Varuförsäljning',
|
||
income_other: 'Övrig intäkt',
|
||
expense_equipment: 'Förbrukningsinventarier',
|
||
expense_software: 'Programvara',
|
||
expense_travel: 'Resekostnad',
|
||
expense_office: 'Kontorskostnad',
|
||
expense_marketing: 'Marknadsföring',
|
||
expense_professional_services: 'Konsulttjänst',
|
||
expense_education: 'Utbildning',
|
||
expense_representation: 'Representation',
|
||
expense_consumables: 'Förbrukningsvaror',
|
||
expense_vehicle: 'Bil & drivmedel',
|
||
expense_telecom: 'Telefon & internet',
|
||
expense_bank_fees: 'Bankavgift',
|
||
expense_card_fees: 'Kortavgift',
|
||
expense_currency_exchange: 'Valutaväxling',
|
||
expense_other: 'Övrig kostnad',
|
||
private: 'Privat',
|
||
uncategorized: 'Okategoriserad',
|
||
}
|
||
|
||
const description = isBusiness
|
||
? `${categoryLabels[category] || category}: ${transaction.description}`
|
||
: `Privat: ${transaction.description}`
|
||
|
||
return {
|
||
rule: null,
|
||
debit_account: mapping.debitAccount,
|
||
credit_account: mapping.creditAccount,
|
||
risk_level: 'LOW',
|
||
confidence: 1.0, // User explicitly categorized
|
||
requires_review: false,
|
||
default_private: !isBusiness,
|
||
vat_lines: vatLines,
|
||
description,
|
||
}
|
||
}
|
||
|
||
/**
|
||
* Get the expense account number for a category
|
||
* Useful for creating mapping rules
|
||
*/
|
||
export function getExpenseAccountForCategory(category: TransactionCategory): string | null {
|
||
if (category === 'expense_education') return '6991'
|
||
return EXPENSE_ACCOUNTS[category] || null
|
||
}
|
||
|
||
/**
|
||
* Get the default account number for a category.
|
||
* For expense categories: returns the expense account (debit side).
|
||
* For income categories: returns the revenue account (credit side).
|
||
* For private/uncategorized: returns the entity-specific private or fallback account.
|
||
*/
|
||
export function getDefaultAccountForCategory(
|
||
category: TransactionCategory,
|
||
entityType: EntityType
|
||
): string {
|
||
if (category === 'private') {
|
||
return ownerSettlementAccount(entityType, 'withdrawal')
|
||
}
|
||
|
||
if (category.startsWith('expense_')) {
|
||
return getExpenseAccount(category, entityType)
|
||
}
|
||
|
||
if (category.startsWith('income_')) {
|
||
return INCOME_ACCOUNTS[category] || '3999'
|
||
}
|
||
|
||
// uncategorized
|
||
return '6991'
|
||
}
|
||
|
||
/**
|
||
* Get the default VAT treatment for a category.
|
||
* Bank fees, card fees, and currency exchange are VAT-exempt.
|
||
* All other business categories default to standard 25%.
|
||
*/
|
||
export function getDefaultVatTreatmentForCategory(
|
||
category: TransactionCategory
|
||
): VatTreatment | null {
|
||
if (category === 'private' || category === 'uncategorized') {
|
||
return null
|
||
}
|
||
|
||
const vatExemptCategories = ['expense_bank_fees', 'expense_card_fees', 'expense_currency_exchange']
|
||
if (vatExemptCategories.includes(category)) {
|
||
return null
|
||
}
|
||
|
||
// Representation defaults to reduced_12 (ML 13 kap 24-25 §§, max 300 SEK/person).
|
||
// Note: income tax deduction was abolished 2017 (IL 16 kap 2 §), but VAT deduction remains.
|
||
if (category === 'expense_representation') {
|
||
return 'reduced_12'
|
||
}
|
||
|
||
return 'standard_25'
|
||
}
|