Bug/accounting suggestion (#456)

* feat: add bike benefit handling and optional vacation accrual

- Introduced bike benefit (cykelförmån) with calculations for annual market value and monthly taxable value.
- Updated schemas to include new benefit types and validation rules.
- Implemented API routes for creating, updating, and deleting employee benefits.
- Enhanced salary calculation logic to accommodate new vacation rule options, including a 'none' option for no accrual.
- Added UI components for managing employee benefits, including input for bike benefit specifics.
- Created database migrations for employee benefits and updated salary line items to support new benefit types.

* chore: remove Langfuse env var checks

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>

* feat: enhance OAuth callback URL handling and update default scopes for Visma integration

* feat: remove trade_name field and simplify company naming in invoices

* refactor: destructure canWrite from useCanWrite for consistency across components

* feat: enhance PATCH endpoint to validate existing benefits and handle bike benefit updates

* feat: add missing label for bike benefit in salary line item types

---------

Co-authored-by: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
This commit is contained in:
Mattsson
2026-05-13 00:26:04 +02:00
committed by GitHub
co-authored by Claude Opus 4.7
parent cd96e5ec26
commit 3fa871c742
53 changed files with 1182 additions and 125 deletions
@@ -0,0 +1,17 @@
-- =============================================================================
-- Salary: optional vacation accrual ("none" mode)
-- =============================================================================
-- Adds a third value to employees.vacation_rule so a company can disable
-- semesteravsättning entirely. Useful for sole owners (ägare = enda anställd)
-- who don't accrue semester — booking otherwise creates a phantom 2920 liability.
--
-- procentregeln — 12% (or 14.4% for 30+ days) accrued to 2920 (Semesterlagen 26§)
-- sammaloneregeln — semestertillägg only (Semesterlagen 16a§)
-- none — no accrual; salary expense is the full cost
ALTER TABLE public.employees DROP CONSTRAINT IF EXISTS employees_vacation_rule_check;
ALTER TABLE public.employees ADD CONSTRAINT employees_vacation_rule_check
CHECK (vacation_rule IN ('procentregeln', 'sammaloneregeln', 'none'));
NOTIFY pgrst, 'reload schema';
@@ -0,0 +1,93 @@
-- =============================================================================
-- Employee benefits (förmånshantering)
-- =============================================================================
-- Per-employee benefit assignments that drive auto-generated line items on
-- every salary run. Today the engine has helpers for car/meal/wellness benefits
-- but no link from a specific employee to a specific benefit instance — meaning
-- users had to add a benefit_* line item manually each month. This table closes
-- that gap.
--
-- Includes bike benefit (cykelförmån, skattefri schablon 3 000 kr/år from 2022,
-- Skatteverket).
CREATE TABLE public.employee_benefits (
id uuid PRIMARY KEY DEFAULT uuid_generate_v4(),
employee_id uuid NOT NULL REFERENCES public.employees(id) ON DELETE CASCADE,
company_id uuid NOT NULL REFERENCES public.companies(id) ON DELETE CASCADE,
user_id uuid NOT NULL REFERENCES auth.users(id) ON DELETE CASCADE,
benefit_type text NOT NULL
CHECK (benefit_type IN ('bike', 'car', 'meals', 'housing', 'wellness', 'other')),
description text NOT NULL,
-- Monthly taxable förmånsvärde (SEK). For bike, computed as
-- max(0, annual_market_value - 3000) / 12 per Skatteverket schablon.
monthly_value numeric NOT NULL CHECK (monthly_value >= 0),
valid_from date NOT NULL,
valid_to date,
-- Free-form context: bike → { annual_market_value, tax_free_allowance }
-- car → { nybilspris, environmental_type, ... }
metadata jsonb NOT NULL DEFAULT '{}'::jsonb,
is_active boolean NOT NULL DEFAULT true,
created_at timestamptz NOT NULL DEFAULT now(),
updated_at timestamptz NOT NULL DEFAULT now(),
CHECK (valid_to IS NULL OR valid_to >= valid_from)
);
ALTER TABLE public.employee_benefits ENABLE ROW LEVEL SECURITY;
CREATE POLICY "employee_benefits_select" ON public.employee_benefits
FOR SELECT USING (company_id IN (SELECT public.user_company_ids()));
CREATE POLICY "employee_benefits_insert" ON public.employee_benefits
FOR INSERT WITH CHECK (company_id IN (SELECT public.user_company_ids()));
CREATE POLICY "employee_benefits_update" ON public.employee_benefits
FOR UPDATE USING (company_id IN (SELECT public.user_company_ids()));
CREATE POLICY "employee_benefits_delete" ON public.employee_benefits
FOR DELETE USING (company_id IN (SELECT public.user_company_ids()));
CREATE INDEX idx_employee_benefits_employee ON public.employee_benefits (employee_id);
CREATE INDEX idx_employee_benefits_company ON public.employee_benefits (company_id);
CREATE INDEX idx_employee_benefits_active
ON public.employee_benefits (employee_id, valid_from, valid_to)
WHERE is_active = true;
CREATE TRIGGER employee_benefits_updated_at
BEFORE UPDATE ON public.employee_benefits
FOR EACH ROW EXECUTE FUNCTION public.update_updated_at_column();
-- Link a salary_line_items row back to the employee_benefits row that produced
-- it. Used by the calculate flow to delete-then-recreate benefit line items
-- without disturbing manually-added line items.
ALTER TABLE public.salary_line_items
ADD COLUMN source_benefit_id uuid REFERENCES public.employee_benefits(id) ON DELETE SET NULL;
CREATE INDEX idx_salary_line_items_source_benefit
ON public.salary_line_items (source_benefit_id)
WHERE source_benefit_id IS NOT NULL;
-- Extend salary_line_items to accept a benefit_bike type for cykelförmån.
ALTER TABLE public.salary_line_items DROP CONSTRAINT IF EXISTS salary_line_items_item_type_check;
ALTER TABLE public.salary_line_items ADD CONSTRAINT salary_line_items_item_type_check
CHECK (item_type IN (
'monthly_salary', 'hourly_salary', 'overtime', 'bonus', 'commission',
'gross_deduction_pension', 'gross_deduction_other',
'benefit_car', 'benefit_housing', 'benefit_meals', 'benefit_wellness',
'benefit_bike', 'benefit_other',
'sick_karens', 'sick_day2_14', 'sick_day15_plus',
'vab', 'parental_leave', 'vacation',
'traktamente_taxfree', 'traktamente_taxable',
'mileage_taxfree', 'mileage_taxable',
'net_deduction_advance', 'net_deduction_union', 'net_deduction_benefit_payment',
'net_deduction_other',
'correction', 'other'
));
NOTIFY pgrst, 'reload schema';
@@ -0,0 +1,32 @@
-- Simplify company naming: remove trade_name field entirely.
-- Add invoice_show_logo toggle for invoice PDF header.
-- Migrate any existing trade_name into company_name so users who set a
-- handelsnamn keep their preferred invoice display name. Existing users
-- should be notified to verify company_name against Bolagsverket since
-- this value is used in SIE/INK2/NE filings.
ALTER TABLE public.company_settings
ADD COLUMN IF NOT EXISTS invoice_show_logo boolean DEFAULT true;
DO $$
BEGIN
IF EXISTS (
SELECT 1 FROM information_schema.columns
WHERE table_schema = 'public'
AND table_name = 'company_settings'
AND column_name = 'trade_name'
) THEN
EXECUTE $sql$
UPDATE public.company_settings
SET company_name = trade_name
WHERE trade_name IS NOT NULL
AND trade_name <> ''
AND trade_name <> company_name
$sql$;
END IF;
END $$;
ALTER TABLE public.company_settings
DROP COLUMN IF EXISTS trade_name;
NOTIFY pgrst, 'reload schema';