feat(salary): let an enskild firma employ staff while blocking owner/board payroll (#797)
An enskild firma that hires staff should get the payroll module, but its owner or board can never be on payroll (owner compensation is egna uttag / BAS 2013, not lön). - Migration 20260628120000 adds the enforce_ef_no_owner_employee trigger (BEFORE INSERT OR UPDATE OF employment_type) as the all-paths backstop. - lib/salary/employment-rules.ts is the app-layer mirror (forbidden set kept byte-identical to the trigger); getCompanyEntityType() resolves the same company_settings -> companies precedence. - The two UI salary routes and the v1 POST guard before insert/update for a clean 400 with guidance. - Payroll nav + Lön settings now show for any employer (aktiebolag OR company_settings.pays_salaries), wired through the dashboard layout. Fixes #782. Co-authored-by: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
This commit is contained in:
co-authored by
Claude Opus 4.8
parent
5bacda4839
commit
55ba66908b
@@ -0,0 +1,53 @@
|
||||
-- Enforce that an enskild firma cannot put its owner or board on payroll.
|
||||
--
|
||||
-- An enskild firma is not a separate legal person from its owner, so the owner
|
||||
-- cannot be their own employee and cannot be paid lön — owner compensation is
|
||||
-- egna uttag (BAS 2013), booked against equity, never a salary cost.
|
||||
-- board_member (styrelse) is an aktiebolag concept and likewise doesn't exist
|
||||
-- for an EF. Ordinary employees (employment_type 'employee') remain fully
|
||||
-- allowed for an EF that hires staff and book identically to an aktiebolag.
|
||||
--
|
||||
-- This trigger is the all-paths backstop (UI route, /api/v1, MCP, direct SQL)
|
||||
-- for the application-layer guard in lib/salary/employment-rules.ts. The two
|
||||
-- must agree on the forbidden set: company_owner, board_member. See #782.
|
||||
|
||||
CREATE OR REPLACE FUNCTION public.enforce_ef_no_owner_employee()
|
||||
RETURNS trigger
|
||||
LANGUAGE plpgsql
|
||||
AS $$
|
||||
DECLARE
|
||||
v_entity_type text;
|
||||
BEGIN
|
||||
-- Only owner/board employment types are constrained; ordinary employees are
|
||||
-- allowed for every entity type, so leave those untouched.
|
||||
IF NEW.employment_type NOT IN ('company_owner', 'board_member') THEN
|
||||
RETURN NEW;
|
||||
END IF;
|
||||
|
||||
-- Resolve the company's effective entity type: company_settings is the
|
||||
-- read-primary source the app uses, with companies as the canonical
|
||||
-- fallback (mirrors lib/company/context.getCompanyEntityType()).
|
||||
v_entity_type := COALESCE(
|
||||
(SELECT cs.entity_type FROM public.company_settings cs WHERE cs.company_id = NEW.company_id),
|
||||
(SELECT c.entity_type FROM public.companies c WHERE c.id = NEW.company_id)
|
||||
);
|
||||
|
||||
IF v_entity_type = 'enskild_firma' THEN
|
||||
RAISE EXCEPTION 'En enskild firma kan inte ha sin ägare eller styrelse som anställd (employment_type=%). Ägaren tar ut pengar via eget uttag (konto 2013), inte lön.', NEW.employment_type
|
||||
USING ERRCODE = 'check_violation';
|
||||
END IF;
|
||||
|
||||
RETURN NEW;
|
||||
END;
|
||||
$$;
|
||||
|
||||
-- Fires on INSERT and on UPDATEs that touch employment_type only — so an
|
||||
-- unrelated edit to a grandfathered row (created before this guard existed)
|
||||
-- is never blocked, but setting/keeping an owner type on an EF is.
|
||||
DROP TRIGGER IF EXISTS trg_enforce_ef_no_owner_employee ON public.employees;
|
||||
CREATE TRIGGER trg_enforce_ef_no_owner_employee
|
||||
BEFORE INSERT OR UPDATE OF employment_type ON public.employees
|
||||
FOR EACH ROW
|
||||
EXECUTE FUNCTION public.enforce_ef_no_owner_employee();
|
||||
|
||||
NOTIFY pgrst, 'reload schema';
|
||||
Reference in New Issue
Block a user