Refactor to generic ERP base

Remove influencer-specific features (campaigns, TikTok, gifts, shadow ledger,
contracts, briefings) and consolidate into a clean ERP foundation with core
bookkeeping, invoicing, receipts, tax reporting, and calendar functionality.
Reorganize database migrations into a clean numbered sequence.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
This commit is contained in:
Jakob Wennberg
2026-02-13 15:39:13 +01:00
co-authored by Claude Opus 4.6
parent a25d10a528
commit 0aecf5b41c
184 changed files with 1418 additions and 24763 deletions
+1 -1
View File
@@ -1,6 +1,6 @@
/**
* Static configuration of all Swedish tax deadlines (Skatteverket)
* Based on dev_docs/influencer_ai_info/07-kalender-deadlines.md
* Based on Skatteverket's official deadline schedule
*/
import type { TaxDeadlineType, EntityType, MomsPeriod } from '@/types'
-2
View File
@@ -141,8 +141,6 @@ export function getCategoryDisplayName(category: string): string {
const names: Record<string, string> = {
income_services: 'Tjänster',
income_products: 'Produkter',
income_sponsorship: 'Sponsring',
income_affiliate: 'Affiliate',
income_other: 'Övriga intäkter',
expense_equipment: 'Utrustning',
expense_software: 'Programvara',
-145
View File
@@ -1,145 +0,0 @@
/**
* Light Mode Tax Calculator
*
* For influencers working through umbrella companies (egenanställningsföretag).
* They don't pay egenavgifter (the umbrella handles arbetsgivaravgifter).
* Tax exposure comes from:
* - Gifts/PR products → personal income tax liability (municipal + church)
* - Hobby income → egenavgifter + municipal tax on surplus (out of MVP scope)
*/
import type { LightTaxEstimate } from '@/types'
/**
* Calculate light mode tax liability from gifts
* NO egenavgifter (umbrella pays arbetsgivaravgifter)
* NO VAT (umbrella handles)
* NO grundavdrag (consumed by umbrella salary)
*/
export function calculateLightTax(params: {
taxableGiftValue: number
municipalTaxRate: number // e.g. 0.3238 (32.38%)
churchTaxRate: number // e.g. 0.0100 (1.00%), 0 if not member
hobbyIncome?: number
hobbyExpenses?: number
bankBalance: number | null
}): LightTaxEstimate {
const {
taxableGiftValue,
municipalTaxRate,
churchTaxRate,
hobbyIncome = 0,
hobbyExpenses = 0,
bankBalance,
} = params
// Gift tax = taxable gift value × (municipal rate + church rate)
const effectiveRate = municipalTaxRate + churchTaxRate
const giftTax = Math.round(taxableGiftValue * effectiveRate)
// Hobby tax (simplified: surplus × egenavgifter + municipal tax)
const hobbySurplus = Math.max(0, hobbyIncome - hobbyExpenses)
const hobbyTax = hobbySurplus > 0
? Math.round(hobbySurplus * 0.2897 + hobbySurplus * (1 - 0.2897) * municipalTaxRate)
: 0
const totalVirtualDebt = giftTax + hobbyTax
const balance = bankBalance ?? 0
const safeToSpend = Math.max(0, balance - totalVirtualDebt)
const safeToSpendPercent = balance > 0 ? (safeToSpend / balance) * 100 : 0
let safeToSpendLevel: 'green' | 'yellow' | 'red'
if (safeToSpendPercent > 70) {
safeToSpendLevel = 'green'
} else if (safeToSpendPercent > 30) {
safeToSpendLevel = 'yellow'
} else {
safeToSpendLevel = 'red'
}
return {
taxable_gift_value: taxableGiftValue,
municipal_tax_rate: municipalTaxRate,
church_tax_rate: churchTaxRate,
gift_tax: giftTax,
hobby_tax: hobbyTax,
total_virtual_debt: totalVirtualDebt,
safe_to_spend: safeToSpend,
safe_to_spend_percent: Math.round(safeToSpendPercent),
safe_to_spend_level: safeToSpendLevel,
}
}
/**
* Calculate safe-to-spend from bank balance and tax estimate
*/
export function calculateLightSafeToSpend(
bankBalance: number | null,
lightTax: LightTaxEstimate
): { safeToSpend: number; percent: number; level: 'green' | 'yellow' | 'red' } {
return {
safeToSpend: lightTax.safe_to_spend,
percent: lightTax.safe_to_spend_percent,
level: lightTax.safe_to_spend_level,
}
}
/**
* Calculate net payout from gross using umbrella provider fee structure
* Used in shadow ledger form for auto-calculating fields
*/
export function calculatePayoutNet(params: {
gross: number
feePercent: number // e.g. 6.0
pensionPercent: number // e.g. 4.5
estimatedTaxRate: number // e.g. 0.30
}): {
serviceFee: number
pensionDeduction: number
socialFees: number
incomeTaxWithheld: number
net: number
} {
const { gross, feePercent, pensionPercent, estimatedTaxRate } = params
// Service fee (umbrella company fee)
const serviceFee = Math.round(gross * (feePercent / 100) * 100) / 100
// After fee
const afterFee = gross - serviceFee
// Pension deduction (from remaining)
const pensionDeduction = Math.round(afterFee * (pensionPercent / 100) * 100) / 100
// Social fees (arbetsgivaravgifter ~31.42% paid by umbrella, deducted from gross)
const socialFees = Math.round(afterFee * 0.3142 * 100) / 100
// Taxable base after social fees
const taxableBase = afterFee - socialFees - pensionDeduction
// Preliminary income tax
const incomeTaxWithheld = Math.round(taxableBase * estimatedTaxRate * 100) / 100
// Net payout
const net = Math.round((taxableBase - incomeTaxWithheld) * 100) / 100
return {
serviceFee,
pensionDeduction,
socialFees,
incomeTaxWithheld,
net: Math.max(0, net),
}
}
/**
* Calculate virtual tax debt for a gift (light mode)
* Simple: gift value × effective tax rate
*/
export function calculateGiftVirtualTaxDebt(
giftMarketValue: number,
municipalTaxRate: number,
churchTaxRate: number
): number {
return Math.round(giftMarketValue * (municipalTaxRate + churchTaxRate))
}