a9bf24ce0b
Expand supplier invoice module with overdue cron job, credit note journal entries, and event emissions on approve/mark-paid/create flows. Add entity type (EF/AB) awareness to transaction categorization UI and category mapping logic. Add comprehensive tests for supplier-invoice-entries, transaction-entries, and expanded API route coverage. Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
162 lines
5.1 KiB
TypeScript
162 lines
5.1 KiB
TypeScript
/**
|
|
* Expense warnings for non-deductible or partially deductible items
|
|
* Based on Swedish tax law and Kammarrätten rulings
|
|
*/
|
|
|
|
export interface ExpenseWarning {
|
|
category: string
|
|
warningLevel: 'info' | 'warning' | 'danger'
|
|
message: string
|
|
legalBasis?: string
|
|
}
|
|
|
|
const warningPatterns: {
|
|
pattern: RegExp
|
|
warning: ExpenseWarning
|
|
}[] = [
|
|
{
|
|
pattern: /kläder|clothes|mode|fashion|outfit/i,
|
|
warning: {
|
|
category: 'Kläder',
|
|
warningLevel: 'danger',
|
|
message: 'Kläder är normalt inte avdragsgilla, även om de används i arbetet',
|
|
legalBasis: 'RÅ 1988 ref. 35',
|
|
},
|
|
},
|
|
{
|
|
pattern: /kosmetika|smink|makeup|hudvård|skincare|beauty/i,
|
|
warning: {
|
|
category: 'Kosmetika',
|
|
warningLevel: 'warning',
|
|
message: 'Kosmetika är normalt inte avdragsgillt. Undantag kan gälla för professionella artister.',
|
|
legalBasis: 'Skatteverkets ställningstagande',
|
|
},
|
|
},
|
|
{
|
|
pattern: /gym|träning|fitness|sport/i,
|
|
warning: {
|
|
category: 'Träning',
|
|
warningLevel: 'danger',
|
|
message: 'Gymkort och träningsavgifter är inte avdragsgilla som personlig kostnad',
|
|
legalBasis: 'IL 9 kap 2§',
|
|
},
|
|
},
|
|
{
|
|
pattern: /frisör|hår|salon|barber/i,
|
|
warning: {
|
|
category: 'Frisör',
|
|
warningLevel: 'warning',
|
|
message: 'Frisörbesök är normalt privata kostnader och inte avdragsgilla',
|
|
},
|
|
},
|
|
{
|
|
pattern: /restaurang|lunch|middag|dinner|café|fika/i,
|
|
warning: {
|
|
category: 'Representation',
|
|
warningLevel: 'warning',
|
|
message: 'Måltider kan vara avdragsgilla som representation med max 300 kr per person (exkl. moms). Momsen är inte avdragsgill sedan 2017.',
|
|
legalBasis: 'IL 16 kap 2§, ML 8:9',
|
|
},
|
|
},
|
|
{
|
|
pattern: /resa|flyg|flight|tåg|train|hotel|hotell/i,
|
|
warning: {
|
|
category: 'Resor',
|
|
warningLevel: 'info',
|
|
message: 'Resor kan vara avdragsgilla om de är nödvändiga för verksamheten. Dokumentera syftet!',
|
|
},
|
|
},
|
|
{
|
|
pattern: /presenter|gift|gåva/i,
|
|
warning: {
|
|
category: 'Gåvor',
|
|
warningLevel: 'warning',
|
|
message: 'Reklamgåvor är avdragsgilla upp till 300 kr per mottagare. Representationsgåvor max 180 kr.',
|
|
legalBasis: 'IL 16 kap 2§',
|
|
},
|
|
},
|
|
{
|
|
pattern: /mobil|telefon|phone|iphone|samsung/i,
|
|
warning: {
|
|
category: 'Telefon',
|
|
warningLevel: 'info',
|
|
message: 'Arbetstelefon är avdragsgillt. Vid blandad användning, endast den yrkesmässiga delen.',
|
|
},
|
|
},
|
|
{
|
|
pattern: /dator|laptop|computer|mac|ipad/i,
|
|
warning: {
|
|
category: 'Dator',
|
|
warningLevel: 'info',
|
|
message: 'Datorer för yrkesmässig användning är avdragsgilla. Vid blandad användning ska fördelning göras.',
|
|
},
|
|
},
|
|
]
|
|
|
|
/**
|
|
* Check if an expense description triggers any warnings
|
|
*/
|
|
export function checkExpenseWarnings(description: string): ExpenseWarning[] {
|
|
const warnings: ExpenseWarning[] = []
|
|
|
|
for (const { pattern, warning } of warningPatterns) {
|
|
if (pattern.test(description)) {
|
|
warnings.push(warning)
|
|
}
|
|
}
|
|
|
|
return warnings
|
|
}
|
|
|
|
/**
|
|
* Get category suggestions based on description
|
|
*/
|
|
export function suggestCategory(description: string): string | null {
|
|
const categoryPatterns: { pattern: RegExp; category: string }[] = [
|
|
{ pattern: /spotify|netflix|adobe|software|app store/i, category: 'expense_software' },
|
|
{ pattern: /kamera|camera|ljud|mikrofon|ring light|studio/i, category: 'expense_equipment' },
|
|
{ pattern: /flyg|tåg|hotel|taxi|uber/i, category: 'expense_travel' },
|
|
{ pattern: /facebook ads|google ads|instagram|marknadsföring|marketing/i, category: 'expense_marketing' },
|
|
{ pattern: /revisor|advokat|konsult|accountant|lawyer/i, category: 'expense_professional_services' },
|
|
{ pattern: /kurs|utbildning|course|workshop/i, category: 'expense_education' },
|
|
{ pattern: /kontor|office|skriv|hyra/i, category: 'expense_office' },
|
|
{ pattern: /bankavgift|bankfee|monthly fee|kontoavgift|serviceavgift/i, category: 'expense_bank_fees' },
|
|
{ pattern: /kortavgift|card fee|annual fee/i, category: 'expense_card_fees' },
|
|
{ pattern: /valutaväxling|currency|exchange|FX fee/i, category: 'expense_currency_exchange' },
|
|
]
|
|
|
|
for (const { pattern, category } of categoryPatterns) {
|
|
if (pattern.test(description)) {
|
|
return category
|
|
}
|
|
}
|
|
|
|
return null
|
|
}
|
|
|
|
/**
|
|
* Get display name for category
|
|
*/
|
|
export function getCategoryDisplayName(category: string): string {
|
|
const names: Record<string, string> = {
|
|
income_services: 'Tjänster',
|
|
income_products: 'Produkter',
|
|
income_other: 'Övriga intäkter',
|
|
expense_equipment: 'Utrustning',
|
|
expense_software: 'Programvara',
|
|
expense_travel: 'Resor',
|
|
expense_office: 'Kontor',
|
|
expense_marketing: 'Marknadsföring',
|
|
expense_professional_services: 'Konsulter',
|
|
expense_education: 'Utbildning',
|
|
expense_bank_fees: 'Bankavgift',
|
|
expense_card_fees: 'Kortavgift',
|
|
expense_currency_exchange: 'Valutaväxling',
|
|
expense_other: 'Övriga kostnader',
|
|
private: 'Privat',
|
|
uncategorized: 'Ej bokförd',
|
|
}
|
|
|
|
return names[category] || category
|
|
}
|