refactor: clean up codebase, remove dead code and obsolete docs

Remove influencer-era documentation, unused components, boilerplate
assets, and ghost tiktok cron job. Add supplier invoice management,
document API routes, and PWA icons. Replace boilerplate README.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
This commit is contained in:
Jakob Wennberg
2026-02-20 12:23:17 +01:00
co-authored by Claude Opus 4.6
parent e8743e6e03
commit 838dc6b8b5
113 changed files with 6289 additions and 19036 deletions
-137
View File
@@ -1,137 +0,0 @@
# Product Requirements Document: Influencer Business-in-a-Box MVP
## Executive Summary
A financial management system for Swedish influencers and content creators operating as sole proprietors (enskild firma). The core value proposition: eliminate tax anxiety and administrative burden through automated compliance.
## Target User
- Swedish content creators operating as:
- Enskild näringsidkare (sole proprietor)
- Aktiebolag (limited company)
- Income from multiple sources: brand deals, affiliate
- Revenue range: 50k-2M SEK/year
- Pain: Fear of Skatteverket, hatred of admin, confusion about VAT on international payments
## Entity Type Differences
| Aspect | Enskild Firma | Aktiebolag |
|--------|---------------|------------|
| Tax on profit | Egenavgifter (28.97%) + kommunalskatt (~32%) | Bolagsskatt (20.6%) |
| Owner payment | Eget uttag (withdrawal) | Lön + utdelning |
| Annual filing | NE-bilaga | Årsredovisning + INK2 |
| VAT | Same rules | Same rules |
| Personal liability | Yes | No (limited) |
MVP focuses on shared features (invoicing, VAT, transactions). Entity-specific tax calculations adapt based on company_settings.entity_type.
## MVP Scope
### In Scope (Phase 1)
1. **Real-time Tax Dashboard** ("Disponibelt Saldo")
- Show available balance after estimated tax, employer contributions (egenavgifter), and VAT
- Visual "locked" vs "spendable" money display
- Adapt calculations based on entity type (enskild firma vs AB)
- Compare against user's debiterad preliminärskatt (F-skatt level) to warn about under/overpayment
2. **Bank Integration via Enable Banking (PSD2)**
- Connect multiple bank accounts
- Real-time transaction sync
- Consent renewal handling (90-180 days)
3. **Transaction Categorization**
- Swipe interface: Business / Private / Unsure
- Expense categories with schablonavdrag suggestions
- Smart defaults: hemmakontor 2000 kr/year, bil 18.50 kr/mil
4. **Invoice Generation**
- Create professional PDF invoices
- Multi-currency support (SEK, EUR, USD)
- Auto-convert foreign currency to SEK using Riksbanken daily rates
- Automatic VAT logic:
- Swedish customer: 25% VAT
- EU business (with VAT number): Reverse charge
- Non-EU: No VAT
- Customizable starting invoice number
- Credit notes (kreditfaktura) for corrections
5. **Income Overview**
- Aggregate invoiced revenue + platform payouts
- Track against VAT registration threshold (80k SEK)
- Selectable momsperiod (monthly/quarterly)
### Out of Scope (Future Phases)
- Receipt photo capture and matching
- Benefits/gifts tracking (förmånshantering)
- NE-bilaga / Årsredovisning auto-generation
- Automated payment reminders
- Quotes/proforma invoices
- English UI
## User Stories
### Onboarding (Required before feature access)
- As a new user, I complete a guided setup wizard before accessing the app
- As a user, I select my entity type (enskild firma or aktiebolag)
- As a user, I enter my company details (name, org.nr, address)
- As a user, I confirm my F-skatt registration status
- As a user, I set my VAT registration status and momsperiod
- As a user, I enter my preliminary tax (debiterad F-skatt) amount
- As a user, I add my bank details for invoice payments
- As a user, I connect at least one bank account via Enable Banking
- As a user, I cannot skip required fields - all must be completed
### Dashboard
- As a user, I see my total balance, locked tax amount, and spendable amount immediately on login
- As a user, I see a warning when approaching VAT registration threshold
### Transactions
- As a user, I connect my bank account securely
- As a user, I categorize transactions by swiping
- As a user, I can edit categorization later
- As a user, I see uncategorized transaction count as a badge
### Invoicing
- As a user, I create an invoice in under 60 seconds
- As a user, the system auto-detects if reverse charge applies
- As a user, I download PDF or copy link to hosted invoice
- As a user, I see invoice status: draft, sent, paid, overdue
### Settings
- As a user, I enter my company details once (org.nr, F-skatt, address)
- As a user, I save customer profiles for repeat invoicing
## Success Metrics
- Time to first invoice: <5 minutes from signup
- Transaction categorization rate: >80% within 7 days
- User retention at 30 days: >40%
## Constraints
- Must comply with Swedish Bokföringslagen
- Bank integration must use licensed PSD2 provider (TSP model via Tink/Nordigen)
- Invoice PDF must contain all legally required fields
- All financial calculations are estimates, not tax advice (disclaimer required)
- 7-year retention of all räkenskapsinformation per Bokföringslagen
- Non-custodial design: app never holds user funds
- GDPR compliant with EU/EES data residency
## Regulatory Notes for MVP
### PSD2 Strategy
Use Technical Service Provider (TSP) model. Nordigen offers free AISP tier. Avoids 125k EUR capital requirement and FI licensing process.
### Non-Custodial Principle
Critical: System must never hold client funds. For tax savings feature, use PISP to move funds to user's own separate bank account, not an app-controlled wallet. This avoids Lagen om redovisningsmedel requirements.
### Data Retention
- Bokföringslagen requires 7-year retention of verifikationer
- GDPR "right to be forgotten" does not override this legal obligation
- Technical lock must prevent deletion of accounting records even on account closure
- Non-accounting data (support chats, preferences) can be deleted on request
### Digital Archiving (July 2024 Law)
Receipt photos must be stored in immutable format (PDF/A or locked JPEG). Paper originals no longer required if digital capture is tamper-proof.
-343
View File
@@ -1,343 +0,0 @@
# Technical Architecture Document
## Stack Overview
```
Frontend: Next.js (App Router) + React + TypeScript
Styling: Tailwind CSS + shadcn/ui
Backend: Next.js API Routes + Supabase Edge Functions
Database: Supabase (PostgreSQL)
Auth: Supabase Auth (magic link + Google OAuth)
Storage: Supabase Storage (invoice PDFs)
Banking: Enable Banking (PSD2 AISP)
PDF Gen: @react-pdf/renderer or Puppeteer
Hosting: Vercel
Currency: Riksbanken API for exchange rates
```
## Project Structure
```
/
├── app/
│ ├── (auth)/
│ │ ├── login/page.tsx
│ │ └── callback/page.tsx
│ ├── (onboarding)/
│ │ └── onboarding/
│ │ ├── page.tsx # Wizard container
│ │ ├── step-1/page.tsx # Entity type
│ │ ├── step-2/page.tsx # Company details
│ │ ├── step-3/page.tsx # Tax registration
│ │ ├── step-4/page.tsx # Preliminary tax
│ │ ├── step-5/page.tsx # Bank details for invoices
│ │ └── step-6/page.tsx # Connect bank (Enable Banking)
│ ├── (dashboard)/
│ │ ├── layout.tsx
│ │ ├── page.tsx # Main dashboard
│ │ ├── transactions/
│ │ │ └── page.tsx # Swipe categorization
│ │ ├── invoices/
│ │ │ ├── page.tsx # Invoice list
│ │ │ ├── new/page.tsx # Create invoice
│ │ │ ├── [id]/page.tsx # Invoice detail
│ │ │ └── [id]/credit/page.tsx # Create credit note
│ │ ├── customers/
│ │ │ └── page.tsx # Customer management
│ │ └── settings/
│ │ └── page.tsx # Company profile
│ └── api/
│ ├── banking/
│ │ ├── connect/route.ts # Initiate Enable Banking connection
│ │ ├── callback/route.ts # PSD2 callback
│ │ └── sync/route.ts # Fetch transactions
│ ├── invoices/
│ │ ├── route.ts # CRUD
│ │ ├── [id]/pdf/route.ts # Generate PDF
│ │ └── [id]/credit/route.ts # Create credit note
│ ├── currency/
│ │ └── rates/route.ts # Riksbanken exchange rates
│ └── webhooks/
│ └── banking/route.ts # Transaction webhooks
├── components/
│ ├── ui/ # shadcn components
│ ├── onboarding/
│ │ ├── WizardProgress.tsx
│ │ ├── EntityTypeSelector.tsx
│ │ └── BankConnectButton.tsx
│ ├── dashboard/
│ │ ├── BalanceCard.tsx
│ │ ├── TaxBreakdown.tsx
│ │ ├── FskattWarning.tsx
│ │ └── RevenueChart.tsx
│ ├── transactions/
│ │ ├── SwipeCard.tsx
│ │ └── TransactionList.tsx
│ └── invoices/
│ ├── InvoiceForm.tsx
│ ├── InvoicePreview.tsx
│ ├── CurrencySelector.tsx
│ └── CustomerSelect.tsx
├── lib/
│ ├── supabase/
│ │ ├── client.ts # Browser client
│ │ ├── server.ts # Server client
│ │ └── admin.ts # Service role client
│ ├── banking/
│ │ └── enable-banking.ts # Enable Banking API wrapper
│ ├── currency/
│ │ └── riksbanken.ts # Exchange rate fetching
│ ├── tax/
│ │ ├── calculator.ts # Swedish tax calculations
│ │ ├── expense-warnings.ts # Non-deductible expense alerts
│ │ ├── schablonavdrag.ts # Standard deductions
│ │ └── fskatt-warning.ts # Preliminary tax comparison
│ ├── invoice/
│ │ ├── vat-rules.ts # VAT logic
│ │ └── pdf-generator.ts
│ └── utils/
│ └── currency.ts
├── types/
│ └── index.ts # TypeScript interfaces
└── supabase/
├── migrations/ # Database migrations
└── seed.sql # Test data
```
## Authentication Flow
1. User lands on `/login`
2. Enters email → Supabase sends magic link
3. User clicks link → redirected to `/callback`
4. Callback exchanges code for session
5. **Onboarding check:** Query `company_settings` for user
- If no settings exist → redirect to `/onboarding`
- If settings exist but `onboarding_complete = false` → redirect to `/onboarding`
- If settings complete → redirect to `/` (dashboard)
Alternative: Google OAuth for faster onboarding (still requires completing wizard).
## Onboarding Flow
```
1. User completes auth
2. Middleware checks company_settings.onboarding_complete
3. If incomplete → force redirect to /onboarding
4. Wizard steps (cannot skip):
a. Entity type selection
b. Company details (name, org.nr, address)
c. Tax registration (F-skatt, VAT, momsperiod)
d. Preliminary tax amount (optional but encouraged)
e. Bank details for invoices
f. Connect bank via Enable Banking (required)
5. On completion → set onboarding_complete = true
6. Redirect to dashboard
```
**Middleware Logic:**
```typescript
// middleware.ts - simplified
const publicRoutes = ['/login', '/auth/callback']
const onboardingRoutes = ['/onboarding']
if (!user) {
// Not logged in → login page
return redirect('/login')
}
const settings = await getCompanySettings(user.id)
if (!settings?.onboarding_complete) {
// Onboarding incomplete → force wizard
if (!onboardingRoutes.some(r => path.startsWith(r))) {
return redirect('/onboarding')
}
}
// Onboarding complete → allow access
```
## Bank Connection Flow
### PSD2 Provider: Enable Banking
Using Enable Banking as the AISP provider.
**Why Enable Banking:**
- Good coverage of Swedish banks (Nordea, SEB, Handelsbanken, Swedbank, etc.)
- Reasonable pricing for startups
- TSP model: they hold the license, you process data
- REST API with good documentation
**Consent Lifecycle:**
- PSD2 RTS allows 90-180 day consent periods (bank dependent)
- System must track consent expiry per bank connection
- Proactive notification: "Din bankkoppling till Nordea löper ut om 7 dagar"
**Multiple Accounts:**
- Users can connect multiple bank accounts (business + savings)
- Each connection tracked separately with own consent expiry
- Dashboard aggregates across all connected accounts
```
1. User clicks "Koppla bank"
2. Frontend calls POST /api/banking/connect
3. Backend creates Enable Banking session, returns redirect URL
4. User selects bank, authenticates with BankID (SCA requirement)
5. Enable Banking redirects to /api/banking/callback
6. Backend stores connection metadata (consent expiry, account info)
7. Initial transaction fetch (90 days history typical)
8. User redirected to /transactions
9. Webhook receives new transactions in real-time
10. At consent expiry - 7 days: prompt re-consent flow
```
### Non-Custodial Design (Critical)
The app must NEVER hold user funds. This avoids:
- Lagen om redovisningsmedel (1944:181)
- Klientmedelskonto requirements
- Full betalningsinstitut licensing
For future "tax savings" feature:
- Use PISP to instruct user's bank to transfer to user's OWN savings account
- App orchestrates, never touches money
- User maintains full control and ownership
## Currency Conversion
### Riksbanken Integration
For multi-currency invoicing, convert foreign amounts to SEK for bokföring.
```typescript
// lib/currency/riksbanken.ts
interface ExchangeRate {
currency: string
rate: number
date: string
}
const RIKSBANKEN_API = 'https://api.riksbank.se/swea/v1/CrossRates'
export async function getExchangeRate(
currency: 'EUR' | 'USD' | 'GBP',
date: string // YYYY-MM-DD
): Promise<number> {
// Riksbanken publishes daily rates
// Use booking date for invoice conversion
const response = await fetch(
`${RIKSBANKEN_API}/${currency}/SEK/${date}`
)
const data = await response.json()
return data.value
}
// Invoice creation: store both original and SEK amounts
interface InvoiceAmounts {
originalCurrency: string
originalTotal: number
exchangeRate: number
exchangeRateDate: string
sekTotal: number // This is what goes in bokföring
}
```
**Bokföringslagen requirement:** Foreign currency transactions must be converted to SEK. The exchange rate on the invoice date (fakturadatum) is typically used.
## Tax Calculation Logic
Swedish sole proprietor (enskild näringsidkare) tax components:
```typescript
interface TaxBreakdown {
grossIncome: number; // Total invoiced + received
deductibleExpenses: number; // Categorized business expenses
netIncome: number; // grossIncome - deductibleExpenses
// Egenavgifter (self-employment contributions) ~28.97%
egenavgifter: number;
// Preliminary income tax (kommunalskatt ~32% avg)
incomeTax: number;
// VAT collected (if registered)
vatCollected: number;
vatDeductible: number;
vatPayable: number;
// Total locked
totalLocked: number;
// Available to spend
disponibelt: number;
}
```
**Important**: These are estimates. Display clear disclaimer.
## VAT Rules Engine
```typescript
type VatScenario =
| 'swedish_customer' // 25% VAT
| 'eu_business_valid_vat' // 0% + reverse charge text
| 'eu_consumer' // 25% VAT (or destination country rate for digital services)
| 'non_eu' // 0% VAT, export
interface VatDecision {
rate: number;
reverseCharge: boolean;
invoiceText: string | null; // Legal text to include
}
```
Validation: EU VAT numbers validated via VIES API.
## Security Considerations
- Row Level Security (RLS) on all tables
- User can only access own data
- Banking tokens encrypted at rest (Supabase Vault or env vars)
- Invoice PDFs in private bucket, signed URLs for access
- No PII in logs
- Rate limiting on API routes
## Environment Variables
```
# Supabase
NEXT_PUBLIC_SUPABASE_URL=
NEXT_PUBLIC_SUPABASE_ANON_KEY=
SUPABASE_SERVICE_ROLE_KEY=
# Banking (Tink example)
TINK_CLIENT_ID=
TINK_CLIENT_SECRET=
TINK_REDIRECT_URI=
# App
NEXT_PUBLIC_APP_URL=https://app.yourdomain.se
# PDF
PDF_STORAGE_BUCKET=invoices
```
## Deployment
Vercel configuration:
```json
// vercel.json
{
"crons": [
{
"path": "/api/banking/sync",
"schedule": "0 6 * * *"
}
]
}
```
Database migrations run via Supabase CLI or dashboard.
-688
View File
@@ -1,688 +0,0 @@
# Database Schema
## Entity Relationship Diagram
```
users (Supabase Auth)
│
├── profiles (1:1)
│ └── company_settings (1:1)
│
├── bank_connections (1:N)
│ └── transactions (1:N)
│
├── customers (1:N)
│
└── invoices (1:N)
└── invoice_items (1:N)
```
## Tables
### profiles
Extends Supabase auth.users.
```sql
CREATE TABLE profiles (
id UUID PRIMARY KEY REFERENCES auth.users(id) ON DELETE CASCADE,
email TEXT NOT NULL,
full_name TEXT,
created_at TIMESTAMPTZ DEFAULT NOW(),
updated_at TIMESTAMPTZ DEFAULT NOW()
);
-- RLS
ALTER TABLE profiles ENABLE ROW LEVEL SECURITY;
CREATE POLICY "Users can view own profile"
ON profiles FOR SELECT
USING (auth.uid() = id);
CREATE POLICY "Users can update own profile"
ON profiles FOR UPDATE
USING (auth.uid() = id);
```
### company_settings
Business/tax information for the company (enskild firma or AB).
```sql
CREATE TYPE entity_type AS ENUM (
'enskild_firma',
'aktiebolag'
);
CREATE TYPE moms_period AS ENUM (
'monthly',
'quarterly'
);
CREATE TABLE company_settings (
id UUID PRIMARY KEY DEFAULT gen_random_uuid(),
user_id UUID NOT NULL REFERENCES profiles(id) ON DELETE CASCADE,
-- Onboarding status
onboarding_complete BOOLEAN DEFAULT FALSE,
onboarding_step INTEGER DEFAULT 1, -- Track progress (1-6)
-- Entity type
entity_type entity_type NOT NULL DEFAULT 'enskild_firma',
-- Company info
company_name TEXT NOT NULL,
org_number TEXT, -- Organisationsnummer (required for AB)
vat_number TEXT, -- SE + org_number + 01 (if VAT registered)
f_skatt_registered BOOLEAN DEFAULT TRUE,
-- Address
address_line1 TEXT,
address_line2 TEXT,
postal_code TEXT,
city TEXT,
country TEXT DEFAULT 'SE',
-- Banking
bank_name TEXT,
clearing_number TEXT,
account_number TEXT,
iban TEXT,
bic TEXT,
-- Contact
phone TEXT,
website TEXT,
-- Tax settings
municipal_tax_rate DECIMAL(5,2) DEFAULT 32.00, -- User's kommun (enskild firma)
preliminary_tax_monthly DECIMAL(12,2), -- Debiterad F-skatt per month
vat_registered BOOLEAN DEFAULT FALSE,
vat_registration_date DATE,
moms_period moms_period DEFAULT 'quarterly',
-- Fiscal year (räkenskapsår)
fiscal_year_start_month INTEGER DEFAULT 1, -- 1 = calendar year, other = brutet räkenskapsår
-- Example: 7 = July start (July 1 - June 30)
-- Invoice settings
invoice_prefix TEXT DEFAULT 'INV',
next_invoice_number INTEGER DEFAULT 1001, -- Allow unusual starting numbers
payment_terms_days INTEGER DEFAULT 30,
default_currency TEXT DEFAULT 'SEK',
created_at TIMESTAMPTZ DEFAULT NOW(),
updated_at TIMESTAMPTZ DEFAULT NOW(),
UNIQUE(user_id)
);
-- RLS
ALTER TABLE company_settings ENABLE ROW LEVEL SECURITY;
CREATE POLICY "Users can CRUD own company settings"
ON company_settings FOR ALL
USING (auth.uid() = user_id);
```
### bank_connections
PSD2 bank connection records.
```sql
CREATE TABLE bank_connections (
id UUID PRIMARY KEY DEFAULT gen_random_uuid(),
user_id UUID NOT NULL REFERENCES profiles(id) ON DELETE CASCADE,
provider TEXT NOT NULL, -- 'tink', 'enablebanking'
provider_connection_id TEXT, -- External ID
bank_name TEXT,
account_name TEXT,
account_number_masked TEXT, -- Last 4 digits
status TEXT DEFAULT 'pending', -- pending, active, expired, error
consent_expires_at TIMESTAMPTZ,
last_synced_at TIMESTAMPTZ,
-- Encrypted tokens stored in Supabase Vault or as encrypted column
access_token_encrypted TEXT,
refresh_token_encrypted TEXT,
created_at TIMESTAMPTZ DEFAULT NOW(),
updated_at TIMESTAMPTZ DEFAULT NOW()
);
-- RLS
ALTER TABLE bank_connections ENABLE ROW LEVEL SECURITY;
CREATE POLICY "Users can CRUD own bank connections"
ON bank_connections FOR ALL
USING (auth.uid() = user_id);
```
### transactions
Bank transactions imported via PSD2.
```sql
CREATE TYPE transaction_category AS ENUM (
'uncategorized',
'business_income',
'business_expense',
'private',
'tax_payment',
'vat_payment',
'transfer'
);
CREATE TYPE expense_type AS ENUM (
'equipment',
'software',
'travel',
'meals',
'office',
'marketing',
'professional_services',
'other'
);
CREATE TABLE transactions (
id UUID PRIMARY KEY DEFAULT gen_random_uuid(),
user_id UUID NOT NULL REFERENCES profiles(id) ON DELETE CASCADE,
bank_connection_id UUID REFERENCES bank_connections(id) ON DELETE SET NULL,
-- Transaction data from bank
external_id TEXT, -- Bank's transaction ID
booking_date DATE NOT NULL,
value_date DATE,
amount DECIMAL(12,2) NOT NULL, -- Negative = expense, Positive = income
currency TEXT DEFAULT 'SEK',
description TEXT,
counterparty_name TEXT,
counterparty_account TEXT,
mcc_code TEXT, -- Merchant Category Code from bank
-- Categorization
category transaction_category DEFAULT 'uncategorized',
expense_type expense_type,
business_percentage INTEGER DEFAULT 100, -- For mixed-use items
-- BAS Accounting
bas_account TEXT, -- e.g., '5410', '2013', '5910'
bas_account_auto BOOLEAN DEFAULT FALSE, -- Was this auto-categorized?
bas_account_confidence TEXT, -- 'high', 'medium', 'low'
reverse_charge_applied BOOLEAN DEFAULT FALSE,
-- VAT tracking
vat_amount DECIMAL(12,2), -- Calculated or from receipt
vat_rate DECIMAL(5,2), -- 25, 12, 6, or 0
-- Metadata
notes TEXT,
receipt_url TEXT, -- Link to uploaded receipt
linked_invoice_id UUID, -- If this is payment for an invoice
created_at TIMESTAMPTZ DEFAULT NOW(),
updated_at TIMESTAMPTZ DEFAULT NOW(),
UNIQUE(user_id, external_id)
);
-- Indexes
CREATE INDEX idx_transactions_user_date ON transactions(user_id, booking_date DESC);
CREATE INDEX idx_transactions_category ON transactions(user_id, category);
-- RLS
ALTER TABLE transactions ENABLE ROW LEVEL SECURITY;
CREATE POLICY "Users can CRUD own transactions"
ON transactions FOR ALL
USING (auth.uid() = user_id);
```
### customers
Invoice recipients.
```sql
CREATE TYPE customer_type AS ENUM (
'individual',
'swedish_business',
'eu_business',
'non_eu_business'
);
CREATE TABLE customers (
id UUID PRIMARY KEY DEFAULT gen_random_uuid(),
user_id UUID NOT NULL REFERENCES profiles(id) ON DELETE CASCADE,
customer_type customer_type NOT NULL,
-- Basic info
name TEXT NOT NULL,
email TEXT,
phone TEXT,
-- Business info
org_number TEXT,
vat_number TEXT, -- For EU reverse charge validation
vat_number_validated BOOLEAN DEFAULT FALSE,
vat_number_validated_at TIMESTAMPTZ,
-- Address
address_line1 TEXT,
address_line2 TEXT,
postal_code TEXT,
city TEXT,
country TEXT NOT NULL DEFAULT 'SE',
-- Defaults for invoicing
default_payment_terms_days INTEGER,
default_currency TEXT DEFAULT 'SEK',
notes TEXT,
created_at TIMESTAMPTZ DEFAULT NOW(),
updated_at TIMESTAMPTZ DEFAULT NOW()
);
-- RLS
ALTER TABLE customers ENABLE ROW LEVEL SECURITY;
CREATE POLICY "Users can CRUD own customers"
ON customers FOR ALL
USING (auth.uid() = user_id);
```
### invoices
```sql
CREATE TYPE invoice_status AS ENUM (
'draft',
'sent',
'viewed',
'paid',
'overdue',
'cancelled'
);
CREATE TYPE invoice_type AS ENUM (
'invoice',
'credit_note'
);
CREATE TYPE vat_treatment AS ENUM (
'standard', -- Normal VAT rate
'reverse_charge', -- EU B2B
'export', -- Non-EU
'exempt' -- VAT exempt services
);
CREATE TABLE invoices (
id UUID PRIMARY KEY DEFAULT gen_random_uuid(),
user_id UUID NOT NULL REFERENCES profiles(id) ON DELETE CASCADE,
customer_id UUID NOT NULL REFERENCES customers(id),
-- Invoice identification
invoice_number TEXT NOT NULL,
invoice_type invoice_type NOT NULL DEFAULT 'invoice',
credited_invoice_id UUID REFERENCES invoices(id), -- For credit notes: which invoice is credited
reference TEXT, -- Customer's reference/PO number
-- Dates
invoice_date DATE NOT NULL DEFAULT CURRENT_DATE,
due_date DATE NOT NULL,
-- Original currency (what customer pays)
currency TEXT NOT NULL DEFAULT 'SEK',
subtotal DECIMAL(12,2) NOT NULL DEFAULT 0,
vat_amount DECIMAL(12,2) NOT NULL DEFAULT 0,
total DECIMAL(12,2) NOT NULL DEFAULT 0,
-- SEK conversion (for bokföring)
exchange_rate DECIMAL(10,6) DEFAULT 1.0,
exchange_rate_date DATE,
subtotal_sek DECIMAL(12,2) NOT NULL DEFAULT 0,
vat_amount_sek DECIMAL(12,2) NOT NULL DEFAULT 0,
total_sek DECIMAL(12,2) NOT NULL DEFAULT 0,
-- VAT handling
vat_treatment vat_treatment NOT NULL DEFAULT 'standard',
vat_rate DECIMAL(5,2) DEFAULT 25.00,
reverse_charge_text TEXT, -- Legal text for reverse charge
moms_ruta INTEGER, -- Which ruta in momsdeklaration (5, 39, 40)
-- Status
status invoice_status DEFAULT 'draft',
sent_at TIMESTAMPTZ,
viewed_at TIMESTAMPTZ,
paid_at TIMESTAMPTZ,
paid_amount DECIMAL(12,2),
paid_currency TEXT,
-- PDF
pdf_url TEXT,
pdf_generated_at TIMESTAMPTZ,
-- Additional
notes TEXT, -- Internal notes
customer_notes TEXT, -- Visible on invoice
created_at TIMESTAMPTZ DEFAULT NOW(),
updated_at TIMESTAMPTZ DEFAULT NOW(),
UNIQUE(user_id, invoice_number),
-- Credit notes must reference an original invoice
CONSTRAINT credit_note_requires_original CHECK (
(invoice_type = 'credit_note' AND credited_invoice_id IS NOT NULL) OR
(invoice_type = 'invoice')
)
);
-- Indexes
CREATE INDEX idx_invoices_user_status ON invoices(user_id, status);
CREATE INDEX idx_invoices_due_date ON invoices(user_id, due_date);
CREATE INDEX idx_invoices_type ON invoices(user_id, invoice_type);
-- RLS
ALTER TABLE invoices ENABLE ROW LEVEL SECURITY;
CREATE POLICY "Users can CRUD own invoices"
ON invoices FOR ALL
USING (auth.uid() = user_id);
```
### invoice_items
```sql
CREATE TABLE invoice_items (
id UUID PRIMARY KEY DEFAULT gen_random_uuid(),
invoice_id UUID NOT NULL REFERENCES invoices(id) ON DELETE CASCADE,
description TEXT NOT NULL,
quantity DECIMAL(10,2) NOT NULL DEFAULT 1,
unit TEXT DEFAULT 'st', -- st, timmar, etc.
unit_price DECIMAL(12,2) NOT NULL,
-- Calculated
line_total DECIMAL(12,2) NOT NULL,
sort_order INTEGER DEFAULT 0,
created_at TIMESTAMPTZ DEFAULT NOW()
);
-- RLS
ALTER TABLE invoice_items ENABLE ROW LEVEL SECURITY;
CREATE POLICY "Users can CRUD own invoice items"
ON invoice_items FOR ALL
USING (
EXISTS (
SELECT 1 FROM invoices
WHERE invoices.id = invoice_items.invoice_id
AND invoices.user_id = auth.uid()
)
);
```
### salary_payments (Aktiebolag only)
Track owner salary payments for AB entities.
```sql
CREATE TABLE salary_payments (
id UUID PRIMARY KEY DEFAULT gen_random_uuid(),
user_id UUID NOT NULL REFERENCES profiles(id) ON DELETE CASCADE,
-- Payment details
payment_date DATE NOT NULL,
pay_period_start DATE NOT NULL, -- e.g., 2024-01-01
pay_period_end DATE NOT NULL, -- e.g., 2024-01-31
-- Amounts
gross_salary DECIMAL(12,2) NOT NULL, -- Bruttolön
employer_contributions DECIMAL(12,2) NOT NULL, -- Arbetsgivaravgifter (31.42%)
withheld_tax DECIMAL(12,2) NOT NULL, -- Preliminärskatteavdrag
net_salary DECIMAL(12,2) NOT NULL, -- Nettolön (what's paid out)
-- Total cost to company
total_cost DECIMAL(12,2) NOT NULL, -- gross + employer_contributions
-- Reporting status
agi_reported BOOLEAN DEFAULT FALSE, -- AGI-deklaration submitted
agi_reported_at TIMESTAMPTZ,
notes TEXT,
created_at TIMESTAMPTZ DEFAULT NOW(),
updated_at TIMESTAMPTZ DEFAULT NOW()
);
-- RLS
ALTER TABLE salary_payments ENABLE ROW LEVEL SECURITY;
CREATE POLICY "Users can CRUD own salary payments"
ON salary_payments FOR ALL
USING (auth.uid() = user_id);
-- Index for period queries
CREATE INDEX idx_salary_payments_period ON salary_payments(user_id, pay_period_start);
```
### Constants for AB calculations
```sql
-- Current rates (update annually)
-- Arbetsgivaravgifter: 31.42% (2024)
-- This should be configurable or fetched from a rates table
CREATE TABLE tax_rates (
id UUID PRIMARY KEY DEFAULT gen_random_uuid(),
rate_type TEXT NOT NULL, -- 'arbetsgivaravgifter', 'bolagsskatt', etc.
rate DECIMAL(5,4) NOT NULL, -- 0.3142 for 31.42%
valid_from DATE NOT NULL,
valid_to DATE, -- NULL = currently active
UNIQUE(rate_type, valid_from)
);
-- Seed current rates
INSERT INTO tax_rates (rate_type, rate, valid_from) VALUES
('arbetsgivaravgifter', 0.3142, '2024-01-01'),
('bolagsskatt', 0.206, '2021-01-01'),
('egenavgifter', 0.2897, '2024-01-01');
```
### bas_accounts (Reference Table)
Available BAS accounts for transaction categorization.
```sql
CREATE TABLE bas_accounts (
account_number TEXT PRIMARY KEY, -- e.g., '5410'
name_sv TEXT NOT NULL, -- Swedish name
name_en TEXT, -- English name
account_class INTEGER NOT NULL, -- 1-8
account_group INTEGER NOT NULL, -- e.g., 54
-- Categorization
is_expense BOOLEAN DEFAULT FALSE,
is_income BOOLEAN DEFAULT FALSE,
is_asset BOOLEAN DEFAULT FALSE,
is_liability BOOLEAN DEFAULT FALSE,
-- Influencer-specific
common_for_influencers BOOLEAN DEFAULT FALSE,
risk_level TEXT, -- 'low', 'medium', 'high', 'very_high'
default_vat_rate DECIMAL(5,2),
-- Usage hints
description TEXT,
warning_text TEXT, -- Shown when selected
requires_documentation BOOLEAN DEFAULT FALSE,
active BOOLEAN DEFAULT TRUE
);
-- Seed common influencer accounts
INSERT INTO bas_accounts (account_number, name_sv, name_en, account_class, account_group, is_expense, common_for_influencers, risk_level, description) VALUES
('1210', 'Maskiner och inventarier', 'Equipment', 1, 12, FALSE, TRUE, 'low', 'Kameror, datorer över 29 400 kr'),
('2013', 'Eget uttag', 'Owner withdrawals', 2, 20, FALSE, TRUE, 'low', 'Privata utgifter betalda med företagskort'),
('3001', 'Försäljning 25%', 'Sales 25% VAT', 3, 30, FALSE, TRUE, 'low', 'Svenska samarbeten'),
('3044', 'Försäljning tjänst EU', 'EU service sales', 3, 30, FALSE, TRUE, 'low', 'AdSense, EU-affiliates. Kräver periodisk sammanställning'),
('3045', 'Försäljning tjänst export', 'Export service sales', 3, 30, FALSE, TRUE, 'low', 'Twitch, YouTube US. Momsfritt'),
('5410', 'Förbrukningsinventarier', 'Consumable equipment', 5, 54, TRUE, TRUE, 'low', 'Kameror, datorer under 29 400 kr'),
('5420', 'Programvaror', 'Software', 5, 54, TRUE, TRUE, 'low', 'Adobe CC, Epidemic Sound, SaaS'),
('5480', 'Arbetskläder', 'Work clothes', 5, 54, TRUE, TRUE, 'very_high', 'Endast skyddskläder/uniformer. EJ civila kläder!'),
('5800', 'Resekostnader', 'Travel expenses', 5, 58, TRUE, TRUE, 'medium', 'Tåg, flyg, hyrbil för tjänsteresor'),
('5820', 'Taxikostnader', 'Taxi', 5, 58, TRUE, TRUE, 'medium', 'Taxi/Uber för tjänsteresor'),
('5910', 'Annonsering', 'Advertising', 5, 59, TRUE, TRUE, 'low', 'Facebook Ads, Google Ads, TikTok'),
('6071', 'Representation, avdragsgill', 'Deductible representation', 6, 60, TRUE, TRUE, 'high', 'Max 60 kr/person. Kräver dokumentation'),
('6072', 'Representation, ej avdragsgill', 'Non-deductible representation', 6, 60, TRUE, TRUE, 'high', 'Måltider vid representation'),
('6230', 'Datakommunikation', 'Data/web hosting', 6, 62, TRUE, TRUE, 'low', 'Webbhotell, domäner, bredband'),
('6500', 'Övriga externa tjänster', 'Other services', 6, 65, TRUE, TRUE, 'low', 'Fotografer, klippare med F-skatt');
```
### mcc_mappings (Reference Table)
MCC code to BAS account mappings for auto-categorization.
```sql
CREATE TABLE mcc_mappings (
mcc_code TEXT PRIMARY KEY,
description TEXT NOT NULL,
risk_level TEXT NOT NULL, -- 'low', 'medium', 'high', 'very_high'
primary_account TEXT REFERENCES bas_accounts(account_number),
secondary_account TEXT REFERENCES bas_accounts(account_number),
requires_user_input BOOLEAN DEFAULT FALSE,
auto_reject_reason TEXT, -- If very_high risk, why?
categorization_logic TEXT,
active BOOLEAN DEFAULT TRUE
);
-- Seed MCC mappings (subset - see 08-BAS-ACCOUNTING-GUIDE.md for full list)
INSERT INTO mcc_mappings (mcc_code, description, risk_level, primary_account, secondary_account, requires_user_input, auto_reject_reason, categorization_logic) VALUES
('7311', 'Advertising Services', 'low', '5910', '5930', FALSE, NULL, 'Standard for Facebook/Google Ads. Triggers EU VAT check.'),
('5732', 'Electronics Stores', 'medium', '5410', '1210', FALSE, NULL, 'Amount determines routing. >29,400 kr → 1210.'),
('5946', 'Camera & Photo Supply', 'medium', '5410', '1210', FALSE, NULL, 'Same as electronics.'),
('5812', 'Eating Places/Restaurants', 'high', '6072', '2013', TRUE, NULL, 'Requires user input: Representation or private?'),
('5611', 'Mens Clothing', 'very_high', '2013', '5480', TRUE, 'Clothes usable privately are not deductible (RÅ81 1:29)', 'Default MUST be 2013.'),
('5621', 'Womens Ready-to-Wear', 'very_high', '2013', '5480', TRUE, 'Clothes usable privately are not deductible', 'Default MUST be 2013.'),
('5977', 'Cosmetic Stores', 'very_high', '2013', '5490', TRUE, 'Cosmetics are private expenses', 'Default private.'),
('4111', 'Commuter Transport', 'low', '5800', NULL, FALSE, NULL, 'Train/bus. 6% VAT in Sweden.'),
('4722', 'Travel Agencies', 'medium', '5800', '2013', TRUE, NULL, 'Is the trip business-related?'),
('7372', 'Computer Programming', 'low', '6230', '5420', FALSE, NULL, 'Web hosting, SaaS.'),
('5921', 'Package Stores (Systembolaget)', 'very_high', '2013', NULL, FALSE, 'Alcohol is not deductible', 'Always 2013.'),
('7941', 'Sports Clubs/Gyms', 'very_high', '2013', NULL, FALSE, 'Gym is private expense even for fitness influencers', 'Always 2013.');
```
## Functions
### Calculate invoice totals
```sql
CREATE OR REPLACE FUNCTION calculate_invoice_totals()
RETURNS TRIGGER AS $$
BEGIN
UPDATE invoices
SET
subtotal = (
SELECT COALESCE(SUM(line_total), 0)
FROM invoice_items
WHERE invoice_id = NEW.invoice_id
),
vat_amount = (
SELECT COALESCE(SUM(line_total), 0) * (vat_rate / 100)
FROM invoice_items
WHERE invoice_id = NEW.invoice_id
),
total = (
SELECT COALESCE(SUM(line_total), 0) * (1 + vat_rate / 100)
FROM invoice_items
WHERE invoice_id = NEW.invoice_id
),
updated_at = NOW()
WHERE id = NEW.invoice_id;
RETURN NEW;
END;
$$ LANGUAGE plpgsql SECURITY DEFINER;
CREATE TRIGGER trigger_calculate_invoice_totals
AFTER INSERT OR UPDATE OR DELETE ON invoice_items
FOR EACH ROW
EXECUTE FUNCTION calculate_invoice_totals();
```
### Auto-create profile on signup
```sql
CREATE OR REPLACE FUNCTION handle_new_user()
RETURNS TRIGGER AS $$
BEGIN
INSERT INTO profiles (id, email, full_name)
VALUES (
NEW.id,
NEW.email,
NEW.raw_user_meta_data->>'full_name'
);
RETURN NEW;
END;
$$ LANGUAGE plpgsql SECURITY DEFINER;
CREATE TRIGGER on_auth_user_created
AFTER INSERT ON auth.users
FOR EACH ROW
EXECUTE FUNCTION handle_new_user();
```
## Views
### Dashboard summary
```sql
CREATE VIEW dashboard_summary AS
SELECT
p.id AS user_id,
-- Revenue YTD
COALESCE(SUM(
CASE WHEN t.amount > 0 AND t.category = 'business_income'
AND t.booking_date >= DATE_TRUNC('year', CURRENT_DATE)
THEN t.amount ELSE 0 END
), 0) AS revenue_ytd,
-- Expenses YTD
COALESCE(SUM(
CASE WHEN t.amount < 0 AND t.category = 'business_expense'
AND t.booking_date >= DATE_TRUNC('year', CURRENT_DATE)
THEN ABS(t.amount) ELSE 0 END
), 0) AS expenses_ytd,
-- Uncategorized count
COUNT(CASE WHEN t.category = 'uncategorized' THEN 1 END) AS uncategorized_count,
-- Unpaid invoices
(SELECT COUNT(*) FROM invoices i
WHERE i.user_id = p.id
AND i.status IN ('sent', 'overdue')) AS unpaid_invoice_count,
(SELECT COALESCE(SUM(total), 0) FROM invoices i
WHERE i.user_id = p.id
AND i.status IN ('sent', 'overdue')) AS unpaid_invoice_total
FROM profiles p
LEFT JOIN transactions t ON t.user_id = p.id
GROUP BY p.id;
```
## Migration File
Save as `supabase/migrations/001_initial_schema.sql`:
```sql
-- Copy all CREATE TABLE, CREATE TYPE, CREATE FUNCTION, CREATE VIEW statements above
-- Run with: supabase db push
```
-672
View File
@@ -1,672 +0,0 @@
# API Specification
## Authentication
All API routes require authentication via Supabase session cookie.
```typescript
// lib/supabase/server.ts
import { createServerClient } from '@supabase/ssr'
import { cookies } from 'next/headers'
export async function createClient() {
const cookieStore = cookies()
return createServerClient(
process.env.NEXT_PUBLIC_SUPABASE_URL!,
process.env.NEXT_PUBLIC_SUPABASE_ANON_KEY!,
{
cookies: {
get(name: string) {
return cookieStore.get(name)?.value
},
},
}
)
}
// Usage in route
export async function GET() {
const supabase = await createClient()
const { data: { user } } = await supabase.auth.getUser()
if (!user) {
return Response.json({ error: 'Unauthorized' }, { status: 401 })
}
// ... route logic
}
```
---
## Endpoints
### Company Settings
#### GET /api/settings
Returns current user's company settings.
**Response 200:**
```json
{
"id": "uuid",
"company_name": "Anna Andersson Content AB",
"org_number": "123456-7890",
"vat_number": "SE1234567890",
"vat_registered": true,
"address_line1": "Storgatan 1",
"city": "Stockholm",
"postal_code": "11122",
"payment_terms_days": 30,
"next_invoice_number": 15
}
```
#### PUT /api/settings
Updates company settings.
**Request:**
```json
{
"company_name": "Anna Andersson Content AB",
"municipal_tax_rate": 31.5,
"payment_terms_days": 14
}
```
---
### Transactions
#### GET /api/transactions
List transactions with optional filters.
**Query params:**
- `category`: filter by category
- `from`: start date (YYYY-MM-DD)
- `to`: end date
- `uncategorized`: boolean, only uncategorized
- `limit`: default 50
- `offset`: pagination
**Response 200:**
```json
{
"data": [
{
"id": "uuid",
"booking_date": "2024-01-15",
"amount": -1500.00,
"currency": "SEK",
"description": "ADOBE SYSTEMS",
"counterparty_name": "Adobe",
"category": "uncategorized",
"expense_type": null
}
],
"total": 142,
"uncategorized_count": 23
}
```
#### PATCH /api/transactions/:id
Categorize a transaction.
**Request:**
```json
{
"category": "business_expense",
"expense_type": "software",
"business_percentage": 100,
"notes": "Creative Cloud subscription"
}
```
#### POST /api/transactions/bulk-categorize
Categorize multiple transactions at once.
**Request:**
```json
{
"transaction_ids": ["uuid1", "uuid2"],
"category": "private"
}
```
---
### Banking
#### POST /api/banking/connect
Initiates PSD2 bank connection flow.
**Request:**
```json
{
"provider": "tink"
}
```
**Response 200:**
```json
{
"redirect_url": "https://link.tink.com/..."
}
```
#### GET /api/banking/callback
OAuth callback from banking provider. Handles token exchange.
**Query params:** Provider-specific (code, state, etc.)
**Redirects to:** `/transactions?connected=true`
#### POST /api/banking/sync
Manually trigger transaction sync. Also called by cron.
**Response 200:**
```json
{
"synced": 12,
"new_transactions": 3
}
```
---
### Customers
#### GET /api/customers
List all customers.
**Response 200:**
```json
{
"data": [
{
"id": "uuid",
"name": "Influencer Agency AB",
"customer_type": "swedish_business",
"org_number": "556677-8899",
"country": "SE",
"invoice_count": 5
}
]
}
```
#### POST /api/customers
Create new customer.
**Request:**
```json
{
"name": "Google Ireland Ltd",
"customer_type": "eu_business",
"vat_number": "IE6388047V",
"address_line1": "Gordon House",
"city": "Dublin",
"country": "IE",
"email": "payments@google.com"
}
```
**Response 201:** Created customer object
#### GET /api/customers/:id
Get single customer with invoice history.
#### PUT /api/customers/:id
Update customer.
#### DELETE /api/customers/:id
Soft delete (only if no invoices linked).
---
### VAT Validation
#### POST /api/vat/validate
Validate EU VAT number via VIES (VAT Information Exchange System).
**Critical for Reverse Charge:** Swedish law requires validated VAT number before applying 0% rate on EU B2B sales. Without validation, you must charge 25% Swedish VAT.
**Request:**
```json
{
"vat_number": "IE6388047V"
}
```
**Response 200:**
```json
{
"valid": true,
"country_code": "IE",
"vat_number": "6388047V",
"name": "GOOGLE IRELAND LIMITED",
"address": "GORDON HOUSE, BARROW STREET, DUBLIN 4",
"validated_at": "2024-01-15T10:30:00Z"
}
```
**Response 200 (Invalid):**
```json
{
"valid": false,
"country_code": "IE",
"vat_number": "INVALID123",
"error": "VAT number not found in VIES database"
}
```
**Implementation Notes:**
- Use EU VIES SOAP service or REST wrapper
- Cache validation for 24 hours (VAT numbers rarely change)
- Store `validated_at` timestamp on customer record
- Re-validate periodically for long-term customers
---
### Foreign Purchase Handling (Fiktiv Moms)
#### POST /api/transactions/:id/foreign-purchase
Mark a transaction as foreign service purchase requiring fiktiv moms.
When influencers buy services from abroad (Adobe CC, Facebook Ads, AWS), they must self-report VAT.
**Request:**
```json
{
"supplier_country": "US",
"service_type": "digital_service",
"vat_rate": 25
}
```
**Response 200:**
```json
{
"transaction_id": "uuid",
"fiktiv_moms_amount": 375.00,
"bookings": [
{ "account": "2645", "debit": 375.00, "description": "Beräknad ingående moms utländskt förvärv" },
{ "account": "2614", "credit": 375.00, "description": "Utgående moms utländskt förvärv" }
],
"moms_ruta_21": 1500.00,
"moms_ruta_48": 375.00
}
```
**Momsdeklaration Impact:**
- Ruta 21: Purchase amount (before VAT)
- Ruta 48: Self-reported output VAT
- Net effect is zero if input VAT is deductible, but both must be declared
```
---
### Invoices
#### GET /api/invoices
List invoices with filters.
**Query params:**
- `status`: draft, sent, paid, overdue
- `customer_id`: filter by customer
- `from`, `to`: date range
**Response 200:**
```json
{
"data": [
{
"id": "uuid",
"invoice_number": "INV-2024-015",
"customer": {
"id": "uuid",
"name": "Agency AB"
},
"invoice_date": "2024-01-15",
"due_date": "2024-02-14",
"total": 25000.00,
"currency": "SEK",
"status": "sent"
}
]
}
```
#### POST /api/invoices
Create new invoice.
**Request:**
```json
{
"customer_id": "uuid",
"invoice_date": "2024-01-15",
"due_date": "2024-02-14",
"reference": "PO-12345",
"items": [
{
"description": "Instagram kampanj November",
"quantity": 1,
"unit": "st",
"unit_price": 20000.00
}
],
"customer_notes": "Tack för samarbetet!"
}
```
The API automatically determines VAT treatment based on customer type:
```typescript
// lib/invoice/vat-rules.ts
interface VatDecision {
rate: number
reverseCharge: boolean
invoiceText: string | null
momsRuta: number // Which ruta in momsdeklaration
}
function determineVatTreatment(customer: Customer, settings: CompanySettings): VatDecision {
// User not VAT registered - no VAT on any invoice
if (!settings.vat_registered) {
return {
rate: 0,
reverseCharge: false,
invoiceText: 'Säljaren är inte momsregistrerad',
momsRuta: 0 // Not applicable
}
}
switch (customer.customer_type) {
case 'swedish_business':
case 'individual':
return {
rate: 25,
reverseCharge: false,
invoiceText: null,
momsRuta: 5 // Ruta 05: Momspliktig försäljning
}
case 'eu_business':
if (customer.vat_number_validated) {
return {
rate: 0,
reverseCharge: true,
invoiceText: 'Omvänd skattskyldighet / Reverse charge - Article 196 Council Directive 2006/112/EC',
momsRuta: 39 // Ruta 39: Tjänsteförsäljning EU
}
}
// EU business without valid VAT = charge Swedish VAT
return {
rate: 25,
reverseCharge: false,
invoiceText: null,
momsRuta: 5
}
case 'non_eu_business':
return {
rate: 0,
reverseCharge: false,
invoiceText: 'Export av tjänst - moms utgår ej',
momsRuta: 40 // Ruta 40: Export
}
}
}
```
**Momsdeklaration Ruta Reference:**
| Ruta | Description | When Used |
|------|-------------|-----------|
| 05 | Momspliktig försäljning | Swedish domestic sales |
| 39 | Tjänsteförsäljning EU | EU B2B with reverse charge |
| 40 | Export | Non-EU sales |
| 21 | Inköp av tjänster från EU | Foreign service purchases (input) |
| 48 | Utgående moms på inköp | Fiktiv moms on foreign purchases |
**EU Sales Reporting:**
Transactions with `momsRuta: 39` must be aggregated into Periodisk Sammanställning (quarterly report to Skatteverket), separate from regular momsdeklaration.
**Response 201:** Created invoice with calculated totals
#### GET /api/invoices/:id
Get full invoice details including items.
#### PUT /api/invoices/:id
Update draft invoice. Cannot modify sent/paid invoices.
#### POST /api/invoices/:id/send
Mark invoice as sent and optionally email to customer.
**Request:**
```json
{
"send_email": true,
"email_to": "invoice@client.com",
"email_message": "Hej! Här kommer fakturan för vårt samarbete."
}
```
**Email Implementation:** Uses Supabase built-in email via Edge Functions.
**Response 200:**
```json
{
"id": "uuid",
"status": "sent",
"sent_at": "2024-01-15T10:30:00Z",
"email_sent": true
}
```
**Note:** Invoice PDF attached to email. Payment method shown: bank transfer only (Bankgiro/IBAN). Future: third-party payment links (Stripe, Klarna).
#### POST /api/invoices/:id/mark-paid
Mark invoice as paid.
**Request:**
```json
{
"paid_at": "2024-02-10",
"paid_amount": 25000.00,
"linked_transaction_id": "uuid"
}
```
#### GET /api/invoices/:id/pdf
Generate and return invoice PDF.
**Query params:**
- `regenerate`: force regenerate cached PDF
**Response:** PDF file or redirect to signed storage URL
---
### Salary Payments (Aktiebolag only)
#### GET /api/salary
List salary payments for current fiscal year.
**Response 200:**
```json
{
"data": [
{
"id": "uuid",
"payment_date": "2024-01-25",
"pay_period_start": "2024-01-01",
"pay_period_end": "2024-01-31",
"gross_salary": 50000.00,
"employer_contributions": 15710.00,
"withheld_tax": 15000.00,
"net_salary": 35000.00,
"total_cost": 65710.00,
"agi_reported": false
}
],
"ytd_totals": {
"gross_salary": 50000.00,
"employer_contributions": 15710.00,
"withheld_tax": 15000.00,
"total_cost": 65710.00
}
}
```
#### POST /api/salary
Create salary payment record.
**Request:**
```json
{
"pay_period_start": "2024-01-01",
"pay_period_end": "2024-01-31",
"gross_salary": 50000.00,
"tax_table": 33
}
```
**Automatic calculations:**
- `employer_contributions`: gross × 31.42%
- `withheld_tax`: from Swedish tax tables based on gross + table number
- `net_salary`: gross - withheld_tax
- `total_cost`: gross + employer_contributions
**Response 201:** Created salary payment with all calculated fields.
#### POST /api/salary/:id/mark-agi-reported
Mark salary payment as reported in AGI-deklaration.
**Response 200:**
```json
{
"id": "uuid",
"agi_reported": true,
"agi_reported_at": "2024-02-12T14:00:00Z"
}
```
---
### Dashboard
#### GET /api/dashboard/summary
Get aggregated financial overview.
**Response 200:**
```json
{
"period": {
"year": 2024,
"month": 1
},
"revenue": {
"ytd": 450000.00,
"this_month": 75000.00
},
"expenses": {
"ytd": 85000.00,
"this_month": 12000.00
},
"net_income": {
"ytd": 365000.00
},
"tax_estimate": {
"egenavgifter": 105745.00,
"income_tax": 116800.00,
"vat_payable": 91250.00,
"total_locked": 313795.00
},
"disponibelt": 136205.00,
"vat_threshold": {
"limit": 80000,
"current": 450000.00,
"registered": true
},
"pending": {
"uncategorized_transactions": 23,
"unpaid_invoices": 2,
"unpaid_amount": 45000.00
}
}
```
#### GET /api/dashboard/chart
Monthly revenue/expense data for charts.
**Query params:**
- `months`: number of months (default 12)
**Response 200:**
```json
{
"data": [
{
"month": "2024-01",
"revenue": 75000,
"expenses": 12000,
"net": 63000
}
]
}
```
---
## Error Responses
All endpoints return errors in consistent format:
```json
{
"error": {
"code": "VALIDATION_ERROR",
"message": "Invalid VAT number format",
"details": {
"field": "vat_number",
"value": "invalid"
}
}
}
```
**Common status codes:**
- 400: Validation error
- 401: Not authenticated
- 403: Forbidden (RLS violation)
- 404: Resource not found
- 409: Conflict (duplicate invoice number)
- 500: Internal error
-728
View File
@@ -1,728 +0,0 @@
# UI/UX Specification
## Design Principles
1. **Calm over busy**: Financial apps induce anxiety. Use whitespace, muted colors, clear hierarchy.
2. **One action per screen**: Each view has one primary action.
3. **Instant feedback**: Every action shows immediate result.
4. **Mobile-first**: Influencers manage finances on phone.
Color System
CSS
:root {
/* Primary - Deep Forest & Sage (Trustworthy but aesthetic) */
--primary-50: #f4fbf7;
--primary-500: #4d8b73;
--primary-600: #3a6b58;
--primary-700: #2c4f42;
/* Success - Muted Mint (Profit/Growth) */
--success-500: #10b981;
/* Warning - Burnt Amber (Pending/Alerts) */
--warning-500: #d97706;
/* Danger - Soft Brick (Expenses/Loss) */
--danger-500: #e11d48;
/* Neutral - "Stone" (Warmer than standard gray, feels like paper) */
--gray-50: #fafaf9; /* Background base */
--gray-100: #f5f5f4; /* Card background */
--gray-200: #e7e5e4; /* Borders */
--gray-500: #78716c; /* Secondary text */
--gray-900: #1c1917; /* Primary text (Soft Black) */
}
Typography
Headings: Plus Jakarta Sans or Outfit (600/700 weight) — More editorial feel
Body: Inter (400/500 weight) — Clean readability
Numbers/Money: JetBrains Mono or Inter (Tabular figures enabled)
## Page Layouts
### Onboarding Wizard (/onboarding)
Full-screen wizard. User cannot access app until complete. Progress saved between sessions.
```
┌─────────────────────────────────────────────────────────────┐
│ │
│ Logo Steg 1 av 6 │
│ │
│ ━━━━━━━━━━○○○○○○ │
│ │
├─────────────────────────────────────────────────────────────┤
│ │
│ VÄLKOMMEN! LÅT OSS KOMMA IGÅNG │
│ │
│ Vilken bolagsform har du? │
│ │
│ ┌─────────────────────────────────────┐ │
│ │ │ │
│ │ ○ Enskild firma │ │
│ │ Enskild näringsidkare │ │
│ │ │ │
│ └─────────────────────────────────────┘ │
│ │
│ ┌─────────────────────────────────────┐ │
│ │ │ │
│ │ ○ Aktiebolag (AB) │ │
│ │ Eget bolag med org.nummer │ │
│ │ │ │
│ └─────────────────────────────────────┘ │
│ │
│ │
│ │
│ ┌─────────────────────────────────────┐ │
│ │ Fortsätt → │ │
│ └─────────────────────────────────────┘ │
│ │
└─────────────────────────────────────────────────────────────┘
```
**Wizard Steps:**
1. **Entity Type** - Enskild firma or Aktiebolag
2. **Company Details** - Name, org.nr (required for AB), address
3. **Tax Registration** - F-skatt status, VAT registered?, momsperiod
4. **Preliminary Tax** - Monthly F-skatt amount (for comparison warnings)
5. **Bank Details** - Account info for invoice payments (IBAN/BIC or Swedish format)
6. **Connect Bank** - Enable Banking integration (at least one account required)
```
STEP 2: FÖRETAGSUPPGIFTER
┌─────────────────────────────────────────────────────────────┐
│ │
│ Företagsnamn * │
│ ┌─────────────────────────────────────────────────────┐ │
│ │ Anna Andersson Content │ │
│ └─────────────────────────────────────────────────────┘ │
│ │
│ Organisationsnummer * (för AB) │
│ ┌─────────────────────────────────────────────────────┐ │
│ │ 559123-4567 │ │
│ └─────────────────────────────────────────────────────┘ │
│ │
│ Adress * │
│ ┌─────────────────────────────────────────────────────┐ │
│ │ Storgatan 1 │ │
│ └─────────────────────────────────────────────────────┘ │
│ │
│ Postnummer * Ort * │
│ ┌──────────────┐ ┌────────────────────────────┐ │
│ │ 111 22 │ │ Stockholm │ │
│ └──────────────┘ └────────────────────────────┘ │
│ │
│ │
│ ┌──────────────┐ ┌─────────────────────────────────┐ │
│ │ ← Tillbaka │ │ Fortsätt → │ │
│ └──────────────┘ └─────────────────────────────────┘ │
│ │
└─────────────────────────────────────────────────────────────┘
```
```
STEP 3: SKATTEREGISTRERING
┌─────────────────────────────────────────────────────────────┐
│ │
│ ☑ Jag har F-skattsedel │
│ │
│ ───────────────────────────────────────────────────────── │
│ │
│ Räkenskapsår * │
│ ○ Kalenderår (jan-dec) │
│ ○ Brutet räkenskapsår │
│ │
│ [Visas om brutet:] │
│ Räkenskapsåret börjar * │
│ ┌─────────────────────────────────────────────────────┐ │
│ │ Juli (1 jul - 30 jun) ▼ │ │
│ └─────────────────────────────────────────────────────┘ │
│ │
│ ───────────────────────────────────────────────────────── │
│ │
│ Är du momsregistrerad? * │
│ │
│ ○ Ja, jag är momsregistrerad │
│ ○ Nej, min omsättning är under 80 000 kr/år │
│ │
│ [Visas om momsregistrerad:] │
│ │
│ Momsnummer │
│ ┌─────────────────────────────────────────────────────┐ │
│ │ SE559123456701 │ │
│ └─────────────────────────────────────────────────────┘ │
│ │
│ Momsperiod * │
│ ○ Kvartalsvis (vanligast) │
│ ○ Månadsvis │
│ │
└─────────────────────────────────────────────────────────────┘
```
```
STEP 4: PRELIMINÄRSKATT
┌─────────────────────────────────────────────────────────────┐
│ │
│ Hur mycket betalar du i F-skatt per månad? │
│ │
│ ┌─────────────────────────────────────────────────────┐ │
│ │ 15 000 kr │ │
│ └─────────────────────────────────────────────────────┘ │
│ │
│ ⓘ Vi använder detta för att varna dig om din beräknade │
│ skatt skiljer sig mycket från det du betalar in. │
│ │
│ Hittar du det på: │
│ • Ditt senaste F-skattbeslut från Skatteverket │
│ • Skattekontot på skatteverket.se │
│ │
│ ┌─────────────────────────────────────────────────────┐ │
│ │ Jag vet inte just nu - hoppa över │ │
│ └─────────────────────────────────────────────────────┘ │
│ │
└─────────────────────────────────────────────────────────────┘
```
```
STEP 6: KOPPLA BANK
┌─────────────────────────────────────────────────────────────┐
│ │
│ Koppla ditt företagskonto │
│ │
│ Vi hämtar dina transaktioner automatiskt så du slipper │
│ mata in dem manuellt. │
│ │
│ 🔒 Säkert via BankID. Vi kan aldrig flytta dina pengar. │
│ │
│ ┌─────────────────────────────────────┐ │
│ │ 🏦 Koppla bankkonto │ │
│ └─────────────────────────────────────┘ │
│ │
│ Populära banker: │
│ ┌─────────┐ ┌─────────┐ ┌─────────┐ ┌─────────┐ │
│ │ Nordea │ │ SEB │ │Swedbank │ │ Handels │ │
│ └─────────┘ └─────────┘ └─────────┘ └─────────┘ │
│ │
│ ⓘ Du kan koppla fler konton senare i inställningar. │
│ │
└─────────────────────────────────────────────────────────────┘
```
**Validation Rules:**
- All fields marked * are required
- Org.nr validated against format (NNNNNN-NNNN)
- At least one bank account must be connected
- Cannot proceed without completing current step
---
### Dashboard (/)
```
┌─────────────────────────────────────────────────────────────┐
│ Logo [Settings] [Avatar]│
├─────────────────────────────────────────────────────────────┤
│ │
│ ┌─────────────────────────────────────────────────────┐ │
│ │ │ │
│ │ DISPONIBELT ATT SPENDERA │ │
│ │ │ │
│ │ 136 205 kr │ │
│ │ ─────────────────── │ │
│ │ │ │
│ │ Total: 450 000 kr │ │
│ │ ┌────────────────────────────┬──────────────┐ │ │
│ │ │██████████████████████████ │░░░░░░░░░░░░░░│ │ │
│ │ │ Disponibelt 30% │ Låst 70% │ │ │
│ │ └────────────────────────────┴──────────────┘ │ │
│ │ │ │
│ │ [Visa skatteberäkning ↓] │ │
│ │ │ │
│ └─────────────────────────────────────────────────────┘ │
│ │
│ ┌─────────────────────┐ ┌─────────────────────┐ │
│ │ ⚠️ 23 transaktioner │ │ 📄 2 obetalda │ │
│ │ att sortera │ │ fakturor │ │
│ │ │ │ 45 000 kr │ │
│ │ [Sortera nu →] │ │ [Visa →] │ │
│ └─────────────────────┘ └─────────────────────┘ │
│ │
│ INTÄKTER 2024 ┌────────────────────────┐ │
│ ────────────── │ ____│ │
│ 450 000 kr │ ____/ │ │
│ │ ____/ │ │
│ │ ____/ │ │
│ │____/ │ │
│ └────────────────────────┘ │
│ Jan Feb Mar Apr May Jun │
│ │
└─────────────────────────────────────────────────────────────┘
│ [Dashboard] [Transaktioner] [Fakturor] [+] │
└─────────────────────────────────────────────────────────────┘
```
**Components:**
1. **BalanceCard**: Hero component showing available balance
- Large number, animated on load
- Expandable tax breakdown panel
- Color-coded bar chart (green=available, gray=locked)
2. **AlertCard**: Action items requiring attention
- Badge with count
- Clear CTA button
- Subtle warning color for urgency
3. **RevenueChart**: Simple line/area chart
- Recharts or Chart.js
- Hover shows month details
- No unnecessary decoration
---
### Tax Breakdown (Expanded)
```
┌─────────────────────────────────────────────────────────────┐
│ │
│ SKATTEBERÄKNING 2024 [Så räknar vi] │
│ ───────────────────────────────────────────────────────── │
│ │
│ Intäkter 450 000 kr │
│ - Avdragsgilla kostnader -85 000 kr │
│ ───────────────────────────────────────────────────────── │
│ = Resultat 365 000 kr │
│ │
│ Egenavgifter (28,97%) -105 745 kr ░░░░ │
│ Inkomstskatt (~32%) -116 800 kr ░░░░░ │
│ Moms att betala -91 250 kr ░░░░ │
│ ───────────────────────────────────────────────────────── │
│ = Låst för skatt 313 795 kr │
│ │
│ ⓘ Detta är en uppskattning. Faktisk skatt kan variera. │
│ │
└─────────────────────────────────────────────────────────────┘
```
---
### Transactions - Swipe View (/transactions)
```
┌─────────────────────────────────────────────────────────────┐
│ ← Tillbaka Sortera transaktioner │
├─────────────────────────────────────────────────────────────┤
│ │
│ 23 kvar att sortera │
│ │
│ ← PRIVAT FÖRETAG → │
│ │
│ ┌─────────────────────────┐ │
│ │ │ │
│ │ ADOBE SYSTEMS │ │
│ │ -1 500,00 kr │ │
│ │ │ │
│ │ 15 jan 2024 │ │
│ │ │ │
│ │ ┌─────────────────┐ │ │
│ │ │ Företag │ │ │
│ │ │ [▼ Välj typ] │ │ │
│ │ └─────────────────┘ │ │
│ │ │ │
│ └─────────────────────────┘ │
│ │
│ [Osäker?] │
│ │
│ ──────────────────────────────────────────────────────────│
│ │
│ Redan sorterade [Visa alla] │
│ │
│ ● Adobe Systems -1 500 kr Programvara ✓ │
│ ● Spotify AB -149 kr Privat ✓ │
│ ● Influencer AB +25 000 kr Inkomst ✓ │
│ │
└─────────────────────────────────────────────────────────────┘
```
**Interaction:**
- Swipe right = Business (show expense type selector)
- Swipe left = Private
- Tap "Osäker?" = Mark for later / add note
- Cards animate smoothly with spring physics
**Components:**
1. **SwipeCard**: Draggable card with gesture handling
- Use `@use-gesture/react` + `framer-motion`
- Threshold: 100px triggers categorization
- Background color hints direction
2. **ExpenseTypeSelector**: Dropdown/bottom sheet
- Only shown for business expenses
- Quick-select common types
---
### Transactions - List View (/transactions?view=list)
```
┌─────────────────────────────────────────────────────────────┐
│ Transaktioner [Filter] [+ Manual] │
├─────────────────────────────────────────────────────────────┤
│ │
│ ┌─────────────────────────────────────────────────────┐ │
│ │ 🔍 Sök transaktioner... │ │
│ └─────────────────────────────────────────────────────┘ │
│ │
│ [Alla] [Osorterade (23)] [Företag] [Privat] │
│ │
│ JANUARI 2024 │
│ ───────────────────────────────────────────────────────── │
│ │
│ 15 jan ADOBE SYSTEMS -1 500,00 kr │
│ Software subscription [Programvara] → │
│ │
│ 14 jan Influencer Agency AB +25 000,00 kr │
│ Faktura #INV-2024-012 [Inkomst] → │
│ │
│ 12 jan ICA MAXI -847,00 kr │
│ Osorterad [Sortera] → │
│ │
└─────────────────────────────────────────────────────────────┘
```
---
### Create Invoice (/invoices/new)
```
┌─────────────────────────────────────────────────────────────┐
│ ← Avbryt Ny faktura [Förhandsv]│
├─────────────────────────────────────────────────────────────┤
│ │
│ KUND │
│ ┌─────────────────────────────────────────────────────┐ │
│ │ 🔍 Sök eller skapa ny kund... │ │
│ └─────────────────────────────────────────────────────┘ │
│ │
│ ┌──────────────────┐ ┌──────────────────┐ │
│ │ Google Ireland │ │ Influencer AB │ │
│ │ EU-företag │ │ Svenskt företag │ │
│ └──────────────────┘ └──────────────────┘ │
│ │
│ ───────────────────────────────────────────────────────── │
│ │
│ FAKTURADETALJER │
│ │
│ Fakturadatum Förfallodatum │
│ ┌────────────┐ ┌────────────┐ │
│ │ 2024-01-15 │ │ 2024-02-14 │ (30 dagar) │
│ └────────────┘ └────────────┘ │
│ │
│ Er referens │
│ ┌─────────────────────────────────────────────────────┐ │
│ │ PO-12345 │ │
│ └─────────────────────────────────────────────────────┘ │
│ │
│ ───────────────────────────────────────────────────────── │
│ │
│ RADER │
│ │
│ Beskrivning Antal À-pris │
│ ┌────────────────────────────────┐ ┌────┐ ┌─────────┐ │
│ │ Instagram kampanj November │ │ 1 │ │ 20 000 │ │
│ └────────────────────────────────┘ └────┘ └─────────┘ │
│ = 20 000 kr │
│ │
│ [+ Lägg till rad] │
│ │
│ ───────────────────────────────────────────────────────── │
│ │
│ Summa exkl. moms 20 000,00 kr │
│ Moms 25% 5 000,00 kr │
│ ───────────────────────────────────────────────────────── │
│ ATT BETALA 25 000,00 kr │
│ │
│ ┌─────────────────────────────────────────────────────┐ │
│ │ [Spara utkast] [Skapa & skicka] │ │
│ └─────────────────────────────────────────────────────┘ │
│ │
└─────────────────────────────────────────────────────────────┘
```
**VAT Logic Display:**
For EU customer with valid VAT:
```
│ Summa 20 000,00 kr │
│ Moms 0,00 kr │
│ ───────────────────────────────────────────────────────── │
│ ATT BETALA 20 000,00 kr │
│ │
│ ⓘ Omvänd skattskyldighet tillämpas (EU B2B) │
```
**Components:**
1. **CustomerSelect**: Searchable dropdown with recent customers
- Shows customer type badge
- "Create new" option at bottom
2. **InvoiceLineItem**: Repeatable row
- Auto-calculate line total
- Drag to reorder
3. **VatSummary**: Auto-updating totals
- Shows applicable VAT rule
- Explanation tooltip
---
### Invoice Preview (Modal/Slide-over)
```
┌─────────────────────────────────────────────────────────────┐
│ Förhandsgranskning [✕ Stäng] │
├─────────────────────────────────────────────────────────────┤
│ ┌─────────────────────────────────────────────────────┐ │
│ │ │ │
│ │ ANNA ANDERSSON CONTENT │ │
│ │ Storgatan 1, 111 22 Stockholm │ │
│ │ F-skatt: Ja | Moms: SE123456789001 │ │
│ │ │ │
│ │ ───────────────────────────────────────────────── │ │
│ │ │ │
│ │ FAKTURA │ │
│ │ Fakturanummer: INV-2024-015 │ │
│ │ Fakturadatum: 2024-01-15 │ │
│ │ Förfallodatum: 2024-02-14 │ │
│ │ │ │
│ │ Till: │ │
│ │ Google Ireland Limited │ │
│ │ Gordon House, Barrow Street │ │
│ │ Dublin 4, Ireland │ │
│ │ VAT: IE6388047V │ │
│ │ │ │
│ │ ───────────────────────────────────────────────── │ │
│ │ │ │
│ │ Beskrivning Antal Pris Summa │ │
│ │ ───────────────────────────────────────────────── │ │
│ │ YouTube AdSense Jan 2024 1 st €1,850 €1,850 │ │
│ │ │ │
│ │ ───────────────────────────────────────────────── │ │
│ │ Summa exkl. moms €1,850 │ │
│ │ Moms (0%) €0 │ │
│ │ ───────────────────────────────────────────────── │ │
│ │ ATT BETALA €1,850 │ │
│ │ │ │
│ │ Reverse charge - Article 196 Council Directive │ │
│ │ 2006/112/EC │ │
│ │ │ │
│ │ ───────────────────────────────────────────────── │ │
│ │ Betalning till: IBAN SE12 3456 7890 1234 5678 90 │ │
│ │ BIC: SWEDSESS │ │
│ │ │ │
│ └─────────────────────────────────────────────────────┘ │
│ │
│ ┌─────────────────────────────────────────────────────┐ │
│ │ [Ladda ner PDF] [Skicka via email] │ │
│ └─────────────────────────────────────────────────────┘ │
│ │
└─────────────────────────────────────────────────────────────┘
```
---
### Salary Management (/salary) - Aktiebolag only
Only visible for users with entity_type = 'aktiebolag'.
```
┌─────────────────────────────────────────────────────────────┐
│ ← Tillbaka Lön & Arbetsgivaravgifter│
├─────────────────────────────────────────────────────────────┤
│ │
│ 2024 [+ Registrera lön]│
│ │
│ ┌─────────────────────────────────────────────────────┐ │
│ │ HITTILLS I ÅR │ │
│ │ │ │
│ │ Bruttolön 150 000 kr │ │
│ │ Arbetsgivaravgifter 47 130 kr │ │
│ │ ───────────────────────────────── │ │
│ │ Total lönekostnad 197 130 kr │ │
│ │ │ │
│ │ Innehållen skatt 45 000 kr ⚠️ Att betala in │ │
│ └─────────────────────────────────────────────────────┘ │
│ │
│ LÖNEUTBETALNINGAR │
│ ───────────────────────────────────────────────────────── │
│ │
│ Mars 2024 │
│ ┌─────────────────────────────────────────────────────┐ │
│ │ Brutto 50 000 kr Arb.avg 15 710 kr │ │
│ │ Skatt -15 000 kr Totalt 65 710 kr │ │
│ │ ───────────────── │ │
│ │ Netto 35 000 kr AGI: ✓ Rapporterad │ │
│ └─────────────────────────────────────────────────────┘ │
│ │
│ Februari 2024 │
│ ┌─────────────────────────────────────────────────────┐ │
│ │ Brutto 50 000 kr Arb.avg 15 710 kr │ │
│ │ Skatt -15 000 kr Totalt 65 710 kr │ │
│ │ ───────────────── │ │
│ │ Netto 35 000 kr AGI: ⚠️ Ej rapporterad │ │
│ └─────────────────────────────────────────────────────┘ │
│ │
└─────────────────────────────────────────────────────────────┘
```
**Register Salary Modal:**
```
┌─────────────────────────────────────────────────────────────┐
│ Registrera löneutbetalning [✕] │
├─────────────────────────────────────────────────────────────┤
│ │
│ Löneperiod * │
│ ┌──────────────┐ till ┌──────────────┐ │
│ │ 2024-03-01 │ │ 2024-03-31 │ │
│ └──────────────┘ └──────────────┘ │
│ │
│ Bruttolön * │
│ ┌─────────────────────────────────────────────────────┐ │
│ │ 50 000 kr │ │
│ └─────────────────────────────────────────────────────┘ │
│ │
│ Skattetabell * │
│ ┌─────────────────────────────────────────────────────┐ │
│ │ Tabell 33 - Stockholm ▼ │ │
│ └─────────────────────────────────────────────────────┘ │
│ ⓘ Hittas på ditt skattsedelsbesked │
│ │
│ ───────────────────────────────────────────────────────── │
│ │
│ BERÄKNAT │
│ Arbetsgivaravgifter (31,42%) 15 710 kr │
│ Preliminärskatt (enligt tabell) 15 000 kr │
│ ───────────────────────────────────────────────────────── │
│ Nettolön till dig 35 000 kr │
│ Total kostnad för bolaget 65 710 kr │
│ │
│ ┌─────────────────────────────────────────────────────┐ │
│ │ Registrera löneutbetalning │ │
│ └─────────────────────────────────────────────────────┘ │
│ │
└─────────────────────────────────────────────────────────────┘
```
---
### Settings (/settings)
```
┌─────────────────────────────────────────────────────────────┐
│ ← Tillbaka Inställningar │
├─────────────────────────────────────────────────────────────┤
│ │
│ FÖRETAGSINFORMATION │
│ │
│ Företagsnamn │
│ ┌─────────────────────────────────────────────────────┐ │
│ │ Anna Andersson Content │ │
│ └─────────────────────────────────────────────────────┘ │
│ │
│ ☑ Jag har F-skattsedel │
│ ☑ Jag är momsregistrerad │
│ │
│ Momsnummer │
│ ┌─────────────────────────────────────────────────────┐ │
│ │ SE199001011234 │ │
│ └─────────────────────────────────────────────────────┘ │
│ │
│ ───────────────────────────────────────────────────────── │
│ │
│ SKATTEBERÄKNING │
│ │
│ Kommunalskattesats │
│ ┌─────────────────────────────────────────────────────┐ │
│ │ 31.5 % [Hitta min →] │ │
│ └─────────────────────────────────────────────────────┘ │
│ ⓘ Används för att beräkna ungefärlig inkomstskatt │
│ │
│ ───────────────────────────────────────────────────────── │
│ │
│ FAKTURAINSTÄLLNINGAR │
│ │
│ Betalningsvillkor (dagar) │
│ ┌────────┐ │
│ │ 30 │ │
│ └────────┘ │
│ │
│ ───────────────────────────────────────────────────────── │
│ │
│ BANKUPPGIFTER (visas på fakturor) │
│ │
│ Bank Clearingnummer Kontonummer │
│ ┌──────────┐ ┌──────────┐ ┌──────────────────┐ │
│ │ Nordea │ │ 3300 │ │ 123 456 789 │ │
│ └──────────┘ └──────────┘ └──────────────────┘ │
│ │
│ ───────────────────────────────────────────────────────── │
│ │
│ KOPPLAD BANK │
│ │
│ ✓ Nordea ****4567 [Koppla bort] │
│ Senast synkad: idag 08:15 │
│ │
│ [Spara ändringar] │
│ │
└─────────────────────────────────────────────────────────────┘
```
---
## Component Library
Use shadcn/ui as base. Key components needed:
### Core
- Button (primary, secondary, ghost, destructive)
- Input (text, number, date)
- Select (with search)
- Checkbox
- Card
- Badge
- Dialog / Sheet (slide-over)
- Toast (notifications)
### Custom
- SwipeCard (gesture-based)
- MoneyDisplay (formatted SEK/EUR with color)
- StatusBadge (invoice/transaction status)
- PercentageBar (disponibelt visualization)
- DatePicker (Swedish locale)
---
## Responsive Behavior
| Breakpoint | Layout |
|------------|--------|
| < 640px (mobile) | Single column, bottom nav, swipe UI default |
| 640-1024px (tablet) | Two columns where appropriate |
| > 1024px (desktop) | Sidebar nav, multi-column dashboard |
Mobile is primary. All features must work on 375px width.
---
## Accessibility
- All interactive elements keyboard accessible
- ARIA labels for icons
- Color contrast AA minimum
- Focus indicators visible
- Screen reader tested
File diff suppressed because it is too large Load Diff
-519
View File
@@ -1,519 +0,0 @@
# Future Features & Regulatory Integrations
## Phase 2: SIE File Import (Migration)
### Overview
SIE (Standard Import Export) is the Swedish standard for accounting data exchange. Users migrating from Fortnox, Visma, or other systems can export their data as SIE files.
### SIE File Types
| Type | Content | Use Case |
|------|---------|----------|
| SIE1 | Årssaldon | Year-end balances only |
| SIE2 | Periodsaldon | Monthly/periodic balances |
| SIE3 | Objektsaldon | Balances with cost centers |
| SIE4 | Transaktioner | Full transaction history |
**Recommended:** Support SIE4 for complete migration, SIE1/2 for simpler imports.
### Implementation Notes
```typescript
// lib/import/sie-parser.ts
interface SIETransaction {
verifikationsnummer: string
datum: Date
kontonummer: string
belopp: number
text: string
}
interface SIEImportResult {
company: {
name: string
orgNumber: string
fiscalYearStart: Date
}
accounts: Array<{ number: string; name: string }>
transactions: SIETransaction[]
balances: Array<{ account: string; balance: number }>
}
// SIE files use CP437 encoding and specific format
// Example line: #VER A 1 20240115 "Faktura 1001"
```
### Mapping Decisions Needed
Before implementing, determine mapping for:
- BAS account numbers → system categories
- Opening balances → how to represent
- Customer/supplier data (if present in SIE)
- Historical invoices → create as "imported" status?
---
## Phase 2: Benefits & Gifts Module (Förmånshantering)
### Problem
Influencers receive products ("PR-utskick", "gifted collabs") that may be taxable income. Skatteverket actively audits this. Getting it wrong = skattetillägg.
### Decision Tree Logic
```typescript
// lib/benefits/gift-classifier.ts
interface GiftClassification {
taxable: boolean
marketValue: number
deductibleAsExpense: boolean
bookingType: 'income' | 'income_and_expense' | 'tax_free'
reasoning: string
}
interface GiftInput {
estimatedValue: number
hasMotprestation: boolean // Required post/video/mention?
usedInBusiness: boolean // Used as props/equipment?
usedPrivately: boolean // Personal use?
isSimplePromoItem: boolean // Pen, mug, basic merch?
}
const TAX_FREE_PROMO_THRESHOLD = 450 // SEK, Skatteverket guideline
export function classifyGift(input: GiftInput): GiftClassification {
// Rule 1: Simple promotional items under threshold = tax free
if (input.isSimplePromoItem && input.estimatedValue <= TAX_FREE_PROMO_THRESHOLD && !input.hasMotprestation) {
return {
taxable: false,
marketValue: input.estimatedValue,
deductibleAsExpense: false,
bookingType: 'tax_free',
reasoning: 'Enklare reklamgåva under 450 kr utan krav på motprestation'
}
}
// Rule 2: Motprestation exists = always taxable
if (input.hasMotprestation) {
if (input.usedInBusiness && !input.usedPrivately) {
return {
taxable: true,
marketValue: input.estimatedValue,
deductibleAsExpense: true,
bookingType: 'income_and_expense',
reasoning: 'Skattepliktig förmån (motprestation krävdes). Avdragsgill som rekvisita då den endast används i verksamheten.'
}
}
return {
taxable: true,
marketValue: input.estimatedValue,
deductibleAsExpense: false,
bookingType: 'income',
reasoning: 'Skattepliktig förmån (motprestation krävdes). Ej avdragsgill då produkten används privat.'
}
}
// Rule 3: High value without motprestation but used privately
if (input.estimatedValue > TAX_FREE_PROMO_THRESHOLD && input.usedPrivately) {
return {
taxable: true,
marketValue: input.estimatedValue,
deductibleAsExpense: false,
bookingType: 'income',
reasoning: 'Värdet överstiger gränsen för skattefria reklamgåvor och produkten används privat.'
}
}
// Default: taxable if significant value
return {
taxable: input.estimatedValue > TAX_FREE_PROMO_THRESHOLD,
marketValue: input.estimatedValue,
deductibleAsExpense: input.usedInBusiness,
bookingType: input.estimatedValue > TAX_FREE_PROMO_THRESHOLD ? 'income' : 'tax_free',
reasoning: 'Klassificering baserad på värde och användning'
}
}
```
### UI Flow
1. User taps "Logga gåva/produkt"
2. Photo capture or manual entry
3. System queries price APIs (Prisjakt, Google Shopping) for value estimate
4. Decision tree questions:
- "Fanns krav på att du skulle posta om denna produkt?" [Ja/Nej]
- "Använder du produkten i din content-produktion?" [Ja/Nej]
- "Använder du produkten privat?" [Ja/Nej]
5. System shows classification + tax impact
6. Auto-booking to correct accounts
---
## Phase 2: NE-bilaga Generation
### SRU File Format
Skatteverket accepts SRU (Standardiserat RäkenskapsUtdrag) files for tax form import.
```typescript
// lib/tax/sru-generator.ts
interface NEBilaga {
// R1-series: Income
R1_nettoomsattning: number // Net revenue
R2_ovriga_intakter: number // Other income
// R3-series: Costs
R3_varor: number // Cost of goods
R4_ovriga_kostnader: number // Other expenses
R5_personal: number // Personnel costs (usually 0 for sole proprietor)
R6_avskrivningar: number // Depreciation
// R7-series: Financial
R7_ranteintakter: number
R8_rantekostnader: number
// Result
R9_resultat: number // Profit/loss before adjustments
// Adjustments
R10_periodiseringsfond_avsatt: number // Max 30% of profit
R11_periodiseringsfond_aterforing: number
R12_expansionsfond: number
// Final
R14_overskott_underskott: number
}
function generateSRUFile(data: NEBilaga, personnummer: string, year: number): string {
const lines: string[] = []
// Header
lines.push('#DATABESKRIVNING_START')
lines.push('#PRODUKT SIE')
lines.push('#FORMAT PC8')
lines.push('#FILTYP NARINGSBILAGA')
lines.push(`#UPPGIFTSLAMNARE ${personnummer}`)
lines.push(`#INKOMSTAR ${year}`)
lines.push('#DATABESKRIVNING_SLUT')
// Data rows (simplified)
lines.push(`#UPPGIFT 7001 ${data.R1_nettoomsattning}`) // R1
lines.push(`#UPPGIFT 7002 ${data.R2_ovriga_intakter}`) // R2
// ... continue for all fields
lines.push('#UPPGIFT_SLUT')
return lines.join('\n')
}
```
### BAS Account to NE Mapping
| BAS Account | Description | NE Ruta |
|-------------|-------------|---------|
| 3000-3999 | Intäkter | R1 |
| 3900-3999 | Övriga rörelseintäkter | R2 |
| 4000-4999 | Varuinköp | R3 |
| 5000-6999 | Övriga externa kostnader | R4 |
| 7000-7699 | Personalkostnader | R5 |
| 7800-7899 | Avskrivningar | R6 |
| 8300-8399 | Ränteintäkter | R7 |
| 8400-8499 | Räntekostnader | R8 |
### Tax Optimization Suggestions
Before generating NE-bilaga, system should suggest:
1. **Periodiseringsfond** (max 30% of profit)
- Defer tax to future years
- Must be reversed within 6 years
- Good for income smoothing
2. **Expansionsfond**
- Tax profit at 22% corporate rate instead of marginal personal rate
- Good if marginal rate > 22%
- More complex rules
```typescript
function suggestTaxOptimization(profit: number, marginalTaxRate: number) {
const suggestions = []
if (profit > 0) {
const maxPeriodisering = profit * 0.30
suggestions.push({
type: 'periodiseringsfond',
amount: maxPeriodisering,
taxSaved: maxPeriodisering * marginalTaxRate,
description: `Sätt av ${formatSEK(maxPeriodisering)} till periodiseringsfond för att skjuta upp ${formatSEK(maxPeriodisering * marginalTaxRate)} i skatt.`
})
}
if (profit > 100000 && marginalTaxRate > 0.30) {
suggestions.push({
type: 'expansionsfond',
description: 'Din marginalskatt är hög. Överväg expansionsfond för att beskattas med 22% istället.'
})
}
return suggestions
}
```
---
## Phase 2b: AB Salary Optimization (3:12-reglerna)
### Overview
For aktiebolag owners, the split between lön (salary) and utdelning (dividend) significantly impacts total tax. The 3:12 rules (Inkomstskattelagen kapitel 57) determine how much dividend can be taxed at the favorable 20% rate vs marginal income tax rates.
### Key Concepts
| Term | Description |
|------|-------------|
| Gränsbelopp | Annual limit for dividend taxed at 20% |
| Löneunderlag | Salary base that increases gränsbelopp |
| Kvalificerade andelar | Shares where owner is "active" in company |
| Sparat utdelningsutrymme | Unused gränsbelopp carried forward |
### Gränsbelopp Calculation (Simplified)
```typescript
// lib/tax/ab-optimization.ts
interface GransbeloppCalculation {
schablonbelopp: number // 2.75 × inkomstbasbelopp
lonebaserat: number // 50% of löneunderlag (if meets salary requirement)
sparat: number // Carried forward from previous years
totalGransbelopp: number
}
const INKOMSTBASBELOPP_2024 = 74300
function calculateGransbelopp(
ownerSalary: number,
totalCompanySalaries: number,
sparatUtdelningsutrymme: number
): GransbeloppCalculation {
// Schablonbelopp: 2.75 × IBB
const schablonbelopp = 2.75 * INKOMSTBASBELOPP_2024 // ~204k
// Lönebaserat: requires owner salary ≥ 6 IBB (or 9.6% of total + 6 IBB)
const minOwnerSalary = Math.min(
6 * INKOMSTBASBELOPP_2024,
0.096 * totalCompanySalaries + 6 * INKOMSTBASBELOPP_2024
)
const lonebaserat = ownerSalary >= minOwnerSalary
? totalCompanySalaries * 0.50
: 0
return {
schablonbelopp,
lonebaserat,
sparat: sparatUtdelningsutrymme,
totalGransbelopp: schablonbelopp + lonebaserat + sparatUtdelningsutrymme
}
}
```
### Optimization Engine
```typescript
interface OptimizationRecommendation {
recommendedSalary: number
recommendedDividend: number
taxOnSalary: number
taxOnDividend: number
totalTax: number
savingsVsAllSalary: number
explanation: string
}
function optimizeSalaryDividendSplit(
availableProfit: number,
ownerMarginalTaxRate: number,
sparatUtdelningsutrymme: number
): OptimizationRecommendation {
// Consider:
// - Minimum salary for lönebaserat gränsbelopp (6 IBB = ~446k)
// - Arbetsgivaravgifter (31.42%) on salary
// - 20% tax on dividend within gränsbelopp
// - Marginal tax on dividend above gränsbelopp
// - Bolagsskatt (20.6%) already paid on profit
// Return optimal split with explanation
}
```
### UI Concept
```
┌─────────────────────────────────────────────────────────────┐
│ 💡 OPTIMERINGSFÖRSLAG │
│ │
│ Baserat på ditt resultat och skattesituation: │
│ │
│ Rekommenderad lön: 445 800 kr/år │
│ Rekommenderad utdelning: 204 325 kr │
│ │
│ Beräknad total skatt: ~142 000 kr │
│ vs allt som lön: ~185 000 kr │
│ ───────────────────────────────────────────────────────── │
│ Potentiell besparing: ~43 000 kr │
│ │
│ ⚠️ Detta är en uppskattning. Rådgör med revisor. │
└─────────────────────────────────────────────────────────────┘
```
### Implementation Requirements
- Track sparat utdelningsutrymme (historical data)
- Input for owner's other income (affects marginal rate)
- Annual update of inkomstbasbelopp
- Strong disclaimer: Not tax advice
---
## Phase 3: Skatteverket API Integrations
### Available APIs
Skatteverket offers several APIs that can enhance the product:
#### 1. Skattekonto API
Fetch user's tax account balance and transactions.
**Prerequisite:** User must grant "Läsombud" access in Skatteverket's system.
```typescript
// Conceptual - actual implementation requires OAuth2 + organizational agreement
interface SkattekontoBala {
saldo: number
senastInbetalning: Date
kommandeForfall: Array<{
datum: Date
belopp: number
typ: string // 'F-skatt', 'Moms', etc.
}>
}
// Use case: "Du har 15 000 kr på skattekontot, men beräknad skatt är 20 000 kr. Sätt in 5 000 kr nu."
```
#### 2. Momsdeklaration API
Submit VAT declaration directly from app.
**Prerequisites:**
- Registered as e-tjänstleverantör with Skatteverket
- User authentication via BankID
- Signed agreement with Skatteverket
```typescript
interface MomsdeklarationSubmission {
period: string // '2024-01' for January
ruta05: number // Momspliktig försäljning
ruta39: number // Tjänsteförsäljning EU
ruta40: number // Export
ruta21: number // Inköp tjänster EU
ruta48: number // Utgående moms inköp
// ... all required rutor
}
```
#### 3. Arbetsgivardeklaration API
Not typically needed for sole proprietors without employees, but relevant if they hire.
### Integration Roadmap
| Phase | Integration | User Value |
|-------|-------------|------------|
| MVP | None (manual) | - |
| v1.5 | VIES VAT validation | Automated reverse charge |
| v2.0 | Skattekonto (read) | Real-time tax position |
| v2.5 | Momsdeklaration (submit) | One-click VAT filing |
| v3.0 | NE-bilaga (submit) | Full declaration automation |
---
## Phase 2: Reklammärkning Compliance
### Marknadsföringslagen Requirement
Swedish law requires clear ad disclosure. The "Kissie" case established that marking must be:
- At the very beginning of content
- Immediately visible
- Unambiguous ("Reklam" or "Annons", not just "Samarbete")
### Implementation
In Deal-to-Invoice flow, add mandatory checkbox:
```typescript
interface DealComplianceCheck {
reklamMarkerad: boolean
markeringTyp: 'inlagg' | 'video' | 'story' | 'podcast'
bekraftelse: string // Timestamp of user confirmation
}
// Before invoice can be marked as complete:
const complianceText = `Jag intygar att allt publicerat material för detta samarbete har reklammarkerats tydligt i början av varje inlägg/video i enlighet med Marknadsföringslagen (2008:486).`
```
---
## Data Retention & GDPR
### Conflict Resolution
**Bokföringslagen**: 7 years mandatory retention for räkenskapsinformation
**GDPR**: Right to erasure
Resolution:
- Accounting records (verifikationer, fakturor, kvitton): 7-year mandatory retention
- Non-accounting data (support chats, preferences, analytics): Can be deleted on request
- Technical implementation: Separate data stores with different retention policies
```typescript
// Database design consideration
interface UserDataPolicy {
accountingData: {
retention: '7_years_from_fiscal_year_end',
deletable: false,
legal_basis: 'Bokföringslagen 7 kap. 2 §'
},
operationalData: {
retention: 'until_deletion_request',
deletable: true,
legal_basis: 'Consent / Legitimate interest'
}
}
```
### Digital Archive Requirements (July 2024 Law)
Receipt photos must be:
- Stored in immutable format (PDF/A-3, locked JPEG)
- Backed up regularly
- Hosted within EU/EES or adequate country
- Tamper-evident (hash verification)
```typescript
interface ReceiptArchive {
originalFilename: string
storedAs: string // UUID.pdf
format: 'PDF/A-3'
sha256Hash: string
capturedAt: Date
linkedTransactionId: string
storageLocation: 'eu-north-1' // Must be EU
}
```
-903
View File
@@ -1,903 +0,0 @@
# BAS Accounting Guide for Creator Economy
## Overview
This document provides the accounting logic layer for automatic transaction categorization using the Swedish BAS-kontoplan. It covers MCC code mapping, expense validation rules, and VAT handling for influencer-specific transactions.
## BAS Account Structure
The BAS-kontoplan uses a hierarchical 4-digit system where each position carries semantic meaning:
| Position | Name | Example | Meaning |
|----------|------|---------|---------|
| 1 | Kontoklass | **5**410 | Category (5 = External costs) |
| 2 | Kontogrupp | 5**4**10 | Type (54 = Consumables) |
| 3 | Huvudkonto | 54**1**0 | Main account for reporting |
| 4 | Underkonto | 541**0** | Detail level, VAT codes |
This enables validation logic built directly on account number structure.
---
## Account Classes Overview
| Class | Swedish | English | Influencer Relevance |
|-------|---------|---------|---------------------|
| 1xxx | Tillgångar | Assets | Equipment, bank accounts |
| 2xxx | Eget kapital & Skulder | Equity & Liabilities | VAT, owner withdrawals |
| 3xxx | Intäkter | Revenue | Platform income, sponsorships |
| 4xxx | Direkta kostnader | Direct costs | Merchandise COGS (rare) |
| 5xxx | Övriga externa kostnader | Other external costs | 90% of expenses land here |
| 6xxx | Övriga externa kostnader | Other external costs | Services, representation |
| 7xxx | Personalkostnader | Personnel costs | AB salary (see salary module) |
| 8xxx | Finansiella poster | Financial items | Interest, currency gains/losses |
---
## Critical BAS Accounts for Influencers
### Assets (Class 1)
| Account | Name | Use Case | Notes |
|---------|------|----------|-------|
| 1210 | Maskiner och inventarier | Cameras, computers > 29,400 kr | Depreciate over 5 years (20%/year) |
| 1930 | Företagskonto | Main bank account | Primary reconciliation account |
### Equity & Liabilities (Class 2)
| Account | Name | Use Case | Notes |
|---------|------|----------|-------|
| 2010 | Eget kapital | Owner's equity | Rolling balance |
| 2013 | Eget uttag | Private expenses paid with business card | **Critical for rejected deductions** |
| 2018 | Egen insättning | Owner deposits / travel allowance credit | Traktamente booking |
| 2440 | Leverantörsskulder | Accounts payable | If not paying immediately |
| 2611 | Utgående moms 25% | Output VAT 25% | Swedish domestic sales |
| 2614 | Utg. moms utländska förvärv | Output VAT on foreign purchases | Reverse charge (credit side) |
| 2641 | Ingående moms | Input VAT | Deductible VAT on purchases |
| 2645 | Ing. moms utländska förvärv | Input VAT on foreign purchases | Reverse charge (debit side) |
### Revenue (Class 3)
| Account | Name | Use Case | Notes |
|---------|------|----------|-------|
| 3001 | Försäljning 25% | Swedish sponsorships/collaborations | Standard domestic |
| 3044 | Försäljning tjänst EU | AdSense (Ireland), EU affiliates | VAT-free, requires Periodisk sammanställning |
| 3045 | Försäljning tjänst Export | Twitch (USA), non-EU platforms | VAT-free (export) |
| 3900 | Övriga intäkter | Stipends, royalties, misc | |
### Costs (Class 5-6)
| Account | Name | Use Case | Notes |
|---------|------|----------|-------|
| 5010 | Lokalhyra | Studio rental | Not for home office (strict rules) |
| 5410 | Förbrukningsinventarier | Cameras, computers < 29,400 kr | Direct deduction |
| 5420 | Programvaror | Adobe CC, Epidemic Sound, SaaS | Licenses/subscriptions |
| 5480 | Arbetskläder | Protective gear, uniforms | **NOT regular clothes** |
| 5490 | Övriga förbrukningsartiklar | Props, consumable supplies | |
| 5800 | Resekostnader | Train, flights, car rental | VAT varies (6% domestic travel) |
| 5820 | Taxikostnader | Taxi, Uber | |
| 5831 | Kost och logi Sverige | Hotels in Sweden | 12% VAT |
| 5832 | Kost och logi utlandet | Hotels abroad | No VAT recovery |
| 5841 | Traktamente, avdragsgillt | Travel allowance | Calculated per Skatteverket rates |
| 5890 | Övriga resekostnader | Non-deductible travel portion | For mixed-use trips |
| 5910 | Annonsering | Facebook Ads, Google Ads | Often triggers reverse charge |
| 5930 | Reklamtrycksaker | Promotional materials | |
| 6071 | Representation, avdragsgill | Simple refreshments (max 60 kr/person) | Very limited deduction |
| 6072 | Representation, ej avdragsgill | Meals for representation | VAT still deductible |
| 6230 | Datakommunikation | Web hosting, domains, internet | Infrastructure |
| 6500 | Övriga externa tjänster | Photographers, editors (with F-skatt) | Consultant services |
| 6991 | Övriga avdragsgilla kostnader | Miscellaneous deductible | Use sparingly |
| 6992 | Övriga ej avdragsgilla | Fines, penalties | No tax effect |
---
## MCC Code Mapping
Merchant Category Codes (MCC) are the primary signal for automatic categorization from bank transactions.
### Implementation
```typescript
// lib/accounting/mcc-mapping.ts
interface MCCMapping {
mcc: string
description: string
riskLevel: 'low' | 'medium' | 'high' | 'very_high'
primaryAccount: string
secondaryAccount?: string
requiresUserInput: boolean
autoRejectReason?: string
logic: string
}
const MCC_MAPPINGS: MCCMapping[] = [
// ADVERTISING & MARKETING
{
mcc: '7311',
description: 'Advertising Services',
riskLevel: 'low',
primaryAccount: '5910',
secondaryAccount: '5930',
requiresUserInput: false,
logic: 'Standard for Facebook/Google Ads. Triggers EU VAT check.'
},
// ELECTRONICS & EQUIPMENT
{
mcc: '5732',
description: 'Electronics Stores',
riskLevel: 'medium',
primaryAccount: '5410',
secondaryAccount: '1210',
requiresUserInput: false,
logic: 'Amount determines routing. >29,400 kr → 1210 (asset).'
},
{
mcc: '5946',
description: 'Camera & Photo Supply',
riskLevel: 'medium',
primaryAccount: '5410',
secondaryAccount: '1210',
requiresUserInput: false,
logic: 'Same as electronics. Check amount threshold.'
},
// RESTAURANTS & FOOD - HIGH RISK
{
mcc: '5812',
description: 'Eating Places/Restaurants',
riskLevel: 'high',
primaryAccount: '6072',
secondaryAccount: '2013',
requiresUserInput: true,
logic: 'Requires user input: Representation or private? Default to private (2013).'
},
{
mcc: '5814',
description: 'Fast Food',
riskLevel: 'high',
primaryAccount: '2013',
requiresUserInput: false,
autoRejectReason: 'Fast food is typically private expense',
logic: 'Default to eget uttag. Rarely valid business expense.'
},
// CLOTHING - VERY HIGH RISK (Almost always rejected)
{
mcc: '5611',
description: "Men's Clothing",
riskLevel: 'very_high',
primaryAccount: '2013',
secondaryAccount: '5480',
requiresUserInput: true,
autoRejectReason: 'Clothes that can be worn privately are not deductible (RÅ81 1:29)',
logic: 'Default MUST be 2013 (private). Only 5480 if protective/uniform.'
},
{
mcc: '5621',
description: "Women's Ready-to-Wear",
riskLevel: 'very_high',
primaryAccount: '2013',
secondaryAccount: '5480',
requiresUserInput: true,
autoRejectReason: 'Clothes that can be worn privately are not deductible',
logic: 'Default MUST be 2013 (private).'
},
{
mcc: '5631',
description: "Women's Accessories",
riskLevel: 'very_high',
primaryAccount: '2013',
requiresUserInput: false,
autoRejectReason: 'Accessories are private expenses',
logic: 'Always 2013.'
},
// COSMETICS - VERY HIGH RISK
{
mcc: '5977',
description: 'Cosmetic Stores',
riskLevel: 'very_high',
primaryAccount: '2013',
secondaryAccount: '5490',
requiresUserInput: true,
autoRejectReason: 'Cosmetics are private expenses unless specific props',
logic: 'Default private (2013). Only 5490 if clearly production props.'
},
// TRAVEL - MEDIUM RISK
{
mcc: '4111',
description: 'Commuter Transport',
riskLevel: 'low',
primaryAccount: '5800',
requiresUserInput: false,
logic: 'Train/bus. 6% VAT in Sweden.'
},
{
mcc: '4112',
description: 'Passenger Railways',
riskLevel: 'low',
primaryAccount: '5800',
requiresUserInput: false,
logic: 'Train tickets. 6% VAT.'
},
{
mcc: '4121',
description: 'Taxicabs/Limousines',
riskLevel: 'medium',
primaryAccount: '5820',
secondaryAccount: '2013',
requiresUserInput: true,
logic: 'Business trip or private? Ask user.'
},
{
mcc: '4722',
description: 'Travel Agencies',
riskLevel: 'medium',
primaryAccount: '5800',
secondaryAccount: '2013',
requiresUserInput: true,
logic: 'Is the trip business-related? May need to split.'
},
{
mcc: '7011',
description: 'Hotels/Motels',
riskLevel: 'medium',
primaryAccount: '5831',
secondaryAccount: '5832',
requiresUserInput: false,
logic: '5831 for Sweden (12% VAT), 5832 for abroad (no VAT).'
},
// SOFTWARE & DIGITAL SERVICES - LOW RISK
{
mcc: '7372',
description: 'Computer Programming',
riskLevel: 'low',
primaryAccount: '6230',
secondaryAccount: '5420',
requiresUserInput: false,
logic: 'Web hosting, domains, SaaS subscriptions.'
},
{
mcc: '5734',
description: 'Computer Software Stores',
riskLevel: 'low',
primaryAccount: '5420',
requiresUserInput: false,
logic: 'Software licenses.'
},
// ALCOHOL - ALWAYS REJECT
{
mcc: '5921',
description: 'Package Stores (Systembolaget)',
riskLevel: 'very_high',
primaryAccount: '2013',
requiresUserInput: false,
autoRejectReason: 'Alcohol is not deductible',
logic: 'Always 2013. No exceptions.'
},
// GROCERIES - HIGH RISK
{
mcc: '5411',
description: 'Grocery Stores',
riskLevel: 'high',
primaryAccount: '2013',
secondaryAccount: '6072',
requiresUserInput: true,
autoRejectReason: 'Food is typically private expense',
logic: 'Default 2013. Only 6072 if documented representation.'
},
// STREAMING SERVICES
{
mcc: '4899',
description: 'Cable/Streaming Services',
riskLevel: 'medium',
primaryAccount: '5420',
secondaryAccount: '2013',
requiresUserInput: true,
logic: 'Netflix for research? Usually private. Ask user.'
},
// GYM & FITNESS - ALWAYS REJECT
{
mcc: '7941',
description: 'Sports Clubs/Gyms',
riskLevel: 'very_high',
primaryAccount: '2013',
requiresUserInput: false,
autoRejectReason: 'Gym memberships are private expenses, even for fitness influencers',
logic: 'Always 2013. IL 9 kap. 2 §'
}
]
```
---
## Keyword-Based Categorization
For generic MCCs (Amazon, PayPal), analyze transaction description text.
```typescript
// lib/accounting/keyword-rules.ts
interface KeywordRule {
keywords: string[]
account: string
confidence: 'high' | 'medium' | 'low'
vatTreatment?: 'standard' | 'reverse_charge' | 'none'
}
const KEYWORD_RULES: KeywordRule[] = [
// Software & Subscriptions
{
keywords: ['adobe', 'creative cloud'],
account: '5420',
confidence: 'high',
vatTreatment: 'reverse_charge' // Adobe Ireland
},
{
keywords: ['epidemic sound'],
account: '5420',
confidence: 'high',
vatTreatment: 'standard' // Swedish company
},
{
keywords: ['canva'],
account: '5420',
confidence: 'high',
vatTreatment: 'reverse_charge'
},
{
keywords: ['spotify', 'spotify ab'],
account: '2013', // Usually private
confidence: 'medium'
},
{
keywords: ['spotify for business', 'soundtrack'],
account: '5420',
confidence: 'high'
},
// Web Infrastructure
{
keywords: ['one.com', 'loopia', 'godaddy', 'namecheap', 'cloudflare'],
account: '6230',
confidence: 'high'
},
{
keywords: ['vercel', 'netlify', 'heroku', 'aws', 'digitalocean'],
account: '6230',
confidence: 'high',
vatTreatment: 'reverse_charge'
},
// Advertising Platforms
{
keywords: ['facebook', 'meta', 'instagram ads', 'fb ads'],
account: '5910',
confidence: 'high',
vatTreatment: 'reverse_charge' // Meta Ireland
},
{
keywords: ['google ads', 'adwords'],
account: '5910',
confidence: 'high',
vatTreatment: 'reverse_charge' // Google Ireland
},
{
keywords: ['tiktok ads', 'tiktok for business'],
account: '5910',
confidence: 'high',
vatTreatment: 'reverse_charge'
},
// Transport
{
keywords: ['uber', 'bolt', 'taxi'],
account: '5820',
confidence: 'medium' // Could be private
},
{
keywords: ['sj', 'mtrx', 'sj biljett'],
account: '5800',
confidence: 'high'
},
{
keywords: ['sas', 'norwegian', 'ryanair', 'flygbiljett'],
account: '5800',
confidence: 'medium' // Could be private trip
},
// Always Private
{
keywords: ['systembolaget'],
account: '2013',
confidence: 'high'
},
{
keywords: ['apotek', 'apoteket', 'apotea'],
account: '2013',
confidence: 'high'
},
{
keywords: ['ica', 'coop', 'willys', 'hemköp', 'lidl'],
account: '2013',
confidence: 'medium' // Usually private groceries
}
]
```
---
## Amount-Based Routing (Asset vs Expense)
```typescript
// lib/accounting/asset-routing.ts
const PRISBASBELOPP_2025 = 58800
const HALF_PBB = PRISBASBELOPP_2025 / 2 // 29,400 kr
interface AssetDecision {
shouldCapitalize: boolean
account: string
depreciationYears?: number
depreciationRate?: number
}
function routeEquipmentPurchase(
amountExVat: number,
mccCode: string
): AssetDecision {
const isEquipmentMCC = ['5732', '5946', '5045'].includes(mccCode)
if (!isEquipmentMCC) {
return { shouldCapitalize: false, account: '5410' }
}
if (amountExVat > HALF_PBB) {
return {
shouldCapitalize: true,
account: '1210',
depreciationYears: 5,
depreciationRate: 0.20
}
}
return {
shouldCapitalize: false,
account: '5410'
}
}
// Update HALF_PBB annually - Skatteverket publishes in November
```
---
## Reverse Charge VAT Handling
When purchasing services from foreign (usually EU) companies, Swedish businesses must self-report VAT.
### Detection Logic
```typescript
// lib/accounting/reverse-charge.ts
interface ReverseChargeResult {
applies: boolean
reason: string
bookings: Array<{
account: string
debit?: number
credit?: number
description: string
}>
}
const EU_DIGITAL_SERVICES_VENDORS = [
{ name: 'meta', country: 'IE', keywords: ['facebook', 'meta', 'instagram'] },
{ name: 'google', country: 'IE', keywords: ['google', 'youtube', 'adwords'] },
{ name: 'adobe', country: 'IE', keywords: ['adobe'] },
{ name: 'microsoft', country: 'IE', keywords: ['microsoft', 'office 365', 'azure'] },
{ name: 'amazon', country: 'LU', keywords: ['aws', 'amazon web services'] },
{ name: 'canva', country: 'AU', keywords: ['canva'] }, // Non-EU, different treatment
]
function handleForeignPurchase(
amount: number,
vendorName: string,
vendorCountry: string,
expenseAccount: string
): ReverseChargeResult {
const isEU = isEUCountry(vendorCountry)
const vatRate = 0.25
const vatAmount = amount * vatRate
if (isEU && vendorCountry !== 'SE') {
// EU purchase - reverse charge applies
return {
applies: true,
reason: `Omvänd skattskyldighet - inköp från ${vendorCountry}`,
bookings: [
{ account: expenseAccount, debit: amount, description: 'Kostnad' },
{ account: '2645', debit: vatAmount, description: 'Beräknad ing. moms utl. förvärv' },
{ account: '2614', credit: vatAmount, description: 'Utg. moms utl. förvärv' },
{ account: '1930', credit: amount, description: 'Utbetalning' }
]
}
}
if (!isEU) {
// Non-EU purchase - no reverse charge, just expense
return {
applies: false,
reason: 'Import från land utanför EU',
bookings: [
{ account: expenseAccount, debit: amount, description: 'Kostnad' },
{ account: '1930', credit: amount, description: 'Utbetalning' }
]
}
}
// Swedish domestic
return {
applies: false,
reason: 'Inrikes inköp',
bookings: [] // Normal VAT handling
}
}
```
### Momsdeklaration Mapping
| Scenario | Ruta | Description |
|----------|------|-------------|
| Domestic purchase 25% VAT | 48 (utgående), deduct in 48 | Normal |
| EU service purchase (reverse charge) | 21 (purchase amount), 48 (calculated VAT) | Self-report |
| Non-EU service purchase | No VAT reporting | Just expense |
---
## Revenue Account Selection
```typescript
// lib/accounting/revenue-routing.ts
interface RevenueRouting {
account: string
vatRate: number
momsRuta: number
requiresPeriodiskSammanstallning: boolean
}
function routeRevenue(
customerCountry: string,
customerType: 'business' | 'consumer',
hasValidVatNumber: boolean
): RevenueRouting {
// Swedish customer
if (customerCountry === 'SE') {
return {
account: '3001', // Försäljning 25%
vatRate: 25,
momsRuta: 5,
requiresPeriodiskSammanstallning: false
}
}
// EU business with VAT number
if (isEUCountry(customerCountry) && customerType === 'business' && hasValidVatNumber) {
return {
account: '3044', // Försäljning tjänst EU
vatRate: 0,
momsRuta: 39,
requiresPeriodiskSammanstallning: true // Quarterly report required!
}
}
// EU consumer (OSS rules may apply if >10k EUR/year)
if (isEUCountry(customerCountry) && customerType === 'consumer') {
return {
account: '3001', // Or OSS-specific account
vatRate: 25, // Swedish VAT until OSS threshold
momsRuta: 5,
requiresPeriodiskSammanstallning: false
}
}
// Non-EU (USA, etc.) - Export
return {
account: '3045', // Försäljning tjänst Export
vatRate: 0,
momsRuta: 40,
requiresPeriodiskSammanstallning: false
}
}
```
---
## Conservative Bias Principle
The system must be designed with a **conservative bias** - prefer rejecting deductions over accepting questionable ones.
### Rationale
- Burden of proof lies with taxpayer (IL 9 kap. 2 §)
- Skattetillägg (tax surcharge) is 40% of avoided tax
- Better to warn user than cause audit problems
### Implementation
```typescript
// lib/accounting/risk-assessment.ts
interface RiskAssessment {
riskLevel: 'low' | 'medium' | 'high' | 'very_high'
suggestedAccount: string
requiresConfirmation: boolean
warningMessage?: string
legalBasis?: string
}
function assessTransactionRisk(
mcc: string,
amount: number,
description: string
): RiskAssessment {
const mapping = getMCCMapping(mcc)
if (mapping.riskLevel === 'very_high') {
return {
riskLevel: 'very_high',
suggestedAccount: '2013', // Default to private (eget uttag)
requiresConfirmation: true,
warningMessage: mapping.autoRejectReason,
legalBasis: 'IL 9 kap. 2 § - Privata levnadskostnader'
}
}
if (mapping.riskLevel === 'high') {
return {
riskLevel: 'high',
suggestedAccount: mapping.primaryAccount,
requiresConfirmation: true,
warningMessage: `Denna typ av utgift kräver dokumentation. ${mapping.logic}`
}
}
// Low/medium risk - auto-categorize but allow override
return {
riskLevel: mapping.riskLevel,
suggestedAccount: mapping.primaryAccount,
requiresConfirmation: false
}
}
```
---
## Private Use Split
For items with mixed business/private use:
```typescript
// lib/accounting/split-usage.ts
interface SplitTransaction {
businessPortion: {
account: string
amount: number
}
privatePortion: {
account: string // Always 2013
amount: number
}
}
function splitMixedUse(
totalAmount: number,
businessPercentage: number,
expenseAccount: string
): SplitTransaction {
const businessAmount = totalAmount * (businessPercentage / 100)
const privateAmount = totalAmount - businessAmount
return {
businessPortion: {
account: expenseAccount,
amount: Math.round(businessAmount * 100) / 100
},
privatePortion: {
account: '2013',
amount: Math.round(privateAmount * 100) / 100
}
}
}
// UI should ask: "Används denna uteslutande i företaget?"
// If no → prompt for percentage split
```
---
## Travel Allowance (Traktamente)
For business trips, the owner can claim tax-free travel allowance at Skatteverket's rates.
```typescript
// lib/accounting/traktamente.ts
// 2024 rates - update annually
const TRAKTAMENTE_RATES = {
sweden: {
fullDay: 260, // Hel dag
halfDay: 130, // Halv dag
night: 130 // Nattraktamente
},
// Foreign rates vary by country - see Skatteverket's list
foreign: {
USA: { fullDay: 620, reduced: 434 },
UK: { fullDay: 550, reduced: 385 },
Germany: { fullDay: 420, reduced: 294 },
// ... etc
}
}
interface TraktamenteCalculation {
amount: number
bookings: Array<{
account: string
debit?: number
credit?: number
}>
}
function calculateTraktamente(
destination: 'sweden' | string,
fullDays: number,
halfDays: number,
nights: number
): TraktamenteCalculation {
const rates = destination === 'sweden'
? TRAKTAMENTE_RATES.sweden
: TRAKTAMENTE_RATES.foreign[destination] || TRAKTAMENTE_RATES.sweden
const amount =
(fullDays * rates.fullDay) +
(halfDays * (rates.halfDay || rates.fullDay / 2)) +
(nights * (rates.night || 0))
return {
amount,
bookings: [
{ account: '5841', debit: amount }, // Traktamente, avdragsgillt
{ account: '2018', credit: amount } // Egen insättning (skuld till ägare)
]
}
}
```
---
## Non-Cash Transactions (Barter/Gifts)
Influencers receive products that must be accounted for even without bank transaction.
```typescript
// lib/accounting/non-cash.ts
interface BarterTransaction {
productValue: number
hasMotprestation: boolean // Required post/exposure?
usedInBusiness: boolean
bookings: Array<{
account: string
debit?: number
credit?: number
description: string
}>
}
function bookBarterTransaction(
productValue: number,
hasMotprestation: boolean,
usedInBusiness: boolean
): BarterTransaction['bookings'] {
if (!hasMotprestation && productValue < 450) {
// Tax-free promotional gift
return [] // No booking required
}
if (hasMotprestation) {
// Taxable barter - must recognize as income
const bookings = [
{ account: '3001', credit: productValue, description: 'Förmån/Byteshandel' },
{ account: '1510', debit: productValue, description: 'Fordran (kvittad mot produkt)' }
]
if (usedInBusiness) {
// Can also expense it
bookings.push(
{ account: '5490', debit: productValue, description: 'Rekvisita' },
{ account: '1510', credit: productValue, description: 'Kvittning fordran' }
)
} else {
// Private use - eget uttag
bookings.push(
{ account: '2013', debit: productValue, description: 'Eget uttag (förmån)' },
{ account: '1510', credit: productValue, description: 'Kvittning fordran' }
)
}
return bookings
}
// Gift without motprestation but valuable - still taxable
return [
{ account: '3001', credit: productValue, description: 'Skattepliktig gåva' },
{ account: '2013', debit: productValue, description: 'Eget uttag (gåva)' }
]
}
```
---
## System Configuration
```typescript
// lib/accounting/config.ts
export const ACCOUNTING_CONFIG = {
// Update annually
prisbasbelopp: 58800, // 2025
halfPrisbasbelopp: 29400,
// VAT rates
vatRates: {
standard: 25,
reduced: 12, // Food, hotels
low: 6, // Transport, books
},
// Depreciation defaults
depreciation: {
equipment: { years: 5, rate: 0.20 },
computers: { years: 3, rate: 0.33 },
vehicles: { years: 5, rate: 0.20 },
},
// Risk thresholds
riskSettings: {
autoRejectVeryHigh: true,
requireConfirmationHigh: true,
allowOverrideMedium: true,
}
}
```
---
## Integration Points
This guide integrates with:
1. **Transaction categorization UI** (05-UI-SPECIFICATION.md) - Swipe interface uses MCC mapping
2. **Tax calculator** (06-IMPLEMENTATION-GUIDE.md) - Uses account classifications for tax estimation
3. **Invoice VAT rules** (04-API-SPECIFICATION.md) - Revenue account selection aligns with momsdeklaration
4. **Expense warnings** (06-IMPLEMENTATION-GUIDE.md) - Risk levels drive warning display
---
## Annual Maintenance
Update each January:
1. Prisbasbelopp (from Skatteverket, published November)
2. Traktamente rates (Skatteverket)
3. Any BAS account changes (BAS-kontogruppen)
4. Tax rates if changed by Riksdag
File diff suppressed because one or more lines are too long
-406
View File
@@ -1,406 +0,0 @@
Base ERP Architecture: Core + Add-on System (v2)
The Core
Every item here is non-negotiable for legal compliance or market viability. Nothing below can be an add-on.
Authentication, Tenancy & Access Control
Magic link auth via Supabase
Onboarding wizard (entity type EF/AB, company details, tax registration, fiscal year, bank connection)
RLS on every table, user_id scoping
Multi-company support per user
Delegated access roles: owner, accountant (full read/write), audit:read (read-only, scoped to fiscal year). The audit role is the foundation for Digital Audit 2026 compliance where Skatteverket gets API access to a specific fiscal year.
Document Archive (Compliance Layer)
The July 2024 Bokföringslagen amendment makes the system the legal archive. This is not a feature, it is a legal obligation.
Hash-on-upload. Every uploaded file (receipt image, e-invoice XML, PDF) gets a SHA-256 hash computed and stored alongside the blob. The hash is the proof of integrity.
WORM storage. Uploaded documents are write-once. Any modification (crop, contrast, re-scan) creates a new version. The original remains accessible and linked in the version chain.
Deletion blocking. The system hard-rejects any attempt to delete a document linked to a committed voucher or a locked period. No admin override.
Digitization metadata. Every upload logs: user who uploaded, timestamp, source (camera, file upload, e-invoice), and a digitization date field. This justifies destruction of the paper original.
Linkage integrity. Strict foreign key from journal_entry_lines to document_attachments. No orphaned documents, no undocumented entries.
Seven-Year Retention & Purge Prevention
System calculates retention expiry: fiscal year end + 7 calendar years.
All delete operations (company, fiscal year, journal entries, documents) are blocked within the retention window.
"Delete Company" requires a verified full SIE4 export + linked document archive before proceeding, and only after retention expires.
GDPR conflict resolution: pseudonymize CRM master data on request, but never touch the ledger. Invoice snapshots with names remain intact as part of the fiscal record.
Chart of Accounts (BAS Kontoplan)
BAS seeding per entity type (EF/AB), K1 vs full plan
Account CRUD: add, deactivate, rename. Deactivated accounts preserve historical data but block new postings.
Account metadata: type (tillgång/skuld/intäkt/kostnad), default tax code, SRU code mapping
SRU mapping table. Every BAS account maps to an SRU code. This is what makes tax filing work. Without it the system cannot generate Inkomstdeklaration 2 data.
Annual BAS updates. Migration mechanism for BAS Group changes. Deprecated accounts get frozen (no new postings), not deleted.
Dimensions. Minimum two dimension types: Kostnadsställe (cost center) and Projekt. Stored on journal entry lines. Required for SIE4 dimension export (#OBJEKT) and expected by any consultancy or construction firm.
Double-Entry Bookkeeping (Immutable Ledger)
Draft/Commit lifecycle:
Journal entries start as drafts with temporary IDs (TMP-xxxx). Drafts are freely editable.
On commit ("Bokför"), the system assigns the next permanent voucher number from the series. At this moment the row becomes immutable.
DB-level enforcement: committed rows have UPDATE and DELETE restrictions. Application code cannot bypass this.
Voucher series management:
Sequential numbering per series per fiscal year. Gaps are impermissible under Bokföringslagen.
Gap detection: background check that flags any missing numbers in a committed series.
Concurrent write safety: SELECT ... FOR UPDATE or advisory locks to prevent duplicate number assignment.
Storno correction logic:
Posted vouchers are never edited. Corrections follow the three-step flow:
Step 1: System generates a reversal voucher (storno) that nullifies the original.
Step 2: System generates the corrected entry with the right data.
Step 3: All three vouchers (original, reversal, correction) are linked in the behandlingshistorik.
UI presents a single "Correct" button. The user sees a "Corrected" status tag. The triple-entry logic runs in the background.
Debit == Credit validation: enforced at DB level via check constraint or trigger. No exceptions.
Guaranteed delivery: transactional outbox pattern for journal creation from upstream events (invoice created, payment received, etc.). Failed entries go to a dead letter queue with alerting. Silent failure is not acceptable.
Audit Trail (Behandlingshistorik)
Every mutation to journal entries, accounts, documents, settings, user roles logged with: actor, timestamp, action type, before-state, after-state.
Committed vouchers log all correction chains (original -> storno -> corrected).
Attempted deletions of protected data logged as security events.
The audit log itself is append-only. No updates, no deletes.
Period Management
Fiscal year definition with support for broken fiscal years (brutet räkenskapsår).
Multi-fiscal-year support with clean year boundaries.
Period locking (låsning av period). Locked periods reject all writes to journal entries and documents within that period. Locking is one-way without admin unlock + audit log entry.
Year-end closing (årsbokslut):
Zero out result accounts (class 3-8).
Transfer net result to equity (account 2099).
Generate closing entries as committed vouchers.
Calculate and verify that UB of year N == IB of year N+1.
Block manual editing of IB to prevent breaking continuity.
Opening balances workflow for new companies or mid-year migrations.
Tax Code Engine
Decoupled from the chart of accounts. Tax codes tag transaction lines independently.
Code
Rate
Description
Momsdeklaration Boxes
MP1
25%
Standard output VAT
05 (basis) + 10 (tax)
MP2
12%
Food/hotel
06 (basis) + 11 (tax)
MP3
6%
Books/transport/culture
07 (basis) + 12 (tax)
MPI
25/12/6%
Standard input VAT
48
IV
0%
Intra-community acquisition
20 (basis) + 30 (input) + 30 (output)
EUS
0%
EU sale of goods/services
35/36 + Periodisk sammanställning
IP
0%
Import of goods
50 (basis) + 60 (output) + 48 (input)
EXP
0%
Export outside EU
08 (basis)
OSS
varies
One Stop Shop (e-commerce)
Excluded from boxes 05-49, routed to OSS report
Momsdeklaration generated by summing per tax code, not per account. This survives any account plan customization.
Validation: calculated tax (basis * rate) must match reported tax within tolerance. Deviations trigger warnings.
Periodisk sammanställning (EC Sales List) auto-populated from EUS-tagged lines.
Financial Reports
Resultaträkning (income statement) by BAS class
Balansräkning (balance sheet) with assets == equity + liabilities validation
Råbalans (trial balance) with zero-sum verification
Momsdeklaration (all rutor 05-49) generated from tax code engine
SRU-based tax data for Inkomstdeklaration 2 (sums per SRU code)
All reports respect period locks, fiscal year boundaries, and dimension filters (kostnadsställe, projekt)
SIE4
Export: spec-validated output including #IB, #UB, #RES, #VER, #OBJEKT (dimensions), #KONTO with all used accounts. Explicit character encoding handling (CP437/Latin-1) with Swedish character validation. #ORGNR validated against Luhn algorithm.
Import: 4-step wizard (upload, parse, map accounts, review & execute). Creates journal entries from imported data. Validates that imported IB matches existing UB if prior year exists.
Round-trip integrity: export from system, re-import, verify all balances match with zero difference.
Cross-system validation: export must parse without errors in Visma and Fortnox.
Invoicing
Create, edit, send, track invoices
Credit notes with automatic storno reversal entries
VAT via tax code engine (not hardcoded per account)
Multi-currency with Riksbanken exchange rates
Currency gain/loss (kursdifferens). When payment arrives at a different rate than invoiced, system auto-books the difference to 3960/7960.
PDF generation
Peppol BIS Billing 3.0. Generate and send e-invoices via Peppol network. This is the mandated B2G standard and increasingly B2B. Validate output against Peppol Schematron. This replaces email delivery for Peppol-capable recipients.
Public payment/dispute page (token-based, no auth)
Configurable reminder system (intervals, templates, enable/disable)
Banking
PSD2 connection via Enable Banking for transaction sync
OAuth consent flow with 90-day renewal handling
Transaction sync with deduplication (unique(user_id, external_id))
Invoice-to-payment matching (amount + date + OCR reference)
ISO 20022 file handling:
PAIN.001 generation for outgoing supplier payments. Batch multiple payments per PaymentInformation block. Validate against bank-specific XSD before download.
CAMT.053 parsing for end-of-day bank statements. Feed into reconciliation engine matching to general ledger.
CAMT.054 parsing for incoming payment notifications with OCR references. Auto-mark invoices as paid.
Transaction Management
Transaction list with categorization
Manual categorization creates journal entries (via draft/commit flow)
Mapping rules engine: MCC code, merchant name, description pattern, amount threshold
Extensible rule types (hook for add-ons to register custom rules)
Customers
Name, org number, VAT number (validated format), address, payment terms, international flag
Peppol participant ID (for e-invoice routing)
Linked to invoices
Subject to GDPR pseudonymization (but not deletion if linked to fiscal records)
Tax Calendar
Auto-generated Swedish tax deadlines: F-skatt, arbetsgivardeklaration, momsdeklaration (monthly/quarterly), inkomstdeklaration, årsredovisning, bokslut
Calendar views (month/week/day) + ICS export
Deadline status tracking (upcoming, due, overdue, filed)
The Extension Architecture
1. Event Bus
The core emits events. Extensions subscribe. One-way dependency.
typescript
// lib/events/types.ts
export type CoreEvent =
// Bookkeeping
| { type: 'journal_entry.drafted'; payload: DraftJournalEntry }
| { type: 'journal_entry.committed'; payload: JournalEntry }
| { type: 'journal_entry.corrected'; payload: { original: JournalEntry; storno: JournalEntry; corrected: JournalEntry } }
// Documents
| { type: 'document.uploaded'; payload: Document & { hash: string } }
// Invoicing
| { type: 'invoice.created'; payload: Invoice }
| { type: 'invoice.sent'; payload: Invoice }
| { type: 'invoice.paid'; payload: Invoice & { transaction: Transaction; kursdifferens?: number } }
| { type: 'invoice.overdue'; payload: Invoice & { days: number } }
| { type: 'credit_note.created'; payload: CreditNote }
// Banking
| { type: 'transaction.synced'; payload: Transaction[] }
| { type: 'transaction.categorized'; payload: Transaction & { account: string; taxCode: string } }
| { type: 'bank.statement_received'; payload: CAMT053Statement }
| { type: 'bank.payment_notification'; payload: CAMT054Notification }
// Periods
| { type: 'period.locked'; payload: { fiscalYear: number; period: number } }
| { type: 'period.year_closed'; payload: { fiscalYear: number } }
// Customers
| { type: 'customer.created'; payload: Customer }
| { type: 'customer.pseudonymized'; payload: { customerId: string } }
// Audit
| { type: 'audit.security_event'; payload: AuditSecurityEvent }
Implementation: in-process handlers initially. Add webhook dispatch (POST to registered URLs) when external plugin consumers exist.
2. Extension Registry
typescript
// lib/extensions/types.ts
export interface Extension {
id: string
name: string
version: string
// Surfaces
routes?: RouteDefinition[]
apiRoutes?: ApiRouteDefinition[]
sidebarItems?: SidebarItem[]
eventHandlers?: EventSubscription[]
mappingRuleTypes?: MappingRuleType[]
reportTypes?: ReportDefinition[]
settingsPanel?: SettingsPanelDef
taxCodes?: TaxCodeDefinition[] // for add-ons introducing new tax scenarios
dimensionTypes?: DimensionDefinition[] // for add-ons adding custom dimensions beyond the base two
onInstall?(ctx: ExtensionContext): Promise<void>
onUninstall?(ctx: ExtensionContext): Promise<void>
}
3. Database Extension Pattern
First-party add-ons: own migration folder, own tables with user_id + RLS.
Third-party add-ons: use API routes + webhook events + generic extension_data table:
sql
create table extension_data (
id uuid primary key default gen_random_uuid(),
user_id uuid references auth.users not null,
extension_id text not null,
key text not null,
value jsonb not null,
created_at timestamptz default now(),
unique(user_id, extension_id, key)
);
```
### Core constraint
The base never imports from `extensions/`. Dependency flows one direction: extensions import from `lib/core/`, `lib/events/`, `lib/extensions/`.
### Type ownership
Extension-specific types live in their extension directory (e.g. `extensions/ne-bilaga/types.ts`). For convenience, `types/index.ts` re-exports them so existing importers continue to work with `import type { NEDeclaration } from '@/types'`. The canonical source is always the extension file.
| Extension | Type file | Types |
|-----------|-----------|-------|
| `push-notifications` | `extensions/push-notifications/types.ts` | `PushSubscription`, `NotificationSettings`, `NotificationType`, `NotificationLog` |
| `receipt-ocr` | `extensions/receipt-ocr/types.ts` | `Receipt`, `ReceiptLineItem`, `ReceiptExtractionResult`, `ExtractedLineItem`, `ReceiptMatchCandidate`, `ReceiptQueueSummary`, `CameraQualityFeedback`, etc. |
| `ne-bilaga` | `extensions/ne-bilaga/types.ts` | `NEDeclaration`, `NEDeclarationRutor`, `NEAccountMapping`, `SRURecord`, `SRUFile`, `NE_RUTA_LABELS` |
| `sru-export` | `extensions/sru-export/types.ts` | `SRUExportResult`, `SRUCoverageStats` |
### API route convention
All extension API routes live under `app/api/extensions/<extension-name>/`. No extension routes should exist outside this namespace.
---
## Add-ons
Each add-on is self-contained. Listed by priority tier.
### Tier 1: High value, build soon after core
**`receipt-ocr`**
- Subscribes to: `document.uploaded`
- Does: Claude Vision OCR, extracts merchant/date/line items/totals, fuzzy-matches to bank transactions (±3 days, amount similarity), suggests BAS account + tax code
- Special rules: Systembolaget -> non-deductible, restaurant -> representation (90 kr/person limit)
- Registers: custom mapping rule types for OCR-based categorization
**`ai-categorization`**
- Subscribes to: `transaction.synced`
- Does: suggests BAS account + tax code for uncategorized transactions
- No hard dependency on any specific AI provider. Interface-based so the model is swappable.
**`ne-bilaga`**
- Registers as: `reportType` via extension registry
- Does: generates NE-bilaga (income tax appendix for enskild firma, fields R1-R11) from journal entries
- Only relevant for EF entity type. Hidden for AB.
**`sru-export`**
- Registers as: `reportType`
- Does: generates SRU files for Skatteverket electronic filing. Reads SRU mappings from core account metadata.
**`push-notifications`**
- Subscribes to: `invoice.overdue`, `period.locked`, deadline events from tax calendar
- Does: Web Push via VAPID. Per-user preferences with quiet hours.
**`owner-payroll`**
- Registers: routes, sidebar item, settings panel
- Does: single-employee salary for AB owner. Gross salary, tax deduction, employer contributions (arbetsgivaravgifter). Monthly AGI XML generation for Skatteverket. Box 821 absence reporting (VAB, parental leave) with date tracking. Bilförmån and traktamente input fields with Skatteverket standard rates.
- Subscribes to: tax calendar deadline events for arbetsgivardeklaration due dates
### Tier 2: Market differentiation
**`annual-report`**
- Registers as: `reportType` + routes
- Does: K2 taxonomy mapping from BAS accounts. Generates iXBRL for Bolagsverket digital filing. API integration: validate, upload, redirect to BankID signing. Board member signature flow.
- K3 support as a sub-toggle within this add-on (component depreciation, fair value).
**`ai-chat`**
- Registers: floating widget component, routes for session management
- Does: RAG-powered Swedish tax/accounting assistant using LangChain + embeddings. Session history. Rate limited.
- No event subscriptions. Read-only access to user's bookkeeping data for context.
**`bankid`**
- Registers: auth provider, signing flow component
- Does: BankID integration for login and document signing. Secure Start (animated QR code, mandatory since May 2024). Certificate management for merchant certificates.
- Used by: `annual-report` (Bolagsverket signing), `owner-payroll` (AGI signing), future audit access.
**`deductions`**
- Registers: routes, sidebar item, report types
- Does: Schablonavdrag for mileage (korjournal, 25 kr/mil) and home office (2,000-4,000 kr/year). Generates journal entries.
### Tier 3: Vertical enablers
**`inventory-value`**
- Registers: routes, report type
- Does: tracks financial value of stock on account 1400. Accepts journal entries from vertical inventory modules (retail, construction, food). Does not do logistics, variants, batches, or expiry tracking. That is the vertical's job.
**`multi-currency-advanced`**
- Registers: additional tax codes, report types
- Does: automated unrealized gain/loss calculations at period end. Currency revaluation entries. Beyond the base kursdifferens on invoice payment.
**`oss-reporting`**
- Registers: report type, tax codes
- Does: OSS (One Stop Shop) VAT return for e-commerce sellers. Transactions tagged with OSS tax codes excluded from standard momsdeklaration and routed here.
**`saf-t-export`**
- Registers: report type
- Does: SAF-T XML generation. Forward-looking compliance for potential 2026 EU mandate. Maps from the core's granular data model (header -> line -> tax detail).
---
## Repo Structure
```
app/
(auth)/
(onboarding)/
(dashboard)/
bookkeeping/
invoices/
transactions/
banking/
customers/
reports/
calendar/
settings/
extensions/ → marketplace / management
(public)/
api/
journal-entries/
invoices/
transactions/
banking/
reports/
customers/
deadlines/
documents/ → upload, hash verification, version history
audit/ → audit log queries, security events
extensions/ → register, list, config
webhooks/ → outbound event delivery
lib/
core/
bookkeeping/ → draft/commit, storno, voucher series, period locking
accounts/ → BAS kontoplan, SRU mapping, dimensions
reports/ → resultaträkning, balansräkning, råbalans, moms
invoicing/ → create, send, credit, VAT, Peppol, reminders
banking/ → PSD2, sync, matching, ISO 20022 (PAIN/CAMT)
transactions/ → categorization, mapping rules
tax/ → tax code engine, deadlines, fiscal year, year-end closing
sie/ → import + export with dimension support
documents/ → hash-on-upload, WORM storage, deletion blocking, versioning
audit/ → append-only audit log, behandlingshistorik
retention/ → purge prevention, retention expiry calculation
events/ → event bus, types, webhook dispatch
extensions/ → registry, types, loader
extensions/ → first-party add-ons
receipt-ocr/
ai-categorization/
ai-chat/
ne-bilaga/
sru-export/
push-notifications/
owner-payroll/
annual-report/
bankid/
deductions/
inventory-value/
multi-currency-advanced/
oss-reporting/
saf-t-export/
components/
ui/ → Radix primitives, design system
core/ → base feature components
extensions/ → shared extension UI patterns
supabase/
migrations/ → base schema only
types/
-357
View File
@@ -1,357 +0,0 @@
# Förtydligad feedback med tolkningar
## 1. Onboarding-flöde
**Influencer Business → Influencer Assistant**
- "Influencer Business" står troligen som appnamn/rubrik i onboarding-flödet
- Ska ersättas med "Influencer Assistant" överallt där det förekommer
- Förmodligen i header, sidtitel och eventuellt metadata
**Localhost redirect problem vid auth genom mail**
- Magic link-autentisering redirectar till `localhost:3000` istället för produktions-URL
- Environment variable för NEXT_PUBLIC_SITE_URL är förmodligen inte satt korrekt i Supabase
**Ta bort alla detaljer kring EF/AB**
- Nuvarande onboarding har förklarande text typ "Enklare bokföring, lägre egenavgifter" vid bolagsformsval
- Ska bli en ren dropdown med endast "Enskild firma", "Aktiebolag", "Inget bolag ännu (frilans)"
- Ingen pedagogisk text, bara valet
**Lägg till "Inget bolag ännu (frilans)"**
- Tredje alternativ för creators som tar sina första samarbeten men inte formaliserat verksamheten än
- Låter dem använda appen för att spåra inkomster innan de registrerar bolag
- Förmodligen med begränsad funktionalitet (ingen bokföring, bara spårning)
**Verksamhetsnamn lägg till (Eller ditt namn vid EF)**
- Labeln "Verksamhetsnamn" ska ha suffix-text "(Eller ditt namn vid EF)"
- Placeholder bör vara "Alices Influencer-verksamhet" eller "Alice Andersson"
- Gör det tydligt att EF-användare kan använda personnamn
**Org nummer förklara att det är ens personnummer vid EF**
- Placeholder-text: "ÅÅMMDD-XXXX (ditt personnummer vid EF)"
- Eller hjälptext under fältet: "Vid enskild firma är orgnummer samma som ditt personnummer"
- Görs obligatoriskt för EF/AB (ta bort "frivilligt")
**Ta bort frivilligt för EF**
- Orgnummer är obligatoriskt för både EF och AB
- Required-validering ska gälla
**Ändra till brun färg där det är grönt**
- Primärfärg i appen är troligen grön idag
- Ska bytas till brun enligt brand guidelines
- Gäller knappar, accenter, aktiva tillstånd
**Bankkonto ta bort? Eller direkt till Enable Banking?**
- Nuvarande flöde: manuellt IBAN-fält i onboarding
- Ska ersättas med Enable Banking-integration direkt
- Dvs. istället för textfält → knapp "Koppla bankkonto" som öppnar Enable Banking
- Hoppar över manuell input helt? Eller?
**Fixa knapp så att den går direkt vid klick på bank och inte två steg**
- Nuvarande: Klicka på en bank , sedan ändå behöva scrolla ner och klicka "autentisera till X" knapp.
---
## 2. Dashboard (första sidan)
**"Godmorgon Alice, idag ska du göra X" eller "Allt är som det ska"**
- Personaliserad hero-sektion högst upp på dashboarden
- "Godmorgon Alice, idag ska du: Skicka faktura till H&M (förfaller om 3 dagar), Betala moms (deadline 12 feb)"
- Om inga pending items: "Godmorgon Alice, allt är som det ska"
- Hämtar från kommande kalenderhändelser + fakturor nära förfall + obetalda fakturor
**Pengarna under**
- "Pengarna under" = summan pengar som finns under/kvar efter att preliminärskatt dragits av
- Visar nettoresultat efter beräknad skatt
- Dashboard ska visa: Bruttoinkomst, Beräknad skatt, Kvar att ta ut
- "Kvar att ta ut: 45 000 kr" typ
**Undersök skatteberäkning?**
**Intäkter / Kostnader också 0, undersök**
- Dashboard visar 0 kr för både intäkter och kostnader, undersök detta om det stämmer
**Ny kampanj → Nytt samarbete**
- CTA-knapp eller sektionshuvud säger "Ny kampanj"
- Ska vara "Nytt samarbete" eller "+ Skapa samarbete"
- Matchar creators mentala modell bättre
**Spåra leverabler svårt ord**
- "Leverabler" är projektledarjargong, inte creator-språk
- Ska vara "Spåra innehåll" eller "Håll koll på publiceringar"
- Mindre formellt, mer intuitivt
**Ta bort logga körning från första sidan**
- Det finns en sektion/knapp för mileage tracking på dashboarden
- Ska tas bort helt (inte relevant för målgruppen)
- Creators kör sällan tjänsteresor, prioritera inte detta
**Skita i personlig kortkoppling, arbeta endast med företagskort**
- Endast företagskort via Enable Banking
- Kategorisera alla transaktioner från företagskonto
- Enklare modell, färre edge cases
**Ta bort sammanfattning månadsvis kalendervy**
- Kalendervyn har förmodligen en rad per månad som aggregerar events
- Tar onödigt utrymme och är inte så användbar
- Visa bara enskilda events i kronologisk ordning
**Kalender större och mer tilltalande**
- Nuvarande kalender är förmodligen en liten widget eller sidomodul
- Ska ta mer plats (förmodligen halva skärmbredden eller större)
- Kanske full-width på mobil
- Mer visuellt prominent, då det är core feature
**Ta bort "Håll koll på betalningar och deadlines"**
- Beskrivande text under kalendersektion
- Är överflödig/upprepande
- Ta bort, låt kalendern tala för sig själv
---
## 3. Samarbeten (f.d. kampanjer)
**Kampanjer → Samarbete**
- Överallt där "Kampanj" används som term
**Flytta upp så att det är "+ Skapa samarbete" och "Importera avtal" där uppe istället**
- Nuvarande: CTA-knappar längre ner på sidan eller i subnavigation
- Ska vara: Primary actions i toppen av sidan (header-nivå)
**Start och slutdatum för samarbete ändras när man ändrar publiceringsdatum**
- Bugg: När användaren ändrar publiceringsdatum, ändras även start/slutdatum automatiskt
- Ska inte hända - olika fält, oberoende av varandra
- Eller: Ta bort start/slutdatum helt (se nästa punkt)
**Start och slutdatum irrelevant**
- Start/slutdatum är inte viktiga för creators
- Relevant är: Publiceringsdatum (när contentet går live) och Utkastdeadline (när drafts ska skickas)
- Ta bort Start/Slutdatum-fält helt, ersätt med:
- "Publiceringsdatum" (datum)
- "Utkastdeadline" (datum, optional)
**Att det ska stå varumärke / direktkund men inte lägga in som kund**
- Fält för "Varumärke" (t.ex. "H&M") - fritextfält
- Fält för "Byrå" (t.ex. "Swim Communication") - dropdown/autocomplete som skapar Customer-post
- Endast byrå ska finnas i Customers-tabellen
- Varumärke är metadata på samarbetet, inte en relation
**Endast byrå ska läggas in som kund**
- Customer-tabellen innehåller bara byråer (de som faktiskt betalar)
- Varumärken är bara en textsträng på samarbetet
- Fakturan går till byrån, inte varumärket
**Slutkund är ibland varumärke, ibland byrå**
- "Slutkund" = den entitet som ska synas på fakturan och i kommunikation
- Om direktkund (inget mellanled): slutkund = varumärke
- Om via byrå: slutkund = byrå
- Automatisk logik: Om byrå finns → byrå är slutkund. Annars → varumärke är slutkund
**"Skapa ny kund" knapp inte helt intuitiv i ladda upp avtal flödet**
**Byt ut leverabler (svårt ord), extraheras kan också beskrivas lättare**
- "Leverabler" → "Innehåll" eller "Publiceringar"
- "Extraheras" → "Hittades i avtalet" eller "AI fyllde i automatiskt"
- Enklare språk, mindre jargong
**Exklusivitet inte extraherat korrekt (competing brands i x veckor)**
- AI-extrahering hittar inte exklusivitetsklausuler korrekt
- Exempel: "No competing brands for 4 weeks" ska extraheras till "4 veckor"
- Förbättra prompt eller lägg till specifik parsing för exklusivitet
**Lägg till funktion att man kan lägga in exklusivitet själv**
- Om AI missar exklusivitet, ska användaren kunna lägga till manuellt (även där det står "ingen exklusivitet hittades")
- Fält: "Exklusivitet (valfritt)" med dropdown för antal veckor och fritextfält för competing brands
- Eller: Edit-knapp vid extraherad exklusivitet
**Textinnehåll sammanfatta på briefing med AI**
-"Klistra in mailetråd"
- Användaren klistrar in hela mejlkonversationen eller annan text.
- AI extraherar och sammanfattar till strukturerad briefing
**Från kampanj till faktura: populera automatiskt**
- När användaren skapar faktura från ett samarbete
- Auto-populera: Kund (byrå), Belopp (från samarbetsavtalet), Beskrivning (samarbetsnamn), Förfallodatum (publiceringsdatum + 30 dagar)
- Knapp i samarbetsdetaljer: "Skapa faktura" som öppnar fakturaformulär med förifyllda fält
**Fixa UI kundsida**
- Kundsidan (kund-detaljer) har dålig layout/design
- Behöver omarbetas: bättre typografi, spacing, färgschema enligt brand
- Förmodligen legacy-design som inte uppdaterats efter rebrand
---
## 4. Transaktioner & Kategorisering
**Automatiskt fetcha banktransaktioner**
- Enable Banking är redan integrerat men transaktioner hämtas inte automatiskt
- Implementera: Cron job eller webhook som hämtar transaktioner dagligen
- Supabase Edge Function + scheduled trigger för att synka transactions
**Kategorisera logiken lägger av efter 3/6 transaktioner**
- AI-kategorisering fungerar för de första transaktionerna
- Efter 3-6 transaktioner slutar den kategorisera (returnerar null eller default-kategori)
- Förmodligen: Rate limit, token limit, eller bugg i loop-logiken
- Fix: Batch-processa i chunks, kolla error handling
**Undersök balans/resultat??**
- Rapporterad balans/resultat matchar inte faktiskt banksaldo eller förväntad vinst (kanske)
- Förmodligen: Kategoriseringsbuggen ovan gör att många transaktioner blir okategoriserade
- Eller: Dubbelbokföring, felaktig summering
- Fix: Debugga query som beräknar balans, verifiera mot banksaldo
**Gör om swipe funktionen till att man swipear till rätt kostnadskonto istället**
- De mest troliga kostnadskonton visas på skärmen, alternativ för fler om det inte stämmer.
**Lägga till fler alternativ för varje kostnad (ex bankkostnad är inte med)**
- Nuvarande kategorilista är ofullständig
- "Bankkostnad" saknas (avgifter från banken)
- Lägg till: Bankkostnad, Kortavgifter, Valutaväxling, eventuellt fler
- Förmodligen hårdkodad enum, utöka listan
**Faktura ska mappas till transaktioner**
- När en faktura betalas, ska den kopplas till motsvarande banktransaktion
- Auto-matching baserat på: Belopp (±1%), Datum (±7 dagar), Kund-referens
- UI: Ikon som visar "Kopplad till faktura #123" på transaktionen
- Underlättar reconciliation
- Detta kan vara en del av kategoriserings swipe funktionen.
---
## 5. Kalender & Deadlines
**Lägg in så att prel skatte inbetalningsdatum ligger i kalendern**
- Preliminärskatt ska visas som återkommande events i kalendern
- Månatlig (om stor omsättning), kvartalsvis (standard), eller årlig (låg omsättning)
- Automatisk beräkning baserat på ackumulerad vinst
- Event: "Preliminärskatt 12 500 kr" med datum och länk till betalning
**Månadsvis/kvartal/år**
- Intervall för prelskatt beror på förväntad omsättning:
- Månad: >1M kr/år
- Kvartal: 100k-1M kr/år (default)
- År: <100k kr/år
- Automatisk detektering baserat på faktiska intäkter
- Eller: Användarinställning om de vet sin skatteklass
---
## 6. Intäktstyper
**Lägg till "Podcast" som sätt att man får betalt**
- Nuvarande intäktstyper: Instagram, TikTok, YouTube, Sponsrad post, etc.
- Lägg till: "Podcast" (brandintegration i podcast-episoder)
- Ska vara eget val i dropdown "Typ av samarbete" eller "Plattform"
- Samma hantering som andra samarbetstyper: fakturering, kategorisering, momsklassificering
ISSUES:
## Error Type
Runtime SyntaxError
## Error Message
Unexpected token '<'
Next.js version: 16.1.5 (Turbopack)
Fixa så att det är tre cards istället: EF, AB, Frilans (coming soon)
Blir inte automatiskt redirectad till nästa steg efter bank connect:
POST /api/banking/connect 200 in 988ms (compile: 37ms, render: 951ms)
GET /api/banking/callback?state=acdf00c6-82a1-4068-ac07-7510ee41667d&code=061d67b7-81d7-4996-8bb5-9cd731651108 307 in 2.0s (compile: 35ms, render: 2.0s)
GET /onboarding 200 in 167ms (compile: 5ms, proxy.ts: 127ms, render: 35ms)
GET /onboarding 200 in 315ms (compile: 5ms, proxy.ts: 276ms, render: 33ms)
Dessa är på fel ställe och är inkorrekta:
Välkommen till din översikt!
Steg 1 av 5
Stäng guide
Här ser du hela din ekonomiska situation på ett ställe. Låt oss gå igenom de viktigaste delarna.
Fixa namn / bolagsnamn efter God kväll på main page.
Trippla lägg till samarbetsknappar på https://marguerite-nonhieratical-becki.ngrok-free.dev/campaigns
Får ## Error Type
Runtime SyntaxError
## Error Message
Unexpected token '<'
Next.js version: 16.1.5 (Turbopack)
när jag försöker ta bort exklusivitetsmärke från AI extraheringen.
Och detta när jag förösker lägga till exklusivitet manuellt:
## Error Type
Runtime TypeError
## Error Message
Cannot read properties of undefined (reading 'length')
at Step5Exclusivity (components/contracts/ContractImportWizard.tsx:1169:50)
at renderStepContent (components/contracts/ContractImportWizard.tsx:324:16)
at ContractImportWizard (components/contracts/ContractImportWizard.tsx:403:10)
at CampaignImportPage (app/(dashboard)/campaigns/import/page.tsx:37:7)
## Code Frame
1167 | </div>
1168 |
> 1169 | {extraction.exclusivity.excludedBrands.length > 0 && (
| ^
1170 | <div>
1171 | <Label className="mb-2 block">Uteslutna varumärken</Label>
1172 | <div className="flex flex-wrap gap-2">
Next.js version: 16.1.5 (Turbopack)
"Kunde inte kategorisera. Tryck "Hoppa över" för att gå vidare." när jag förösker välja bankkostnader / kortkostnader
POST /api/transactions/d100f6a5-5512-464f-a500-1f63f561e72b/categorize 200 in 1440ms (compile: 474ms, render: 966ms)
Failed to update transaction: {
code: '23514',
details: null,
hint: null,
message: 'new row for relation "transactions" violates check constraint "transactions_category_check"'
}
POST /api/transactions/5d926784-7acb-483c-8859-e34ff29bdd49/categorize 500 in 1045ms (compile: 10ms, render: 1035ms)
Failed to update transaction: {
code: '23514',
details: null,
hint: null,
message: 'new row for relation "transactions" violates check constraint "transactions_category_check"'
}
POST /api/transactions/5d926784-7acb-483c-8859-e34ff29bdd49/categorize 500 in 799ms (compile: 6ms, render: 793ms)
POST /api/transactions/c81e8863-0085-465a-a96b-f3647f28551f/categorize 200 in 1055ms (compile: 6ms, render: 1049ms)
POST /api/transactions/7001bd21-c4e0-46a2-b069-fbb6dc16cd43/categorize 200 in 848ms (compile: 7ms, render: 842ms)
POST /api/transactions/f7dc202a-0938-4614-99cc-254c43e63144/categorize 200 in 902ms (compile: 6ms, render: 896ms)
POST /api/transactions/batch-match-invoices 200 in 425ms (compile: 3ms, render: 422ms)
POST /api/transactions/suggest-categories 200 in 291ms (compile: 1823µs, render: 289ms)
POST /api/transactions/2ebaeaa7-b452-437c-94d3-3716d45085d4/categorize 200 in 836ms (compile: 3ms, render: 833ms)
POST /api/transactions/9bc5e6bb-0dec-4ce0-8732-e6540f44e229/categorize 200 in 860ms (compile: 6ms, render: 853ms)
POST /api/transactions/batch-match-invoices 200 in 124ms (compile: 5ms, render: 119ms)
POST /api/transactions/suggest-categories 200 in 235ms (compile: 2ms, render: 233ms)
Failed to update transaction: {
code: '23514',
details: null,
hint: null,
message: 'new row for relation "transactions" violates check constraint "transactions_category_check"'
}
POST /api/transactions/5d926784-7acb-483c-8859-e34ff29bdd49/categorize 500 in 920ms (compile: 6ms, render: 914ms)
Fixa transaktionslogiken allmänt. Ta bort privatvalet helt.
Undersök om alla intäkter ska vara 3900 övriga rörelseintäkter
Gör en TODO (tex "två passerade deadlines" är direkt under God kväll)
Fixa så titel på samarbete syns.
Värde -> Arvode
Se över bankuppgifter i faktura. Och fakturanr, IBAN, EU momsnmr.
Ta bort skatte "ditt att spendera"
-2192
View File
File diff suppressed because it is too large Load Diff
-469
View File
@@ -1,469 +0,0 @@
1/9
BAS Förenklat årsbokslut (K1) – Kontoplan 2018
Inga ändring eller tillägg har gjorts
jämfört med 2017.
1 Tillgångar
10 Immateriella anläggningstillgångar
BAS-konton Rad Underkonton Rad
1000 Immateriella
anläggningstillgångar
B1 1009 Årets avskrivningar på
immateriella
B1
anläggningstillgångar
11 Byggnader och mark
BAS-konton Rad Underkonton Rad
1110 Byggnader B2 1119 Ackumulerade avskrivningar på
byggnader
B2
1130 Mark B3
1150 Markanläggningar B2 1159 Ackumulerade avskrivningar på
markanläggningar
B2
1180 Pågående nyanläggningar och
förskott för byggnader och
B3
mark
12 Maskiner och inventarier
BAS-konton Underkonton
Rad Rad
1220 Maskiner och inventarier B4 1221 Årets nyanskaffning av
maskiner och inventarier
B4
1222 Årets ersättning för maskiner
och inventarier
B4
1229 Årets avskrivningar på maskiner
och inventarier
B4
1230 Byggnads- och markinventarier B4 1231 Årets nyanskaffning av
byggnads- och markinventarier
B4
1232 Årets ersättning för byggnads-
och markinventarier
B4
1239 Årets avskrivningar på
byggnads- och markinventarier
B4
1240 Bilar och andra transportmedel B4 1241 Årets nyanskaffning av bilar och
andra transportmedel
B4
1242 Årets ersättning för bilar och
andra transportmedel
B4
1249 Årets avskrivningar på bilar och
andra transportmedel
B4
13 Övriga anläggningstillgångar
BAS-konton Underkonton
Rad Rad
1300 Andelar B5
14 Lager
BAS-konton
Underkonton
Rad Rad
Kontoplan_K1_2018_ver1
2/9
1400 Lager B6
15 Kundfordringar
BAS-konton
Underkonton
Rad Rad
1500 Kundfordringar B7
16 Övriga fordringar
BAS-konton Underkonton
Rad Rad
1600 Övriga fordringar B8
1650 Momsfordran B8
17 Förskott till leverantörer
BAS-konton Underkonton
Rad
1700 Förskott till leverantörer B8
19 Kassa och bank
BAS-konton
Underkonton
Rad
1910 Kassa B9
1920 PlusGiro B9
1930 Företagskonto/checkkonto/affär
skonto
B9
1940 Övriga bankkonton B9
1970 Särskilda bankkonton B9
2 EGET KAPITAL OCH SKULDER
20 Eget kapital
BAS-konton Underkonton
Rad Rad
2010 Eget kapital, delägare 1 B10 2011 Egna varuuttag B10
2012 Avräkning för skatter och
avgifter (skattekonto)
B10
2013 Övriga egna uttag B10
2014 Uttag förmåner B10
2017 Egna insättningar B10
2019 Årets resultat, delägare 1 B10
2020 Eget kapital, delägare 2 B10 Se delägre 1
2030 Eget kapital, delägare 3 B10 Se delägre 1
2040 Eget kapital, delägare 4 B10 Se delägre 1
2050 Avsättning till expansionsfond U2
2060 Ersättningsfond U3
2070 Insatsemissioner,
avbetalningsplan på skog,
skogskonto,
U4
2080 Periodiseringsfonder upphovsmannakonto
U1 2083 Periodiseringsfond vid 2012 års
taxering
U1
Kontoplan_K1_2018_ver1
3/9
2084 Periodiseringsfond vid 2013 års
taxering
U1
2085 Periodiseringsfond 2013 U1
2086 Periodiseringsfond 2014 U1
2087 Periodiseringsfond 2015 U1
2088 Periodiseringsfond 2016 U1
2089 Periodiseringsfond 2017 U1
2090 Utjämningskonto upplysningar
1-4
23 Låneskulder
BAS-konton Underkonton
Rad Rad
2330 Checkräkningskredit B13
2350 Skulder till kreditinstitut B13
2390 Övriga låneskulder B13
24 Skulder till leverantörer
BAS-konton
Underkonton
Rad Rad
2440 Leverantörsskulder B15
26 Moms och särskilda punktskatter
BAS-konton Underkonton
Rad Rad
2610 Utgående moms, 25 % B14 2611 Utgående moms på försäljning
inom Sverige, 25 %
B14
2612 Utgående moms på egna uttag,
25 %
B14
2613 Utgående moms för uthyrning,
25 %
B14
2614 Utgående moms omvänd
skattskyldighet, 25 %
B14
2615 Utgående moms import av
varor, 25 %
B14
2618 Vilande utgående moms, 25 % B14
2620 Utgående moms, 12 % B14 2621 Utgående moms på försäljning
inom Sverige, 12 %
B14
2622 Utgående moms på egna uttag,
12 %
B14
2623 Utgående moms för uthyrning,
12 %
B14
2624 Utgående moms omvänd
skattskyldighet, 12 %
B14
2625 Utgående moms import av
varor, 12 %
B14
2628 Vilande utgående moms, 12 % B14
2630 Utgående moms, 6 % B14 2631 Utgående moms på försäljning
inom Sverige, 6 %
B14
2632 Utgående moms på egna uttag,
6 %
B14
Kontoplan_K1_2018_ver1
4/9
2633 Utgående moms för uthyrning,
6 %
B14
2634 Utgående moms omvänd
skattskyldighet, 6 %
B14
2635 Utgående moms import av
varor, 6 %
B14
2638 Vilande utgående moms, 6 % B14
2640 Ingående moms B14 2641 Debiterad ingående moms B14
2642 Debiterad ingående moms i
anslutning till frivillig
B14
2645 Beräknad ingående moms på
skattskyldighet
förvärv från utlandet
B14
2646 Ingående moms på uthyrning B14
2648 Vilande ingående moms B14
2649 Ingående moms, blandad
verksamhet
B14
2650 Redovisningskonto för moms B14 B14
2660 Särskilda punktskatter B14 B14
27 Personalens skatter, avgifter och löneavdrag
BAS-konton Underkonton
Rad Rad
2710 Personalskatt B14
2730 Lagstadgade/avtalade sociala
avgifter och särskild löneskatt
B14
29 Övriga skulder
BAS-konton Underkonton
Rad Rad
2900 Övriga skulder B16
3 RÖRELSENS INKOMSTER/INTÄKTER
30-34 Huvudintäkter
BAS-konton
Underkonton
Rad Rad
3000 Försäljning och utfört arbete
samt övriga momspliktiga
R1
3100 intäkter
Momsfria intäkter R2
3200 Bil- och bostadsförmån m.m. R3
35 Fakturerade kostnader
BAS-konton
Underkonton
Rad Rad
3500 Fakturerade kostnader R1
37 Intäktskorrigeringar
BAS-konton
Underkonton
Rad Rad
3700 Lämnade rabatter, bonus etc. R1/R2
Kontoplan_K1_2018_ver1
5/9
39 Övriga rörelseintäkter
BAS-konton
Underkonton
Rad Rad
3900 Övriga rörelseintäkter R1/R2
3970 Vinst vid avyttring av
immateriella och materiella
R2
3980 anläggningstillgångar
Erhållna bidrag R2
4 UTGIFTER/KOSTNADER FÖR VAROR, MATERIAL OCH VISSA
KÖPTA TJÄNSTER
40-45 Inköp av varor och material
BAS-konton
Underkonton
Rad Rad
4000 Varor R5
46 Legoarbeten, underentreprenader
BAS-konton Underkonton
Rad Rad
4600 Legoarbeten och
underentreprenader
R5
47 Reduktion av inköpspriser
BAS-konton Underkonton
Rad Rad
4700 Erhållna rabatter, bonus etc. R6
49 Förändring av lager
BAS-konton
Underkonton
Rad Rad
4900 Förändring av lager R5
5-6 ÖVRIGA EXTERNA RÖRELSEUTGIFTER/KOSTNADER
50 Lokalkostnader
BAS-konton
Underkonton
Rad Rad
5000 Lokalkostnader R6
51 Fastighetskostnader
BAS-konton
Underkonton
Rad Rad
5100 Fastighetskostnader R6
52 Hyra av anläggningstillgångar
BAS-konton Underkonton
Rad Rad
5200 Hyra av anläggningstillgångar R6
54 Förbrukningsinventarier och förbrukningsmaterial
Kontoplan_K1_2018_ver1
6/9
BAS-konton Underkonton
Rad Rad
5400 Förbrukningsinventarier och
förbrukningsmaterial
R6
55 Reparation och underhåll
BAS-konton
Underkonton
Rad Rad
5500 Reparation och underhåll R6
56 Kostnader för transportmedel
BAS-konton
Underkonton
Rad Rad
5600 Kostnader för transportmedel R6
5610 Personbilskostnader R6 5611 Drivmedel för personbilar R6
5612 Försäkring och skatt för
R6
5613 Reparation och underhåll av
personbilar
personbilar
R6
5615 Leasing av personbilar R6
5618 Schablonmässig milkostnad
privat personbil
R6
5619 Övriga personbilskostnader R6
5620 Lastbilskostnader R6
57 Frakter och transporter
BAS-konton
Underkonton
Rad Rad
5700 Frakter och transporter R6
58 Resekostnader
BAS-konton
Underkonton
Rad Rad
5800 Resekostnader R6
59 Reklam och PR
BAS-konton
Underkonton
Rad Rad
5900 Reklam och PR R6
60 Övriga försäljningskostnader
BAS-konton Underkonton
Rad Rad
6000 Övriga försäljningskostnader
R6
R6 R6
6070 Representation R6 R6 6071 Representation, avdragsgill
6072 Representation, ej avdragsgill R6
+
NE
sid.
2
Kontoplan_K1_2018_ver1
7/9
61 Kontorsmateriel och trycksaker
BAS-konton Underkonton
Rad Rad
6100 Kontorsmateriel och trycksaker R6
62 Tele och post
BAS-konton Underkonton
Rad Rad
6200 Tele och post R6
63 Företagsförsäkringar och övriga riskkostnader
BAS-konton Underkonton
Rad Rad
6300 Företagsförsäkringar och
övriga riskkostnader
R6
6310 Företagsförsäkringar R6
65 Övriga externa tjänster
BAS-konton Underkonton
Rad Rad
6500 Övriga externa tjänster R6
68 Inhyrd personal
BAS-konton Underkonton
Rad Rad
6800 Inhyrd personal R6
69 Övriga kostnader
BAS-konton
Underkonton
Rad Rad
6900 Övriga kostnader R6
6980 Föreningsavgifter R6
7 UTGIFTER/KOSTNADER FÖR PERSONAL, AVSKRIVNINGAR
70 Löner till anställda
BAS-konton Underkonton
Rad Rad
7000 Löner till anställda R7
73 Kostnadsersättningar och förmåner
BAS-konton Underkonton
Rad Rad
7300 Kostnadsersättningar och
förmåner
R7
74 Pensionskostnader
BAS-konton Underkonton
Rad Rad
7400 Pensionskostnader R7
Kontoplan_K1_2018_ver1
8/9
75 Sociala och andra avgifter enligt lag och avtal
BAS-konton Underkonton
Rad Rad
7500 Sociala och andra avgifter
enligt lag och avtal
R7
76 Övriga personalkostnader
BAS-konton Underkonton
Rad Rad
7600 Övriga personalkostnader R7 7631 Personalrepresentation,
avdragsgill
R7
7632 Personalrepresentation, ej
avdragsgill
R7
+
NE
sid.
2
77 Nedskrivningar och återföring av nedskrivningar
BAS-konton Underkonton
Rad Rad
7700 Nedskrivningar R9
alt.
R10
78 Avskrivningar
BAS-konton
Underkonton
Rad Rad
7810 Avskrivningar på immateriella
anläggningstillgångar
R10
7820 Avskrivningar på byggnader
och markanläggningar
R9
7830 Avskrivningar på maskiner och
inventarier
R10
79 Övriga rörelsekostnader
BAS-konton Underkonton
Rad Rad
7970 Förlust vid avyttring av
immateriella och materiella
anläggningstillgångar
R6
7980 Ersättningsfonder R9/
10
8 FINANSIELLA OCH ANDRA INKOMSTER/INTÄKTER OCH
UTGIFTER/ KOSTNADER
83 Övriga ränteintäkter och liknande resultatposter
BAS-konton Underkonton
Rad Rad
Kontoplan_K1_2018_ver1
9/9
8310 Ränteintäkter och utdelningar R4 8314 Skattefria ränteintäkter R4
+
NE
sid.
2
8330 Valutakursdifferenser på
fordringar och placeringar
R4
84 Räntekostnader och liknande
BAS-konton Underkonton
Rad Rad
8410 Räntekostnader för skulder R8
8430 Valutakursdifferenser på
skulder R8
88 Bokslutsdispositioner
89 Årets resultat
BAS-konton Underkonton
Rad Rad
8990 Resultat R11 R11 8999 Årets resultat R11
Kontoplan_K1_2018_ver1
-41
View File
@@ -1,41 +0,0 @@
. Media Kit-generator
Standardverktyg i creator-ekonomin 2025, men inget svenskt verktyg erbjuder det. Kreatörer bygger idag media kits manuellt i Canva.
Implementation: Dra in analytics från Instagram, TikTok, YouTube via API. Generera professionell PDF med demografi, engagemangsgrad, tidigare samarbeten. Inkludera prisförslag baserat på följarantal och engagemang. Automatisk uppdatering kvartalsvis.
Svårighetsgrad: Medel. API-integrationer finns redan delvis i din app.
3. Avtalsmallar och e-signering
Creator-specifika avtal saknas helt på den svenska marknaden. Verktyg som CreatorIQ, RosterGrid och HoneyBook erbjuder detta internationellt. Svenska kreatörer använder inga avtal alls eller kopierar generiska mallar.
Implementation: Mallbibliotek anpassat för svenska influencersamarbeten: leveranser, betalningsvillkor, contenträttigheter, exklusivitet, bytesaffärer. Integrera e-signering (BankID om möjligt). Lagra i kampanjhanteringssystemet.
Svårighetsgrad: Medel. Juridisk granskning krävs men templaten är straightforward.
4. Automatiserade betalningspåminnelser och inkasso-workflow
Ingen kreatör vill jaga betalningar. Det är det näst vanligaste klagomålet efter sena betalningar överhuvudtaget.
Implementation: Automatiska påminnelser vid 15, 30 och 45 dagar. Eskaleringsflöde. Professionell inkassokommunikation. Eventuellt kopplad till fakturafinaliseringen ovan.
Svårighetsgrad: Låg. Primärt e-post/SMS-automation.
5. Multi-currency rate cards och prisriktlinjer
Kreatörer, särskilt nya, underprissätter sig systematiskt. Internationella plattformar erbjuder pricing benchmarks.
Implementation: Föreslå priser baserat på följarantal, engagemang, plattform, nisch. Svenska branschriktlinjer. Visa vad liknande kreatörer tar betalt. Integrera med faktureringsflödet.
Svårighetsgrad: Låg-medel. Datainsamling är huvudutmaningen.
Features att FÖRBÄTTRA
1. AI-assistent: Scenariospecifik, inte generisk
Din kunskapsbas med 30+ scenarion är en stark grund. Men assistenten behöver bli proaktiv, inte reaktiv.
Förbättring: "Du har 3 obetalda fakturor äldre än 30 dagar." "Din barter-deal med X saknar värdering, jag behöver marknadsvärdet." "Du har inte deklarerat periodisk sammanställning för Q3." Assistenten ska agera som en digital revisor som håller koll, inte som en FAQ.
2. Bokföring: "Creator Mode" som default
BAS-kontoplanen är korrekt men onödig för 90% av målgruppen. De behöver inte se konto 2641.
Förbättring: Förenklad vy med creator-kategorier: "Plattformsintäkter", "Samarbeten", "Barter", "Resor", "Utrustning". Full BAS i bakgrunden för SIE-export. Automatisk kategorisering via AI. En-klicks-avstämning för vanliga transaktioner.
3. Bankintegration: Automatisk matchning
PSD2-synk och swipe-kategorisering finns. Saknas: automatisk matchning av banktransaktioner mot fakturor. Flagga transaktioner som kan vara oredovisade intäkter. Kassaflödesprognos baserad på betalningshistorik.
4. Gåvo-/produktspårning: Fullständig skattehantering
Nuvarande feature finns men behöver utökas: automatisk beräkning av marknadsvärde för skatt, spåra om produkt behålls/returneras/ges bort/används som arbetsredskap, generera dokumentation mot Skatteverket, varning när en produkt blir en skattepliktig händelse. Skatteverket granskar influencersektorn aktivt. Det här är compliance-kritiskt.
5. Kampanjhantering: Content-workflow
Lägg till content approval-flöden, varumärkesportal där kunder kan granska leveranser, automatisk spårning av publiceringsschema, prestandamätning per kampanj (räckvidd, engagemang, konverteringar).
Features att ÖVERVÄGA (lägre prioritet)
Affiliate-integration: Många kreatörer tjänar via Adtraction, Awin, Amazon Associates. Auto-import av provisionsdata och skatterapportering. Relevant men inte brådskande.
Kvittomatchning: OCR finns. Lägg till automatisk matchning av kvitto mot banktransaktion. Minskar dubbelinmatning.
Team-access: Kreatörer som skalar anställer assistenter. Begränsade roller (bokförare, assistent, revisor) med auditlogg.
Features att IFRÅGASÄTTA
Fullständigt bokföringsmodulen
De flesta kreatörer behöver inte fullständig BAS-bokföring och kommer ändå outsourca till revisor. Överväg att göra "Exportera till revisor"-paket till huvudflödet och behålla förenklat läge som default. Den avancerade vyn blir opt-in, inte standard.
Kalender som fristående feature
Deadlines är viktiga men kan duplicera verktyg kreatören redan använder. Om användningen är låg, integrera med Google Calendar istället och ta bort egen kalendervy. Deadlines och påminnelser kan leva i appen utan att vara en separat sektion.
-186
View File
@@ -1,186 +0,0 @@
# Influencer Business-in-a-Box MVP
## Quick Context for AI Builders
Financial management SaaS for Swedish influencers/content creators. Supports both enskild firma (sole proprietor) and aktiebolag (limited company). Core problem: tax anxiety and admin overload. Core solution: automated compliance and simplified invoicing.
## Tech Stack
- **Framework**: Next.js 14 (App Router) + TypeScript
- **UI**: Tailwind CSS + shadcn/ui + Framer Motion
- **Database**: Supabase (PostgreSQL with RLS)
- **Auth**: Supabase Auth (magic link)
- **Hosting**: Vercel
- **PDF**: @react-pdf/renderer
- **Banking**: Enable Banking (PSD2 AISP)
- **Currency**: Riksbanken API for exchange rates
## Documentation Index
| Doc | Purpose |
|-----|---------|
| [01-PRD.md](./docs/01-PRD.md) | Product requirements, user stories, success metrics |
| [02-ARCHITECTURE.md](./docs/02-ARCHITECTURE.md) | Technical architecture, PSD2 strategy, security |
| [03-DATABASE-SCHEMA.md](./docs/03-DATABASE-SCHEMA.md) | Full Supabase schema with RLS policies |
| [04-API-SPECIFICATION.md](./docs/04-API-SPECIFICATION.md) | All API endpoints, VAT rules, momsdeklaration rutor |
| [05-UI-SPECIFICATION.md](./docs/05-UI-SPECIFICATION.md) | Wireframes, component specs, design system |
| [06-IMPLEMENTATION-GUIDE.md](./docs/06-IMPLEMENTATION-GUIDE.md) | Step-by-step build guide with code examples |
| [07-FUTURE-FEATURES.md](./docs/07-FUTURE-FEATURES.md) | Phase 2+ features: benefits tracking, NE-bilaga, Skatteverket APIs |
| [08-BAS-ACCOUNTING-GUIDE.md](./docs/08-BAS-ACCOUNTING-GUIDE.md) | BAS account mapping, MCC codes, auto-categorization rules |
## MVP Feature Scope
### Included in MVP
1. **Entity Support**: Both enskild firma and aktiebolag with appropriate tax logic
2. **Bank Integration**: Enable Banking (PSD2) with multiple account support
3. **Tax Dashboard**: "Disponibelt saldo" with F-skatt comparison warning
4. **Transaction Categorization**: Swipe UI with schablonavdrag suggestions
5. **Multi-currency Invoicing**: SEK, EUR, USD with Riksbanken auto-conversion
6. **VAT Automation**: Swedish (25%), EU reverse charge (0%), Export (0%)
7. **Credit Notes**: Kreditfaktura support for invoice corrections
8. **Expense Warnings**: Alert on non-deductible lifestyle costs
9. **Configurable Settings**: Momsperiod (monthly/quarterly), custom invoice numbering
### Deferred to Later
- Receipt photo capture and matching
- Benefits/gifts tracking (förmånshantering)
- NE-bilaga / Årsredovisning auto-generation
- Automated payment reminders
- Quotes/proforma invoices
- English UI
## Entity Type Tax Differences
| Aspect | Enskild Firma | Aktiebolag |
|--------|---------------|------------|
| Profit tax | Egenavgifter (28.97%) + kommunalskatt (~32%) | Bolagsskatt (20.6%) |
| Owner payment | Eget uttag | Lön + utdelning |
| Annual filing | NE-bilaga | Årsredovisning + INK2 |
## Key Swedish Tax & Legal Concepts
| Term | English | Rate/Rule |
|------|---------|-----------|
| Enskild firma | Sole proprietorship | Personal liability, NE-bilaga |
| Aktiebolag (AB) | Limited company | 20.6% bolagsskatt, årsredovisning |
| Egenavgifter | Self-employment contributions | 28.97% (enskild firma only) |
| Arbetsgivaravgifter | Employer contributions | 31.42% (AB paying salary) |
| Bolagsskatt | Corporate tax | 20.6% on AB profit |
| Kommunalskatt | Municipal income tax | ~30-35% varies |
| F-skatt | Business tax registration | Required for invoicing |
| Debiterad preliminärskatt | Preliminary tax set | Monthly F-skatt payment amount |
| Moms | VAT | 25% standard, threshold 80k SEK |
| Momsperiod | VAT reporting period | Monthly or quarterly |
| Räkenskapsår | Fiscal year | Calendar or brutet (broken) |
| Brutet räkenskapsår | Non-calendar fiscal year | e.g., July 1 - June 30 |
| Omvänd skattskyldighet | Reverse charge | EU B2B: buyer reports VAT |
| Kreditfaktura | Credit note | Corrects/cancels issued invoice |
| Schablonavdrag | Standard deduction | Fixed amounts (hemmakontor 2000 kr/år) |
| AGI-deklaration | Employer declaration | Monthly report for AB salaries |
| SIE-fil | Standard Import Export | Accounting data exchange format |
## Entity Type Tax Differences
| Aspect | Enskild Firma | Aktiebolag |
|--------|---------------|------------|
| Profit tax | Egenavgifter (28.97%) + kommunalskatt (~32%) | Bolagsskatt (20.6%) |
| Owner payment | Eget uttag | Lön + utdelning |
| Salary costs | N/A | Arbetsgivaravgifter (31.42%) |
| Annual filing | NE-bilaga | Årsredovisning + INK2 |
| Periodiseringsfond | Max 30% | Max 25% |
**MVP Note:** For AB, system tracks salary payments and calculates arbetsgivaravgifter. Lön/utdelning optimization (3:12-reglerna) deferred to future phase.
## MVP Technical Notes
| Aspect | Implementation |
|--------|----------------|
| Banking | Enable Banking (PSD2 AISP) |
| Email | Supabase built-in |
| Payment on invoices | Bank transfer only (future: third-party) |
| Partial payments | Not supported (paid/unpaid binary) |
| Fiscal year | Calendar + brutet räkenskapsår |
| Multi-user | Single user per account |
| UI Language | Swedish only |
| Data import | SIE file (future feature) |
## Momsdeklaration Ruta Quick Reference
| Ruta | When Used |
|------|-----------|
| 05 | Swedish domestic sales with 25% VAT |
| 39 | EU B2B services (reverse charge) |
| 40 | Export to non-EU |
| 21 | Service purchases from EU (input) |
| 48 | Self-assessed output VAT on foreign purchases |
## Critical Implementation Notes
1. **All tax figures are estimates** - display disclaimer prominently
2. **VIES validation required** for EU reverse charge - without it, charge 25% VAT
3. **Non-custodial design** - app NEVER holds user funds
4. **Multi-currency**: Store original + SEK converted amounts, use Riksbanken rate on invoice date
5. **Invoice numbers**: Allow custom starting number (user may have history elsewhere)
6. **Credit notes**: Must reference original invoice, use negative amounts
7. **Expense warnings** - clothing, cosmetics, gym are NOT deductible per Kammarrätten rulings
8. **7-year retention** - Bokföringslagen requires, GDPR does not override for accounting records
9. **RLS on all tables** - users must only access own data
10. **Swedish UI only** for MVP
## Schablonavdrag Defaults
| Type | Amount | Notes |
|------|--------|-------|
| Hemmakontor | 2 000 kr/år | If dedicated room |
| Bilkostnader | 18.50 kr/mil | Business travel |
| Telefon | Actual or % split | Based on business use |
## Quick Start for Builder AI
1. Read docs in order: PRD → Architecture → Schema → API → UI → Implementation → Future
2. Start with auth + middleware (including onboarding redirect logic)
3. Build onboarding wizard (6 steps, all required except preliminary tax)
4. Implement Enable Banking integration in onboarding step 6
5. Build dashboard with tax calculations (adapt for entity type)
6. Add invoice creation with multi-currency + VIES validation
7. Implement credit note flow
8. Polish UI (Swedish language)
## File Structure
```
/
├── app/
│ ├── (auth)/login/page.tsx
│ ├── (onboarding)/onboarding/ # Wizard steps 1-6
│ ├── (dashboard)/
│ │ ├── page.tsx # Dashboard
│ │ ├── transactions/page.tsx
│ │ ├── invoices/
│ │ │ ├── page.tsx
│ │ │ ├── new/page.tsx
│ │ │ └── [id]/credit/page.tsx # Credit notes
│ │ ├── customers/page.tsx
│ │ └── settings/page.tsx
│ └── api/...
├── components/
│ ├── ui/ # shadcn components
│ ├── onboarding/ # Wizard components
│ ├── dashboard/
│ ├── transactions/
│ └── invoices/
├── lib/
│ ├── supabase/
│ ├── banking/enable-banking.ts
│ ├── currency/riksbanken.ts
│ ├── tax/
│ │ ├── calculator.ts
│ │ ├── expense-warnings.ts
│ │ ├── schablonavdrag.ts
│ │ └── fskatt-warning.ts
│ └── invoice/vat-rules.ts
├── types/index.ts
└── supabase/migrations/
```
File diff suppressed because one or more lines are too long
@@ -1,381 +0,0 @@
# AI Categorization Extension — Implementation Summary
This document describes the ai-categorization extension: the second first-party extension built on the Part 3 event bus and extension registry. It uses Claude Haiku to suggest BAS account categorizations for bank transactions, following the same canonical pattern established by receipt-ocr.
---
## Problem
Transaction categorization is the most frequent daily task. Every downstream report (momsdeklaration, income statement, balance sheet, NE-bilaga, SRU export) depends on transactions being mapped to the correct BAS accounts.
Before this extension, suggestions came only from:
- **Mapping rules** — user-defined merchant/description patterns (confidence 0.8)
- **Pattern matching** — built-in regex heuristics from `expense-warnings.ts` (confidence 0.6)
- **User history** — most frequently used categories (confidence 0.1–0.5)
These sources cover common recurring transactions but fail on novel descriptions, edge cases, and new users with no history.
## Solution
An `ai-categorization` extension that:
1. Listens to `transaction.synced` events and auto-generates AI-powered category suggestions for uncategorized transactions
2. Stores suggestions in `extension_data` (pre-computed, ready when the user opens the transaction list)
3. Exposes an on-demand API for manual "AI suggest" triggers
4. Merges AI suggestions into the existing suggestion pipeline alongside rule/pattern/history sources
**Key constraint:** Suggestions only, never auto-commit. The extension stores suggestions in `extension_data` but never creates journal entries. The user confirms via the existing categorization UI, preserving audit trail integrity.
---
## Files Changed
### New files
| File | Purpose |
|------|---------|
| `extensions/ai-categorization/categorizer.ts` | AI provider interface + Anthropic implementation |
| `extensions/ai-categorization/index.ts` | Extension: settings, event handler, public API, extension object |
| `app/api/extensions/ai-categorization/settings/route.ts` | GET/PATCH API for per-user extension settings |
| `app/api/extensions/ai-categorization/suggestions/route.ts` | GET (pre-computed) / POST (on-demand) suggestions API |
### Modified files
| File | Change |
|------|--------|
| `lib/extensions/loader.ts` | Imported and registered `aiCategorizationExtension` |
| `lib/transactions/category-suggestions.ts` | Added `'ai'` to `SuggestedCategory.source` union; added `mergeAiSuggestions()` |
| `app/api/transactions/suggest-categories/route.ts` | Reads pre-computed AI suggestions from `extension_data` and merges into results |
---
## Provider Abstraction
The architecture doc requires "no hard dependency on any specific AI provider". The categorizer implements this via a `CategorizationProvider` interface.
### `CategorizationProvider` interface
```typescript
interface CategorizationProvider {
categorize(
transactions: TransactionForCategorization[],
context: CategorizationContext
): Promise<CategorizationSuggestion[]>
}
```
### `TransactionForCategorization`
Minimal transaction data sent to the AI:
```typescript
interface TransactionForCategorization {
id: string
description: string
amount: number // negative = expense, positive = income
date: string
merchant_name: string | null
mcc_code: number | null
currency: string
}
```
### `CategorizationContext`
Contextual data that improves accuracy:
```typescript
interface CategorizationContext {
entityType: EntityType // 'enskild_firma' | 'aktiebolag'
recentHistory: { description: string; category: string }[] // last 50 categorized
}
```
### `CategorizationSuggestion`
The result per transaction:
```typescript
interface CategorizationSuggestion {
transactionId: string
category: TransactionCategory
basAccount: string // BAS account number (e.g. '5420')
taxCode: string | null // 'MPI', 'MP1', or null
confidence: number // 0.0–1.0
reasoning: string // Swedish-language explanation
isPrivate: boolean // true = likely private expense
}
```
### `AnthropicCategorizationProvider`
The default implementation using `@anthropic-ai/sdk` (already a project dependency):
- Model: `claude-haiku-4-5-20251001` (same as receipt-analyzer, chosen for cost efficiency)
- Batch size: max 20 transactions per API call (cross-transaction pattern recognition)
- Retry logic: 3 attempts with exponential backoff, no retry on JSON parse errors
- Response validation: filters to valid transaction IDs and valid `TransactionCategory` values
The system prompt includes:
1. Full `TransactionCategory` → BAS account mapping table
2. Entity type (EF uses 2013 for private, AB uses 2893)
3. Swedish non-deductible expense rules (kläder, gym, kosmetika, etc. with legal references)
4. VAT treatment rules (bank fees exempt, standard 25% otherwise)
5. User's recent categorization history (up to 30 entries) for learning patterns
---
## Extension: `extensions/ai-categorization/index.ts`
### Settings
```typescript
interface AiCategorizationSettings {
autoSuggestEnabled: boolean // default: true
confidenceThreshold: number // default: 0.7
providerModel: string // default: 'claude-haiku-4-5-20251001'
}
```
Stored as an `extension_data` row with `extension_id='ai-categorization'`, `key='settings'`, `value=<jsonb>`.
- `getSettings(userId)` reads from DB and merges with defaults (forward-compatible)
- `saveSettings(userId, partial)` merges partial update with current, upserts on `(user_id, extension_id, key)`
### Event Handler: `transaction.synced`
When new transactions arrive from banking sync:
1. **Gate:** Is `autoSuggestEnabled` in user's settings? — if not, return
2. **Gate:** Filter to uncategorized transactions only (`is_business === null`) — if none, return
3. Fetch entity type from `company_settings`
4. Fetch user's last 50 categorized transactions (for learning patterns)
5. Call `provider.categorize(batch, context)`
6. Filter suggestions to those above `confidenceThreshold`
7. Store each qualified suggestion to `extension_data` as `key: "suggestion:{transactionId}"`
8. Log summary with `[ai-categorization]` prefix
### Public API: `categorizeTransactions(userId, transactionIds)`
Exported function for on-demand categorization (used by the suggestions POST endpoint):
1. Fetch transactions by IDs
2. Build context (entity type + history)
3. Call provider
4. Store all suggestions (no threshold filtering — user explicitly requested)
5. Return suggestions
### Extension Object
```typescript
export const aiCategorizationExtension: Extension = {
id: 'ai-categorization',
name: 'AI Kategorisering',
version: '1.0.0',
eventHandlers: [
{ eventType: 'transaction.synced', handler: handleTransactionSynced },
],
settingsPanel: {
label: 'AI Kategorisering',
path: '/settings/extensions/ai-categorization',
},
async onInstall(ctx) { await saveSettings(ctx.userId, DEFAULT_SETTINGS) },
}
```
---
## Suggestion Storage
Suggestions are stored as individual rows in `extension_data`:
| Column | Value |
|--------|-------|
| `user_id` | The user who owns the transaction |
| `extension_id` | `'ai-categorization'` |
| `key` | `'suggestion:{transactionId}'` |
| `value` | The full `CategorizationSuggestion` object as JSONB |
This per-transaction key scheme allows:
- Fast lookup by transaction ID (used by the suggest-categories route)
- Batch lookup via `IN` clause on keys
- Natural overwrite on re-categorization (upsert on unique constraint)
---
## Suggestions API: `app/api/extensions/ai-categorization/suggestions/route.ts`
### `GET ?transaction_ids=id1,id2,...`
Reads pre-computed suggestions from `extension_data`. Returns only what's already stored — no AI call.
Response: `{ suggestions: { [txId]: CategorizationSuggestion } }`
### `POST { transaction_ids: [...] }`
Triggers on-demand AI categorization via `categorizeTransactions()`. Stores results and returns them.
Response: `{ suggestions: { [txId]: CategorizationSuggestion } }`
Both endpoints limit to 50 transaction IDs per request.
---
## Settings API: `app/api/extensions/ai-categorization/settings/route.ts`
Mirrors the receipt-ocr settings route exactly:
- **GET** — Returns the current user's merged settings (DB value + defaults)
- **PATCH** — Accepts a partial settings object, validates keys against allowlist (`autoSuggestEnabled`, `confidenceThreshold`, `providerModel`), saves via `saveSettings()`
---
## Integration with Existing Suggestion Pipeline
### `lib/transactions/category-suggestions.ts`
Two changes:
1. **Source type extended:** `SuggestedCategory.source` union widened from `'mapping_rule' | 'pattern' | 'history'` to `'mapping_rule' | 'pattern' | 'history' | 'ai'`
2. **New merge function:**
```typescript
function mergeAiSuggestions(
existing: SuggestedCategory[],
aiSuggestions: { category: string; basAccount: string; confidence: number; reasoning: string }[]
): SuggestedCategory[]
```
Inserts AI suggestions into the list, deduplicating by category (skips categories already present from higher-priority sources). Returns top 5 sorted by confidence.
### `app/api/transactions/suggest-categories/route.ts`
After computing rule/pattern/history suggestions for each transaction, the route now:
1. Fetches pre-computed AI suggestions from `extension_data` for all requested transaction IDs (single batch query)
2. For each transaction with an AI suggestion, calls `mergeAiSuggestions()` to blend it in
3. Returns the merged result
This means AI suggestions appear alongside existing sources with no latency — they were pre-computed during bank sync.
---
## Suggestion Priority
The existing pipeline already sorts by confidence. With AI added, the effective priority becomes:
| Source | Typical Confidence | When |
|--------|--------------------|------|
| Mapping rules | 0.8 | User-defined patterns match |
| AI | 0.7–0.95 | Pre-computed from sync |
| Pattern matching | 0.6 | Built-in regex matches |
| User history | 0.1–0.5 | Most frequently used categories |
AI suggestions naturally slot between mapping rules and pattern matching. For novel transactions where no mapping rule or pattern exists, AI becomes the top suggestion.
---
## Event Flow
```
Bank Sync
|
POST /banking/sync
|
emit transaction.synced
|
+---> receipt-ocr extension (auto-match receipts)
|
+---> ai-categorization extension
|
Gate: autoSuggestEnabled?
Gate: has uncategorized transactions?
|
Fetch entity type + history
Call AnthropicCategorizationProvider.categorize()
Filter by confidenceThreshold
Store to extension_data (suggestion:{txId})
|
[suggestions pre-computed and waiting]
User opens transaction list
|
POST /api/transactions/suggest-categories
|
+---> getSuggestedCategories() [mapping rules + patterns + history]
+---> Read extension_data [pre-computed AI suggestions]
+---> mergeAiSuggestions()
|
v
Response: merged suggestions with source labels
|
User sees: "AI: Programvara (5420) — confidence 0.9"
User clicks "AI suggest" button (on-demand)
|
POST /api/extensions/ai-categorization/suggestions
|
+---> categorizeTransactions()
| Call AI provider
| Store results
|
v
Response: fresh AI suggestions
```
---
## Existing Code Reused
| Import | From | Used in |
|--------|------|---------|
| `Anthropic` | `@anthropic-ai/sdk` | `AnthropicCategorizationProvider` |
| `getSettings()`/`saveSettings()` pattern | `extensions/receipt-ocr/index.ts` | Settings management (same pattern) |
| `getSuggestedCategories()` | `lib/transactions/category-suggestions.ts` | Existing pipeline (unchanged) |
| `createClient()` | `lib/supabase/server.ts` | DB access throughout |
No existing service logic was duplicated. The extension adds a new AI-powered source to the existing suggestion pipeline.
---
## Architectural Patterns Followed
1. **Suggestions only, never auto-commit.** AI writes to `extension_data`, never to `journal_entries`. The user confirms via existing categorization UI.
2. **Provider abstraction from day one.** `CategorizationProvider` interface means the AI model is swappable without changing extension logic.
3. **Cost-efficient model.** Claude Haiku (same as receipt-analyzer) keeps per-sync costs low.
4. **Batch processing.** One AI call per sync handles up to 20 transactions. Cross-transaction context (e.g., "all ICA transactions = groceries") improves accuracy.
5. **Pre-computed suggestions.** AI runs on sync, results are stored. No user-facing latency when opening the transaction list.
6. **Graceful degradation.** If the AI call fails, the handler catches and logs. Existing rule/pattern/history suggestions still work. No user-facing error.
7. **Gate-guarded.** Every handler checks user settings before doing work.
8. **One-way dependency.** Base never imports from `extensions/`. Only `loader.ts` imports the extension object.
9. **`[ai-categorization]` prefix.** Console logging convention for grep-ability.
---
## No New Migrations
No database schema changes were needed. The existing `extension_data` table (created in Part 3, migration `20240101000020_extension_data.sql`) handles all storage:
- Settings: `key='settings'`
- Per-transaction suggestions: `key='suggestion:{transactionId}'`
The unique constraint `(user_id, extension_id, key)` ensures upsert semantics.
---
## Verification
- `npx tsc --noEmit` — zero TypeScript errors
- `npx vitest run` — all 78 existing tests pass (11 test files)
- Manual: trigger bank sync → check console for `[ai-categorization]` logs
- Manual: open transactions page → uncategorized transactions show AI suggestions (source: `'ai'`) alongside existing pattern/history suggestions
- Manual: disable `autoSuggestEnabled` in settings → sync does not trigger AI
- Manual: `POST /api/extensions/ai-categorization/suggestions` with transaction IDs → returns on-demand suggestions
- Manual: `GET /api/extensions/ai-categorization/settings` → returns default settings
- Manual: `PATCH /api/extensions/ai-categorization/settings` → updates settings
@@ -1,260 +0,0 @@
# NE-bilaga Extension
## Overview
The NE-bilaga extension generates the NE appendix (Näringsverksamhet) for income tax reporting of enskild firma (sole proprietorship) to Skatteverket. It maps BAS account balances to NE declaration rutor R1-R11 and optionally produces downloadable SRU files.
Only relevant for **enskild firma** (`entity_type = 'enskild_firma'`). The tab is hidden for AB users.
Previously embedded as core code in `lib/reports/ne-declaration.ts` and `app/api/reports/ne-declaration/route.ts`, the logic was extracted into a proper extension following the same pattern as `extensions/sru-export/`.
## Architecture
```
extensions/ne-bilaga/
├── index.ts Extension definition (registered in loader)
├── ne-engine.ts Account-to-ruta mapping and balance calculation
├── types.ts NE-specific type definitions (canonical source)
└── NEDeclarationView.tsx UI component for the NE-bilaga reports tab
app/api/extensions/ne-bilaga/
└── route.ts GET /api/extensions/ne-bilaga (json + sru)
```
### Relationship to SRU Export
The NE-bilaga extension and the SRU Export extension serve different purposes but share SRU file generation utilities:
```
NE-bilaga path (EF only):
ne-engine.ts (hard-coded R1-R11 account mappings) → lib/reports/sru-generator.ts
SRU Export path (EF + AB):
sru-engine.ts (reads sru_code from DB) → sru-generator.ts (any form type)
```
The NE-bilaga engine uses hard-coded account ranges to map balances to NE rutor. The SRU Export engine reads `sru_code` from `chart_of_accounts` for a more generic approach. Both use the shared `sruFileToString()` and `generateSRUFile()` from `lib/reports/sru-generator.ts`.
### Dead code removed
The backward-compatibility shim `lib/reports/ne-declaration.ts` (which re-exported from the extension) has been deleted. It had zero importers after the API route `app/api/reports/ne-declaration/route.ts` was removed earlier. All consumers now import directly from the extension or from `@/types`.
## NE Declaration Rutor
### Revenue (R1-R4)
| Ruta | Account Range | Description |
|---|---|---|
| R1 | 3000-3499 (excl 3100) | Forsaljning med moms (25%) |
| R2 | 3100, 3900, 3970-3980 | Momsfria intakter |
| R3 | 3200-3299 | Bil/bostadsforman |
| R4 | 8310-8330 | Ranteintakter |
### Expenses (R5-R10)
| Ruta | Account Range | Description |
|---|---|---|
| R5 | 4000-4990 | Varuinkop |
| R6 | 5000-6990, 7970 | Ovriga kostnader |
| R7 | 7000-7699 | Lonekostnader |
| R8 | 8400-8499 | Rantekostnader |
| R9 | 7820 | Avskrivningar fastighet |
| R10 | 7700-7899 (excl 7820) | Avskrivningar ovrigt |
### Result
| Ruta | Calculation | Description |
|---|---|---|
| R11 | (R1+R2+R3+R4) - (R5+R6+R7+R8+R9+R10) | Arets resultat |
### Gift handling
- Gifts **with** consideration: R1 (VAT-liable exchange transaction)
- Gifts **without** consideration: R2 via account 3900
- Deductible gifts: R6 via account 5460
## API Reference
### GET /api/extensions/ne-bilaga
Generate NE declaration for a fiscal period.
**Query parameters:**
| Parameter | Required | Description |
|---|---|---|
| `period_id` | Yes | Fiscal period UUID |
| `format` | No | `json` (default) or `sru` |
**Response (format=json):**
```json
{
"data": {
"fiscalYear": {
"id": "uuid",
"name": "2025",
"start": "2025-01-01",
"end": "2025-12-31",
"isClosed": false
},
"rutor": {
"R1": 150000,
"R2": 0,
"R3": 0,
"R4": 500,
"R5": 45000,
"R6": 30000,
"R7": 0,
"R8": 1200,
"R9": 0,
"R10": 5000,
"R11": 69300
},
"breakdown": {
"R1": {
"accounts": [
{ "accountNumber": "3001", "accountName": "Forsaljning tjanster 25%", "amount": 150000 }
],
"total": 150000
}
},
"companyInfo": {
"companyName": "Mitt Foretag",
"orgNumber": "801234-5678"
},
"warnings": ["Rakenskapsaret ar inte stangt. Siffrorna kan andras."]
}
}
```
**Response (format=sru):** Downloads a `.sru` file with `Content-Disposition: attachment`.
**Error responses:**
| Status | Condition |
|---|---|
| 400 | Missing `period_id` parameter |
| 401 | Not authenticated |
| 500 | Entity type is not `enskild_firma`, period not found, or database error |
## Engine Details
### Balance calculation
1. Fetch all **posted** journal entries for the given fiscal period
2. Sum `debit_amount - credit_amount` per account number
3. Match each account to an NE ruta using `NE_ACCOUNT_MAPPINGS`
4. For revenue accounts (credit-normal): negate the balance so positive = income
5. For expense accounts (debit-normal): use as-is
6. Round each ruta to whole kronor
7. Calculate R11 as total revenue minus total expenses
### Entity type guard
The engine throws if `company_settings.entity_type !== 'enskild_firma'`. This prevents accidental NE generation for AB entities.
### Warnings
The engine emits warnings when:
- The fiscal period is not closed (balances may change)
- No revenue or expenses were found (empty period)
## UI Integration
### NEDeclarationView component
The NE-bilaga UI lives in `extensions/ne-bilaga/NEDeclarationView.tsx` and is imported by the reports page:
```typescript
import { NEDeclarationView } from '@/extensions/ne-bilaga/NEDeclarationView'
```
### Reports Page
The **NE-bilaga** tab in `/reports` is conditionally rendered based on `entity_type`:
- **Visible** when `entity_type === 'enskild_firma'`
- **Hidden** for all other entity types (e.g. `aktiebolag`)
The entity type is fetched from `GET /api/settings` on page load. The tab shows:
1. **Info card** with "Hamta NE-bilaga" and "Ladda ner SRU-fil" buttons
2. **Warnings card** (orange) if the period is open or no data was found
3. **Company info card** with company name, org number, and fiscal year badge
4. **Revenue table** (R1-R4) with expandable account-level detail
5. **Expenses table** (R5-R10) with expandable account-level detail
6. **Result card** (R11) showing net result with green/red color coding
Each ruta row is clickable to expand and show contributing accounts with individual amounts.
## Extension Registration
The extension is registered in `lib/extensions/loader.ts`:
```typescript
import { neBilagaExtension } from '@/extensions/ne-bilaga'
const FIRST_PARTY_EXTENSIONS: Extension[] = [
receiptOcrExtension,
aiCategorizationExtension,
pushNotificationsExtension,
sruExportExtension,
neBilagaExtension, // ← added
]
```
The extension declares a single report type (`ne-bilaga`) for discovery via `extensionRegistry.getByCapability('reportTypes')`. It has no event handlers, no settings panel, and no sidebar items.
## Type Definitions
Canonical source: `extensions/ne-bilaga/types.ts` (re-exported from `types/index.ts` for convenience):
- **`NEDeclaration`** — Top-level response shape with fiscal year, rutor, breakdown, company info, warnings
- **`NEDeclarationRutor`** — Record of R1-R11 number values
- **`NEAccountMapping`** — Mapping config: ruta, account ranges (with exclusions), isExpense flag
- **`NE_RUTA_LABELS`** — Display labels for each ruta
- **`SRURecord`** — Single SRU field code + value pair
- **`SRUFile`** — Collection of SRU records with generation timestamp
## Files Changed
### Created
| File | Purpose |
|---|---|
| `extensions/ne-bilaga/index.ts` | Extension definition with report type registration |
| `extensions/ne-bilaga/ne-engine.ts` | Account mapping + balance calculation (moved from `lib/reports/`) |
| `extensions/ne-bilaga/types.ts` | Canonical type definitions (`NEDeclaration`, `NEDeclarationRutor`, etc.) |
| `extensions/ne-bilaga/NEDeclarationView.tsx` | UI component (extracted from reports page) |
| `app/api/extensions/ne-bilaga/route.ts` | API endpoint with json/sru format support |
### Modified
| File | Change |
|---|---|
| `lib/extensions/loader.ts` | Added `neBilagaExtension` to `FIRST_PARTY_EXTENSIONS` |
| `app/(dashboard)/reports/page.tsx` | Imports `NEDeclarationView` from extension instead of inlining it |
| `types/index.ts` | NE types replaced with re-exports from `extensions/ne-bilaga/types.ts` |
### Deleted
| File | Reason |
|---|---|
| `app/api/reports/ne-declaration/route.ts` | Replaced by `app/api/extensions/ne-bilaga/route.ts` |
| `lib/reports/ne-declaration.ts` | Dead backward-compat shim with zero importers |
### Reused (not modified)
| File | What was reused |
|---|---|
| `lib/reports/sru-generator.ts` | `generateSRUFile()`, `sruFileToString()`, `getSRUFilename()` |
## Verification
- `npx tsc --noEmit` — zero errors
- `GET /api/extensions/ne-bilaga?period_id=X&format=json` returns NE declaration data
- `GET /api/extensions/ne-bilaga?period_id=X&format=sru` downloads `.sru` file
- NE-bilaga tab visible for EF users, hidden for AB users
- Reports page renders identically to before for EF users
- Extension appears in `extensionRegistry.getAll()` and `extensionRegistry.getByCapability('reportTypes')`
- `next build` succeeds with zero errors
@@ -1,169 +0,0 @@
# Part 1: Database Foundation & Compliance Core — Implementation Record
## What was implemented
8 Supabase migrations, TypeScript type updates, 4 new service files, and modifications to 3 existing files. No UI changes.
---
## Migrations
### Migration 11: ALTER Existing Tables
`supabase/migrations/20240101000011_alter_existing_tables.sql`
- `chart_of_accounts` — added `sru_code text` for Skatteverket SRU mapping
- `journal_entries` — added `committed_at timestamptz`, `reversed_by_id uuid FK→self`, `reverses_id uuid FK→self`, `correction_of_id uuid FK→self`
- `journal_entries` — expanded `source_type` CHECK to include `storno`, `correction`, `import`, `system`
- `journal_entry_lines` — added `tax_code text`, `cost_center text`, `project text`
- `fiscal_periods` — added `locked_at timestamptz`, `retention_expires_at date`
### Migration 12: Tax Code Engine
`supabase/migrations/20240101000012_tax_codes.sql`
New table `tax_codes` with columns: `id`, `user_id`, `code`, `description`, `rate`, `moms_basis_boxes text[]`, `moms_tax_boxes text[]`, `moms_input_boxes text[]`, flags (`is_output_vat`, `is_reverse_charge`, `is_eu`, `is_export`, `is_oss`, `is_system`).
RLS: select own + system (user_id IS NULL), insert/update/delete own only.
Seeded 12 system tax codes: MP1 (25%), MP2 (12%), MP3 (6%), MPI, MPI12, MPI6, IV (intra-EU), EUS (EU sale), IP (import), EXP (export), OSS, NONE.
New function `seed_tax_codes_for_user(p_user_id)` copies system codes to user scope.
### Migration 13: Document Archive
`supabase/migrations/20240101000013_document_archive.sql`
New table `document_attachments` with: storage fields (`storage_path`, `file_name`, `file_size_bytes`, `mime_type`), integrity (`sha256_hash NOT NULL`), version chain (`version`, `original_id FK→self`, `superseded_by_id FK→self`, `is_current_version`), digitization metadata (`uploaded_by`, `upload_source`, `digitization_date`), linkage (`journal_entry_id FK ON DELETE RESTRICT`, `journal_entry_line_id FK ON DELETE RESTRICT`).
No DELETE RLS policy — deletion handled by trigger in migration 17.
### Migration 14: Audit Log
`supabase/migrations/20240101000014_audit_log.sql`
New table `audit_log`: `user_id uuid NOT NULL` (no FK cascade — survives user deletion), `action text` with CHECK constraint, `table_name`, `record_id`, `actor_id`, `old_state jsonb`, `new_state jsonb`, `description`. No `updated_at` — append-only.
BEFORE UPDATE and BEFORE DELETE triggers raise exception to enforce immutability.
### Migration 15: Dimensions
`supabase/migrations/20240101000015_dimensions.sql`
Two new tables:
- `cost_centers` (`user_id`, `code`, `name`, `is_active`) with UNIQUE(user_id, code)
- `projects` (`user_id`, `code`, `name`, `is_active`, `start_date`, `end_date`) with UNIQUE(user_id, code)
Both with standard RLS and updated_at triggers.
### Migration 16: Voucher Sequence Hardening
`supabase/migrations/20240101000016_voucher_sequences.sql`
New table `voucher_sequences` (`user_id`, `fiscal_period_id`, `voucher_series`, `last_number`) for tracking sequence state.
Replaced `next_voucher_number()` with concurrent-safe version using `INSERT ON CONFLICT DO UPDATE RETURNING` (row-level lock instead of MAX+1).
New DEFERRABLE constraint trigger `check_balance_on_post` validates debit==credit when an entry transitions from draft to posted.
New function `detect_voucher_gaps(p_user_id, p_fiscal_period_id, p_series)` returns gap ranges for compliance reporting.
### Migration 17: Enforcement Triggers
`supabase/migrations/20240101000017_enforcement_triggers.sql`
8 trigger functions:
1. **`enforce_journal_entry_immutability()`** — allows draft→draft, draft→posted, posted→reversed. Blocks all other updates/deletes on committed entries.
2. **`enforce_journal_entry_line_immutability()`** — blocks modifications to lines of posted/reversed entries.
3. **`enforce_period_lock()`** — rejects journal_entries writes when `is_closed=true` OR `locked_at IS NOT NULL`.
4. **`enforce_period_lock_documents()`** — blocks document attachment to entries in locked periods.
5. **`block_document_deletion()`** — blocks deletion if linked to committed entry or within retention window. Logs blocked attempts to audit_log.
6. **`enforce_retention_journal_entries()`** — blocks journal entry deletion within 7-year retention window.
7. **`set_committed_at()`** — auto-sets `committed_at = now()` on draft→posted transition.
8. **`calculate_retention_expiry()`** — auto-sets `retention_expires_at = period_end + 7 years`. Backfills existing rows.
### Migration 18: Audit Logging Triggers
`supabase/migrations/20240101000018_audit_triggers.sql`
SECURITY DEFINER function `write_audit_log()` that detects action type from TG_OP and state transitions (draft→posted = COMMIT, posted→reversed = REVERSE, locked_at set = LOCK_PERIOD, is_closed set = CLOSE_PERIOD). Captures old_state/new_state as JSONB.
AFTER triggers on: `journal_entries`, `journal_entry_lines`, `chart_of_accounts`, `document_attachments`, `fiscal_periods`, `company_settings`, `tax_codes`.
---
## TypeScript Changes
### Modified types in `types/index.ts`
| Type | Change |
|------|--------|
| `JournalEntrySourceType` | Added `'storno' \| 'correction' \| 'import' \| 'system'` |
| `JournalEntry` | Added `committed_at`, `reversed_by_id`, `reverses_id`, `correction_of_id` |
| `JournalEntryLine` | Added `tax_code`, `cost_center`, `project` |
| `CreateJournalEntryLineInput` | Added optional `tax_code`, `cost_center`, `project` |
| `FiscalPeriod` | Added `locked_at`, `retention_expires_at` |
| `BASAccount` | Added `sru_code` |
### New types added to `types/index.ts`
- `TaxCode` interface, `TaxCodeId` union type
- `DocumentAttachment` interface, `DocumentUploadSource` type, `CreateDocumentAttachmentInput`
- `AuditLogEntry` interface, `AuditAction` union type
- `CostCenter` interface
- `Project` interface
- `VoucherGap` interface
---
## New Service Files
### `lib/core/audit/audit-service.ts`
Read-only service (audit log is written by DB triggers):
- `getAuditLog(userId, filters)` — paginated query with action/table/date filters
- `getEntityHistory(userId, tableName, recordId)` — full mutation history of one record
- `getCorrectionChain(userId, journalEntryId)` — traces original→storno→corrected via linked IDs
### `lib/core/documents/document-service.ts`
- `uploadDocument(userId, file, metadata)` — computes SHA-256 via Web Crypto, uploads to Supabase Storage, creates record
- `createNewVersion(userId, originalId, file)` — creates new version, marks old as superseded (WORM)
- `linkToJournalEntry(userId, documentId, journalEntryId)` — links document to entry
- `verifyIntegrity(userId, documentId)` — re-downloads, re-hashes, compares to stored hash
### `lib/core/tax/tax-code-service.ts`
- `getTaxCodes(userId)` — returns user codes + system codes
- `getTaxCodeByCode(userId, code)` — single lookup, user code takes precedence
- `calculateMomsFromTaxCodes(userId, periodStart, periodEnd)` — sums journal lines by tax_code, maps to moms boxes via tax_codes table
- `seedTaxCodes(userId)` — calls `seed_tax_codes_for_user` RPC
### `lib/core/bookkeeping/storno-service.ts`
- `correctEntry(userId, originalEntryId, correctedLines)` — 3-step correction:
1. Creates storno entry with swapped debits/credits, `source_type='storno'`, `reverses_id` set
2. Creates corrected entry with new data, `source_type='correction'`, `correction_of_id` set
3. Marks original as reversed with `reversed_by_id` set
4. Returns `{ reversal, corrected }`
---
## Modified Existing Files
### `lib/bookkeeping/engine.ts`
- New `buildLineInserts()` helper that includes `tax_code`, `cost_center`, `project` in all line inserts
- New `createDraftEntry(userId, input)` — inserts as draft with `voucher_number=0`, no commit
- New `commitEntry(userId, entryId)` — assigns voucher number via `next_voucher_number` RPC, transitions to posted (DB triggers handle `committed_at` and balance validation)
- Existing `createJournalEntry()` kept as convenience wrapper (create + immediate commit)
- `reverseEntry()` rewritten: now sets `reverses_id` on the reversal entry, sets `reversed_by_id` on the original, uses `source_type='storno'`, preserves dimensions on reversed lines
### `lib/reports/vat-declaration.ts`
- Added `TaxCode` import
- New `calculateVatDeclarationFromTaxCodes(userId, periodType, year, period)` — generates momsdeklaration by querying journal_entry_lines grouped by `tax_code`, then mapping via `tax_codes` table to moms boxes
- Legacy `calculateVatDeclaration()` preserved for backward compatibility (invoice/transaction/receipt approach)
### `lib/reports/sie-export.ts`
- Now fetches `cost_centers` and `projects` tables
- Outputs `#DIM 1 "Kostnadsställe"` and `#DIM 6 "Projekt"` dimension definitions
- Outputs `#OBJEKT` records for each cost center and project
- Outputs `#SRU` records from `chart_of_accounts.sru_code` after each `#KONTO`
- `#TRANS` lines now include dimension object lists: `{1 "CC01" 6 "P01"}` when cost_center/project are set
---
## Verification
- `npx tsc --noEmit` passes with zero errors
@@ -1,198 +0,0 @@
# Part 2: Period Management & Year-End Closing
## Overview
Part 2 implements **year-end closing (årsbokslut)** — the process that legally closes a fiscal year per Bokföringslagen. This includes period locking, closing entry generation, opening balance propagation, and the API surface to drive the workflow.
**Depends on Part 1:** immutable ledger, audit trail, tax codes, document archive, period lock enforcement, retention protection.
---
## What Was Built
### Migration 19: Period Closing Metadata
**File:** `supabase/migrations/20240101000019_period_closing.sql`
Three new columns on `fiscal_periods`:
| Column | Type | Purpose |
|--------|------|---------|
| `closing_entry_id` | `uuid FK → journal_entries` | Links to the year-end closing journal entry |
| `opening_balance_entry_id` | `uuid FK → journal_entries` | Links to the opening balance entry in this period |
| `previous_period_id` | `uuid FK → fiscal_periods` | Chain link to the prior period for validation |
One new trigger:
- **`enforce_opening_balance_immutability`** — Once `opening_balance_entry_id` or `closing_entry_id` are set, they cannot be changed. This prevents tampering with the closing chain after the fact.
---
### TypeScript Types
**File:** `types/index.ts`
Extended `FiscalPeriod` with the three new nullable fields.
New interfaces:
| Interface | Purpose |
|-----------|---------|
| `YearEndValidation` | Result of readiness check: `ready`, `errors[]`, `warnings[]`, `draftCount`, `voucherGaps[]`, `trialBalanceBalanced` |
| `YearEndPreview` | Preview of closing: `netResult`, `closingAccount` (2099/2010), `closingLines[]`, `resultAccountSummary[]` |
| `YearEndResult` | Result of execution: `closingEntry`, `nextPeriod`, `openingBalanceEntry` |
| `PeriodStatus` | Status summary: lock/close/draft/opening state |
---
### Period Service
**File:** `lib/core/bookkeeping/period-service.ts`
| Function | What it does |
|----------|-------------|
| `lockPeriod(userId, fiscalPeriodId)` | Sets `locked_at = now()`. Validates period exists, belongs to user, isn't already locked/closed. After locking, the `enforce_period_lock` trigger (from Part 1) blocks new journal entries. |
| `closePeriod(userId, fiscalPeriodId)` | Sets `is_closed = true, closed_at = now()`. Requires: already locked AND `closing_entry_id` is set. This is the final, permanent state. |
| `createNextPeriod(userId, currentPeriodId)` | Creates the next fiscal year. Computes dates from the current period's length to handle **brutet räkenskapsår** (broken fiscal years, e.g. Jul–Jun). Sets `previous_period_id` for chain validation. Auto-generates name like "FY 2025" or "FY 2025/2026". |
| `getPeriodStatus(userId, fiscalPeriodId)` | Returns a summary: `is_locked`, `is_closed`, `has_closing_entry`, `has_opening_balances`, `draft_count`, `next_period_exists`. |
---
### Year-End Service
**File:** `lib/core/bookkeeping/year-end-service.ts`
This is the core new logic.
#### `validateYearEndReadiness(userId, fiscalPeriodId)` → `YearEndValidation`
Checks preconditions before allowing year-end closing:
- **Blocking errors** (prevent closing):
- Period already closed
- Closing entry already exists
- Draft journal entries exist (must be posted or deleted)
- Trial balance is not balanced
- **Warnings** (informational):
- Voucher number gaps detected (via `detect_voucher_gaps()` SQL function)
- No posted entries in the period
#### `previewYearEndClosing(userId, fiscalPeriodId)` → `YearEndPreview`
Generates a preview without persisting anything:
1. Looks up `entity_type` from `company_settings` → determines closing account:
- **Aktiebolag (AB):** account `2099` (Årets resultat)
- **Enskild firma (EF):** account `2010` (Eget kapital)
2. Runs income statement to get `net_result`
3. Gets trial balance, filters to class 3–8 accounts
4. For each account with a non-zero balance: creates a line that zeros it
5. Adds a final balancing line to the closing account (2099/2010)
6. Returns the preview with all lines and a summary of result accounts
#### `executeYearEndClosing(userId, fiscalPeriodId)` → `YearEndResult`
Full orchestration (the main entry point):
```
1. validateYearEndReadiness() → abort if errors
2. previewYearEndClosing() → get closing lines
3. createJournalEntry() → create closing entry (source_type: 'year_end')
4. UPDATE fiscal_periods → set closing_entry_id
5. lockPeriod() → lock the period
6. closePeriod() → permanently close
7. createNextPeriod() → create next fiscal year
8. generateOpeningBalances() → carry forward class 1-2 balances
9. Return { closingEntry, nextPeriod, openingBalanceEntry }
```
#### `generateOpeningBalances(userId, closedPeriodId, nextPeriodId)` → `JournalEntry`
Creates opening balance entries in the new period:
1. Gets trial balance of the closed period (after closing entry)
2. Filters to balance sheet accounts (class 1–2) with non-zero closing balance
3. Creates a journal entry with `source_type: 'opening_balance'`:
- Debit accounts get debit opening, credit accounts get credit opening
4. Verifies the entry is balanced (total debit = total credit)
5. Sets `opening_balance_entry_id` and `opening_balances_set = true` on the next period
**Key invariant:** UB (utgående balans) of year N == IB (ingående balans) of year N+1.
---
### API Routes
All follow the existing pattern: authenticate via Supabase, delegate to service, return JSON.
| Method | Path | Handler |
|--------|------|---------|
| `POST` | `/api/bookkeeping/fiscal-periods/[id]/lock` | `lockPeriod()` |
| `GET` | `/api/bookkeeping/fiscal-periods/[id]/year-end` | `validateYearEndReadiness()` + `previewYearEndClosing()` |
| `POST` | `/api/bookkeeping/fiscal-periods/[id]/year-end` | `executeYearEndClosing()` |
| `POST` | `/api/bookkeeping/fiscal-periods/[id]/close` | `closePeriod()` |
---
## Reused Components
| Component | From | Used by |
|-----------|------|---------|
| `generateTrialBalance()` | `lib/reports/trial-balance.ts` | Balance aggregation for closing + opening entries |
| `generateIncomeStatement()` | `lib/reports/income-statement.ts` | Net result calculation |
| `createJournalEntry()` | `lib/bookkeeping/engine.ts` | Creating closing + opening entries (auto-posts) |
| `validateBalance()` | `lib/bookkeeping/engine.ts` | Pre-flight balance check |
| `detect_voucher_gaps()` | Migration 16 SQL function | Gap validation during readiness check |
| `enforce_period_lock` trigger | Migration 17 | Blocks writes after locking |
| `enforce_journal_entry_immutability` trigger | Migration 17 | Protects closing/opening entries after posting |
---
## Period Lifecycle Diagram
```
┌─────────┐
│ OPEN │ ← Journal entries can be posted
└────┬────┘
│ lockPeriod()
▼
┌─────────┐
│ LOCKED │ ← No new entries (enforce_period_lock trigger)
└────┬────┘
│ closePeriod() (requires closing_entry_id)
▼
┌─────────┐
│ CLOSED │ ← Permanent, immutable
└─────────┘
```
The `executeYearEndClosing()` function drives the full flow from OPEN → CLOSED in one call, including creating the closing entry, locking, closing, creating the next period, and generating opening balances.
---
## Verification Checklist
- [x] `npx tsc --noEmit` — zero TypeScript errors
- [ ] Migration 19 applies cleanly (`\d fiscal_periods` shows new columns)
- [ ] `GET /api/bookkeeping/fiscal-periods/[id]/year-end` returns preview with net result
- [ ] `POST /api/bookkeeping/fiscal-periods/[id]/year-end` creates closing entry, locks, closes, creates next period, generates opening balances
- [ ] Closing entry zeros all class 3–8 accounts
- [ ] Opening balance entry in next period matches UB of closed period (class 1–2 only)
- [ ] Closed period rejects new journal entries (period lock trigger)
- [ ] Period with draft entries → validation fails with blocking error
- [ ] Period already closed → validation fails
- [ ] EF entity type → closing goes to 2010 (not 2099)
---
## Files Changed/Created
| File | Action |
|------|--------|
| `supabase/migrations/20240101000019_period_closing.sql` | **Created** — 3 ALTER columns + 1 trigger |
| `types/index.ts` | **Modified** — extended FiscalPeriod, added 4 new interfaces |
| `lib/core/bookkeeping/period-service.ts` | **Created** — lockPeriod, closePeriod, createNextPeriod, getPeriodStatus |
| `lib/core/bookkeeping/year-end-service.ts` | **Created** — validateYearEndReadiness, previewYearEndClosing, executeYearEndClosing, generateOpeningBalances |
| `app/api/bookkeeping/fiscal-periods/[id]/lock/route.ts` | **Created** — POST lock endpoint |
| `app/api/bookkeeping/fiscal-periods/[id]/year-end/route.ts` | **Created** — GET preview + POST execute |
| `app/api/bookkeeping/fiscal-periods/[id]/close/route.ts` | **Created** — POST close endpoint |
@@ -1,235 +0,0 @@
Part 3: Event Bus & Extension Registry — Implementation
## What Was Built
An in-process event bus, extension registry with static discovery, database tables for extension data and event observability, and event emission retrofitted into all existing service and API route code paths. Backend only, no UI.
This is the foundation for all Tier 1 add-ons (receipt-ocr, ai-categorization, push-notifications, ne-bilaga, etc.). Without it, every add-on would need to be hardwired into core services.
---
## New Files
### Event Bus — `lib/events/`
**`lib/events/types.ts`**
Defines the `CoreEvent` discriminated union covering 18 event types across six domains:
| Domain | Events |
|--------|--------|
| Bookkeeping | `journal_entry.drafted`, `journal_entry.committed`, `journal_entry.corrected` |
| Documents | `document.uploaded` |
| Invoicing | `invoice.created`, `invoice.sent`, `invoice.paid`, `invoice.overdue`, `credit_note.created` |
| Banking | `transaction.synced`, `transaction.categorized`, `bank.statement_received`, `bank.payment_notification` |
| Periods | `period.locked`, `period.year_closed` |
| Customers | `customer.created`, `customer.pseudonymized` |
| Audit | `audit.security_event` |
Helper types for consuming events:
- `CoreEventType` — string literal union of all event type names
- `EventPayload<T>` — extracts the payload type for a given event type
- `EventHandler<T>` — handler function signature for a specific event type
- `EventSubscription<T>` — event type + handler pair
**`lib/events/bus.ts`**
The event bus singleton. Key design:
- `eventBus.on(eventType, handler)` — subscribe, returns unsubscribe function
- `eventBus.emit(event)` — runs all handlers via `Promise.allSettled` (a failing handler never crashes the emitter)
- `eventBus.clear()` — remove all handlers (for testing)
- Handlers run concurrently, errors logged to console
- Module-level singleton (persists across requests in same Node.js process)
**`lib/events/index.ts`** — Barrel export.
### Extension Registry — `lib/extensions/`
**`lib/extensions/types.ts`**
The `Extension` interface — the contract for all add-ons:
```typescript
interface Extension {
id: string
name: string
version: string
// Surfaces
routes?: RouteDefinition[]
apiRoutes?: ApiRouteDefinition[]
sidebarItems?: SidebarItem[]
eventHandlers?: ExtensionEventHandler[]
mappingRuleTypes?: MappingRuleTypeDefinition[]
reportTypes?: ReportDefinition[]
settingsPanel?: SettingsPanelDefinition
taxCodes?: TaxCodeDefinition[]
dimensionTypes?: DimensionDefinition[]
// Lifecycle
onInstall?(ctx: ExtensionContext): Promise<void>
onUninstall?(ctx: ExtensionContext): Promise<void>
}
```
Supporting types: `RouteDefinition`, `ApiRouteDefinition`, `SidebarItem`, `ReportDefinition`, `SettingsPanelDefinition`, `TaxCodeDefinition`, `DimensionDefinition`, `MappingRuleTypeDefinition`, `ExtensionEventHandler`, `ExtensionContext`.
**`lib/extensions/registry.ts`**
The `extensionRegistry` singleton:
- `register(extension)` — stores extension, wires event handlers to the bus
- `unregister(extensionId)` — unhooks handlers, removes extension
- `getAll()` — all registered extensions
- `get(id)` — specific extension by ID
- `getByCapability(key)` — extensions that have a specific surface (e.g. all extensions with `reportTypes`)
- `clear()` — remove all (for testing)
**`lib/extensions/loader.ts`**
Static extension discovery. Next.js bundling requires explicit imports, not dynamic filesystem scanning. Contains an empty `FIRST_PARTY_EXTENSIONS` array — extensions are added here as they are built. `loadExtensions()` has an idempotency guard.
**`lib/extensions/index.ts`** — Barrel export.
### Initialization — `lib/init.ts`
`ensureInitialized()` — calls `loadExtensions()` once. Called from API routes that emit events (at module scope, not per-request).
### Example Extension — `extensions/example-logger/index.ts`
Minimal reference implementation that logs `journal_entry.committed` and `document.uploaded` events to console. Not wired into the loader by default — exists as a template for building real extensions.
---
## Migration
**`supabase/migrations/20240101000020_extension_data.sql`**
Two tables:
**`extension_data`** — generic key-value store for extensions:
- Columns: `id`, `user_id`, `extension_id`, `key`, `value` (jsonb), `created_at`, `updated_at`
- `UNIQUE(user_id, extension_id, key)`
- RLS: select, insert, update, delete own rows
- Auto-update `updated_at` trigger
**`event_log`** — append-only event observability:
- Columns: `id`, `user_id`, `event_type`, `payload` (jsonb), `created_at`
- RLS: select + insert only (no update, no delete — append-only)
- Indexes on `(user_id, event_type)` and `created_at`
---
## Type Additions — `types/index.ts`
Placeholder types for event payloads not yet fully built:
| Type | Purpose |
|------|---------|
| `CreditNote` | Extends `Invoice` with required `credited_invoice_id` |
| `CAMT053Statement` | Bank statement (CAMT parsing not yet implemented) |
| `CAMT054Notification` | Payment notification (CAMT parsing not yet implemented) |
| `AuditSecurityEvent` | Security event payload for audit events |
| `ExtensionDataRecord` | Row type for the `extension_data` table |
---
## Retrofitted Event Emissions
The pattern is identical everywhere: import `eventBus`, call `await eventBus.emit(...)` after the successful operation. No control flow changes, no return type changes. All events include a `userId` field for RLS-scoped observability.
### Phase A — Service Layer
| File | Function | Event |
|------|----------|-------|
| `lib/bookkeeping/engine.ts` | `createDraftEntry()` | `journal_entry.drafted` |
| `lib/bookkeeping/engine.ts` | `commitEntry()` | `journal_entry.committed` |
| `lib/bookkeeping/engine.ts` | `createJournalEntry()` | `journal_entry.committed` |
| `lib/bookkeeping/engine.ts` | `reverseEntry()` | `journal_entry.committed` |
| `lib/core/bookkeeping/storno-service.ts` | `correctEntry()` | `journal_entry.corrected` |
| `lib/core/documents/document-service.ts` | `uploadDocument()` | `document.uploaded` |
| `lib/core/bookkeeping/period-service.ts` | `lockPeriod()` | `period.locked` |
| `lib/core/bookkeeping/year-end-service.ts` | `executeYearEndClosing()` | `period.year_closed` |
### Phase B — API Routes
| File | Event |
|------|-------|
| `app/api/invoices/route.ts` (POST) | `invoice.created` |
| `app/api/invoices/route.ts` (createCreditNote) | `credit_note.created` |
| `app/api/invoices/[id]/send/route.ts` | `invoice.sent` |
| `app/api/customers/route.ts` (POST) | `customer.created` |
| `app/api/transactions/[id]/categorize/route.ts` | `transaction.categorized` |
| `app/api/banking/sync/route.ts` | `transaction.synced` |
API routes also call `ensureInitialized()` at module scope to ensure extensions are loaded before events are emitted.
### Deferred (Phase C)
These events are defined in the type system but not yet emitted because the underlying infrastructure doesn't exist:
| Event | Reason |
|-------|--------|
| `invoice.paid` | Payment matching with kursdifferens not fully wired |
| `invoice.overdue` | Needs cron-based detection |
| `bank.statement_received` | CAMT053 parsing not implemented |
| `bank.payment_notification` | CAMT054 parsing not implemented |
| `customer.pseudonymized` | GDPR flow not implemented |
| `audit.security_event` | Already logged at DB level; app-level TBD |
---
## Design Decisions
1. **In-process bus** — architecture specifies "in-process handlers initially, add webhook dispatch when external plugin consumers exist." No message queue, no outbox pattern at the event bus level.
2. **`Promise.allSettled`** — a failing handler never crashes the emitting service. Errors are logged to console. The emitter's control flow is never affected.
3. **Module-level singletons** — `eventBus` and `extensionRegistry` persist across requests in the same Node.js process. They are not per-request or per-user.
4. **Static extension imports** — Next.js bundling requires explicit imports in `loader.ts`, not dynamic `fs.readdirSync`. Extensions are added to the `FIRST_PARTY_EXTENSIONS` array as they are built.
5. **One-way dependency** — `lib/events/` depends on nothing except `types/`. Core services import from `lib/events/`. Extensions import from `lib/core/`, `lib/events/`, and `lib/extensions/`. The base never imports from `extensions/`.
6. **`ensureInitialized()` at module scope** — API routes call this at the top of the file (not inside request handlers). This means extensions are loaded once when the module is first imported by Next.js, not on every request.
7. **Every event payload includes `userId`** — enables RLS-scoped event logging and per-user extension behavior without needing to pass auth context through the bus.
---
## How to Build an Extension
1. Create a directory under `extensions/your-extension/`
2. Export an object satisfying the `Extension` interface
3. Import it in `lib/extensions/loader.ts` and add to `FIRST_PARTY_EXTENSIONS`
Example (see `extensions/example-logger/index.ts`):
```typescript
import type { Extension } from '@/lib/extensions/types'
import type { EventPayload } from '@/lib/events/types'
export const myExtension: Extension = {
id: 'my-extension',
name: 'My Extension',
version: '0.1.0',
eventHandlers: [
{
eventType: 'journal_entry.committed',
handler: async (payload: EventPayload<'journal_entry.committed'>) => {
// Your logic here
},
},
],
}
```
---
## Verification
- `npx tsc --noEmit` — zero errors
- All existing API routes unchanged in behavior — event emission is additive, never blocking
- Extension system is fully wired but dormant (empty extension list) until extensions are added to the loader
@@ -1,251 +0,0 @@
# Part 4: Year-End Closing UI (Årsbokslut)
## Overview
Part 4 implements the **user interface for year-end closing** — a 4-step wizard that guides the user through validating, previewing, and executing the annual closing per Bokföringslagen. This is a pure frontend implementation; all backend services, API routes, and database migrations were completed in Part 2.
**Depends on Part 2:** period-service, year-end-service, fiscal period API routes (`GET`/`POST` at `/api/bookkeeping/fiscal-periods/[id]/year-end`).
---
## What Was Built
### Year-End Wizard Page
**File:** `app/(dashboard)/bookkeeping/year-end/page.tsx`
A single `'use client'` page containing a 4-step wizard at route `/bookkeeping/year-end`. The wizard maps directly to the existing API surface:
| Step | Label | API Call | Purpose |
|------|-------|----------|---------|
| 0 | Välj period | `GET /api/bookkeeping/fiscal-periods` | Select which fiscal period to close |
| 1 | Validering | `GET /api/bookkeeping/fiscal-periods/[id]/year-end` | Check readiness (errors + warnings) |
| 2 | Förhandsgranskning | *(uses data from step 1)* | Review closing entry before committing |
| 3 | Genomför | `POST /api/bookkeeping/fiscal-periods/[id]/year-end` | Execute with confirmation dialog |
#### Step 0: Period Selection
- Fetches all fiscal periods on mount
- Pre-selects the first open (non-closed) period
- Displays each period as a selectable card with:
- Period name and date range (`period_start – period_end`)
- Status badge: **Öppen** (default), **Låst** (outline), **Stängd** (secondary, disabled)
- Closed periods are visually dimmed and not selectable
#### Step 1: Validation
- Calls `GET /api/bookkeeping/fiscal-periods/[id]/year-end` which returns `{ validation, preview }` from parallel `validateYearEndReadiness()` + `previewYearEndClosing()`
- Displays a ready/not-ready banner:
- Green `CheckCircle2` + "Perioden är redo för årsbokslut" when `validation.ready === true`
- Red `AlertCircle` + "Perioden kan inte stängas ännu" when `validation.ready === false`
- **Blocking errors** (red): draft entries, unbalanced trial balance, already closed, closing entry exists
- **Warnings** (amber): voucher number gaps, no posted entries
- Detail cards showing draft count and trial balance status
- Voucher gap badges when gaps exist
- "Validera igen" button to re-run checks after fixing issues
- "Nästa" button gated on `validation.ready === true`
#### Step 2: Preview
Three cards displaying the preview data:
1. **Net result highlight** — Large centered number with color coding (green for profit, red for loss), closing account label (e.g. "2099 — Årets resultat")
2. **Result account summary** — Table of class 3–8 accounts being zeroed (account number, name, amount)
3. **Closing journal lines** — Expandable/collapsible table showing the full closing entry (account, description, debit, credit) with a totals row
#### Step 3: Execute
Pre-execution state:
- Summary of actions: closing entry creation, period lock + close, next period + opening balances
- Irreversibility warning banner (amber) referencing Bokföringslagen
- "Genomför årsbokslut" button (destructive variant) opens a confirmation dialog
Confirmation dialog (`Dialog` component):
- Repeats period name and net result
- "Avbryt" and "Stäng perioden" (destructive) buttons
Post-execution success state:
- `SuccessAnimation` overlay with celebration variant
- Summary card showing: closing entry link, closed period badge, new period name, opening balances status
- "Tillbaka till bokföring" navigation
---
### Bookkeeping Page Link
**File:** `app/(dashboard)/bookkeeping/page.tsx`
Added a header action button linking to the year-end wizard:
```tsx
<Button variant="outline" asChild>
<Link href="/bookkeeping/year-end">
<Lock className="mr-2 h-4 w-4" />
Årsbokslut
</Link>
</Button>
```
The header was restructured from a plain `<div>` to a `flex items-center justify-between` layout to accommodate the button alongside the existing title and description.
---
## User Flow Diagram
```
/bookkeeping
│
│ Click "Årsbokslut" button
▼
┌─────────────────────────────────────────────────────┐
│ Step 0: Välj period │
│ ┌───────────────────────────────────┐ │
│ │ FY 2024 (2024-01-01 – 2024-12-31) │ [Öppen] │
│ └───────────────────────────────────┘ │
│ ┌───────────────────────────────────┐ │
│ │ FY 2023 (2023-01-01 – 2023-12-31) │ [Stängd] │
│ └───────────────────────────────────┘ │
│ [Nästa →] │
└────────────────────────┬────────────────────────────┘
│
GET /api/bookkeeping/fiscal-periods/[id]/year-end
│
▼
┌─────────────────────────────────────────────────────┐
│ Step 1: Validering │
│ ✅ Perioden är redo för årsbokslut │
│ ─ or ─ │
│ ❌ 3 draft entries must be posted │
│ ⚠️ Voucher gaps: 5–7 │
│ │
│ [← Tillbaka] [Validera igen] [Nästa →] │
└────────────────────────┬────────────────────────────┘
│
▼
┌─────────────────────────────────────────────────────┐
│ Step 2: Förhandsgranskning │
│ │
│ Årets resultat: 150 000,00 kr │
│ Bokförs på 2099 — Årets resultat │
│ │
│ ┌─ Resultatkonton som nollställs ────────────────┐ │
│ │ 3001 Tjänsteintäkter -500 000,00 │ │
│ │ 5010 Lokalhyra 200 000,00 │ │
│ │ 6570 Bankavgifter 150 000,00 │ │
│ └────────────────────────────────────────────────┘ │
│ │
│ [← Tillbaka] [Nästa →] │
└────────────────────────┬────────────────────────────┘
│
▼
┌─────────────────────────────────────────────────────┐
│ Step 3: Genomför │
│ │
│ ⚠️ Denna åtgärd kan inte ångras │
│ │
│ [← Tillbaka] [Genomför årsbokslut] │
│ │ │
│ ┌─────────▼──────────┐ │
│ │ Bekräfta årsbokslut │ │
│ │ Stäng FY 2024? │ │
│ │ │ │
│ │ [Avbryt] [Stäng] │ │
│ └─────────┬──────────┘ │
│ │ │
│ POST /api/.../year-end │
│ │ │
│ ▼ │
│ ✅ Årsbokslutet är genomfört │
│ • Bokslutsverifikation [Visa] │
│ • Period stängd [Stängd] │
│ • Nytt räkenskapsår FY 2025 │
│ • Ingående balanser [Skapade] │
│ │
│ [← Tillbaka till bokföring] │
└─────────────────────────────────────────────────────┘
```
---
## State Management
All state is local to the page component via `useState`:
| State | Type | Purpose |
|-------|------|---------|
| `step` | `number` (0–3) | Current wizard step |
| `periods` | `FiscalPeriod[]` | All fiscal periods from API |
| `selectedPeriodId` | `string` | Currently selected period |
| `validation` | `YearEndValidation \| null` | Validation result from API |
| `preview` | `YearEndPreview \| null` | Preview result from API |
| `result` | `YearEndResult \| null` | Execution result from API |
| `loading` | `boolean` | Loading state for validation fetch |
| `loadingPeriods` | `boolean` | Loading state for periods fetch |
| `executing` | `boolean` | Loading state for POST execution |
| `error` | `string \| null` | Error message banner |
| `showConfirmDialog` | `boolean` | Confirmation dialog visibility |
| `showLinesDetail` | `boolean` | Expandable closing lines table |
| `showSuccess` | `boolean` | Success animation overlay |
---
## Reused Components
| Component | From | Used for |
|-----------|------|----------|
| `Card`, `CardContent`, `CardHeader`, `CardTitle` | `components/ui/card.tsx` | All step containers |
| `Button` | `components/ui/button.tsx` | Navigation, actions, links |
| `Badge` | `components/ui/badge.tsx` | Period status, voucher gaps, success indicators |
| `Table`, `TableBody`, `TableCell`, `TableHead`, `TableHeader`, `TableRow` | `components/ui/table.tsx` | Result accounts + closing lines |
| `Dialog`, `DialogContent`, `DialogHeader`, `DialogTitle`, `DialogDescription`, `DialogFooter` | `components/ui/dialog.tsx` | Execution confirmation |
| `Skeleton` | `components/ui/skeleton.tsx` | Loading states |
| `SuccessAnimation` | `components/ui/success-animation.tsx` | Post-execution celebration overlay |
| `useToast` | `components/ui/use-toast.tsx` | Error notifications |
| `formatAmount()` | Inline helper (same pattern as `reports/page.tsx`) | Swedish locale number formatting |
Lucide icons used: `CheckCircle2`, `AlertCircle`, `AlertTriangle`, `ArrowLeft`, `ArrowRight`, `Loader2`, `Lock`, `BookOpen`, `ChevronDown`, `ChevronUp`.
---
## API Endpoints Used
No new API routes were created. The wizard consumes existing endpoints from Part 2:
| Method | Path | Response | Used in step |
|--------|------|----------|--------------|
| `GET` | `/api/bookkeeping/fiscal-periods` | `{ data: FiscalPeriod[] }` | 0 (period list) |
| `GET` | `/api/bookkeeping/fiscal-periods/[id]/year-end` | `{ data: { validation: YearEndValidation, preview: YearEndPreview } }` | 1 + 2 (validation + preview) |
| `POST` | `/api/bookkeeping/fiscal-periods/[id]/year-end` | `{ data: YearEndResult }` | 3 (execution) |
---
## Files Changed/Created
| File | Action |
|------|--------|
| `app/(dashboard)/bookkeeping/year-end/page.tsx` | **Created** — 4-step year-end closing wizard (700 lines) |
| `app/(dashboard)/bookkeeping/page.tsx` | **Modified** — Added "Årsbokslut" link button in header, restructured header layout |
**No new API routes.** No backend changes. No new dependencies. No database migrations.
---
## Verification Checklist
- [x] `npx tsc --noEmit` — zero TypeScript errors
- [x] `npm run build` — builds clean, `/bookkeeping/year-end` route registered
- [x] `npx vitest run` — all 78 existing tests pass
- [ ] Navigate to `/bookkeeping` → "Årsbokslut" button visible in header
- [ ] Click "Årsbokslut" → wizard loads at step 0 with period selector
- [ ] Closed periods appear dimmed and cannot be selected
- [ ] Select open period → "Nästa" → validation step loads with skeleton, then shows results
- [ ] Period with draft entries → red error "X draft journal entries must be posted or deleted"
- [ ] Period with unbalanced trial balance → red error "Trial balance is not balanced"
- [ ] Period with voucher gaps → amber warning with gap badges
- [ ] Fully valid period → green "Perioden är redo för årsbokslut", "Nästa" enabled
- [ ] Preview step → net result displayed with correct color, result accounts table, expandable closing lines
- [ ] EF entity type → closing account shows "2010 — Eget kapital"
- [ ] AB entity type → closing account shows "2099 — Årets resultat"
- [ ] Execute step → irreversibility warning shown, "Genomför årsbokslut" opens confirmation dialog
- [ ] Confirmation dialog → "Stäng perioden" triggers POST, success animation + summary displayed
- [ ] Success state → shows new period name, closing entry link, opening balances badge
@@ -1,312 +0,0 @@
# Push Notifications Extension
## Overview
The `push-notifications` extension converts system events into instant push notifications delivered via the Web Push API. It is the third first-party extension, following `receipt-ocr` and `ai-categorization`.
The extension operates in two modes:
1. **Event-driven** — Instant notifications triggered by the event bus (`period.locked`, `receipt.matched`, etc.)
2. **Cron-based** — Scheduled checks for time-dependent conditions (upcoming tax deadlines, overdue invoices)
Both modes share a single send pipeline that handles settings checks, quiet hours, duplicate prevention, delivery, logging, and subscription cleanup.
---
## Architecture
```
Event Bus
|
+---------+---------+
| |
period.locked receipt.matched
invoice.sent receipt.extracted
period.year_closed
| |
v v
+-----------------------------+
| Event Handlers (index.ts) |
| gate: check setting |
| build: payload-builders |
+-------------+---------------+
|
v
+-----------------------------+ +---------------------------+
| sendNotificationToUser() | <---- | Cron Scheduler |
| (notification-sender.ts) | | (notification-scheduler) |
| | | tax deadlines, invoices |
| 1. push_enabled? | +---------------------------+
| 2. quiet hours? |
| 3. duplicate? |
| 4. get subscriptions |
| 5. web-push send |
| 6. log to notification_log |
| 7. disable 410 subs |
+-----------------------------+
```
### File Structure
```
extensions/push-notifications/
index.ts # Extension object, settings, 5 event handlers
notification-sender.ts # Unified send pipeline, VAPID config, helpers
payload-builders.ts # All notification payload constructors
notification-scheduler.ts # Cron-based tax deadline & invoice scheduling
app/api/extensions/push-notifications/
settings/route.ts # GET/PATCH settings API
cron/route.ts # Daily cron (imports from extension)
subscribe/route.ts # Subscription management (imports VAPID from extension)
```
---
## Event Subscriptions
| Event | Handler | Default Enabled | Notification Content |
|-------|---------|-----------------|---------------------|
| `period.locked` | `handlePeriodLocked` | Yes | "{period.name} har lasts" |
| `period.year_closed` | `handleYearClosed` | Yes | "Arsbokslut klart for {period.name}" |
| `invoice.sent` | `handleInvoiceSent` | No | "Faktura {number} skickad" |
| `receipt.extracted` | `handleReceiptExtracted` | Yes | "Kvitto analyserat: {merchant}" |
| `receipt.matched` | `handleReceiptMatched` | Yes | "Kvitto matchat mot transaktion" |
Each event handler follows the **gate pattern**:
1. Extract `userId` from event payload
2. Load extension settings from `extension_data`
3. Check if the specific notification type is enabled
4. Build the payload via `payload-builders.ts`
5. Call `sendNotificationToUser()` from the sender module
---
## Cron Scheduling
The cron endpoint (`GET /api/extensions/push-notifications/cron`) runs daily at 09:00 via Vercel Cron and handles two time-dependent notification types:
### Tax Deadlines
Queries the `deadlines` table for uncompleted tax deadlines due in 7 days, 1 day, or today. Checks the user-level `tax_deadlines_enabled` setting in `notification_settings` before sending.
### Invoice Reminders
Queries the `invoices` table for sent/overdue invoices due in 3 days, today, or overdue by 3/7 days. Checks the user-level `invoice_reminders_enabled` setting before sending.
Both call `sendNotificationToUser()` which handles the full pipeline (quiet hours, duplicate check, send, log).
---
## Settings
### Extension Settings (event-driven toggles)
Stored in the `extension_data` table under `extension_id = 'push-notifications'`, `key = 'settings'`.
```typescript
interface PushNotificationSettings {
periodLockedEnabled: boolean // default: true
periodYearClosedEnabled: boolean // default: true
invoiceSentEnabled: boolean // default: false
receiptExtractedEnabled: boolean // default: true
receiptMatchedEnabled: boolean // default: true
}
```
### First-Party Settings (existing tables)
The `notification_settings` table controls transport-level and category-level preferences:
| Column | Type | Purpose |
|--------|------|---------|
| `push_enabled` | boolean | Master push toggle |
| `tax_deadlines_enabled` | boolean | Cron: tax deadline notifications |
| `invoice_reminders_enabled` | boolean | Cron: invoice due/overdue |
| `quiet_start` | text | Quiet hours start (e.g., "21:00") |
| `quiet_end` | text | Quiet hours end (e.g., "08:00") |
Both layers are checked before sending. Extension settings gate event-driven notifications; `notification_settings` gates the transport and cron-based categories.
---
## API
### GET /api/extensions/push-notifications/settings
Returns the current user's event-driven notification toggles.
**Response 200:**
```json
{
"data": {
"periodLockedEnabled": true,
"periodYearClosedEnabled": true,
"invoiceSentEnabled": false,
"receiptExtractedEnabled": true,
"receiptMatchedEnabled": true
}
}
```
**Response 401:** `{ "error": "Unauthorized" }`
### PATCH /api/extensions/push-notifications/settings
Updates one or more event-driven notification toggles. Only the provided keys are updated; others remain unchanged.
**Request:**
```json
{
"receiptExtractedEnabled": false,
"invoiceSentEnabled": true
}
```
**Response 200:**
```json
{
"data": {
"periodLockedEnabled": true,
"periodYearClosedEnabled": true,
"invoiceSentEnabled": true,
"receiptExtractedEnabled": false,
"receiptMatchedEnabled": true
}
}
```
**Response 400:** `{ "error": "No valid settings provided" }`
**Response 401:** `{ "error": "Unauthorized" }`
**Allowed keys:** `periodLockedEnabled`, `periodYearClosedEnabled`, `invoiceSentEnabled`, `receiptExtractedEnabled`, `receiptMatchedEnabled`
---
## Notification Types
The `NotificationType` union is defined in `extensions/push-notifications/types.ts` (re-exported from `types/index.ts` for convenience):
| Type | Source | Description |
|------|--------|-------------|
| `tax_deadline` | Cron | Upcoming tax deadline |
| `invoice_due` | Cron | Invoice approaching due date |
| `invoice_overdue` | Cron | Invoice past due date |
| `period_locked` | Event | Fiscal period was locked |
| `period_year_closed` | Event | Year-end closing completed |
| `receipt_extracted` | Event | Receipt OCR completed |
| `receipt_matched` | Event | Receipt matched to transaction |
| `invoice_sent` | Event | Invoice was sent |
These are stored as text in `notification_log.notification_type` (no migration needed).
---
## Database Tables Used
No new tables or migrations were required. The extension uses existing tables:
| Table | Usage |
|-------|-------|
| `push_subscriptions` | User's active Web Push subscriptions |
| `notification_settings` | Transport-level and category-level preferences |
| `notification_log` | Sent notification history (duplicate prevention) |
| `extension_data` | Event-driven toggle settings |
| `deadlines` | Tax deadline queries (cron) |
| `invoices` | Invoice due/overdue queries (cron) |
---
## Send Pipeline
`sendNotificationToUser(supabase, userId, payload, notificationType, referenceId, daysBefore?)` executes the following steps:
1. **Settings check** — Load `notification_settings` for the user. If `push_enabled` is false, skip.
2. **Quiet hours** — Convert current time to Sweden timezone (`Europe/Stockholm`). If within the user's quiet hours window, skip.
3. **Duplicate check** — Query `notification_log` for a matching `(user_id, notification_type, reference_id, days_before)` tuple. If found, skip.
4. **Get subscriptions** — Fetch all active push subscriptions from `push_subscriptions`. If none, skip.
5. **Send** — Deliver via the `web-push` library to all subscriptions using `Promise.allSettled`.
6. **Log** — Insert a record into `notification_log` with `delivery_status: 'sent'`.
7. **Cleanup** — Any subscription returning HTTP 410 (Gone) is marked `is_active: false`.
Returns `{ sent: boolean, reason?: string }` where reason can be: `push_disabled`, `quiet_hours`, `duplicate`, `no_subscriptions`, `send_failed`, or `error`.
---
## Registration
The extension is registered in `lib/extensions/loader.ts` alongside the other first-party extensions:
```typescript
const FIRST_PARTY_EXTENSIONS: Extension[] = [
receiptOcrExtension,
aiCategorizationExtension,
pushNotificationsExtension,
]
```
When `loadExtensions()` is called, the registry wires up all 5 event handlers to the event bus.
---
## Refactoring Notes
### Phase 1: Logic extraction (original)
The following files were deleted. All logic was moved into the extension:
| Deleted File | Logic Moved To |
|-------------|----------------|
| `lib/push/web-push.ts` | `extensions/push-notifications/notification-sender.ts` (VAPID config, send functions, types) + `extensions/push-notifications/payload-builders.ts` (payload constructors) |
| `lib/push/notification-scheduler.ts` | `extensions/push-notifications/notification-scheduler.ts` (cron scheduling) + `extensions/push-notifications/notification-sender.ts` (quiet hours, duplicate check, logging, subscription management) |
### Phase 2: Route relocation
The API routes were moved from `app/api/push/` into the extension namespace to enforce the architecture rule that the core never depends on extension code:
| Old Path | New Path |
|----------|----------|
| `app/api/push/cron/route.ts` | `app/api/extensions/push-notifications/cron/route.ts` |
| `app/api/push/subscribe/route.ts` | `app/api/extensions/push-notifications/subscribe/route.ts` |
Updated consumers:
- `components/push/PushPrompt.tsx` — fetch URLs updated to `/api/extensions/push-notifications/subscribe`
- `components/settings/NotificationSettings.tsx` — fetch URLs updated to `/api/extensions/push-notifications/subscribe`
- `vercel.json` — cron path updated to `/api/extensions/push-notifications/cron`
### Phase 3: Type extraction
Push notification types (`PushSubscription`, `NotificationSettings`, `NotificationType`, `NotificationLog`) were moved from `types/index.ts` to `extensions/push-notifications/types.ts`. Re-exports in `types/index.ts` preserve backward compatibility.
---
## Environment Variables
The extension requires the following environment variables (unchanged from before):
| Variable | Purpose |
|----------|---------|
| `NEXT_PUBLIC_VAPID_PUBLIC_KEY` | VAPID public key for client-side subscription |
| `VAPID_PRIVATE_KEY` | VAPID private key for server-side sending |
| `VAPID_SUBJECT` | VAPID subject (defaults to `mailto:support@erp-base.se`) |
| `CRON_SECRET` | Secret for authenticating cron requests |
| `NEXT_PUBLIC_SUPABASE_URL` | Supabase project URL (used by cron service client) |
| `SUPABASE_SERVICE_ROLE_KEY` | Supabase service role key (used by cron service client) |
---
## Testing
### Automated
- `npx tsc --noEmit` — Zero TypeScript errors
- `npx vitest run` — All 78 existing tests pass
### Manual Verification
1. **Period locked** — Lock a period via the UI. If the user has an active push subscription and `periodLockedEnabled` is true, they receive a push notification.
2. **Receipt extracted** — Upload a receipt image. After OCR completes, the `receipt.extracted` event fires and triggers a push notification.
3. **Receipt matched** — When a receipt is auto-matched to a transaction, the `receipt.matched` event triggers a push notification.
4. **Invoice sent** — Send an invoice. If `invoiceSentEnabled` is true (default: false), a push notification is sent.
5. **Year closed** — Complete a year-end closing. The `period.year_closed` event triggers a push notification.
6. **Settings API** — `GET /api/extensions/push-notifications/settings` returns default settings. `PATCH` with `{ "receiptExtractedEnabled": false }` disables that specific notification type.
7. **Cron** — `GET /api/extensions/push-notifications/cron` (with Bearer token) runs the tax deadline and invoice reminder checks using the extension's scheduler module.
8. **Disable toggle** — Set `receiptExtractedEnabled: false` via PATCH, then upload a receipt. No push notification should be sent for the extraction event.
@@ -1,260 +0,0 @@
# Receipt-OCR Extension — Implementation Summary
This document describes the receipt-ocr extension: the first real extension built on the Part 3 event bus and extension registry infrastructure. It bridges the document archive to the receipt pipeline via events and establishes the canonical pattern for all future extensions.
---
## Problem
Receipt-OCR functionality existed as built-in code (`lib/receipts/`, `app/api/receipts/`), but was disconnected from the event system:
- Uploading a document via the archive did not trigger OCR
- New bank transactions did not auto-match to receipts
- No domain events were emitted when receipts were extracted, matched, or confirmed
The event bus and extension registry (Part 3) were built but had zero real extensions using them.
## Solution
A `receipt-ocr` extension that:
1. Listens to `document.uploaded` events and auto-triggers OCR on images
2. Listens to `transaction.synced` events and auto-matches receipts to new transactions
3. Emits its own domain events (`receipt.extracted`, `receipt.matched`, `receipt.confirmed`) so downstream extensions can react
**Principle followed:** Services = reusable logic in `lib/`. Extensions = event-driven glue. API routes = HTTP interface.
---
## Files Changed
### New files
| File | Purpose |
|------|---------|
| `extensions/receipt-ocr/index.ts` | The extension: settings, event handlers, extension object |
| `app/api/extensions/receipt-ocr/settings/route.ts` | GET/PATCH API for per-user extension settings |
### Modified files
| File | Change |
|------|--------|
| `lib/events/types.ts` | Added `Receipt` import and 3 new events to `CoreEvent` union |
| `lib/extensions/loader.ts` | Imported and registered `receiptOcrExtension` |
| `app/api/receipts/upload/route.ts` | Emits `receipt.extracted` after successful OCR |
| `app/api/receipts/[id]/match/route.ts` | Emits `receipt.matched` after manual match; upgraded selects to fetch full objects |
| `app/api/receipts/[id]/confirm/route.ts` | Emits `receipt.confirmed` with computed business/private totals |
---
## New Events
Three events were added to `lib/events/types.ts`:
### `receipt.extracted`
Fires when OCR extraction completes on a receipt image, whether via the direct upload path or the document archive path.
```typescript
{ type: 'receipt.extracted'; payload: {
receipt: Receipt;
documentId: string | null; // null when from direct upload path
confidence: number;
userId: string;
}}
```
### `receipt.matched`
Fires when a receipt is linked to a bank transaction, whether by user manual action or extension auto-match.
```typescript
{ type: 'receipt.matched'; payload: {
receipt: Receipt;
transaction: Transaction;
confidence: number;
autoMatched: boolean; // true = extension, false = user manual
userId: string;
}}
```
### `receipt.confirmed`
Fires when a user confirms line item classifications (business vs private).
```typescript
{ type: 'receipt.confirmed'; payload: {
receipt: Receipt;
businessTotal: number;
privateTotal: number;
userId: string;
}}
```
---
## Event Retrofitting
Existing API routes were retrofitted to emit events after their success paths. Each route received:
- `import { eventBus } from '@/lib/events/bus'`
- `import { ensureInitialized } from '@/lib/init'`
- `ensureInitialized()` at module scope
- `await eventBus.emit(...)` after the successful operation, before the response
### `app/api/receipts/upload/route.ts`
Emits `receipt.extracted` after the complete receipt (with line items) is fetched, with `documentId: null` since this is the direct upload path.
### `app/api/receipts/[id]/match/route.ts`
The PATCH handler's ownership verification queries were upgraded from `select('id')` to `select('*, line_items:receipt_line_items(*)')` and `select('*')` respectively, so the full receipt and transaction objects are available for the event payload. Emits `receipt.matched` with `autoMatched: false`.
### `app/api/receipts/[id]/confirm/route.ts`
Computes `businessTotal` and `privateTotal` by iterating over the updated receipt's line items. Emits `receipt.confirmed` with these totals.
---
## Extension: `extensions/receipt-ocr/index.ts`
### Settings
```typescript
interface ReceiptOcrSettings {
autoOcrEnabled: boolean // default: true
autoMatchEnabled: boolean // default: true
autoMatchThreshold: number // default: 0.8
ocrConfidenceThreshold: number // default: 0.6
}
```
Stored as an `extension_data` row with `extension_id='receipt-ocr'`, `key='settings'`, `value=<jsonb>`.
- `getSettings(userId)` reads from DB and merges with defaults (forward-compatible when new settings are added)
- `saveSettings(userId, partial)` merges partial update with current settings, then upserts on the unique constraint `(user_id, extension_id, key)`
### Event Handler: `document.uploaded`
When an image is uploaded via the document archive:
1. **Gate:** Is `document.mime_type` an image? (`image/jpeg|png|webp|gif`) — if not, return
2. **Gate:** Is `autoOcrEnabled` in user's settings? — if not, return
3. Downloads image from `documents` storage bucket
4. Converts to base64, calls `analyzeReceipt()` from `lib/receipts/receipt-analyzer.ts`
5. **Gate:** Is `extraction.confidence >= ocrConfidenceThreshold`? — if not, return
6. Calls `processLineItems()` from `lib/receipts/receipt-categorizer.ts`
7. Creates receipt record (status: `extracted`) + line items in DB
8. Emits `receipt.extracted` with `documentId: document.id`
### Event Handler: `transaction.synced`
When new transactions arrive from banking sync:
1. **Gate:** Is `autoMatchEnabled`? — if not, return
2. Filters to expense transactions only (amount < 0)
3. Fetches unmatched receipts (`status IN ('extracted','confirmed')`, `matched_transaction_id IS NULL`)
4. Calls `autoMatchReceipts()` from `lib/receipts/receipt-matcher.ts` with `settings.autoMatchThreshold`
5. For each match: updates receipt + transaction bidirectional link, emits `receipt.matched` with `autoMatched: true`
### Extension Object
```typescript
export const receiptOcrExtension: Extension = {
id: 'receipt-ocr',
name: 'Receipt OCR',
version: '1.0.0',
eventHandlers: [
{ eventType: 'document.uploaded', handler: handleDocumentUploaded },
{ eventType: 'transaction.synced', handler: handleTransactionSynced },
],
mappingRuleTypes: [
{ id: 'receipt-ocr-merchant', name: 'OCR Merchant Match', ... },
{ id: 'receipt-ocr-category', name: 'OCR Category Suggestion', ... },
],
settingsPanel: { label: 'Receipt OCR', path: '/settings/extensions/receipt-ocr' },
async onInstall(ctx) { await saveSettings(ctx.userId, DEFAULT_SETTINGS) },
}
```
---
## Settings API: `app/api/extensions/receipt-ocr/settings/route.ts`
Establishes the convention `app/api/extensions/{id}/settings/route.ts` for all extensions.
- **GET** — Returns the current user's merged settings (DB value + defaults)
- **PATCH** — Accepts a partial settings object, validates keys against an allowlist, saves via `saveSettings()`
---
## Existing Code Reused
| Import | From | Used in |
|--------|------|---------|
| `analyzeReceipt()` | `lib/receipts/receipt-analyzer.ts` | `handleDocumentUploaded` |
| `processLineItems()` | `lib/receipts/receipt-categorizer.ts` | `handleDocumentUploaded` |
| `autoMatchReceipts()` | `lib/receipts/receipt-matcher.ts` | `handleTransactionSynced` |
| `eventBus` | `lib/events/bus.ts` | Both handlers + retrofit |
| `createClient()` | `lib/supabase/server.ts` | Settings + DB ops |
No service logic was duplicated. The extension only acts as event-driven glue between existing services.
---
## Event Flow
```
Document Archive Upload Direct Receipt Upload Bank Sync
| | |
uploadDocument() POST /receipts/upload POST /banking/sync
| | |
emit document.uploaded analyzeReceipt() inline emit transaction.synced
| | |
v v v
+-----------------+ emit receipt.extracted +---------------------+
| receipt-ocr | | receipt-ocr |
| extension | | extension |
| | | |
| Gate: image? | | Gate: enabled? |
| Gate: enabled? | | Fetch unmatched |
| Download image | | autoMatchReceipts() |
| analyzeReceipt()| | Link matches |
| Create receipt | | emit receipt.matched|
| emit receipt. | +---------------------+
| extracted |
+-----------------+
User confirms receipt --> POST /receipts/[id]/confirm
|
emit receipt.confirmed
|
v
[Future extensions]
push-notifications
ne-bilaga, etc.
```
---
## Architectural Patterns Established
1. **Extensions never duplicate service logic.** They call existing functions from `lib/`.
2. **Extensions are gate-guarded.** Every handler checks user settings before doing work.
3. **Extensions emit domain events.** Downstream extensions react without coupling.
4. **Events fire from both paths.** Whether a receipt enters via archive (event-driven) or direct upload (API), the same `receipt.extracted` event fires.
5. **Settings use `extension_data` with `key='settings'`.** Helpers merge with defaults for forward-compatible schema evolution.
6. **`onInstall` seeds defaults.** Idempotent via upsert.
7. **Handlers never crash the emitter.** `Promise.allSettled` in the bus handles this.
8. **Console logging with `[extension-id]` prefix.** Convention for grep-ability.
9. **One-way dependency.** Base never imports from `extensions/`. Only `loader.ts` imports extension objects.
---
## Verification
- `npx tsc --noEmit` passes with zero errors
- Manual: upload image via document archive -> receipt auto-created with OCR extraction
- Manual: sync bank transactions -> unmatched receipts auto-matched
- Manual: direct receipt upload still works unchanged, now also emits `receipt.extracted`
- Manual: confirm receipt -> emits `receipt.confirmed`
@@ -1,257 +0,0 @@
# SRU Export Extension
## Overview
The SRU Export extension generates SRU (Standardiserat Räkenskapsutdrag) files for electronic tax filing with Skatteverket. It reads `sru_code` from `chart_of_accounts`, aggregates balances by SRU code, and produces downloadable `.sru` files.
Supports both:
- **NE** (Enskild firma) — field codes 7310-7350
- **INK2** (Aktiebolag) — field codes 7200-7499
The form type is determined automatically from the user's `entity_type` in `company_settings`.
## Architecture
```
extensions/sru-export/
├── index.ts Extension definition (registered in loader)
├── sru-engine.ts Balance aggregation by SRU code
├── sru-generator.ts Generic SRU file generation
├── types.ts SRU-specific type definitions (canonical source)
└── SRUExportView.tsx UI component for the SRU-export reports tab
app/api/extensions/sru-export/
├── route.ts GET /api/extensions/sru-export (export endpoint)
└── coverage/
└── route.ts GET /api/extensions/sru-export/coverage (stats)
```
### Relationship to NE-bilaga extension
The NE-bilaga extension (`extensions/ne-bilaga/`) handles the NE-specific declaration path with hard-coded R1-R11 account mappings, served via `/api/extensions/ne-bilaga`. See `dev_docs/base_architecture/NE-BILAGA-EXTENSION.md`.
The generic SRU export is an alternative path that works from raw `account → sru_code` mappings stored in `chart_of_accounts`, rather than hard-coded NE ruta mappings. The generic generator reuses `sruFileToString()` and `validateSRUFile()` from the shared module.
```
NE-bilaga path (EF only):
extensions/ne-bilaga/ne-engine.ts → lib/reports/sru-generator.ts (NE field codes)
Generic path (NE + INK2):
extensions/sru-export/sru-engine.ts (reads sru_code from DB) → sru-generator.ts (any form type)
```
## Database Changes
### Migration: `20240101000021_sru_codes.sql`
Applied as three remote migrations:
1. `add_sru_code_column` — `ALTER TABLE chart_of_accounts ADD COLUMN sru_code text`
2. `sru_codes_backfill` — Updates existing accounts with SRU codes based on account number ranges
3. `sru_codes_seed_function` — Updates `seed_chart_of_accounts()` to include `sru_code` for new users
The backfill only updates accounts where `sru_code IS NULL`, preserving any manual assignments.
### SRU Code Mappings
#### NE form (EF) — Revenue & Expense accounts
| Account Range | SRU Code | NE Ruta | Description |
|---|---|---|---|
| 3000-3499 (excl 3100) | 7310 | R1 | Försäljning med moms |
| 3100, 3900, 3970-3980 | 7311 | R2 | Momsfria intäkter |
| 3200-3299 | 7312 | R3 | Bil/bostadsförmån |
| 8310-8330 | 7313 | R4 | Ränteintäkter |
| 4000-4990 | 7320 | R5 | Varuinköp |
| 5000-6990, 7970 | 7321 | R6 | Övriga kostnader |
| 7000-7699 | 7322 | R7 | Lönekostnader |
| 8400-8499 | 7323 | R8 | Räntekostnader |
| 7820 | 7324 | R9 | Avskrivningar fastighet |
| 7700-7899 (excl 7820) | 7325 | R10 | Avskrivningar övrigt |
#### INK2 form (AB) — Balance Sheet accounts
| Account Range | SRU Code | Description |
|---|---|---|
| 1000-1099 | 7201 | Immateriella anläggningstillgångar |
| 1100-1299 | 7202 | Materiella anläggningstillgångar |
| 1300-1399 | 7203 | Finansiella anläggningstillgångar |
| 1400-1499 | 7210 | Varulager |
| 1500-1599 | 7211 | Kundfordringar |
| 1600-1999 | 7212 | Övriga omsättningstillgångar |
| 2081 | 7220 | Aktiekapital |
| 2085-2098 | 7221 | Övrigt eget kapital |
| 2099 | 7222 | Årets resultat |
| 2100-2499 | 7230 | Skulder |
| 2500-2999 | 7231 | Övriga skulder |
#### INK2 form (AB) — Income Statement accounts
| Account Range | SRU Code | Description |
|---|---|---|
| 3000-3999 | 7310 | Nettoomsättning |
| 4000-4999 | 7320 | Varuinköp |
| 5000-6999 | 7330 | Övriga externa kostnader |
| 7000-7699 | 7340 | Personalkostnader |
| 7700-7899 | 7350 | Avskrivningar |
| 7900-7999 | 7360 | Övriga rörelsekostnader |
| 8000-8499 | 7370 | Finansiella poster |
| 8500-8999 | 7380 | Extraordinära poster |
## API Reference
### GET /api/extensions/sru-export
Generate SRU export for a fiscal period.
**Query parameters:**
| Parameter | Required | Description |
|---|---|---|
| `period_id` | Yes | Fiscal period UUID |
| `format` | No | `json` (default) or `sru` |
**Response (format=json):**
```json
{
"data": {
"formType": "NE",
"entityType": "enskild_firma",
"companyName": "Mitt Företag AB",
"orgNumber": "556123-4567",
"fiscalYear": {
"id": "uuid",
"name": "2025",
"start": "2025-01-01",
"end": "2025-12-31"
},
"balances": [
{
"sruCode": "7310",
"description": "Försäljning med moms",
"amount": 150000,
"accounts": [
{ "accountNumber": "3001", "accountName": "Försäljning tjänster 25%", "amount": 150000 }
]
}
],
"warnings": ["Räkenskapsåret är inte stängt. Siffrorna kan ändras."]
}
}
```
**Response (format=sru):** Downloads a `.sru` file with `Content-Disposition: attachment`.
### GET /api/extensions/sru-export/coverage
Returns SRU code coverage statistics for the authenticated user's chart of accounts.
**Response:**
```json
{
"data": {
"totalAccounts": 30,
"accountsWithSRU": 28,
"accountsWithoutSRU": 2,
"coveragePercent": 93,
"missingAccounts": [
{ "accountNumber": "1220", "accountName": "Inventarier" }
]
}
}
```
## SRU File Format
The generated `.sru` file follows the Skatteverket standard:
```
#PRODUKT KONTROLLUPPGIFTER
#SESSION 1
#PROGRAMNAMN ERPBase
#PROGRAMVERSION 1.0
#SKAPAT 20260219
#BLANKETT NE
#IDENTITET 5561234567
#UPPGIFT 7000 20250101-20251231
#UPPGIFT 7310 150000
#UPPGIFT 7320 -45000
#UPPGIFT 7321 -30000
#BLANKETTSLUT
```
Each `#UPPGIFT` line contains an SRU field code and the rounded (whole kronor) amount. Zero-amount entries are omitted.
## UI Integration
### SRUExportView component
The SRU export UI lives in `extensions/sru-export/SRUExportView.tsx` and is imported by the reports page:
```typescript
import { SRUExportView } from '@/extensions/sru-export/SRUExportView'
```
### Reports Page
The **SRU-export** tab in `/reports` shows:
1. **Info card** with "Förhandsgranska" and "Ladda ner SRU-fil" buttons
2. **Coverage warning** if accounts lack SRU codes (fetched from `/coverage` endpoint)
3. **Company info card** showing entity type badge (NE/INK2) and fiscal year
4. **SRU balances table** with expandable rows showing per-account detail
### Chart of Accounts
A new **SRU** column is added to the accounts table in the Kontoplan view. The column is inline-editable: click a cell to type a new SRU code, press Enter to save, Escape to cancel. Updates go directly to `chart_of_accounts.sru_code` via the Supabase client.
## Extension Registration
The extension is registered in `lib/extensions/loader.ts`:
```typescript
import { sruExportExtension } from '@/extensions/sru-export'
const FIRST_PARTY_EXTENSIONS: Extension[] = [
receiptOcrExtension,
aiCategorizationExtension,
pushNotificationsExtension,
sruExportExtension, // ← added
]
```
The extension has no event handlers, no settings panel, and no sidebar items. It only declares a report type for discovery purposes.
## Type Definitions
Canonical source: `extensions/sru-export/types.ts` (re-exported from `types/index.ts` for convenience):
- **`SRUExportResult`** — Response shape for the JSON format export endpoint
- **`SRUCoverageStats`** — Response shape for the coverage endpoint
## Files Changed
### Created
| File | Purpose |
|---|---|
| `supabase/migrations/20240101000021_sru_codes.sql` | Add column, backfill SRU codes, update seed function |
| `extensions/sru-export/index.ts` | Extension definition |
| `extensions/sru-export/sru-engine.ts` | Balance aggregation by SRU code |
| `extensions/sru-export/sru-generator.ts` | Generic SRU file generation |
| `extensions/sru-export/types.ts` | Canonical type definitions (`SRUExportResult`, `SRUCoverageStats`) |
| `extensions/sru-export/SRUExportView.tsx` | UI component (extracted from reports page) |
| `app/api/extensions/sru-export/route.ts` | Export API endpoint |
| `app/api/extensions/sru-export/coverage/route.ts` | Coverage stats endpoint |
### Modified
| File | Change |
|---|---|
| `lib/extensions/loader.ts` | Added `sruExportExtension` to `FIRST_PARTY_EXTENSIONS` |
| `app/(dashboard)/reports/page.tsx` | Imports `SRUExportView` from extension instead of inlining it |
| `types/index.ts` | SRU types replaced with re-exports from `extensions/sru-export/types.ts` |
| `components/bookkeeping/ChartOfAccounts.tsx` | Added inline-editable SRU code column |
### Reused (not modified)
| File | What was reused |
|---|---|
| `lib/reports/sru-generator.ts` | `sruFileToString()`, `validateSRUFile()` |
| `extensions/ne-bilaga/ne-engine.ts` | Balance calculation pattern |
| `lib/reports/sie-export.ts` | `calculateBalances()` pattern |
@@ -1,234 +0,0 @@
# AI-Agent Snabbreferens
## Dokumentöversikt
| Fil | Innehåll | Användning |
|-----|----------|------------|
| 01-karnregler.md | Grundläggande regler, tröskelvärden, bolagsformer | Faktafrågor om regler |
| 02-scenariobibliotek.md | 30 vanliga situationer med lösningar | "Vad gör jag om..." frågor |
| 03-plattformskatalog.md | Plattformsspecifik info (VAT, moms, utbetalningar) | Plattformsfrågor |
| 04-berakningsexempel.md | Konkreta sifferexempel | "Hur beräknar jag..." frågor |
| 05-beslutstrаd.md | Visuella beslutsflöden | Hjälp med val |
| 06-felsokningsguide.md | Problemlösning | "Det stämmer inte..." frågor |
| 07-kalender-deadlines.md | Alla deadlines och påminnelser | Tidsfrågor |
| 08-varningsflaggor.md | Automatiska kontroller och varningar | Proaktiva varningar |
---
## Trigger-ord → Dokument
### Produkter och barter
- "fick produkt", "pressutskick", "gratis", "barter" → Scenario 001-004
- "marknadsvärde", "värdering" → Beräkningsexempel 1.2
### Plattformar
- "YouTube", "AdSense", "Google" → Plattformskatalog (YouTube)
- "Twitch", "subs", "bits" → Plattformskatalog (Twitch)
- "Instagram", "Meta", "Reels bonus" → Plattformskatalog (Instagram)
- "TikTok", "Creator Fund" → Plattformskatalog (TikTok)
- "Patreon" → Plattformskatalog (Patreon)
- "affiliate", "Adtraction" → Plattformskatalog (Affiliate)
### Moms
- "moms", "VAT", "faktura" → Kärnregler sektion 3, Beslutsträd 2.1
- "periodisk sammanställning" → Kärnregler 3.3, Kalender
- "Reverse Charge", "EU" → Scenario 007-010, Beslutsträd 2.1
- "OSS", "privatperson EU" → Kärnregler 3.4, Scenario 018-019
### Bolagsform
- "EF", "enskild firma" → Kärnregler 4.1, Beslutsträd 3.1
- "AB", "aktiebolag" → Kärnregler 4.2, Beslutsträd 3.1
- "byta bolagsform", "starta AB" → Scenario 028, Beslutsträd 3.2
- "utdelning", "3:12" → Scenario 027, Beräkningsexempel 3.1
### Avdrag
- "avdrag", "dra av" → Kärnregler 5, Beslutsträd 4.1
- "kläder", "smink" → Kärnregler 5.2, Scenario 016-017
- "resa", "arbetsresa" → Scenario 005-006, Beslutsträd 1.2
- "hemmakontor", "arbetsrum" → Kärnregler 5.3, Beräkningsexempel 5.2
### Social trygghet
- "SGI", "sjukpenning" → Kärnregler 7, Scenario 023
- "föräldraledighet", "VAB", "barn" → Beräkningsexempel 3.3, Varningsflaggor 3.3
- "pension" → Kärnregler 7.2
### Juridik
- "reklam", "markering", "MFL" → Kärnregler 6, Scenario 024
- "upphovsrätt", "musik", "licens" → Scenario 025, Varningsflaggor 5.2
### Dubbel struktur
- "EF och AB", "båda bolag" → Kärnregler 8, Scenario 029-030
- "internfaktura" → Scenario 030, Varningsflaggor 4.1-4.2
### Deadlines
- "deadline", "när ska", "förfaller" → Kalender
- "momsdeklaration" → Kalender (kvartalsvis/månadsvis)
- "inkomstdeklaration" → Kalender (maj/juli)
### Problem
- "stämmer inte", "fel", "problem" → Felsökningsguide
- "Skatteverket", "granskning" → Felsökningsguide 8
---
## Kritiska tröskelvärden (snabbåtkomst)
| Belopp | Betydelse |
|--------|-----------|
| 1 800 kr | Skattefri tävlingsvinst |
| 2 000 kr | Schablonavdrag arbetsrum EF |
| 5 000 kr | Skattefritt friskvårdsbidrag AB |
| 25 000 kr | Aktiekapital AB |
| 80 000 kr | Momsbefrielse omsättning |
| 99 680 kr | OSS-tröskel EU-privatpersoner |
| 204 325 kr | Schablonbelopp 3:12 (2024) |
| 615 300 kr | Brytpunkt statlig skatt |
| 681 600 kr | Lönekrav för lönebaserat 3:12-utrymme |
---
## Vanliga plattformars momshantering (snabbåtkomst)
| Plattform | Moms | Ruta | Period. samnst. |
|-----------|------|------|-----------------|
| YouTube | Reverse Charge | 39 | Ja |
| Twitch | Export | 40 | Nej |
| Instagram | Reverse Charge | 39 | Ja |
| TikTok | Reverse Charge | 39 | Ja |
| Patreon | Export | 40 | Nej |
| Spotify | Svensk 25% | 05-08 | Nej |
| Adtraction | Svensk 25% | 05-08 | Nej |
---
## Standardsvar-mallar
### Vid fråga om skatteplikt
```
[Inkomsttyp] är [skattepliktig/ej skattepliktig].
Värdering: [Marknadsvärde inkl. moms / Nominellt belopp]
Bokföring:
- Debet [konto]: [belopp]
- Kredit [konto]: [belopp]
[Eventuella villkor eller undantag]
```
### Vid fråga om momshantering
```
Kund: [Kundtyp och land]
Momsregel: [Svensk 25% / Reverse Charge / Export]
Faktura:
- Belopp exkl. moms: [X] kr
- Moms: [Y] kr / 0 kr
- Text på faktura: [Om tillämpligt]
Redovisning:
- Momsdeklaration ruta: [X]
- Periodisk sammanställning: [Ja/Nej]
```
### Vid fråga om avdragsrätt
```
[Kostnadstyp] är [avdragsgill/ej avdragsgill].
Villkor för avdrag:
- [Villkor 1]
- [Villkor 2]
[Om delvis avdragsgill: Beräkning av andel]
Dokumentationskrav:
- [Krav]
```
### Vid varning
```
VARNING: [Typ av varning]
Problem: [Beskrivning]
Konsekvens om ej åtgärdat:
- [Konsekvens 1]
- [Konsekvens 2]
Åtgärd:
1. [Steg 1]
2. [Steg 2]
Deadline: [Om tillämpligt]
```
---
## Konteringsguide (vanliga poster)
### Intäkter
| Typ | Debet | Kredit |
|-----|-------|--------|
| Svensk försäljning | 1510 Kundfordringar | 3010 Intäkter + 2610 Utg. moms |
| EU-försäljning (B2B) | 1510 Kundfordringar | 3011 Försäljning EU |
| Export (utanför EU) | 1510 Kundfordringar | 3012 Försäljning export |
| Kontant plattformsintäkt | 1940 Bank | 3011/3012 beroende på plattform |
| Barter-produkt | 4010 Inköp + 2640 Ing. moms | 3010 Intäkter |
### Kostnader
| Typ | Debet | Kredit |
|-----|-------|--------|
| Inköp med moms | [Kostnadskonto] + 2640 Ing. moms | 1940 Bank / 2440 Lev.skuld |
| Inventarier | 1220 Inventarier + 2640 Ing. moms | 1940 Bank |
| Prenumerationer | 6540 IT-tjänster + 2640 Ing. moms | 1940 Bank |
### Eget uttag (EF)
| Typ | Debet | Kredit |
|-----|-------|--------|
| Privat uttag | 2013 Eget uttag | 1940 Bank |
| Barter till privat bruk | 2013 Eget uttag | 4010 Inköp + 2640 Ing. moms |
### Valutakursdifferenser
| Typ | Debet | Kredit |
|-----|-------|--------|
| Kursvinst | 1940 Bank | 1510 Kundfordringar + 3960 Kursvinst |
| Kursförlust | 1940 Bank + 7960 Kursförlust | 1510 Kundfordringar |
---
## Eskaleringsmatris
| Situation | Åtgärd |
|-----------|--------|
| Enkel faktafråga | Svara direkt från dokumentation |
| Komplex beräkning | Använd beräkningsexempel som mall |
| Osäkerhet om regler | Hänvisa till Skatteverket.se |
| Juridisk tvist | Rekommendera jurist |
| Skatteverket-kontakt | Rekommendera redovisningskonsult |
| Allvarlig varning | Flagga tydligt, föreslå åtgärd |
| Utanför scope | Tydliggör begränsning, hänvisa vidare |
---
## Begränsningar att kommunicera
AI-agenten kan INTE:
- Ge juridisk rådgivning (endast information)
- Garantera skattekonsekvenser
- Ersätta revisor/redovisningskonsult
- Lämna in deklarationer
- Kontakta myndigheter
AI-agenten SKA:
- Ge saklig information baserad på dokumentation
- Flagga risker och varningar
- Rekommendera professionell hjälp vid behov
- Hänvisa till officiella källor
@@ -1,219 +0,0 @@
# Kärnregler: Influencer Ekonomi
## 1. Verksamhetsklassificering
### Tre kumulativa krav för näringsverksamhet
| Krav | Definition | Influencer-exempel |
|------|------------|-------------------|
| Vinstsyfte | Objektiv förväntan på överskott över tid | Affärsplan visar skalbar intäktsmodell |
| Varaktighet | Regelbunden aktivitet, längre period | Daglig/veckovis publicering, löpande samarbeten |
| Självständighet | Egen utrustning, flera kunder, eget risktagande | Egen kamera, 5+ varumärkessamarbeten/år |
### Klassificeringslogik
```
ALLA tre krav uppfyllda → Näringsverksamhet (F-skatt, NE/AB)
EJ alla krav men inkomst finns → Hobbyverksamhet (T2-blankett)
Tillfälliga uppdrag utan F-skatt → Tjänsteinkomst (A-skatt dras av uppdragsgivare)
```
### Hobbyverksamhet
- Beskattas som inkomst av tjänst
- Egenavgifter: 25-29%
- Underskott: Sparas max 5 år, kvittas endast mot framtida hobbyöverskott
- Moms: Registreringsplikt om omsättning > 80 000 kr
---
## 2. Skattepliktiga inkomster
**Grundprincip:** Allt som erhålls på grund av verksamheten är skattepliktigt oavsett form.
### Inkomsttabell
| Typ | Skattepliktigt | Värdering | Undantag |
|-----|----------------|-----------|----------|
| Kontant ersättning | Ja | Nominellt belopp | - |
| Produkter (avtalat samarbete) | Ja | Marknadsvärde inkl. moms | - |
| Pressutskick (oombett) | Villkorat | Marknadsvärde inkl. moms | Returnerad/kasserad utan användning |
| Sponsrad resa | Delvis/helt | Resans faktiska värde | Arbete >30h/vecka, försumbart nöjesinslag |
| Medföljande på resa | Ja | Medföljarens resekostnad | - |
| Donations/Bits/Stars/Subs | Ja | Mottaget belopp | - |
| Affiliate-provision | Ja | Utbetalt belopp | - |
| Rabattkod eget bruk | Ja | Marknadspris minus betalt pris | - |
| Tävlingsvinster | Villkorat | Vinstens värde | ≤1 800 kr eller minnesföremål |
| Virtuella items (skins etc.) | Ja vid försäljning | Försäljningspris | - |
---
## 3. Momsregler
### Tröskelvärden
| Gräns | Belopp | Konsekvens |
|-------|--------|------------|
| Momsbefrielse | 80 000 kr omsättning | Under: Valfri registrering. Över: Obligatorisk. |
| OSS-tröskel | 99 680 kr (10 000 EUR) | Försäljning till EU-privatpersoner |
### Momshantering per kundtyp
| Kund | Plats | Moms på faktura | Redovisning |
|------|-------|-----------------|-------------|
| Svenskt företag | Sverige | 25% | Ruta 05-08 |
| EU-företag | Annat EU-land | 0% (Reverse Charge) | Ruta 39 + Periodisk sammanställning |
| Företag utanför EU | Tredjeland | 0% (Export) | Ruta 40 |
| Privatperson EU | Annat EU-land | Mottagarlandets moms* | OSS-deklaration |
| Privatperson utanför EU | Tredjeland | 0% | Ruta 40 |
*Under 99 680 kr/år: Valfritt svensk moms 25%
### Periodisk sammanställning
- **När:** Kvartalsvis vid försäljning till EU-företag
- **Innehåll:** Köparens VAT-nummer, totalvärde per kund
- **Deadline:** 20:e i månaden efter kvartalets slut
- **Förseningsavgift:** 1 250 kr
- **Kritiskt:** Belopp måste matcha Ruta 39 i momsdeklaration
---
## 4. Bolagsformer
### Jämförelse
| Aspekt | Enskild Firma | Aktiebolag |
|--------|---------------|------------|
| Kapitalinsats | 0 kr | 25 000 kr |
| Ansvar | Personligt, obegränsat | Begränsat till aktiekapital |
| Beskattning | Inkomstskatt + egenavgifter på överskott | Bolagsskatt 20,6% + skatt på lön/utdelning |
| Utdelning 20% skatt | Ej tillgängligt | Ja, upp till gränsbelopp (3:12) |
| Friskvårdsbidrag | Ej avdragsgillt | Skattefritt ≤5 000 kr |
| SGI-grund | Nettoöverskott | Uttagen lön |
| Administration | NE-bilaga | Årsredovisning |
| Rekommenderad vinst | <500 000 kr | >500 000 kr |
### Brytpunkter 2024/2025
| Gräns | Belopp |
|-------|--------|
| Statlig inkomstskatt | 615 300 kr |
| Aktiekapital AB | 25 000 kr |
---
## 5. Avdragsregler
### Avdragsgilla kostnader
| Kategori | Exempel | Villkor |
|----------|---------|---------|
| Teknik | Kamera, dator, mikrofon, ljus, drönare | Yrkesmässig användning |
| Mjukvara | Redigeringsprogram, molntjänster | Verksamhetsrelaterad |
| Agentarvode | Provision till manager/agentur | Direkt kopplad till intäkt |
| Extern lokal | Studiohyra | Exklusiv verksamhetsanvändning |
| Resor | Arbetsresor med dokumenterat syfte | Ej hem-arbete |
### Ej avdragsgilla kostnader
| Kategori | Motivering |
|----------|------------|
| Kläder (normala) | Användbart privat |
| Smink, hudvård | Privat levnadskostnad |
| Skönhetsingrepp | Privat levnadskostnad |
| Accessoarer (väskor, smycken) | Användbart privat |
| Träning, gym | Privat (undantag: friskvård i AB) |
### Arbetsrum i bostad
| Typ | Krav | Avdrag |
|-----|------|--------|
| Faktiskt avdrag | Separat rum, ej bostad, exklusiv användning | Faktisk kostnad |
| Schablon (EF) | >800 h arbete i bostad/år | 2 000 kr/år |
---
## 6. Marknadsföringslagen
### Krav på reklammarkering
| Element | Krav |
|---------|------|
| Placering | Början av inlägg/video |
| Text | "REKLAM" eller "ANNONS" |
| Synlighet | Framträdande färg, storlek |
| Video | Muntligt och/eller textgrafik |
| Sändarangivelse | Vem som betalar ska framgå |
### Överträdelser
- Influencern har eget medverkansansvar
- Förbud vid vite
- Marknadsstörningsavgift
- Skadestånd
### Upphovsrätt
- Licens krävs för musik/material i kommersiella sammanhang
- Referensfall: ABBA-fallet, 200 000 kr ersättning
---
## 7. Social trygghet
### SGI (Sjukpenninggrundande inkomst)
| Bolagsform | SGI baseras på |
|------------|----------------|
| Enskild Firma | Skattemässigt överskott |
| Aktiebolag | Uttagen lön (ej utdelning) |
| Dubbel struktur | Summan av båda |
**Uppbyggnadsskede EF:** Första 36 månaderna kan jämförelseinkomst användas.
### Pension
- Egenföretagare saknar automatisk tjänstepension
- Avdragsgillt sparande: Max 35% av inkomst, max ~573 000 kr/år
- Rekommendation: Spara 4,5-6% av inkomst
---
## 8. Dubbel struktur (EF + AB)
### Användningsområden
| EF används för | AB används för |
|----------------|----------------|
| Småuppdrag <50 000 kr | Större kampanjer >100 000 kr |
| Lågriskuppdrag | Högriskkampanjer |
| Konsultation | Huvudsaklig verksamhet |
| Avvecklingsperiod | Ny huvudverksamhet |
### Interntransaktioner
- Armlängdsprissättning obligatorisk
- Skriftliga avtal krävs
- Moms på internfakturor
- Separata bankkonton och bokföring
### SGI vid dubbel struktur
```
Total SGI = Överskott EF + Lön från AB
```
---
## 9. Kritiska tröskelvärden
| Belopp | Betydelse |
|--------|-----------|
| 1 800 kr | Skattefri tävlingsvinst |
| 2 000 kr | Schablonavdrag arbetsrum EF |
| 5 000 kr | Skattefritt friskvårdsbidrag AB |
| 25 000 kr | Aktiekapital AB |
| 80 000 kr | Momsbefrielse omsättning |
| 99 680 kr | OSS-tröskel EU-privatpersoner |
| 615 300 kr | Brytpunkt statlig skatt |
@@ -1,678 +0,0 @@
# Scenariobibliotek: Vanliga situationer
## Produkter och barter
### SCENARIO 001: Mottagen produkt med samarbetsavtal
**Triggers:** "fick produkt", "samarbete", "ska posta om", "i utbyte mot"
**Situation:** Influencer får produkt värd X kr mot att göra inlägg.
**Beslutsträd:**
```
Finns skriftligt/muntligt avtal om motprestation?
→ Ja → Hela marknadsvärdet inkl. moms är skattepliktig intäkt
```
**Skattemässig hantering:**
- Intäkt: Produktens marknadsvärde inkl. moms
- Om EF: Bokför som intäkt + eget uttag om privat bruk
- Om AB: Bokför som intäkt + förmån om privat bruk
**Bokföringsexempel (EF):**
```
Produkt: Väska värd 8 000 kr inkl. moms
Debet 4010 Inköp varor: 6 400 kr
Debet 2640 Ingående moms: 1 600 kr
Kredit 3010 Intäkter: 8 000 kr
Om privat bruk:
Debet 2013 Eget uttag: 8 000 kr
Kredit 4010 Inköp varor: 6 400 kr
Kredit 2640 Ingående moms: 1 600 kr
```
**Checklista:**
- [ ] Dokumentera marknadsvärde (spara prislapp/screenshot)
- [ ] Spara avtal/mailkonversation
- [ ] Bokför som intäkt
- [ ] Märk inlägg med REKLAM
---
### SCENARIO 002: Oombett pressutskick (behålls)
**Triggers:** "fick skickat", "pressutskick", "utan att fråga", "PR-paket"
**Situation:** Influencer får produkt utan förfrågan, behåller och använder privat.
**Beslutsträd:**
```
Fick du produkten oombedd?
→ Ja
Behåller du den / använder privat?
→ Ja → Skattepliktig intäkt (marknadsvärde)
→ Nej (returnerar/slänger utan användning) → Ej skattepliktig
```
**Skattemässig hantering:**
- Skattepliktig även utan motprestation om produkten behålls
- Värderas till marknadsvärde inkl. moms
**Dokumentationskrav:**
- Fotografera produkten vid mottagande
- Notera avsändare och datum
- Om returnerad: Spara kvitto på returfrakten
---
### SCENARIO 003: Oombett pressutskick (returneras/kastas)
**Triggers:** "skickade tillbaka", "returnerade", "slängde", "gav bort"
**Situation:** Influencer får produkt utan förfrågan, returnerar eller kastar.
**Skattemässig hantering:**
- Ej skattepliktigt om ingen privat användning skett
- Bevisbördan ligger på influencern
**Dokumentationskrav:**
- Kvitto på returfrakten
- Foto av kasserad produkt (om relevant)
- Notering om när och hur produkten avyttrades
**Varning:** Om produkten säljs på "bloppis" blir försäljningsintäkten skattepliktig.
---
### SCENARIO 004: Rabattkod för eget bruk
**Triggers:** "rabattkod", "köpte med rabatt", "fick X% rabatt"
**Situation:** Influencer får personlig rabattkod som del av ersättning.
**Beräkning:**
```
Skattepliktig inkomst = Marknadspris - Faktiskt betalt pris
Exempel:
Marknadspris: 5 000 kr
Betalt: 0 kr
Skattepliktig inkomst: 5 000 kr
```
**Bokföring:** Som intäkt i verksamheten.
---
## Resor och evenemang
### SCENARIO 005: Sponsrad resa (arbetsresa)
**Triggers:** "pressresa", "influencer trip", "betald resa", "bjuden resa"
**Situation:** Influencer bjuds på resa av varumärke.
**Beslutsträd:**
```
Är resan nödvändig för verksamheten?
→ Nej → Helt skattepliktig
→ Ja
Upptar arbete huvudsaklig tid (≥30h/vecka eller ≥6h/dag)?
→ Nej → Delvis/helt skattepliktig (nöjesandel)
→ Ja
Är nöjesinslagen försumbara?
→ Nej → Nöjesandelen skattepliktig
→ Ja → Skattefri
```
**Skattefri resa kräver:**
1. Nödvändig för verksamheten
2. Arbete ≥30 timmar/vecka ELLER ≥6 timmar/dag
3. Nöjesinslag försumbara
**Dokumentationskrav:**
- Detaljerat schema över arbetade timmar
- Leverabler (antal inlägg, stories, videos)
- Avtal som specificerar arbetsuppgifter
---
### SCENARIO 006: Medföljande på resa
**Triggers:** "tog med partner", "min sambo följde med", "vän fick följa med"
**Situation:** Influencerns partner/vän följer med på sponsrad resa kostnadsfritt.
**Skattemässig hantering:**
- Medföljarens resekostnad är skattepliktig förmån för influencern
- Gäller oavsett om influencerns egen resa är skattefri
**Beräkning:**
```
Förmånsvärde = Flygbiljett + Hotell + Eventuella aktiviteter för medföljaren
```
---
## Plattformsintäkter
### SCENARIO 007: YouTube AdSense-utbetalning
**Triggers:** "YouTube betalade", "AdSense", "annonsintäkter YouTube"
**Situation:** Månatlig utbetalning från Google/YouTube.
**Fakta:**
- Utbetalare: Google Ireland Ltd
- VAT-nummer: IE6388047V
- Valuta: USD (normalt)
- Momsregel: Reverse Charge (EU B2B)
**Bokföringsexempel:**
```
Mottaget: $3,200 USD
Valutakurs vid inbetalning: 10.45 SEK/USD
Belopp i SEK: 33,440 kr
Debet 1940 Bank: 33,440 kr
Kredit 3011 Försäljning tjänster EU: 33,440 kr
```
**Momsredovisning:**
- Ruta 39: 33,440 kr
- Periodisk sammanställning: Google Ireland Ltd, IE6388047V, 33,440 kr
---
### SCENARIO 008: Twitch-utbetalning
**Triggers:** "Twitch betalade", "subs", "bits", "Twitch revenue"
**Situation:** Utbetalning från Twitch (prenumerationer, bits, annonser).
**Fakta:**
- Utbetalare: Twitch Interactive Inc
- Säte: USA (San Francisco)
- Momsregel: Export (utanför EU)
**Bokföringsexempel:**
```
Mottaget: $1,500 USD
Valutakurs vid inbetalning: 10.52 SEK/USD
Belopp i SEK: 15,780 kr
Debet 1940 Bank: 15,780 kr
Kredit 3012 Försäljning tjänster utanför EU: 15,780 kr
```
**Momsredovisning:**
- Ruta 40: 15,780 kr
- Ingen periodisk sammanställning (ej EU)
---
### SCENARIO 009: Instagram/Meta bonusar
**Triggers:** "Instagram bonus", "Reels bonus", "Meta betalade"
**Situation:** Bonusutbetalning från Meta för Reels-visningar etc.
**Fakta:**
- Utbetalare: Meta Platforms Ireland Ltd
- VAT-nummer: IE9692928F
- Momsregel: Reverse Charge (EU B2B)
**Hantering:** Samma som YouTube (Scenario 007).
---
### SCENARIO 010: TikTok Creator Fund
**Triggers:** "TikTok betalade", "Creator Fund", "TikTok pengar"
**Situation:** Utbetalning från TikTok Creator Fund.
**Fakta:**
- Utbetalare: TikTok Technology Ltd (ofta)
- VAT-nummer: IE3434547SH
- Säte: Irland
- Momsregel: Reverse Charge (EU B2B)
**OBS:** Kontrollera alltid aktuellt avtal då TikToks struktur varierar.
---
### SCENARIO 011: Donations/Tips (Twitch, YouTube, etc.)
**Triggers:** "donation", "tips", "någon donerade", "fick pengar av tittare"
**Situation:** Tittare skickar pengar direkt via plattform.
**Skattemässig hantering:**
- INTE gåva i skatterättslig mening
- Ersättning för prestation (underhållning)
- Fullt skattepliktig inkomst i näringsverksamheten
**Bokföring:**
```
Debet 1940 Bank: [belopp]
Kredit 3010 Intäkter: [belopp]
```
---
## Affiliate och samarbeten
### SCENARIO 012: Affiliate-provision
**Triggers:** "affiliate", "provision", "trackad länk", "kommission"
**Situation:** Influencer får provision på försäljning via spårningslänk.
**Skattemässig hantering:**
- Fullt skattepliktig intäkt
- Momshantering beror på var affiliate-nätverket är baserat
**Vanliga nätverk och momshantering:**
| Nätverk | Säte | Momsregel |
|---------|------|-----------|
| Amazon Associates | Luxemburg (EU) | Reverse Charge |
| Adtraction | Sverige | 25% moms |
| Tradedoubler | Sverige | 25% moms |
| AWIN | Varierar | Kontrollera avtal |
---
### SCENARIO 013: Ambassadörsavtal med fast månadsersättning
**Triggers:** "ambassadör", "fast ersättning", "månadsarvode", "retainer"
**Situation:** Löpande avtal med fast månadsersättning.
**Skattemässig hantering:**
- Intäktsför månadsvis
- Moms beroende på kundtyp (se momsregler)
- Om svensk kund: Fakturera med 25% moms
**Bokföring:**
```
Månadsersättning: 25,000 kr + moms
Debet 1510 Kundfordringar: 31,250 kr
Kredit 3010 Intäkter: 25,000 kr
Kredit 2610 Utgående moms: 6,250 kr
```
---
## Teknik och utrustning
### SCENARIO 014: Köp av kamera/utrustning
**Triggers:** "köpte kamera", "ny utrustning", "investerade i teknik"
**Situation:** Inköp av professionell utrustning.
**Beslutsträd:**
```
Används utrustningen uteslutande i verksamheten?
→ Ja → Fullt avdrag + momsavdrag
→ Nej (även privat bruk)
→ Proportionera avdrag efter verksamhetsandel
→ Professionell utrustning: Ofta godtas helavdrag ändå
```
**Bokföringsexempel (kamera 45,000 kr inkl. moms):**
```
Debet 1220 Inventarier: 36,000 kr
Debet 2640 Ingående moms: 9,000 kr
Kredit 1940 Bank: 45,000 kr
```
**Avskrivning:**
- Datorer, kameror: Normalt 3-5 år
- Möjlighet till direktavdrag om <25,900 kr (halvt prisbasbelopp)
---
### SCENARIO 015: Prenumerationer och mjukvara
**Triggers:** "Adobe", "prenumeration", "programvara", "app-kostnad"
**Situation:** Löpande kostnader för mjukvara.
**Vanliga avdragsgilla prenumerationer:**
- Adobe Creative Cloud
- Final Cut Pro
- Canva Pro
- Notion, Airtable
- Molnlagring (Google Drive, Dropbox)
- Schemaläggningsverktyg (Later, Buffer)
**Bokföring:** Kostnadsförs löpande som förbrukningsmaterial eller extern tjänst.
---
## Kläder och utseende
### SCENARIO 016: Köp av kläder för content
**Triggers:** "köpte kläder", "outfit för video", "kläder till inlägg"
**Situation:** Influencer köper kläder att visa i content.
**Huvudregel:** Ej avdragsgillt om kläderna kan användas privat.
**Undantag (avdragsgilla):**
- Uniformer
- Skyddskläder
- Extrema scenkläder (ej användbara privat)
- Specifik rekvisita (t.ex. historiska kostymer)
**Dokumentationskrav för undantag:**
- Fotografera plagget
- Motivera varför det ej är användbart privat
- Bevisbördan ligger på influencern
---
### SCENARIO 017: Skönhetsbehandlingar
**Triggers:** "filler", "botox", "behandling", "skönhetsingrepp"
**Situation:** Influencer gör estetisk behandling.
**Skattemässig hantering:**
- Aldrig avdragsgillt
- Klassas som privat levnadskostnad
- Gäller även om utseendet är "del av varumärket"
---
## Försäljning och merchandise
### SCENARIO 018: Försäljning av egen merch till svenska kunder
**Triggers:** "säljer merch", "egen kollektion", "t-shirts"
**Situation:** Försäljning av varor till svenska privatpersoner.
**Skattemässig hantering:**
- Intäkt i verksamheten
- 25% moms på försäljningen
- Inköpskostnad för varorna avdragsgill
**Bokföringsexempel:**
```
Såld t-shirt: 299 kr inkl. moms
Debet 1940 Bank: 299 kr
Kredit 3010 Försäljning varor: 239,20 kr
Kredit 2610 Utgående moms: 59,80 kr
```
---
### SCENARIO 019: Försäljning av merch till EU-kunder
**Triggers:** "kund i Tyskland", "säljer till EU", "europeisk kund"
**Situation:** Försäljning av varor till privatpersoner i annat EU-land.
**Beslutsträd:**
```
Total EU-privatpersonförsäljning under året:
≤99,680 kr → Valfritt: Svensk moms 25% ELLER mottagarlandets moms
>99,680 kr → Obligatoriskt: Mottagarlandets moms via OSS
```
**OSS (One Stop Shop):**
- Registrering via Skatteverket
- Kvartalsvis deklaration
- Samlar all utländsk EU-moms på ett ställe
---
### SCENARIO 020: Försäljning av virtuella items (gaming)
**Triggers:** "sålde skins", "CS:GO", "in-game items"
**Situation:** Försäljning av virtuella föremål för riktiga pengar.
**Beslutsträd:**
```
Sker försäljning yrkesmässigt och regelbundet?
→ Ja → Näringsverksamhet (inkomst av näring)
→ Nej (enstaka försäljningar) → Inkomst av kapital (30% på vinsten)
```
**OBS:** Gränsdragningen är svår. Vid regelbunden handel, behandla som näringsverksamhet.
---
## Valuta och internationellt
### SCENARIO 021: Valutakursdifferens
**Triggers:** "kurs ändrades", "växlingskurs", "valutaförlust"
**Situation:** Skillnad mellan fakturakurs och betalningskurs.
**Beräkning:**
```
Fakturerat: $1,000 @ 10.20 = 10,200 kr
Mottaget: $1,000 @ 10.45 = 10,450 kr
Valutakursvinst: 250 kr
```
**Bokföring:**
```
Valutakursvinst:
Debet 1940 Bank: 10,450 kr
Kredit 1510 Kundfordringar: 10,200 kr
Kredit 3960 Valutakursvinster: 250 kr
Valutakursförlust:
Debet 1940 Bank: 9,950 kr
Debet 7960 Valutakursförluster: 250 kr
Kredit 1510 Kundfordringar: 10,200 kr
```
---
## Sociala avgifter och trygghet
### SCENARIO 022: Uppdrag utan F-skatt
**Triggers:** "har inte F-skatt", "de drog skatt", "fick lön istället"
**Situation:** Influencer utför uppdrag men saknar F-skatt.
**Konsekvenser:**
- Uppdragsgivaren ska dra A-skatt (~30%)
- Uppdragsgivaren betalar arbetsgivaravgifter (31,42%)
- Ersättningen behandlas som lön
**Checklista:**
- [ ] Begär kontrolluppgift från uppdragsgivaren
- [ ] Kontrollera att skatt faktiskt dragits
- [ ] Deklarera som inkomst av tjänst
---
### SCENARIO 023: Beräkna SGI-konsekvens av utdelning vs lön
**Triggers:** "sjukpenning", "VAB", "föräldraledighet", "SGI"
**Situation:** Val mellan lön och utdelning påverkar socialförsäkring.
**Beräkning:**
```
Alt 1: Lön 400,000 kr
SGI: 400,000 kr
Sjukpenning (80%): 320,000 kr/år
Alt 2: Lön 100,000 kr + Utdelning 300,000 kr
SGI: 100,000 kr
Sjukpenning (80%): 80,000 kr/år
Skillnad vid 6 månaders sjukskrivning: 120,000 kr
```
---
## Juridik och avtal
### SCENARIO 024: Glömt reklammarkering
**Triggers:** "glömde skriva reklam", "markerade inte", "fick klagomål"
**Situation:** Inlägg publicerades utan korrekt reklammarkering.
**Åtgärder:**
1. Redigera inlägget omedelbart - lägg till "REKLAM" överst
2. Dokumentera när rättelsen gjordes
3. Informera uppdragsgivaren
**Risker:**
- Varning från Konsumentverket
- Förbud vid vite
- Marknadsstörningsavgift
- Reputationsskada
---
### SCENARIO 025: Använt musik utan licens
**Triggers:** "använde låt", "bakgrundsmusik", "copyright claim"
**Situation:** Musik användes i kommersiellt content utan tillstånd.
**Konsekvenser:**
- Upphovsrättsintrång
- Skadeståndsskyldighet
- Referens: ABBA-fallet (200,000 kr)
**Förebyggande:**
- Använd royalty-free musik
- Köp licens via Epidemic Sound, Artlist etc.
- Kontrollera rättigheter innan publicering
---
## Bokslut och deklaration
### SCENARIO 026: Periodiseringsfond (EF)
**Triggers:** "skjuta upp skatt", "periodiseringsfond", "jämna ut resultat"
**Situation:** Influencer vill jämna ut beskattning över år.
**Regler:**
- Max 30% av överskottet kan avsättas
- Återförs senast år 6
- Vid avveckling av EF: Omedelbar återföring
**Exempel:**
```
År 1: Överskott 500,000 kr
Avsättning (30%): 150,000 kr
Beskattas: 350,000 kr
År 3: Överskott 100,000 kr
Återföring: 150,000 kr
Beskattas: 250,000 kr
```
---
### SCENARIO 027: 3:12-utdelning (AB)
**Triggers:** "utdelning", "gränsbelopp", "20% skatt"
**Situation:** Ägare vill ta ut lågbeskattad utdelning från AB.
**Gränsbelopp 2024:**
- Schablonbelopp: 204,325 kr (2,75 x IBB)
- ELLER lönebaserat utrymme (kräver minst 681,600 kr i egen lön)
**Strategi:**
1. Ta ut lön upp till brytpunkt statlig skatt (~615,300 kr)
2. Ta ut utdelning inom gränsbelopp (20% skatt)
3. Spara överskjutande i bolaget eller ta som lön
---
### SCENARIO 028: Byta från EF till AB
**Triggers:** "vill byta", "starta AB", "övergång"
**Situation:** Influencer vill övergå från EF till AB.
**Process:**
1. Starta AB (25,000 kr aktiekapital)
2. Inkråmsöverlåtelse: EF säljer tillgångar till AB
3. Nya avtal tecknas med AB som part
4. Uppdatera plattformar med nytt VAT-nummer
5. Avveckla EF
**Skattekonsekvenser:**
- Inkråmsöverlåtelse till marknadsvärde
- Moms på försäljningen
- Periodiseringsfonder i EF återförs
- Realisationsvinst i EF beskattas
---
## Dubbel struktur
### SCENARIO 029: Fördela uppdrag mellan EF och AB
**Triggers:** "vilket företag", "ska jag fakturera från", "EF eller AB"
**Situation:** Influencer har både EF och AB, nytt uppdrag inkommer.
**Beslutsmatris:**
| Faktor | → EF | → AB |
|--------|------|------|
| Ersättning <50,000 kr | ✓ | |
| Ersättning >100,000 kr | | ✓ |
| Hög juridisk risk | | ✓ |
| Engångsuppdrag | ✓ | |
| Långt ambassadörskap | | ✓ |
| Behöver bygga SGI | ✓ | |
---
### SCENARIO 030: Internfaktura mellan EF och AB
**Triggers:** "fakturera mig själv", "mellan bolagen", "internfaktura"
**Situation:** EF utför tjänst åt eget AB.
**Krav:**
- Armlängdsprissättning (marknadsmässigt pris)
- Skriftligt avtal
- Affärsmässig motivering
- Moms på fakturan (25%)
**Bokföringsexempel:**
```
EF fakturerar AB för videoproduktion: 20,000 kr + moms
I EF:
Debet 1510 Kundfordringar: 25,000 kr
Kredit 3010 Intäkter: 20,000 kr
Kredit 2610 Utgående moms: 5,000 kr
I AB:
Debet 4010 Köpta tjänster: 20,000 kr
Debet 2640 Ingående moms: 5,000 kr
Kredit 2440 Leverantörsskulder: 25,000 kr
```
**Varning:** Skatteverket granskar internprissättning. Avvikelse >20% från marknad flaggas.
@@ -1,398 +0,0 @@
# Plattformskatalog
## YouTube (Google AdSense)
### Företagsinformation
| Fält | Värde |
|------|-------|
| Juridiskt namn | Google Ireland Ltd |
| VAT-nummer | IE6388047V |
| Adress | Gordon House, Barrow Street, Dublin 4, Ireland |
| Säte | Irland (EU) |
### Momshantering
| Regel | Värde |
|-------|-------|
| Momstyp | Reverse Charge |
| Svensk moms på faktura | Nej (0%) |
| Momsdeklaration ruta | 39 |
| Periodisk sammanställning | Ja, kvartalsvis |
### Utbetalningar
| Parameter | Värde |
|-----------|-------|
| Valuta | USD |
| Utbetalningströskel | $100 |
| Utbetalningsfrekvens | Månadsvis (21:a-26:e) |
| Utbetalningsmetod | Banköverföring, check |
### Var hittar jag underlag?
1. YouTube Studio → Analytics → Revenue
2. AdSense → Payments → Transaction history
3. Ladda ner CSV för bokföringsunderlag
### Bokföringskontering
```
Intäkt från YouTube:
Debet 1940 Bank: [SEK-belopp]
Kredit 3011 Försäljning tjänster EU: [SEK-belopp]
```
---
## Twitch
### Företagsinformation
| Fält | Värde |
|------|-------|
| Juridiskt namn | Twitch Interactive Inc |
| VAT-nummer | Inget (USA) |
| Adress | 350 Bush Street, 2nd Floor, San Francisco, CA 94104, USA |
| Säte | USA (utanför EU) |
### Momshantering
| Regel | Värde |
|-------|-------|
| Momstyp | Export |
| Svensk moms på faktura | Nej (0%) |
| Momsdeklaration ruta | 40 |
| Periodisk sammanställning | Nej |
### Utbetalningar
| Parameter | Värde |
|-----------|-------|
| Valuta | USD |
| Utbetalningströskel | $50 (Affiliate), $100 (Partner) |
| Utbetalningsfrekvens | 15:e varje månad (för föregående månad) |
| Net terms | 45 dagar |
### Intäktsströmmar
| Typ | Beskrivning |
|-----|-------------|
| Subscriptions | Prenumerationer (50% till streamer normalt) |
| Bits | Virtuell valuta från tittare |
| Ads | Annonsintäkter |
| Bounties | Sponsrade kampanjer via Twitch |
### Var hittar jag underlag?
1. Creator Dashboard → Channel Analytics → Revenue
2. Payout History för historiska utbetalningar
3. Tax Interview måste fyllas i (W-8BEN)
### Bokföringskontering
```
Intäkt från Twitch:
Debet 1940 Bank: [SEK-belopp]
Kredit 3012 Försäljning tjänster utanför EU: [SEK-belopp]
```
---
## Instagram / Meta
### Företagsinformation
| Fält | Värde |
|------|-------|
| Juridiskt namn | Meta Platforms Ireland Ltd |
| VAT-nummer | IE9692928F |
| Adress | 4 Grand Canal Square, Grand Canal Harbour, Dublin 2, Ireland |
| Säte | Irland (EU) |
### Momshantering
| Regel | Värde |
|-------|-------|
| Momstyp | Reverse Charge |
| Svensk moms på faktura | Nej (0%) |
| Momsdeklaration ruta | 39 |
| Periodisk sammanställning | Ja, kvartalsvis |
### Utbetalningar (Bonusprogram)
| Parameter | Värde |
|-----------|-------|
| Valuta | USD eller EUR |
| Utbetalningströskel | $100 |
| Utbetalningsfrekvens | Månadsvis |
| Utbetalningsmetod | Banköverföring, PayPal |
### Intäktsströmmar
| Typ | Beskrivning |
|-----|-------------|
| Reels Bonus | Prestationsbaserat på Reels-visningar |
| Badges | Tittare köper badges under Live |
| Subscriptions | Prenumerationer på exklusivt innehåll |
| Branded Content | Märkta samarbeten (betalning från varumärke) |
### Var hittar jag underlag?
1. Professional Dashboard → Monetization → Payouts
2. Meta Business Suite → Insights → Monetization
3. Payout Settings för bankuppgifter och skatteinfo
### Bokföringskontering
```
Intäkt från Instagram/Meta:
Debet 1940 Bank: [SEK-belopp]
Kredit 3011 Försäljning tjänster EU: [SEK-belopp]
```
---
## TikTok
### Företagsinformation
| Fält | Värde |
|------|-------|
| Juridiskt namn | TikTok Technology Ltd (EU) |
| VAT-nummer | IE3434547SH |
| Adress | 10 Earlsfort Terrace, Dublin 2, D02 T380, Ireland |
| Säte | Irland (EU) |
**OBS:** TikToks struktur varierar. Kontrollera alltid avtalet för korrekt entitet.
### Momshantering
| Regel | Värde |
|-------|-------|
| Momstyp | Reverse Charge |
| Svensk moms på faktura | Nej (0%) |
| Momsdeklaration ruta | 39 |
| Periodisk sammanställning | Ja, kvartalsvis |
### Utbetalningar
| Parameter | Värde |
|-----------|-------|
| Valuta | USD |
| Utbetalningströskel | $10 (Creator Fund) |
| Utbetalningsfrekvens | Månadsvis |
### Intäktsströmmar
| Typ | Beskrivning |
|-----|-------------|
| Creator Fund | Prestationsbaserat på visningar |
| Creator Rewards | Förbättrat program ersätter Creator Fund |
| LIVE Gifts | Virtuella gåvor under livestream |
| Series | Betalinnehåll |
### Var hittar jag underlag?
1. TikTok Creator Portal → Balance
2. Transaction History för detaljer
3. Tax Information måste fyllas i
---
## Patreon
### Företagsinformation
| Fält | Värde |
|------|-------|
| Juridiskt namn | Patreon Inc |
| VAT-nummer | Inget (USA) |
| Adress | 600 Townsend Street, Suite 500, San Francisco, CA 94103, USA |
| Säte | USA (utanför EU) |
### Momshantering
| Regel | Värde |
|-------|-------|
| Momstyp | Export |
| Svensk moms på faktura | Nej (0%) |
| Momsdeklaration ruta | 40 |
| Periodisk sammanställning | Nej |
### Utbetalningar
| Parameter | Värde |
|-----------|-------|
| Valuta | USD |
| Utbetalningströskel | $25 |
| Utbetalningsfrekvens | Månadsvis (1:a-5:e) |
| Avgifter | 5-12% plattformsavgift + betalningsavgifter |
### Var hittar jag underlag?
1. Creator Dashboard → Income
2. Payout History med CSV-export
3. Tax Settings för W-8BEN
---
## Spotify (Podcasters)
### Företagsinformation
| Fält | Värde |
|------|-------|
| Juridiskt namn | Spotify AB |
| VAT-nummer | SE556703748501 |
| Adress | Regeringsgatan 19, 111 53 Stockholm, Sweden |
| Säte | Sverige |
### Momshantering
| Regel | Värde |
|-------|-------|
| Momstyp | Svensk inrikes |
| Svensk moms på faktura | Ja (25%) |
| Momsdeklaration ruta | 05-08 |
| Periodisk sammanställning | Nej |
### Bokföringskontering
```
Intäkt från Spotify:
Debet 1940 Bank: [totalbelopp]
Kredit 3010 Försäljning tjänster Sverige: [nettobelopp]
Kredit 2610 Utgående moms: [moms 25%]
```
---
## Amazon Associates (Affiliate)
### Företagsinformation
| Fält | Värde |
|------|-------|
| Juridiskt namn | Amazon EU S.à r.l. |
| VAT-nummer | LU26375245 |
| Adress | 38 avenue John F. Kennedy, L-1855 Luxembourg |
| Säte | Luxemburg (EU) |
**OBS:** Amazon har flera entiteter. Kontrollera vilket program/land som är aktuellt.
### Momshantering
| Regel | Värde |
|-------|-------|
| Momstyp | Reverse Charge |
| Svensk moms på faktura | Nej (0%) |
| Momsdeklaration ruta | 39 |
| Periodisk sammanställning | Ja, kvartalsvis |
### Utbetalningar
| Parameter | Värde |
|-----------|-------|
| Valuta | EUR/USD beroende på program |
| Utbetalningströskel | €25/£25/$10 |
| Utbetalningsfrekvens | ~60 dagar efter månadslut |
---
## Svenska affiliate-nätverk
### Adtraction
| Fält | Värde |
|------|-------|
| Juridiskt namn | Adtraction Marketing AB |
| Organisationsnummer | 556748-8855 |
| VAT-nummer | SE556748885501 |
| Säte | Sverige |
| Moms | 25% svensk moms |
### Tradedoubler
| Fält | Värde |
|------|-------|
| Juridiskt namn | Tradedoubler AB |
| Organisationsnummer | 556575-7423 |
| VAT-nummer | SE556575742301 |
| Säte | Sverige |
| Moms | 25% svensk moms |
### Awin (Nordics)
| Fält | Värde |
|------|-------|
| Juridiskt namn | AWIN AG (filial i UK ofta) |
| Säte | Varierar |
| Moms | Kontrollera avtal |
---
## Sammanfattningstabell
| Plattform | Säte | VAT-nummer | Momsregel | Ruta | Period. sammanst. |
|-----------|------|------------|-----------|------|-------------------|
| YouTube | Irland | IE6388047V | Reverse Charge | 39 | Ja |
| Twitch | USA | - | Export | 40 | Nej |
| Instagram/Meta | Irland | IE9692928F | Reverse Charge | 39 | Ja |
| TikTok | Irland | IE3434547SH | Reverse Charge | 39 | Ja |
| Patreon | USA | - | Export | 40 | Nej |
| Spotify | Sverige | SE556703748501 | Svensk 25% | 05-08 | Nej |
| Amazon EU | Luxemburg | LU26375245 | Reverse Charge | 39 | Ja |
| Adtraction | Sverige | SE556748885501 | Svensk 25% | 05-08 | Nej |
| Tradedoubler | Sverige | SE556575742301 | Svensk 25% | 05-08 | Nej |
---
## Valutakurser
### Huvudregel
Använd valutakursen den dag inbetalningen sker till bankkontot.
### Kurskällor
- Riksbankens dagskurser (officiell)
- Bankens faktiska växlingskurs (om tillgänglig)
### Praktisk hantering
```
1. Notera belopp i originalvaluta
2. Notera inbetalningsdatum
3. Hämta kurs för det datumet
4. Beräkna SEK-belopp
5. Bokför i SEK
6. Vid fakturadatum ≠ betalningsdatum: Bokför kursdifferens
```
---
## Skatteformulär per plattform
| Plattform | Formulär | Beskrivning |
|-----------|----------|-------------|
| YouTube/Google | W-8BEN | US tax withholding |
| Twitch | W-8BEN | US tax withholding |
| TikTok | W-8BEN | US tax withholding |
| Patreon | W-8BEN | US tax withholding |
| Meta | Tax Information | I plattformen |
**W-8BEN:** Formulär som intygar att du inte är amerikansk skattskyldig. Utan detta hålls 30% amerikansk skatt inne.
---
## Utbetalningskalender (typisk)
| Plattform | Intjäningsmånad | Utbetalning |
|-----------|-----------------|-------------|
| YouTube | Januari | 21-26 februari |
| Twitch | Januari | 15 mars |
| Instagram | Januari | Februari (varierar) |
| TikTok | Januari | Februari (varierar) |
| Patreon | Januari | 1-5 februari |
| Amazon Associates | Januari | Mars (60 dagar) |
@@ -1,601 +0,0 @@
# Beräkningsexempel
## 1. Inkomstberäkningar
### 1.1 YouTube-utbetalning med valutaomräkning
**Scenario:** Influencer får månatlig AdSense-utbetalning.
```
Uppgifter:
- Utbetalt: $2,847.63 USD
- Inbetalningsdatum: 2024-02-23
- Valutakurs (Riksbanken): 10.3842 SEK/USD
Beräkning:
$2,847.63 × 10.3842 = 29,571.87 SEK
Bokföring:
Debet 1940 Bank: 29,571.87 kr
Kredit 3011 Försäljning tjänster EU: 29,571.87 kr
Momsdeklaration:
Ruta 39: 29,571.87 kr
Periodisk sammanställning:
Kund: Google Ireland Ltd
VAT: IE6388047V
Belopp: 29,571.87 kr
```
---
### 1.2 Barter-produkt med skatteberäkning
**Scenario:** Influencer får väska värd 12,000 kr mot ett Instagram-inlägg.
```
Uppgifter:
- Produkt: Designerväska
- Marknadsvärde: 12,000 kr inkl. moms
- Bolagsform: Enskild firma
Beräkning skattepliktig inkomst:
Intäkt: 12,000 kr
Om privat bruk (eget uttag):
Nettovärde: 12,000 / 1.25 = 9,600 kr
Moms: 2,400 kr
Bokföring (produkt mottagen):
Debet 4010 Inköp varor: 9,600 kr
Debet 2640 Ingående moms: 2,400 kr
Kredit 3010 Intäkter: 12,000 kr
Bokföring (eget uttag för privat bruk):
Debet 2013 Eget uttag: 12,000 kr
Kredit 4010 Inköp varor: 9,600 kr
Kredit 2640 Ingående moms: 2,400 kr
Skattekonsekvens (antagen marginalskatt 35%):
Skatt på 12,000 kr: ~4,200 kr
```
---
### 1.3 Sponsrad resa med medföljande
**Scenario:** Influencer bjuds på resa, partner följer med gratis.
```
Uppgifter:
- Flyg influencer: 8,500 kr
- Hotell influencer (5 nätter): 15,000 kr
- Flyg medföljande: 8,500 kr
- Hotell medföljande: ingår i dubbelrum
- Aktiviteter (båda): 4,000 kr
- Arbetade timmar: 35 timmar under 5 dagar (7h/dag)
- Dokumenterat arbete: 12 Instagram-inlägg, 4 Stories/dag, 2 Reels
Bedömning influencerns resa:
- Arbete: 35 timmar = 7 timmar/dag ✓
- Kriteriet "huvudsaklig arbetstid" uppfyllt
- Skattefri om nöjesinslag försumbara
Medföljandes resa:
- Alltid skattepliktig förmån
Beräkning förmånsvärde medföljande:
Flyg: 8,500 kr
Hotell: 0 kr (ingen merkostnad för dubbelrum i detta fall)
Aktiviteter (50%): 2,000 kr
Totalt förmånsvärde: 10,500 kr
Skattekonsekvens (marginalskatt 35%):
Skatt på 10,500 kr: ~3,675 kr
```
---
### 1.4 Donations/Tips från Twitch
**Scenario:** Streamer får donations under månad.
```
Uppgifter:
- Bits mottagna: 45,000 bits
- Bits-värde: $0.01 per bit = $450
- Direktdonationer via PayPal: $320
- Subscriptions (50% av $4.99 × 85 subs): $212.08
- Valutakurs: 10.55 SEK/USD
Beräkning total intäkt:
Bits: $450 × 10.55 = 4,747.50 kr
Donations: $320 × 10.55 = 3,376.00 kr
Subs: $212.08 × 10.55 = 2,237.44 kr
Totalt: 10,360.94 kr
Bokföring (Twitch-utbetalning från USA):
Debet 1940 Bank: 10,360.94 kr
Kredit 3012 Försäljning tjänster utanför EU: 10,360.94 kr
Momsdeklaration:
Ruta 40: 10,360.94 kr
```
---
## 2. Momsberäkningar
### 2.1 Faktura till svenskt företag
**Scenario:** Samarbete med svenskt klädmärke.
```
Uppgifter:
- Arvode: 35,000 kr exkl. moms
- Influencer har F-skatt och är momsregistrerad
Fakturabelopp:
Arvode: 35,000 kr
Moms 25%: 8,750 kr
Totalt att betala: 43,750 kr
Bokföring vid fakturering:
Debet 1510 Kundfordringar: 43,750 kr
Kredit 3010 Intäkter: 35,000 kr
Kredit 2610 Utgående moms: 8,750 kr
Bokföring vid betalning:
Debet 1940 Bank: 43,750 kr
Kredit 1510 Kundfordringar: 43,750 kr
```
---
### 2.2 Faktura till EU-företag (Reverse Charge)
**Scenario:** Samarbete med tyskt företag.
```
Uppgifter:
- Arvode: €3,000 EUR
- Valutakurs vid fakturadatum: 11.25 SEK/EUR
- Kundens VAT-nummer: DE123456789
Fakturabelopp:
Arvode: €3,000 (33,750 SEK)
Moms: €0 (Reverse Charge)
Totalt: €3,000
Obligatorisk text på faktura:
"Reverse charge - VAT to be accounted for by the recipient"
"Art. 196 Council Directive 2006/112/EC"
Bokföring vid fakturering:
Debet 1510 Kundfordringar: 33,750 kr
Kredit 3011 Försäljning tjänster EU: 33,750 kr
Momsdeklaration:
Ruta 39: 33,750 kr
Periodisk sammanställning:
Land: DE
VAT-nummer: DE123456789
Belopp: 33,750 kr
```
---
### 2.3 Merch-försäljning till EU-privatperson
**Scenario:** Säljer t-shirt till kund i Finland.
```
Uppgifter:
- T-shirt pris: 349 kr inkl. moms
- Kund: Privatperson i Finland
- Total EU-privatpersonförsäljning hittills i år: 45,000 kr
Bedömning:
Under OSS-tröskel (99,680 kr) → Kan välja svensk moms
Alternativ 1: Svensk moms
Pris: 349 kr inkl. 25% svensk moms
Netto: 279.20 kr
Moms: 69.80 kr
Alternativ 2: Finsk moms (24%)
Netto: 349 / 1.24 = 281.45 kr
Moms: 67.55 kr
Redovisas via OSS
Vid överskridande av tröskel:
- Obligatoriskt mottagarlandets moms
- Registrera för OSS hos Skatteverket
```
---
### 2.4 Kvartalsvis periodisk sammanställning
**Scenario:** Sammanställning Q1.
```
EU-försäljning under Q1:
- Google Ireland Ltd (IE6388047V): 87,450 kr
- Meta Platforms Ireland Ltd (IE9692928F): 12,300 kr
- TikTok Technology Ltd (IE3434547SH): 8,200 kr
- Tyskt varumärke GmbH (DE987654321): 25,000 kr
Periodisk sammanställning:
┌─────────────────────────────────────────────────────┐
│ Period: 2024-01-01 till 2024-03-31 │
├──────────┬────────────────┬─────────────────────────┤
│ Land │ VAT-nummer │ Belopp (SEK) │
├──────────┼────────────────┼─────────────────────────┤
│ IE │ IE6388047V │ 87,450 │
│ IE │ IE9692928F │ 12,300 │
│ IE │ IE3434547SH │ 8,200 │
│ DE │ DE987654321 │ 25,000 │
├──────────┴────────────────┼─────────────────────────┤
│ SUMMA │ 132,950 │
└───────────────────────────┴─────────────────────────┘
Kontrollavstämning:
Ruta 39 i momsdeklarationen Q1: 132,950 kr ✓
```
---
## 3. Skatteoptimering
### 3.1 Lön vs utdelning i AB
**Scenario:** AB med vinst 800,000 kr före lön.
```
Förutsättningar:
- Vinst före lön: 800,000 kr
- Gränsbelopp för lågbeskattad utdelning: 204,325 kr
- Brytpunkt statlig skatt: 615,300 kr
Alternativ 1: Endast lön 800,000 kr
Arbetsgivaravgifter (31.42%): 251,360 kr
Maximal lönekostnad: 800,000 kr
Bruttolön: 800,000 / 1.3142 = 608,719 kr
Inkomstskatt (~32%): 194,790 kr
Netto till ägaren: 413,929 kr
SGI: 608,719 kr ✓
Alternativ 2: Lön 500,000 kr + Utdelning
Lön brutto: 500,000 kr
Arbetsgivaravgifter: 157,100 kr
Total lönekostnad: 657,100 kr
Kvar i bolaget: 142,900 kr
Bolagsskatt (20.6%): 29,438 kr
Tillgängligt för utdelning: 113,462 kr
Utdelningsskatt (20%): 22,692 kr
Netto utdelning: 90,770 kr
Lön efter skatt (~32%): 340,000 kr
Total netto: 430,770 kr
SGI: 500,000 kr ✓
Alternativ 3: Lön till brytpunkt + Max utdelning
Lön brutto: 615,300 kr
Arbetsgivaravgifter: 193,327 kr
Total lönekostnad: 808,627 kr
→ Överstiger vinst, ej genomförbart
Rekommendation:
Alternativ 2 ger högst netto OCH god SGI.
```
---
### 3.2 EF: Periodiseringsfond
**Scenario:** Enskild firma med ojämna resultat.
```
År 1: Överskott 600,000 kr (viralt år)
År 2: Överskott 150,000 kr (normalt år)
År 3: Överskott 100,000 kr (svagt år)
Utan periodiseringsfond:
År 1 skatt (antagen 45% marginal på del): ~200,000 kr
År 2 skatt (~32%): ~48,000 kr
År 3 skatt (~32%): ~32,000 kr
Total skatt: ~280,000 kr
Med periodiseringsfond:
År 1:
Överskott: 600,000 kr
Avsättning (30%): 180,000 kr
Beskattas: 420,000 kr
Skatt (~40% snitt): 168,000 kr
År 2:
Överskott: 150,000 kr
Avsättning (30%): 45,000 kr
Beskattas: 105,000 kr
Skatt (~32%): 33,600 kr
År 3:
Överskott: 100,000 kr
Återföring: 225,000 kr (180k + 45k)
Beskattas: 325,000 kr
Skatt (~38% snitt): 123,500 kr
Total skatt: ~325,100 kr
Analys:
I detta fall blev det dyrare med periodiseringsfond!
Fonden lönar sig när framtida inkomster förväntas vara LÄGRE.
```
---
### 3.3 SGI-optimering
**Scenario:** Influencer planerar barn, vill maximera föräldrapenning.
```
Nuvarande struktur (AB):
- Lön: 25,000 kr/månad = 300,000 kr/år
- Utdelning: 400,000 kr/år
- SGI: 300,000 kr
Föräldrapenning vid SGI 300,000 kr:
Dagpenning (80%): 657 kr/dag (tak)
→ SGI under taket 599,250 kr, får 80% av faktisk SGI
Faktisk dagpenning: 300,000 × 0.8 / 365 = 657 kr/dag
Optimerad struktur (12 månader före barn):
- Lön: 50,000 kr/månad = 600,000 kr/år
- Utdelning: 100,000 kr/år
- SGI: 600,000 kr
Föräldrapenning vid SGI 600,000 kr:
Dagpenning (80%): 1,315 kr/dag
Men tak finns: 1,116 kr/dag (2024)
Beräkning per 390 dagar föräldraledighet:
Låg SGI (300k): 657 × 390 = 256,230 kr
Hög SGI (600k): 1,116 × 390 = 435,240 kr
Skillnad: 179,010 kr mer vid optimerad SGI
Kostnad för omställning:
Ökad lön 300,000 kr/år → ökad skatt ~100,000 kr
Nettofördel efter 1 års planering: ~79,000 kr
```
---
## 4. Valutaberäkningar
### 4.1 Kursdifferens vid försenad betalning
**Scenario:** Faktura i EUR med betalning 45 dagar senare.
```
Fakturadatum: 2024-01-15
Fakturabelopp: €5,000
Kurs 2024-01-15: 11.35 SEK/EUR
Fakturerat i SEK: 56,750 kr
Betalningsdatum: 2024-03-01
Kurs 2024-03-01: 11.52 SEK/EUR
Mottaget i SEK: 57,600 kr
Kursdifferens: 57,600 - 56,750 = 850 kr (vinst)
Bokföring vid fakturering:
Debet 1510 Kundfordringar: 56,750 kr
Kredit 3011 Försäljning tjänster EU: 56,750 kr
Bokföring vid betalning:
Debet 1940 Bank: 57,600 kr
Kredit 1510 Kundfordringar: 56,750 kr
Kredit 3960 Valutakursvinster: 850 kr
```
---
### 4.2 Aggregerad månadsintäkt från flera plattformar
**Scenario:** Månadssammanställning med blandade valutor.
```
Januari 2024 intäkter:
YouTube (USD):
Belopp: $3,245.00
Inbetalning: 2024-02-22
Kurs: 10.42
SEK: 33,812.90
Twitch (USD):
Belopp: $1,876.50
Inbetalning: 2024-02-15
Kurs: 10.38
SEK: 19,477.67
TikTok (USD):
Belopp: $456.00
Inbetalning: 2024-02-28
Kurs: 10.45
SEK: 4,765.20
Adtraction (SEK):
Belopp: 8,450 kr
SEK: 8,450.00
Total intäkt januari: 66,505.77 kr
Momsredovisning:
Ruta 39 (EU - YouTube, TikTok): 38,578.10 kr
Ruta 40 (Export - Twitch): 19,477.67 kr
Ruta 05-08 (Sverige - Adtraction): 6,760 kr netto + 1,690 kr moms
```
---
## 5. Avdragsberäkningar
### 5.1 Datorutrustning med blandad användning
**Scenario:** Köp av MacBook Pro för video och privat.
```
Uppgifter:
- Inköpspris: 35,000 kr inkl. moms
- Bedömd verksamhetsanvändning: 80%
- Bolagsform: Enskild firma
Beräkning avdrag:
Nettopris: 35,000 / 1.25 = 28,000 kr
Moms: 7,000 kr
Verksamhetsandel 80%:
Avdragsgill kostnad: 28,000 × 0.80 = 22,400 kr
Avdragsgill moms: 7,000 × 0.80 = 5,600 kr
Bokföring:
Debet 1220 Inventarier: 22,400 kr
Debet 2640 Ingående moms: 5,600 kr
Debet 2013 Eget uttag: 7,000 kr
Kredit 1940 Bank: 35,000 kr
Avskrivning (5 år):
Årligt avdrag: 22,400 / 5 = 4,480 kr
```
---
### 5.2 Hemmakontor (schablon vs faktiskt)
**Scenario:** Influencer arbetar i lägenhet.
```
Uppgifter:
- Total hyra: 12,000 kr/månad
- Lägenhetens yta: 75 kvm
- Arbetsrummets yta: 12 kvm
- Arbetade timmar hemma: 1,200 timmar/år
Alternativ 1: Schablonavdrag
Krav: >800 timmar/år ✓
Avdrag: 2,000 kr/år
Alternativ 2: Faktiskt avdrag (om kraven uppfylls)
Krav: Separat rum, exklusiv verksamhetsanvändning
Andel: 12/75 = 16%
Årshyra: 12,000 × 12 = 144,000 kr
Avdrag: 144,000 × 0.16 = 23,040 kr
Skillnad: 21,040 kr/år
VARNING: Faktiskt avdrag kräver att rummet INTE används
för boende. Kraven är mycket stränga. Vid tveksamhet,
använd schablon eller hyr extern lokal.
```
---
### 5.3 Resa med delvis privat syfte
**Scenario:** Resa till New York för samarbete + privat.
```
Uppgifter:
- Totalt 7 dagar
- Arbetsdagar: 4 dagar (möten, inspelning)
- Privata dagar: 3 dagar (sightseeing)
- Flygbiljett: 9,500 kr
- Hotell: 3,000 kr/natt × 7 = 21,000 kr
- Traktamente utland (USA): 614 kr/dag
Beräkning avdragsgill del:
Flyg: 9,500 × (4/7) = 5,429 kr
Hotell arbetsdagar: 3,000 × 4 = 12,000 kr
Traktamente arbetsdagar: 614 × 4 = 2,456 kr
Totalt avdragsgillt: 19,885 kr
Ej avdragsgillt (privat): 9,500 - 5,429 + 9,000 = 13,071 kr
```
---
## 6. Jämförande beräkningar
### 6.1 EF vs AB vid olika vinstnivåer
**Scenario:** Jämförelse vid 400k, 600k och 1M kr vinst.
```
Vinst 400,000 kr:
EF:
Egenavgifter (~28%): 112,000 kr
Skattepliktig inkomst: 288,000 kr
Inkomstskatt (~32%): 92,160 kr
Netto: 195,840 kr
AB (lön 300k + utdelning):
Lön brutto: 300,000 kr
Arbetsgivaravgifter: 94,260 kr
Kvar: 5,740 kr
Bolagsskatt: 1,182 kr
Lön efter skatt: 204,000 kr
Utdelning: 4,558 kr × 0.8 = 3,646 kr
Netto: 207,646 kr
Fördel AB: 11,806 kr
---
Vinst 600,000 kr:
EF:
Egenavgifter: 168,000 kr
Skattepliktig: 432,000 kr
Inkomstskatt (~35%): 151,200 kr
Netto: 280,800 kr
AB (lön 400k + utdelning):
Lön brutto: 400,000 kr
Arbetsgivaravgifter: 125,680 kr
Kvar: 74,320 kr
Bolagsskatt: 15,310 kr
Lön efter skatt: 272,000 kr
Utdelning: 59,010 kr × 0.8 = 47,208 kr
Netto: 319,208 kr
Fördel AB: 38,408 kr
---
Vinst 1,000,000 kr:
EF:
Egenavgifter: 280,000 kr
Skattepliktig: 720,000 kr
Inkomstskatt (~42%): 302,400 kr
Netto: 417,600 kr
AB (lön 600k + utdelning):
Lön brutto: 600,000 kr
Arbetsgivaravgifter: 188,520 kr
Kvar: 211,480 kr
Bolagsskatt: 43,565 kr
Lön efter skatt: 390,000 kr
Utdelning: 167,915 kr × 0.8 = 134,332 kr
Netto: 524,332 kr
Fördel AB: 106,732 kr
```
**Slutsats:** AB blir mer fördelaktigt ju högre vinsten är.
Brytpunkt ligger omkring 300-400k kr vinst.
@@ -1,554 +0,0 @@
# Beslutsträd
## 1. Produkter och förmåner
### 1.1 Mottagen produkt
```
┌─ Fick du en produkt?
│
├─► Fanns avtal/överenskommelse om motprestation?
│ │
│ ├─► JA (samarbete)
│ │ └─► SKATTEPLIKTIG INTÄKT
│ │ Värde: Marknadspris inkl. moms
│ │ Bokför som: Intäkt + eget uttag (om privat bruk)
│ │
│ └─► NEJ (oombett pressutskick)
│ │
│ ├─► Behåller du produkten?
│ │ │
│ │ ├─► JA
│ │ │ └─► SKATTEPLIKTIG INTÄKT
│ │ │ Värde: Marknadspris inkl. moms
│ │ │
│ │ └─► NEJ
│ │ │
│ │ ├─► Returnerade
│ │ │ └─► EJ SKATTEPLIKTIG
│ │ │ Krav: Spara returkvitto
│ │ │
│ │ ├─► Kastade/gav bort
│ │ │ └─► EJ SKATTEPLIKTIG
│ │ │ Krav: Ingen privat användning dessförinnan
│ │ │
│ │ └─► Sålde vidare
│ │ └─► FÖRSÄLJNINGSINTÄKT SKATTEPLIKTIG
│ │ (Ej ursprungsvärdet, utan säljpriset)
```
---
### 1.2 Sponsrad resa
```
┌─ Bjuden på resa av uppdragsgivare?
│
├─► Är resan nödvändig för verksamheten?
│ │
│ ├─► NEJ
│ │ └─► HELA VÄRDET SKATTEPLIKTIGT
│ │
│ └─► JA
│ │
│ ├─► Arbetar du ≥30h/vecka ELLER ≥6h/dag under resan?
│ │ │
│ │ ├─► NEJ
│ │ │ └─► DELVIS SKATTEPLIKTIGT
│ │ │ Nöjesandelen beskattas
│ │ │
│ │ └─► JA
│ │ │
│ │ ├─► Är nöjesinslagen försumbara?
│ │ │ │
│ │ │ ├─► JA
│ │ │ │ └─► SKATTEFRI RESA
│ │ │ │
│ │ │ └─► NEJ
│ │ │ └─► DELVIS SKATTEPLIKTIGT
│ │ │ Nöjesandelen beskattas
│
├─► Följer partner/vän med gratis?
│ └─► JA
│ └─► ALLTID SKATTEPLIKTIG FÖRMÅN
│ Värde: Medföljarens faktiska reskostnad
```
---
### 1.3 Rabattkod/personalrabatt
```
┌─ Fick du rabatt som del av ersättning?
│
├─► Rabatt på produkter du ska marknadsföra?
│ └─► SKATTEPLIKTIG
│ Värde: Marknadspris - betalt pris
│
├─► Rabatt för eget bruk (ej kopplat till uppdrag)?
│ │
│ ├─► Erbjuds samma rabatt till allmänheten?
│ │ └─► EJ SKATTEPLIKTIG (öppen kampanj)
│ │
│ └─► Exklusiv rabatt?
│ └─► SKATTEPLIKTIG
│ Värde: Normalrabatt till allmänheten jämförs
```
---
## 2. Momshantering
### 2.1 Val av momsregel vid fakturering
```
┌─ Vem fakturerar du?
│
├─► Svenskt företag
│ └─► SVENSK MOMS 25%
│ Ruta: 05-08
│ Periodisk sammanställning: Nej
│
├─► Företag i annat EU-land
│ │
│ ├─► Har kunden giltigt VAT-nummer?
│ │ │
│ │ ├─► JA
│ │ │ └─► REVERSE CHARGE (0% moms)
│ │ │ Ruta: 39
│ │ │ Periodisk sammanställning: JA
│ │ │ Text på faktura: "Reverse charge"
│ │ │
│ │ └─► NEJ
│ │ └─► Behandla som privatperson
│ │ → Se "Privatperson i EU"
│ │
├─► Företag utanför EU
│ └─► EXPORT (0% moms)
│ Ruta: 40
│ Periodisk sammanställning: Nej
│
├─► Privatperson i Sverige
│ └─► SVENSK MOMS 25%
│ Ruta: 05-08
│
├─► Privatperson i annat EU-land
│ │
│ ├─► Total EU-privatförsäljning ≤99,680 kr/år?
│ │ │
│ │ ├─► JA
│ │ │ └─► VALFRITT:
│ │ │ • Svensk moms 25% (enklast)
│ │ │ • ELLER mottagarlandets moms via OSS
│ │ │
│ │ └─► NEJ (över tröskeln)
│ │ └─► OBLIGATORISKT mottagarlandets moms
│ │ Redovisa via OSS
│ │
├─► Privatperson utanför EU
│ └─► EXPORT (0% moms)
│ Ruta: 40
```
---
### 2.2 Momsregistrering
```
┌─ Behöver jag momsregistrera mig?
│
├─► Bedriver du momspliktig verksamhet?
│ │
│ ├─► NEJ (t.ex. sjukvård, utbildning)
│ │ └─► INGEN MOMSREGISTRERING
│ │
│ └─► JA
│ │
│ ├─► Omsättning >80,000 kr/år?
│ │ │
│ │ ├─► JA
│ │ │ └─► OBLIGATORISK registrering
│ │ │
│ │ └─► NEJ
│ │ │
│ │ ├─► Säljer du till EU-företag (Reverse Charge)?
│ │ │ │
│ │ │ ├─► JA
│ │ │ │ └─► REKOMMENDERAD registrering
│ │ │ │ (för periodisk sammanställning)
│ │ │ │
│ │ │ └─► NEJ
│ │ │ └─► FRIVILLIG registrering
│ │ │ Fördel: Momsavdrag på inköp
```
---
## 3. Bolagsform
### 3.1 EF eller AB?
```
┌─ Vilken bolagsform passar?
│
├─► Förväntad årlig vinst?
│ │
│ ├─► <300,000 kr
│ │ └─► ENSKILD FIRMA
│ │ • Enklare administration
│ │ • Skattemässigt likvärdigt
│ │
│ ├─► 300,000 - 500,000 kr
│ │ │
│ │ ├─► Accepterar du personligt ansvar?
│ │ │ │
│ │ │ ├─► JA
│ │ │ │ └─► EF kan fungera
│ │ │ │ Överväg AB om växande
│ │ │ │
│ │ │ └─► NEJ
│ │ │ └─► AKTIEBOLAG
│ │ │
│ ├─► >500,000 kr
│ │ └─► AKTIEBOLAG
│ │ • Betydande skattefördel via 3:12
│ │ • Ansvarsbegränsning
│ │
├─► Juridisk risk i verksamheten?
│ │
│ ├─► HÖG (kontroversiellt innehåll, upphovsrättskänsligt)
│ │ └─► AKTIEBOLAG
│ │ Personlig ekonomi skyddad
│ │
│ └─► LÅG
│ └─► Båda fungerar
│
├─► Planerar investerare/partners?
│ │
│ ├─► JA
│ │ └─► AKTIEBOLAG
│ │ Ägarandelar möjliga
│ │
│ └─► NEJ
│ └─► Båda fungerar
```
---
### 3.2 När byta från EF till AB?
```
┌─ Ska jag byta till AB?
│
├─► Vinst >500,000 kr konsekvent (2+ år)?
│ └─► JA → Stark indikation för AB
│
├─► Juridisk tvist eller risk?
│ └─► JA → AB skyddar privatekonomin
│
├─► Vill ha utdelning istället för lön?
│ └─► JA → AB möjliggör 20% skatt
│
├─► Vill anställa personal?
│ └─► JA → AB ofta lämpligare
│
├─► Planerar sälja verksamheten?
│ └─► JA → AB enklare att överlåta
│
Om 2+ JA → Överväg starkt att byta till AB
```
---
## 4. Avdrag
### 4.1 Är kostnaden avdragsgill?
```
┌─ Är utgiften avdragsgill?
│
├─► Teknik (kamera, dator, mikrofon, ljus)
│ │
│ ├─► Används i verksamheten?
│ │ │
│ │ ├─► Uteslutande verksamhet
│ │ │ └─► FULLT AVDRAG
│ │ │
│ │ ├─► Delvis privat
│ │ │ └─► PROPORTIONELLT AVDRAG
│ │ │ (Bedöm verksamhetsandel)
│ │ │
│ │ └─► Huvudsakligen privat
│ │ └─► INGET/MARGINELLT AVDRAG
│
├─► Kläder
│ │
│ ├─► Kan användas privat?
│ │ │
│ │ ├─► JA (normala kläder)
│ │ │ └─► EJ AVDRAGSGILL
│ │ │
│ │ └─► NEJ
│ │ │
│ │ ├─► Uniform/skyddskläder
│ │ │ └─► AVDRAGSGILL
│ │ │
│ │ ├─► Extrem scenklädsel
│ │ │ └─► AVDRAGSGILL
│ │ │ (Dokumentera varför ej privat)
│ │ │
│ │ └─► Historisk kostym/rekvisita
│ │ └─► AVDRAGSGILL
│
├─► Smink och skönhet
│ │
│ ├─► Vardagssmink/hudvård
│ │ └─► EJ AVDRAGSGILL
│ │
│ ├─► Teatersmink/sfx
│ │ └─► AVDRAGSGILL
│ │
│ └─► Skönhetsingrepp (filler, botox, kirurgi)
│ └─► EJ AVDRAGSGILL
│ (Oavsett "varumärkes"-argument)
│
├─► Resa
│ │
│ ├─► Arbetsresa med dokumenterat syfte?
│ │ │
│ │ ├─► JA, endast arbete
│ │ │ └─► FULLT AVDRAG
│ │ │
│ │ ├─► JA, men även privata inslag
│ │ │ └─► PROPORTIONELLT AVDRAG
│ │ │ (Endast arbetsdagar)
│ │ │
│ │ └─► Huvudsakligen privat
│ │ └─► EJ AVDRAGSGILL
│ │
│ └─► Pendling hem-arbete
│ └─► EJ AVDRAGSGILL
│
├─► Hemmakontor
│ │
│ ├─► Separat rum, ej bostad, exklusiv användning?
│ │ │
│ │ ├─► JA
│ │ │ └─► FAKTISKT AVDRAG
│ │ │ (Andel av hyra)
│ │ │
│ │ └─► NEJ
│ │ └─► SCHABLONAVDRAG 2,000 kr
│ │ (Krav: >800 h arbete i bostad)
│
├─► Agentarvode/provision
│ └─► FULLT AVDRAG
│ (Direkt kopplat till intäkt)
│
├─► Programvara/prenumerationer
│ │
│ ├─► Verksamhetsrelaterad?
│ │ │
│ │ ├─► JA (Adobe, redigeringsprogram)
│ │ │ └─► FULLT AVDRAG
│ │ │
│ │ └─► NEJ (Netflix, Spotify privat)
│ │ └─► EJ AVDRAGSGILL
```
---
## 5. Social trygghet
### 5.1 SGI-optimering
```
┌─ Hur optimerar jag SGI?
│
├─► Bolagsform?
│ │
│ ├─► Enskild firma
│ │ │
│ │ ├─► SGI = skattemässigt överskott
│ │ │
│ │ ├─► Gör du avdrag som sänker överskottet?
│ │ │ │
│ │ │ ├─► JA
│ │ │ │ └─► VARNING: Lägre SGI!
│ │ │ │ Överväg om avdragen är värda
│ │ │ │ den lägre sjuk-/föräldrapenningen
│ │ │ │
│ │ │ └─► NEJ
│ │ │ └─► OK
│ │ │
│ │ └─► Uppbyggnadsskede (<36 månader)?
│ │ └─► JA → Kan få jämförelseinkomst
│ │
│ └─► Aktiebolag
│ │
│ ├─► SGI = endast utbetald lön
│ │ (Utdelning räknas EJ)
│ │
│ ├─► Tar du ut tillräcklig lön?
│ │ │
│ │ ├─► JA (≥400,000 kr/år rekommenderat)
│ │ │ └─► OK
│ │ │
│ │ └─► NEJ (låg lön + hög utdelning)
│ │ └─► VARNING: Lågt skydd!
│ │ Risk vid sjukdom/barn
│
├─► Planerar du barn inom 12 månader?
│ │
│ └─► JA
│ └─► MAXIMERA SGI NU
│ • EF: Minimera avdrag
│ • AB: Höj lönen, sänk utdelning
│ • SGI tar ~12 månader att bygga upp
│
├─► Befintligt SGI-skydd tillräckligt?
│ │
│ ├─► Test: Vad får du i sjukpenning?
│ │ Sjukpenning = SGI × 0.8 / 365 per dag
│ │
│ └─► Rekommendation: SGI ≥400,000 kr
│ → ~876 kr/dag i sjukpenning
```
---
## 6. Marknadsföringslagen
### 6.1 Ska inlägget märkas som reklam?
```
┌─ Behöver jag märka med REKLAM?
│
├─► Fick du ersättning (pengar, produkter, tjänster)?
│ │
│ ├─► JA
│ │ │
│ │ ├─► Finns avtal/överenskommelse om publicering?
│ │ │ │
│ │ │ ├─► JA
│ │ │ │ └─► MÄRKNING KRÄVS
│ │ │ │ "REKLAM" eller "ANNONS"
│ │ │ │ Placering: Överst i inlägg
│ │ │ │
│ │ │ └─► NEJ (spontan recension)
│ │ │ │
│ │ │ ├─► Behöll du produkten?
│ │ │ │ │
│ │ │ │ ├─► JA
│ │ │ │ │ └─► REKOMMENDERAD märkning
│ │ │ │ │ (Transparens)
│ │ │ │ │
│ │ │ │ └─► NEJ
│ │ │ │ └─► Ingen märkning krävs
│ │ │
│ └─► NEJ (köpte själv, äkta rekommendation)
│ │
│ ├─► Affiliate-länk inkluderad?
│ │ │
│ │ ├─► JA
│ │ │ └─► MÄRKNING KRÄVS
│ │ │ (Du tjänar på klick/köp)
│ │ │
│ │ └─► NEJ
│ │ └─► Ingen märkning krävs
│
├─► Är det osäkert?
│ └─► JA
│ └─► MÄRK ÄNDÅ
│ Hellre för tydlig än otydlig
```
---
## 7. Dubbel struktur (EF + AB)
### 7.1 Vilket företag för uppdraget?
```
┌─ Ska uppdraget faktureras från EF eller AB?
│
├─► Ersättningsnivå?
│ │
│ ├─► <50,000 kr
│ │ └─► EF (enkelhet)
│ │
│ ├─► 50,000 - 100,000 kr
│ │ └─► Bedöm övriga faktorer ↓
│ │
│ └─► >100,000 kr
│ └─► AB (skatteoptimering)
│
├─► Juridisk risk?
│ │
│ ├─► Hög (kontroversiellt, oklara rättigheter)
│ │ └─► AB (ansvarsbegränsning)
│ │
│ └─► Låg
│ └─► Båda fungerar
│
├─► Avtalstyp?
│ │
│ ├─► Engångsuppdrag
│ │ └─► EF (enkelhet)
│ │
│ └─► Långt ambassadörskap (6+ månader)
│ └─► AB (stabilare struktur)
│
├─► Internationell klient?
│ │
│ ├─► JA
│ │ └─► AB (tydligare för utländska parter)
│ │
│ └─► NEJ
│ └─► Båda fungerar
│
├─► Behöver bygga SGI?
│ │
│ ├─► JA, via EF-överskott
│ │ └─► EF
│ │
│ ├─► JA, via AB-lön
│ │ └─► AB
│ │
│ └─► SGI tillräcklig
│ └─► Optimera för skatt
```
---
### 7.2 Internfaktura mellan EF och AB
```
┌─ Ska EF fakturera AB (eller vice versa)?
│
├─► Finns verklig tjänst/vara som levereras?
│ │
│ ├─► JA
│ │ │
│ │ ├─► Är prissättningen marknadsmässig?
│ │ │ │
│ │ │ ├─► JA (inom ±20% av marknad)
│ │ │ │ │
│ │ │ │ ├─► Finns skriftligt avtal?
│ │ │ │ │ │
│ │ │ │ │ ├─► JA
│ │ │ │ │ │ └─► OK - Internfaktura tillåten
│ │ │ │ │ │ Glöm ej moms (25%)
│ │ │ │ │ │
│ │ │ │ │ └─► NEJ
│ │ │ │ │ └─► UPPRÄTTA AVTAL
│ │ │ │ │ Innan fakturering
│ │ │ │ │
│ │ │ └─► NEJ (avviker >20%)
│ │ │ └─► JUSTERA PRISET
│ │ │ Risk: Omklassificering av Skatteverket
│ │ │
│ └─► NEJ (ren skatteplanering)
│ └─► EJ TILLÅTEN
│ Risk: Förtäckt lön/utdelning
```
@@ -1,500 +0,0 @@
# Felsökningsguide
## 1. Momsproblem
### 1.1 Momsen stämmer inte
**Symptom:** Momsdeklarationen balanserar inte eller Skatteverket ifrågasätter.
**Kontrollpunkter:**
```
□ Har du separerat inhemsk/EU/export-försäljning korrekt?
→ Inhemsk (25%): Ruta 05-08
→ EU B2B (0%): Ruta 39
→ Export (0%): Ruta 40
□ Matchar periodisk sammanställning Ruta 39?
→ Summorna MÅSTE vara identiska
→ Kontrollera att alla EU-kunder finns med
□ Har du bokfört barter-produkter med moms?
→ Marknadsvärde INKL moms som intäkt
→ Momsandel ska redovisas
□ Finns valutakursdifferenser som påverkar?
→ Kursen vid fakturering vs betalning
→ Differenser bokförs separat
□ Har du blandat B2B och B2C inom EU?
→ B2B: Reverse Charge
→ B2C: OSS eller svensk moms
```
**Åtgärder:**
1. Exportera alla försäljningsfakturor
2. Kategorisera per momstyp
3. Summera per kategori
4. Jämför med momsdeklaration
5. Korrigera differenser
---
### 1.2 Periodisk sammanställning saknas eller är fel
**Symptom:** Förseningsavgift eller ifrågasättande från Skatteverket.
**Kontrollpunkter:**
```
□ Har du sålt tjänster till EU-företag under perioden?
→ Om ja, periodisk sammanställning obligatorisk
□ Har du kundens korrekta VAT-nummer?
→ Verifiera på VIES: https://ec.europa.eu/taxation_customs/vies/
□ Har du lämnat sammanställningen i tid?
→ Deadline: 20:e i månaden efter kvartalets slut
→ Q1: 20 april
→ Q2: 20 juli
→ Q3: 20 oktober
→ Q4: 20 januari
□ Stämmer beloppen med Ruta 39?
→ Summorna MÅSTE matcha exakt
```
**Åtgärder vid miss:**
1. Lämna in försenad sammanställning omedelbart
2. Betala förseningsavgift (1,250 kr)
3. Korrigera momsdeklaration om nödvändigt
---
### 1.3 Osäker på vilken momsregel som gäller
**Frågor att ställa:**
```
1. Vem är kunden?
□ Företag → Var är de registrerade?
□ Privatperson → Var bor de?
2. Är det en vara eller tjänst?
□ Vara → Leveransplats avgör
□ Tjänst → Kundens säte avgör (B2B)
3. Har kunden VAT-nummer?
□ Ja → Verifiera det
□ Nej → Behandla som privatperson
```
---
## 2. Bokföringsproblem
### 2.1 Blandade privata och företagstransaktioner
**Symptom:** Oklara verifikationer, sammanblandade konton.
**Kontrollpunkter:**
```
□ Har du separata bankkonton?
→ Privat
→ EF (om tillämpligt)
→ AB (om tillämpligt)
□ Har privata inköp betalats från företagskontot?
→ Bokför som eget uttag (EF) eller lån till ägare (AB)
□ Har företagsinköp betalats privat?
→ Bokför som insättning eget kapital (EF) eller lån från ägare (AB)
```
**Åtgärder:**
1. Gå igenom alla kontoutdrag
2. Markera alla transaktioner som företag/privat
3. Bokför korrigeringar för felaktiga
4. Upprätta rutin för framtiden
---
### 2.2 Saknar verifikationer
**Symptom:** Transaktioner utan underlag.
**Kontrollpunkter:**
```
□ Vilken typ av transaktion?
→ Plattformsintäkt: Ladda ner från plattformen
→ Inköp: Kontakta leverantör för kopia
→ Barter: Upprätta eget underlag
□ Kan du rekonstruera underlaget?
→ Screenshot av beställning
→ E-postbekräftelse
→ Bankutdrag som stöd
```
**Barter-underlag som du själv upprättar:**
```
VERIFIKATION - MOTTAGEN PRODUKT
Datum: [mottagningsdatum]
Produkt: [beskrivning]
Avsändare: [företag/person]
Marknadsvärde inkl. moms: [belopp] kr
Källa för värdering: [prislapp/webbshop/etc.]
Motprestation: [beskrivning av inlägg/samarbete]
Undertecknad av: [influencerns namn]
```
---
### 2.3 Valutaomräkning stämmer inte
**Symptom:** Differenser mellan faktura och betalning.
**Kontrollpunkter:**
```
□ Vilken kurs använde du vid fakturering?
→ Ska vara kurs vid fakturadatum
□ Vilken kurs gällde vid betalning?
→ Kontrollera Riksbankens officiella kurs
□ Har du bokfört kursdifferensen separat?
→ Vinst: Konto 3960
→ Förlust: Konto 7960
```
**Beräkningsexempel:**
```
Faktura: €1,000 @ 11.50 = 11,500 kr
Betalning: €1,000 @ 11.35 = 11,350 kr
Kursförlust: 150 kr
Bokföring:
Debet 1940 Bank: 11,350
Debet 7960 Valutakursförluster: 150
Kredit 1510 Kundfordringar: 11,500
```
---
## 3. Skattedeklarationsproblem
### 3.1 Saknar F-skatt men har fakturerat
**Symptom:** Uppdragsgivare har inte dragit skatt, eller faktura utställd utan F-skatt.
**Kontrollpunkter:**
```
□ Hade du F-skatt när fakturan ställdes ut?
→ Om nej: Uppdragsgivaren ska ha dragit A-skatt
□ Drog uppdragsgivaren skatt?
→ Kontrollera lönespecifikation/kontrolluppgift
□ Betalade uppdragsgivaren arbetsgivaravgifter?
→ Ska synas på kontrolluppgift
```
**Åtgärder:**
1. Om skatt EJ drogs: Deklarera inkomsten, du ansvarar för skatten
2. Kontakta uppdragsgivaren för kontrolluppgift
3. Ansök om F-skatt om du planerar fortsätta
---
### 3.2 Fått preliminärskattebesked med fel belopp
**Symptom:** Debiterad F-skatt stämmer inte med förväntad inkomst.
**Kontrollpunkter:**
```
□ Baseras beskedet på gammal/felaktig uppgift?
→ Skatteverket använder senaste deklarationen
□ Har din verksamhet förändrats väsentligt?
→ Ökade/minskade intäkter
□ Har du redovisat alla intäkter korrekt?
```
**Åtgärder:**
1. Logga in på Skatteverket
2. Ändra preliminär inkomstdeklaration
3. Ny debitering beräknas automatiskt
---
### 3.3 Kvarskatt trots F-skatt
**Symptom:** Slutskattebesked visar kvarskatt.
**Vanliga orsaker:**
```
□ Debiterad F-skatt var för låg
→ Inkomsten blev högre än beräknat
□ Egenavgifter ej medräknade korrekt
→ Särskilt relevant för EF
□ Intäkter utan avdragen skatt
→ Utländska plattformar drar ej svensk skatt
→ Barter-produkter har ej skatt avdragen
□ Kapitalvinster/utdelningar
→ Beskattas separat
```
**Åtgärder:**
1. Analysera slutskattebeskedet
2. Justera kommande års preliminärskatt
3. Skapa buffert för kvarskatt
---
## 4. SGI-problem
### 4.1 SGI är 0 eller mycket låg
**Symptom:** Försäkringskassan meddelar låg SGI.
**Kontrollpunkter för EF:**
```
□ Vad visar NE-bilagan som överskott?
→ SGI = överskott
□ Har du gjort stora avdrag?
→ Sänker överskottet och därmed SGI
□ Är verksamheten i uppbyggnadsskede (<36 mån)?
→ Kan få jämförelseinkomst istället
```
**Kontrollpunkter för AB:**
```
□ Hur mycket lön har du tagit ut?
→ SGI = utbetald lön
□ Tar du mest utdelning?
→ Utdelning räknas EJ i SGI
□ Är lönen "marknadsmässig"?
→ För låg lön kan ifrågasättas
```
**Åtgärder:**
1. Beräkna vilken SGI du behöver
2. Justera lön/överskott för att nå nivån
3. Kontakta Försäkringskassan för omprövning
---
### 4.2 Väntar barn men har låg SGI
**Symptom:** Oro för låg föräldrapenning.
**Tidslinje:**
```
SGI fastställs vid:
- Födsel (för föräldrapenning)
- Baseras på inkomst ~12 månader bakåt
Åtgärd om <12 månader kvar:
→ Höj lön/överskott OMEDELBART
→ Varje månad med högre inkomst hjälper
Åtgärd om >12 månader kvar:
→ Planera inkomstfördelning noggrant
→ Sikta på SGI ≥599,250 kr för maximal föräldrapenning
```
---
## 5. Plattformsproblem
### 5.1 Hittar inte fakturaunderlag
**Per plattform:**
| Plattform | Var hittar jag underlag? |
|-----------|--------------------------|
| YouTube | YouTube Studio → Analytics → Revenue → Transaction history i AdSense |
| Twitch | Creator Dashboard → Channel Analytics → Revenue |
| Instagram | Professional Dashboard → Monetization → Payouts |
| TikTok | TikTok Creator Portal → Balance → Transaction History |
| Patreon | Creator Dashboard → Income → Payout History |
**Om underlag saknas:**
1. Kontakta plattformens support
2. Använd bankutdrag som stöd
3. Upprätta egen verifikation med tillgänglig information
---
### 5.2 Plattform har hållit inne skatt (W-8BEN)
**Symptom:** 30% avdrag på USA-baserade plattformar.
**Kontrollpunkter:**
```
□ Har du fyllt i W-8BEN korrekt?
→ Formulär som intygar icke-amerikansk skattestatus
□ Har formuläret löpt ut?
→ Giltigt i 3 år
→ Måste förnyas
□ Har du angett korrekt land?
→ Sverige har skatteavtal med USA
```
**Åtgärder:**
1. Logga in på plattformen
2. Gå till skatteuppgifter/tax information
3. Fyll i nytt W-8BEN
4. Kontakta support om innehållen skatt kan återbetalas
---
## 6. Juridiska problem
### 6.1 Klagomål om reklammarkering
**Symptom:** Varning från Konsumentverket eller klagomål.
**Omedelbara åtgärder:**
```
1. Redigera inlägget
→ Lägg till "REKLAM" överst
→ Se till att det är tydligt synligt
2. Dokumentera
→ Ta screenshot av korrigerat inlägg
→ Notera datum och tid för ändring
3. Informera uppdragsgivaren
→ De kan också bli ansvariga
```
**Förebyggande:**
- Märk ALLA betalda samarbeten
- Placera märkning ÖVERST
- Vid osäkerhet: Märk ändå
---
### 6.2 Upphovsrättskrav (musik, bilder)
**Symptom:** Copyright claim, krav på ersättning, nedtagning.
**Omedelbara åtgärder:**
```
1. Ta ner innehållet
→ Stoppar ytterligare intrång
2. Svara INTE på krav utan juridisk rådgivning
→ Medge ingenting
3. Dokumentera
→ Spara allt material
→ Notera publiceringsperiod
```
**Förebyggande:**
- Använd endast licensierad musik
- Epidemic Sound, Artlist = säkra alternativ
- Kontrollera bildlicenser
---
## 7. Dubbel struktur-problem
### 7.1 Blandat EF och AB
**Symptom:** Oklart vilket företag som fakturerat/betalat vad.
**Kontrollpunkter:**
```
□ Finns transaktioner bokförda i fel företag?
→ Gå igenom alla verifikationer
□ Har samma kostnad bokförts i båda?
→ Dubbelräkning ej tillåten
□ Har internfakturor hanterats korrekt?
→ Moms ska vara med
→ Avtal ska finnas
```
**Åtgärder:**
1. Gör fullständig genomgång av båda bokföringarna
2. Upprätta lista över alla internfakturor
3. Korrigera felaktiga bokningar
4. Upprätta tydliga rutiner
---
### 7.2 Skatteverket ifrågasätter internprissättning
**Symptom:** Frågor om transaktioner mellan EF och AB.
**Dokumentation som behövs:**
```
□ Skriftliga avtal för alla transaktioner
□ Prissättningsunderlag (marknadsjämförelser)
□ Affärsmässig motivering
□ Faktiska leveranser/prestationer
```
**Försvar:**
1. Visa att priset är marknadsmässigt
2. Visa att verklig tjänst/vara levererades
3. Visa affärsmässigt syfte (ej endast skatteplanering)
---
## 8. Akuta situationer
### 8.1 Missad deadline
| Deadline | Konsekvens vid miss | Åtgärd |
|----------|---------------------|--------|
| Momsdeklaration | Förseningsavgift | Lämna in omedelbart |
| Periodisk sammanställning | 1,250 kr avgift | Lämna in omedelbart |
| Inkomstdeklaration | Förseningsavgift + skönstaxering | Kontakta Skatteverket |
| F-skatt betalning | Ränta | Betala + kontakta Skatteverket |
### 8.2 Kontrollbesked från Skatteverket
**Steg:**
1. Läs noggrant vad de frågar om
2. Samla all relevant dokumentation
3. Svara inom angiven tid
4. Vid osäkerhet: Anlita rådgivare
5. Var sanningsenlig men ange inte mer än vad som frågas
### 8.3 Revision
**Förberedelser:**
1. Samla ALL bokföring för perioden
2. Kontrollera att verifikationer finns
3. Förbered förklaringar för ovanliga poster
4. Anlita redovisningskonsult om möjligt
@@ -1,306 +0,0 @@
# Kalender och deadlines
## Årsöversikt
### Januari
| Dag | Händelse | Gäller |
|-----|----------|--------|
| 12 | Momsdeklaration (månads) december | Månadsredovisare |
| 12 | Arbetsgivardeklaration december | AB med anställda |
| 17 | F-skatt inbetalning | EF och AB |
| 20 | Periodisk sammanställning Q4 | EU-försäljning |
| 26 | Momsdeklaration Q4 (okt-dec) | Kvartalsredovisare |
| 31 | Kontrolluppgifter till Skatteverket | AB (löner) |
### Februari
| Dag | Händelse | Gäller |
|-----|----------|--------|
| 12 | Momsdeklaration (månads) januari | Månadsredovisare |
| 12 | Arbetsgivardeklaration januari | AB med anställda |
| 17 | F-skatt inbetalning | EF och AB |
| 28 | Preliminär inkomstdeklaration (ändring) | Om behov finns |
### Mars
| Dag | Händelse | Gäller |
|-----|----------|--------|
| 12 | Momsdeklaration (månads) februari | Månadsredovisare |
| 12 | Arbetsgivardeklaration februari | AB med anställda |
| 17 | F-skatt inbetalning | EF och AB |
| 31 | Bokslut räkenskapsår (kalenderår) | AB |
### April
| Dag | Händelse | Gäller |
|-----|----------|--------|
| 12 | Momsdeklaration (månads) mars | Månadsredovisare |
| 12 | Arbetsgivardeklaration mars | AB med anställda |
| 17 | F-skatt inbetalning | EF och AB |
| 20 | Periodisk sammanställning Q1 | EU-försäljning |
| 26 | Momsdeklaration Q1 (jan-mar) | Kvartalsredovisare |
### Maj
| Dag | Händelse | Gäller |
|-----|----------|--------|
| 2 | Inkomstdeklaration privatperson + NE | EF |
| 12 | Momsdeklaration (månads) april | Månadsredovisare |
| 12 | Arbetsgivardeklaration april | AB med anställda |
| 17 | F-skatt inbetalning | EF och AB |
### Juni
| Dag | Händelse | Gäller |
|-----|----------|--------|
| 12 | Momsdeklaration (månads) maj | Månadsredovisare |
| 12 | Arbetsgivardeklaration maj | AB med anställda |
| 17 | F-skatt inbetalning | EF och AB |
| 30 | Årsredovisning till Bolagsverket | AB (kalenderår) |
### Juli
| Dag | Händelse | Gäller |
|-----|----------|--------|
| 1 | Inkomstdeklaration AB (kalenderår) | AB |
| 12 | Momsdeklaration (månads) juni | Månadsredovisare |
| 12 | Arbetsgivardeklaration juni | AB med anställda |
| 17 | F-skatt inbetalning | EF och AB |
| 20 | Periodisk sammanställning Q2 | EU-försäljning |
| 26 | Momsdeklaration Q2 (apr-jun) | Kvartalsredovisare |
### Augusti
| Dag | Händelse | Gäller |
|-----|----------|--------|
| 12 | Momsdeklaration (månads) juli | Månadsredovisare |
| 12 | Arbetsgivardeklaration juli | AB med anställda |
| 17 | F-skatt inbetalning | EF och AB |
| ~15 | Slutskattebesked | Alla |
### September
| Dag | Händelse | Gäller |
|-----|----------|--------|
| 12 | Momsdeklaration (månads) augusti | Månadsredovisare |
| 12 | Arbetsgivardeklaration augusti | AB med anställda |
| 17 | F-skatt inbetalning | EF och AB |
| ~12 | Kvarskatt inbetalning 1 | Vid kvarskatt |
### Oktober
| Dag | Händelse | Gäller |
|-----|----------|--------|
| 12 | Momsdeklaration (månads) september | Månadsredovisare |
| 12 | Arbetsgivardeklaration september | AB med anställda |
| 17 | F-skatt inbetalning | EF och AB |
| 20 | Periodisk sammanställning Q3 | EU-försäljning |
| 26 | Momsdeklaration Q3 (jul-sep) | Kvartalsredovisare |
### November
| Dag | Händelse | Gäller |
|-----|----------|--------|
| 12 | Momsdeklaration (månads) oktober | Månadsredovisare |
| 12 | Arbetsgivardeklaration oktober | AB med anställda |
| ~12 | Kvarskatt inbetalning 2 | Vid kvarskatt |
| 17 | F-skatt inbetalning | EF och AB |
### December
| Dag | Händelse | Gäller |
|-----|----------|--------|
| 12 | Momsdeklaration (månads) november | Månadsredovisare |
| 12 | Arbetsgivardeklaration november | AB med anställda |
| 17 | F-skatt inbetalning | EF och AB |
| 27 | Sista arbetsdag före jul (myndigheter) | Planering |
| 31 | Räkenskapsårets slut | Kalenderår |
---
## Kvartalsvisa deadlines
### Momsdeklaration (kvartalsredovisare)
| Kvartal | Period | Deadline |
|---------|--------|----------|
| Q1 | Jan-Mar | 12 maj (papper) / 26 april (e-tjänst) |
| Q2 | Apr-Jun | 17 aug (papper) / 26 juli (e-tjänst) |
| Q3 | Jul-Sep | 12 nov (papper) / 26 okt (e-tjänst) |
| Q4 | Okt-Dec | 12 feb (papper) / 26 jan (e-tjänst) |
### Periodisk sammanställning (EU-försäljning)
| Kvartal | Period | Deadline |
|---------|--------|----------|
| Q1 | Jan-Mar | 20 april |
| Q2 | Apr-Jun | 20 juli |
| Q3 | Jul-Sep | 20 oktober |
| Q4 | Okt-Dec | 20 januari |
### OSS-deklaration (EU-privatpersonsförsäljning)
| Kvartal | Period | Deadline |
|---------|--------|----------|
| Q1 | Jan-Mar | 30 april |
| Q2 | Apr-Jun | 31 juli |
| Q3 | Jul-Sep | 31 oktober |
| Q4 | Okt-Dec | 31 januari |
---
## F-skatt inbetalningsdatum
| Månad | Inbetalningsdatum |
|-------|-------------------|
| Januari | 17 januari |
| Februari | 17 februari |
| Mars | 17 mars |
| April | 17 april |
| Maj | 17 maj |
| Juni | 17 juni |
| Juli | 17 juli |
| Augusti | 17 augusti |
| September | 17 september |
| Oktober | 17 oktober |
| November | 17 november |
| December | 17 december |
**OBS:** Om den 17:e infaller på helg, gäller närmast följande vardag.
---
## Löpande uppgifter
### Dagligen/Veckovis
- [ ] Spara kvitton och verifikationer
- [ ] Notera mottagna produkter (barter)
- [ ] Dokumentera arbetsresor
### Månadsvis
- [ ] Stäm av bankkonton mot bokföring
- [ ] Bokför månadens transaktioner
- [ ] Kontrollera plattformsutbetalningar
- [ ] Beräkna eventuella valutakursdifferenser
- [ ] Momsdeklaration (om månadsredovisare)
### Kvartalsvis
- [ ] Momsdeklaration (om kvartalsredovisare)
- [ ] Periodisk sammanställning (om EU-försäljning)
- [ ] OSS-deklaration (om EU-privatpersonsförsäljning)
- [ ] Resultatuppföljning
- [ ] SGI-kontroll
### Årligen
**Januari-februari:**
- [ ] Upprätta årsbokslut för föregående år
- [ ] Sammanställ alla intäkter och kostnader
- [ ] Beräkna avskrivningar
- [ ] Hantera periodiseringsfonder (EF)
**Mars-april:**
- [ ] Förbered inkomstdeklaration
- [ ] Kontrollera kontrolluppgifter
- [ ] Granska förifylld deklaration
**Maj:**
- [ ] Lämna inkomstdeklaration (2 maj)
- [ ] NE-bilaga (EF)
**Juni-juli:**
- [ ] Årsredovisning till Bolagsverket (AB)
- [ ] Inkomstdeklaration AB (1 juli)
**Augusti-september:**
- [ ] Analysera slutskattebesked
- [ ] Justera preliminärskatt för nästa år
- [ ] Planera för eventuell kvarskatt
**Oktober-december:**
- [ ] Skatteplanering inför årsskiftet
- [ ] 3:12-beräkning (AB)
- [ ] Pensionssparande (avdragsgillt)
- [ ] Friskvårdsbidrag (AB)
---
## Påminnelser per bolagsform
### Enskild firma (EF)
| Tidpunkt | Uppgift |
|----------|---------|
| Månadsvis | Bokföring, avstämning |
| Kvartalsvis | Moms, periodisk sammanställning |
| 2 maj | Inkomstdeklaration + NE-bilaga |
| Löpande | Dokumentera barter, resor, avdrag |
### Aktiebolag (AB)
| Tidpunkt | Uppgift |
|----------|---------|
| Månadsvis | Bokföring, lönehantering, arbetsgivardeklaration |
| Kvartalsvis | Moms, periodisk sammanställning |
| 31 mars | Bokslut (kalenderår) |
| 30 juni | Årsredovisning till Bolagsverket |
| 1 juli | Inkomstdeklaration AB |
| December | 3:12-beräkning, utdelningsbeslut |
### Dubbel struktur (EF + AB)
| Tidpunkt | Uppgift |
|----------|---------|
| Månadsvis | Separat bokföring båda, internfakturor |
| Kvartalsvis | Separata momsdeklarationer, periodiska sammanställningar |
| 2 maj | Inkomstdeklaration privatperson + NE |
| 30 juni | Årsredovisning AB |
| 1 juli | Inkomstdeklaration AB |
| Löpande | Avstämning interntransaktioner |
---
## Kritiska datum att INTE missa
### Förseningsavgifter
| Uppgift | Avgift vid miss |
|---------|-----------------|
| Periodisk sammanställning | 1,250 kr |
| Momsdeklaration | 625 kr (ökar vid upprepad försening) |
| Inkomstdeklaration | 1,250 kr (kan öka) |
| Årsredovisning | Förseningsavgift + risk för likvidation |
### Viktiga gränsdatum
| Datum | Händelse |
|-------|----------|
| 2 maj | Sista dag inkomstdeklaration privatperson |
| 30 juni | Sista dag årsredovisning AB (6 mån efter bokslut) |
| 1 juli | Sista dag inkomstdeklaration AB (kalenderår) |
| 31 december | Räkenskapsårets slut, sista dag för avdragsgilla pensionsinbetalningar |
---
## Platshållare för användarspecifika påminnelser
### Min bolagsform: [EF / AB / Båda]
### Mina deadlines:
| Uppgift | Deadline | Status |
|---------|----------|--------|
| Momsdeklaration Q_ | | |
| Periodisk sammanställning Q_ | | |
| F-skatt månad | | |
| Inkomstdeklaration | | |
| Årsredovisning | | |
### Mina återkommande uppgifter:
- [ ] ...
- [ ] ...
@@ -1,542 +0,0 @@
# Varningsflaggor
## 1. Bokföringsvarningar
### 1.1 Sammanblandning av entiteter
**Flagga:** Transaktion verkar bokförd i fel företag
**Triggers:**
- Samma fakturanummer i både EF och AB
- Kostnad bokförd i AB men betald från EF-konto
- Intäkt från plattform bokförd i båda entiteterna
**AI-åtgärd:**
```
VARNING: Denna transaktion kan vara bokförd i fel entitet.
Kontrollera:
- Vilken entitet har avtalet med kunden?
- Från vilket konto gjordes betalningen?
- Har samma transaktion bokförts i den andra entiteten?
```
---
### 1.2 Privata kostnader i verksamheten
**Flagga:** Misstänkt privat kostnad bokförd som företagskostnad
**Triggers:**
- Klädinköp (ej uniformer/rekvisita)
- Smink och skönhetsprodukter
- Gym/träningskort (i EF)
- Restaurangbesök utan dokumenterad affärsanledning
- Resor utan arbetssyfte
**AI-åtgärd:**
```
VARNING: Denna kostnad kan vara privat och ej avdragsgill.
[Typ av kostnad] kräver:
- Dokumentation av affärssyfte
- Kan endast dras av om [specifika villkor]
Vill du fortsätta bokföra detta som företagskostnad?
```
---
### 1.3 Saknad verifikation
**Flagga:** Transaktion utan tillräckligt underlag
**Triggers:**
- Inbetalning utan faktura/avtal
- Barter-produkt utan värderingsunderlag
- Resa utan syftesdokumentation
**AI-åtgärd:**
```
VARNING: Verifikation saknas eller är ofullständig.
För att bokföra behövs:
- [Lista på kravd dokumentation]
Vill du skapa ett eget underlag?
```
---
### 1.4 Orimliga belopp
**Flagga:** Belopp avviker kraftigt från normalt
**Triggers:**
- Enskilt inköp >50,000 kr utan förklaring
- Negativ intäkt (rättelse?)
- Valutakurs utanför rimligt intervall
**AI-åtgärd:**
```
VARNING: Beloppet verkar ovanligt.
- Bokfört belopp: [X] kr
- Normalt intervall: [Y-Z] kr
Kontrollera att beloppet är korrekt.
```
---
## 2. Momsvarningar
### 2.1 Periodisk sammanställning saknas
**Flagga:** EU-försäljning utan periodisk sammanställning
**Triggers:**
- Försäljning bokförd i Ruta 39
- Ingen periodisk sammanställning inlämnad för perioden
- Periodisk sammanställning ≠ Ruta 39
**AI-åtgärd:**
```
VARNING: Periodisk sammanställning krävs.
Du har bokfört [X] kr i EU-försäljning under [kvartal].
Deadline för periodisk sammanställning: [datum]
Förseningsavgift vid miss: 1,250 kr
Kunder att rapportera:
- [Företag 1]: [VAT-nummer], [belopp]
- [Företag 2]: [VAT-nummer], [belopp]
```
---
### 2.2 Felaktig momshantering
**Flagga:** Moms hanterad inkonsekvent med kundtyp
**Triggers:**
- Svensk moms debiterad till EU-företag med VAT-nummer
- Ingen moms debiterad till svensk kund
- Export-moms på EU-försäljning
**AI-åtgärd:**
```
VARNING: Momshanteringen verkar felaktig.
Kund: [Kundnamn]
Kundens säte: [Land]
VAT-nummer: [Om finns]
Korrekt momshantering:
- [Regel som borde gälla]
- Ruta i momsdeklaration: [X]
Vill du korrigera?
```
---
### 2.3 OSS-tröskel överskriden
**Flagga:** EU-privatpersonförsäljning överstiger 99,680 kr
**Triggers:**
- Summa B2C EU-försäljning >99,680 kr under året
- Fortsatt svensk moms efter överskridande
**AI-åtgärd:**
```
VARNING: OSS-tröskel överskriden.
Total EU-privatpersonsförsäljning: [X] kr
Tröskel: 99,680 kr
Konsekvens:
- Obligatoriskt att debitera mottagarlandets moms
- Registrera för OSS hos Skatteverket
- Lämna kvartalsvis OSS-deklaration
Tidigare försäljning under året kan behöva korrigeras.
```
---
### 2.4 Ogiltigt VAT-nummer
**Flagga:** VAT-nummer kunde inte verifieras
**Triggers:**
- VAT-nummer angivet men validering misslyckades
- Format stämmer ej med landskod
**AI-åtgärd:**
```
VARNING: VAT-nummer kunde inte verifieras.
Angivet nummer: [VAT-nummer]
Land: [Landkod]
Verifiera på VIES: https://ec.europa.eu/taxation_customs/vies/
Om ogiltigt:
- Kunden behandlas som privatperson
- Svensk moms eller OSS gäller
```
---
## 3. SGI-varningar
### 3.1 Låg SGI-grund
**Flagga:** SGI-grundande inkomst under rekommenderad nivå
**Triggers:**
- EF: Överskott <200,000 kr/år
- AB: Lön <200,000 kr/år
- Total SGI-grund <400,000 kr/år
**AI-åtgärd:**
```
VARNING: Lågt socialförsäkringsskydd.
Din SGI-grund: [X] kr/år
Rekommenderad miniminivå: 400,000 kr/år
Vid sjukdom:
- Nuvarande sjukpenning: ~[Y] kr/dag
- Med rekommenderad SGI: ~[Z] kr/dag
Vid föräldraledighet (390 dagar):
- Nu: [A] kr totalt
- Rekommenderat: [B] kr totalt
- Skillnad: [C] kr
Överväg att:
- EF: Minska avdrag / öka överskott
- AB: Höja löneuttag
```
---
### 3.2 Hög utdelning, låg lön (AB)
**Flagga:** Obalans mellan lön och utdelning
**Triggers:**
- Utdelning >3x lön
- Lön under 300,000 kr med vinst >500,000 kr
- SGI-konsekvens ej beaktad
**AI-åtgärd:**
```
VARNING: Skatteoptimering påverkar ditt sociala skydd.
Uttagen lön: [X] kr
Planerad utdelning: [Y] kr
Din SGI: [X] kr (endast lön räknas)
Konsekvens:
- Sjukpenning baseras på [X] kr
- Föräldrapenning baseras på [X] kr
- Utdelning [Y] kr ger INGET socialt skydd
Rekommendation: Balansera lön och utdelning utifrån ditt behov av trygghet.
```
---
### 3.3 Planerad föräldraledighet
**Flagga:** Användare nämner barn/föräldraledighet med låg SGI
**Triggers:**
- Nyckelord: "gravid", "barn", "föräldraledigt", "VAB"
- Kombinerat med låg SGI-grund
**AI-åtgärd:**
```
VIKTIGT: Föräldrapenning baseras på din SGI.
Nuvarande SGI-grund: [X] kr
Om barn planeras inom 12 månader:
- SGI fastställs vid barnets födelse
- Varje månad med högre inkomst höjer SGI
Åtgärd NU:
- EF: Redovisa högre överskott
- AB: Höj månadslönen
- Sikta på SGI ≥599,250 kr för maximal föräldrapenning
```
---
## 4. Strukturvarningar (dubbel struktur)
### 4.1 Internprissättning avviker
**Flagga:** Pris mellan EF och AB verkar icke-marknadsmässigt
**Triggers:**
- Timpris <300 kr eller >2,000 kr
- Avvikelse >20% från jämförbar extern prissättning
- Pris utan dokumenterad grund
**AI-åtgärd:**
```
VARNING: Internprissättningen kan ifrågasättas.
Transaktion: [Beskrivning]
Pris: [X] kr
Marknadsmässigt intervall: [Y-Z] kr
Skatteverket kräver armlängdspris.
Vid avvikelse:
- Risk för omklassificering
- Förtäckt lön/utdelning beskattas hårdare
Rekommendation: Dokumentera varför priset är marknadsmässigt.
```
---
### 4.2 Avtal saknas för interntransaktion
**Flagga:** Faktura mellan EF och AB utan avtal
**Triggers:**
- Internfaktura bokförd
- Inget avtal registrerat i systemet
**AI-åtgärd:**
```
VARNING: Avtal saknas för interntransaktion.
Transaktion: [Beskrivning]
Belopp: [X] kr
Krav för godkänd interntransaktion:
1. Skriftligt avtal
2. Marknadsmässig prissättning
3. Verklig tjänst/vara levererad
4. Affärsmässig motivering
Upprätta avtal FÖRE fakturering.
```
---
### 4.3 Alla kostnader i ett företag, alla intäkter i annat
**Flagga:** Misstänkt resultatförflyttning
**Triggers:**
- EF har endast kostnader (förlust)
- AB har endast intäkter (hög vinst)
- Eller vice versa
**AI-åtgärd:**
```
VARNING: Obalanserad resultatfördelning.
EF resultat: [X] kr
AB resultat: [Y] kr
Detta mönster kan tyda på:
- Felaktig allokering av kostnader
- Resultatförflyttning (ej tillåtet utan affärsmässig grund)
Kontrollera:
- Är kostnaderna bokförda i rätt entitet?
- Har rätt entitet fakturerat kunderna?
- Finns underlag för fördelningen?
```
---
## 5. Juridiska varningar
### 5.1 Reklammarkering saknas
**Flagga:** Betalt samarbete utan tydlig reklammarkering
**Triggers:**
- Inkomst från varumärke/plattform
- Ingen notering om reklammarkering
- Samarbetsavtal registrerat
**AI-åtgärd:**
```
PÅMINNELSE: Kontrollera reklammarkering.
Samarbete med: [Företag]
Ersättning: [X] kr
Krav enligt Marknadsföringslagen:
- "REKLAM" eller "ANNONS" överst i inlägg
- Tydlig och omedelbart synlig
- I video: Muntligt OCH/ELLER textgrafik
Underlåtenhet kan leda till:
- Varning från Konsumentverket
- Förbud vid vite
- Marknadsstörningsavgift
```
---
### 5.2 Upphovsrättskänsligt material
**Flagga:** Risk för upphovsrättsintrång
**Triggers:**
- Video/musik-content i kommersiellt sammanhang
- Ingen licensdokumentation
- Känd låt/artist nämnd
**AI-åtgärd:**
```
VARNING: Kontrollera upphovsrätt.
Om du använder musik/bilder/video:
- Har du licens?
- Är materialet royalty-free?
Utan licens:
- Risk för skadestånd (referens: ABBA-fallet 200,000 kr)
- Innehåll kan tas ner
- Plattformskonto kan stängas
Säkra alternativ:
- Epidemic Sound
- Artlist
- Creative Commons
```
---
## 6. Administrativa varningar
### 6.1 Deadline närmar sig
**Flagga:** Viktig deadline inom 7 dagar
**Triggers:**
- Momsdeklaration oinlämnad
- Periodisk sammanställning oinlämnad
- F-skatt obetald
- Inkomstdeklaration oinlämnad
**AI-åtgärd:**
```
DEADLINE-VARNING: [Uppgift]
Deadline: [Datum] ([X] dagar kvar)
Status: [Ej påbörjad / Påbörjad / Klar]
Förseningsavgift vid miss: [Belopp] kr
[Länk/instruktion för att slutföra]
```
---
### 6.2 Saknad kontrolluppgift
**Flagga:** Inkomst utan motsvarande kontrolluppgift
**Triggers:**
- Intäkt bokförd från svenskt företag
- Ingen kontrolluppgift i förväg
- Deklarationsperiod närmar sig
**AI-åtgärd:**
```
VARNING: Kontrolluppgift kan saknas.
Inkomst från: [Företag]
Belopp: [X] kr
Period: [År/Månad]
Kontrollera:
- Fick du kontrolluppgift?
- Om nej, kontakta [Företag]
Kontrolluppgifter ska finnas i e-tjänsten senast [datum].
```
---
### 6.3 Valutakurs ej dokumenterad
**Flagga:** Utländsk transaktion utan kursdokumentation
**Triggers:**
- Belopp i USD/EUR/annan valuta
- Ingen valutakurs angiven
- Stor skillnad mellan möjliga kurser
**AI-åtgärd:**
```
VARNING: Valutakurs behöver dokumenteras.
Transaktion: [Beskrivning]
Belopp: [Valuta] [Originalbelopp]
Datum: [Inbetalningsdatum]
Ange valutakurs eller hämta automatiskt:
- Riksbankens kurs [datum]: [X.XX]
- Beräknat SEK-belopp: [Y] kr
Kursen ska vara den som gällde vid inbetalningsdatum.
```
---
## 7. Automatiska kontroller
### 7.1 Periodiska avstämningar
```
MÅNATLIG KONTROLL:
□ Summa intäkter plattformar = Summa bokförda intäkter?
□ Banksaldo = Bokfört saldo?
□ Ingående moms rimlig i förhållande till kostnader?
□ Utgående moms rimlig i förhållande till intäkter?
KVARTALSVIS KONTROLL:
□ Periodisk sammanställning = Ruta 39?
□ SGI-grund tillräcklig?
□ Internfakturor avstämda mellan EF och AB?
ÅRSVIS KONTROLL:
□ Alla kontrolluppgifter mottagna?
□ Preliminärskatt vs faktisk skatt?
□ Periodiseringsfonder inom gränser?
□ 3:12-gränsbelopp beräknat?
```
---
### 7.2 Flaggningströsklar
| Kontroll | Flagga om |
|----------|-----------|
| SGI-grund | <200,000 kr/år |
| Lön vs utdelning (AB) | Utdelning >3x lön |
| Internpris | Avviker >20% från marknad |
| Periodisk sammanställning | Saknas vid EU-försäljning |
| Momsavvikelse | >5% differens |
| Kostnad utan verifikation | Alltid |
| Privat kostnad i företag | Alltid |
| Deadline | <7 dagar kvar |
---
## 8. Eskaleringsregler
### När ska AI rekommendera extern rådgivning?
```
REKOMMENDERA REVISOR/REDOVISNINGSKONSULT:
- Omsättning >3 MSEK/år
- Komplex dubbel struktur
- Internationella transaktioner med osäker momshantering
- Skatteverket har skickat förfrågan
REKOMMENDERA JURIST:
- Upphovsrättskrav
- Avtalsförhandling med stort värde (>100,000 kr)
- Tvist med uppdragsgivare
- Marknadsföringsrättslig varning
REKOMMENDERA FÖRSÄKRINGSKASSAN:
- SGI-relaterade frågor
- Uppbyggnadsskede-bedömning
- Omprövning av beslut
```