* fix(category-mapping): use leaf BAS accounts instead of group codes 3900, 5800, 6200 are BAS gruppkonton (header codes) and shouldn't carry postings. Switched the default mappings to the matching leaf accounts: - income_other: 3900 -> 3999 (Övriga rörelseintäkter) - expense_travel: 5800 -> 5890 (Övriga resekostnader) - expense_telecom: 6200 -> 6230 (Datakommunikation) The fallback for income_other inside getCategoryAccountMapping was also hardcoded to '3900'; updated to '3999' for consistency. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com> * feat(transactions): split-payment allocator — 1 tx → N invoices Closes one of the two flows that motivated PR #602's foundation: allocating a single bank transaction across multiple customer OR multiple supplier invoices, with one combined verifikat (samlingsverifikation per BFL 5 kap 6§ st 3). ## Backend (Phase 3a) - **PL/pgSQL RPC** match_batch_allocate (~400 lines): locks the tx + each target invoice with SELECT … FOR UPDATE in id order, validates status/currency/remaining/direction before any write, builds the combined verifikat via commit_journal_entry (atomically assigns voucher_number + flips draft→posted), inserts N rows in invoice_payments or supplier_invoice_payments pointing at the same JE, advances paid_amount/remaining_amount/status per invoice. Returns { ok, journal_entry_id, voucher_number, allocations: [...] } on success or { ok: false, code, details } on guard failure. Mixed customer+supplier kinds are rejected (v1 scope). - **Endpoint** POST /api/transactions/[id]/match-batch — thin wrapper around the RPC. Validates body via MatchBatchSchema (zod discriminatedUnion + superRefine to catch mixed-kinds at the schema layer). On RPC success, emits one invoice.match_confirmed or supplier_invoice.match_confirmed event per allocation so existing subscribers (reminders, automations, processing-history) keep working. Maps the structured RPC error envelope to errorResponseFromCode. - **16 new BATCH_* error codes** (sv+en): BATCH_TX_NOT_FOUND, BATCH_TX_ALREADY_BOOKED, BATCH_OVERSHOOT, BATCH_AMOUNT_EXCEEDS_TX, BATCH_MIXED_KINDS_UNSUPPORTED, BATCH_DIRECTION_MISMATCH, BATCH_CURRENCY_MISMATCH, BATCH_PERIOD_LOCKED, BATCH_RPC_FAILED, etc. ## UI (Phase 5a) - **MatchAllocationDialog** (components/transactions/) — direction- aware (positive tx → customer invoices, negative → supplier). Search + selectable list of open invoices. Per-row amount input with default = min(invoice.remaining, tx_remaining_budget). Live tally with green-check balanced state, red overshoot warning, gray leftover note. Confirm button disabled on overshoot. POSTs to /match-batch and on 200 triggers the same exit animation as single-tx match. - **Inbox row** gains a second outline icon button (Split icon) next to the existing 1:1 match button, gated by the same showInvoiceMatchButton predicate. Tooltip explains the direction- aware split. Opens MatchAllocationDialog. - **i18n** strings under tx_match_allocation namespace in sv.json and en.json (32 keys each). ## Tests - tests/pg/match-batch-allocate.pg.test.ts — 5 pg-real tests covering combined verifikat shape, overshoot guard, already-booked tx, direction mismatch, mixed-kinds rejection. - app/api/transactions/[id]/match-batch/__tests__/route.test.ts — 5 unit tests covering schema validation, mixed-kinds, happy path, structured-error mapping, raw-error → BATCH_RPC_FAILED. 63 unit tests pass across the touched paths. The RPC migration was already applied to remote in an earlier Phase 3a session (idempotent CREATE OR REPLACE FUNCTION; the next replay is a no-op). Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com> * fix(match-batch): PR #603 review round 1 + CI fixes Closes both CI failures and the three real review findings. ## CI fixes - **pg-real failure**: the RPC declared `v_journal_entry_id uuid := uuid_generate_v4()` which fails in the CI Postgres image (uuid-ossp extension is off). Switched to `gen_random_uuid()` — the codebase standard already used by supplier_invoices, invoice_inbox, etc. - **core-only failure**: my earlier BAS leaf-account commit (3900→3999, 5800→5890, 6200→6230) didn't update the matching `lib/bookkeeping/__tests__/category-mapping.test.ts` expectations, and `getDefaultAccountForCategory`'s fallback for `income_*` was still hardcoded to '3900'. Updated both. ## Review findings (greptile) - **P1 deadlock-stable locking** (`match_batch_allocate.sql:11`): the validation `FOR UPDATE` loop ran in caller-supplied array order. Two concurrent calls with overlapping invoice sets in opposite orders could deadlock and one would abort with `BATCH_RPC_FAILED`. Now all three loops (validate, build lines, advance invoices) iterate via `SELECT … FROM jsonb_array_elements(…) ORDER BY COALESCE(invoice_id, supplier_invoice_id)`, giving a stable global lock order regardless of how the caller ordered the JSON array. - **P1 duplicate-allocation detection** (`match_batch_allocate.sql:163`): the same invoice_id listed twice would pass the per-row overshoot guard (both iterations read the original `remaining_amount`) and the write loop would insert two `invoice_payments` rows for the same invoice. Added a `v_seen_ids text[]` check in the validation loop and a new `BATCH_DUPLICATE_ALLOCATION` error code (sv + en). The dialog already prevents this UI-side via `if (prev[candidate.id] return prev` — the RPC guard is the defense-in-depth layer. - **P2 zod `.positive()`** (`schemas.ts:544`): allocation amount was `nonNegativeAmount` (allowing 0), passing schema validation only to be rejected by the RPC with `BATCH_INVALID_AMOUNT`. Now `z.number().positive(…)` so 0-amount entries fail at the schema layer with a per-field path, cleaner 400. - **P2 strict `> 0` direction check** (`MatchAllocationDialog.tsx:82`): used `amount >= 0` to pick customer-side, but a zero-amount tx would load customer candidates only to hit `BATCH_TX_ZERO_AMOUNT` at submit time after the user has filled in allocations. Switched to `> 0` so 0-amount tx never reaches the dialog at all (it's rejected by the RPC immediately). The fourth Greptile comment (the schema P2 about amount validation) overlaps with the third; addressed in the same edit. ## Verification - 112 unit tests pass across touched paths - ESLint clean - New pg-real test `tests/pg/match-batch-allocate.pg.test.ts` covers the dedupe scenario (same supplier invoice listed twice with summing amounts that individually pass per-row overshoot) - RPC patch applied to remote via Supabase MCP Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com> * fix(match-batch): PR #603 review round 2 — compliance hardening Addresses the actionable findings from compliance-swarm and Swedish-accounting-compliance reviews. Six small RPC changes + two TS-side guards, all bundled in one follow-up migration. ## Security - **(GDPR Art.5(1)(f) / ISO A.8.2) Caller verification**: SECURITY DEFINER bypasses RLS, and the prior RPC accepted any (p_user_id, p_company_id) pair from the route. Now the function rejects with new `BATCH_UNAUTHORIZED` (sv+en, HTTP 403) if `auth.uid()` is not a member of `p_company_id`. Pattern lifted from `harden_invoice_number_rpcs` (#20260510140000). - **(OWASP V4.2) Allocation cap**: `MatchBatchSchema.allocations` now carries `.max(100)` to prevent DoS via unbounded FOR UPDATE locks. ## Swedish accounting correctness - **source_type per direction**: was hardcoded to `'invoice_paid'` for both customer + supplier batches, mis-routing behandlingshistorik filters. Customer batches keep `'invoice_paid'`, supplier batches now write `'supplier_invoice_paid'`. - **Fiscal-period determinism**: `LIMIT 1` on the period lookup was non-deterministic on overlap (e.g. corrected broken year). Added `ORDER BY period_start DESC` so the most recent matching period wins. - **Tolerance harmonisation**: cross-allocation sum used `+0.01` tolerance while per-row used `+0.005`. Both now `+0.005` so a multi-row batch can't drift ~0.01 SEK while each row passes individually. - **`transactions.category` no longer overwritten**: was forced to `'income_services'` (→ BAS 3001 at 25% VAT) for any customer batch, misrepresenting reduced-rate / export / EU-service invoices. The category is only meaningful 1:1 with a single invoice; batches now leave it as-is, mirroring the supplier-side `ELSE category` branch. ## Tests - `tests/pg/match-batch-allocate.pg.test.ts` now wraps every RPC call in `withUserContext(userId)` so `auth.uid()` resolves to the seeded owner. Without this the new membership check would have failed all existing tests. - New pg-real test: `rejects with BATCH_UNAUTHORIZED when caller is not a member of the company` — outsider user gets explicit refusal. - New happy-path assertion: `source_type = 'supplier_invoice_paid'` on the combined verifikat for supplier batches. 15 unit tests pass on the touched paths. RPC patch applied to remote via Supabase MCP. Out-of-scope mcp-server changes still parked locally. Skipped findings (documented in PR comment thread): - V8.2.1 ownership pre-check at route layer (RPC enforces it) - V4.5 / Art.5(1)(b) narrower API response and event payload — typed contracts require the full shapes - V2.4 rate-limiting — system-level, applies to all match endpoints - A.8.28 client-side RLS reliance — documented architectural choice - Direction pre-check at API layer (RPC catches with cleaner code) - V16 + Art.32 + Art.5(1)(b) low-severity logging nits Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com> --------- Co-authored-by: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
361 lines
12 KiB
TypeScript
361 lines
12 KiB
TypeScript
import type { TransactionCategory, MappingResult, VatJournalLine, Transaction, EntityType, VatTreatment } from '@/types'
|
|
import { getVatRate, generateReverseChargeLines } from './vat-entries'
|
|
|
|
/**
|
|
* 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 by entity type
|
|
const PRIVATE_ACCOUNTS: Record<EntityType, string> = {
|
|
enskild_firma: '2013', // Övriga egna uttag
|
|
aktiebolag: '2893', // Skuld till aktieägare/delägare
|
|
}
|
|
|
|
// 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 = 'enskild_firma'): string {
|
|
if (category === 'expense_education') {
|
|
return entityType === 'aktiebolag' ? '7610' : '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 = 'enskild_firma',
|
|
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
|
|
if (!isBusiness) {
|
|
let privateAccount: string
|
|
if (entityType === 'enskild_firma') {
|
|
privateAccount = amount < 0 ? '2013' : '2018'
|
|
} else {
|
|
privateAccount = PRIVATE_ACCOUNTS[entityType] || PRIVATE_ACCOUNTS.enskild_firma
|
|
}
|
|
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')
|
|
|
|
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
|
|
*/
|
|
export function buildMappingResultFromCategory(
|
|
category: TransactionCategory,
|
|
transaction: Transaction,
|
|
isBusiness: boolean,
|
|
entityType: EntityType = 'enskild_firma',
|
|
vatTreatment?: VatTreatment
|
|
): 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
|
|
if (isBusiness && treatment) {
|
|
const vatRate = getVatRate(treatment)
|
|
if (treatment === 'reverse_charge' && transaction.amount < 0) {
|
|
// EU reverse charge: fiktiv moms (offsetting entries)
|
|
const absAmount = Math.abs(transaction.amount)
|
|
const rcLines = generateReverseChargeLines(absAmount)
|
|
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) {
|
|
const grossAmount = Math.abs(transaction.amount)
|
|
const vatAmount = Math.round((grossAmount * vatRate / (1 + vatRate)) * 100) / 100
|
|
|
|
if (transaction.amount < 0 && mapping.vatDebitAccount) {
|
|
// Expense: Ingående moms (deductible VAT)
|
|
vatLines.push({
|
|
account_number: mapping.vatDebitAccount,
|
|
debit_amount: vatAmount,
|
|
credit_amount: 0,
|
|
description: `Ingående moms ${vatRate * 100}%`,
|
|
})
|
|
} else if (transaction.amount > 0 && mapping.vatCreditAccount) {
|
|
// Income: Utgående moms (output VAT)
|
|
vatLines.push({
|
|
account_number: mapping.vatCreditAccount,
|
|
debit_amount: 0,
|
|
credit_amount: vatAmount,
|
|
description: `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 = 'enskild_firma'
|
|
): string {
|
|
if (category === 'private') {
|
|
return PRIVATE_ACCOUNTS[entityType] || PRIVATE_ACCOUNTS.enskild_firma
|
|
}
|
|
|
|
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'
|
|
}
|