Add/skv salary agi (#423)

* feat: add Bankgirot LB-fil support for salary payments and tax payments

- Implemented `generateBgLb` for salary batch payments, producing opening, payment, and closing records.
- Added tests for `generateBgLb` to ensure correct record generation and validation.
- Created `generateBankgiroPaymentBgLb` for single tax payments to Skatteverket, including validation and formatting.
- Added tests for `generateBankgiroPaymentBgLb` to verify record structure and data integrity.
- Introduced `generateSkattekontoOcr` for generating valid OCR references for Skattekonto payments, with tests for various input formats.
- Updated database schema to track payment file formats and timestamps for salary runs and AGI declarations.
- Created a new table for logging salary payslip deliveries to ensure compliance with audit requirements.

* feat: add write permission check and company ID validation for payment file generation

* feat: add write permission check for salary payment file generation
This commit is contained in:
Mattsson
2026-05-09 12:41:16 +02:00
committed by GitHub
parent c238542596
commit 7e81f661b2
24 changed files with 1870 additions and 20 deletions
@@ -0,0 +1,52 @@
-- Salary payment files: Bankgirot LB-fil support alongside existing pain.001 (SEPA).
--
-- Most Swedish SMBs upload Bankgirot LB-files to Swedbank/SEB/Handelsbanken/Nordea
-- via their corporate portal — pain.001 is also supported but less common.
--
-- This migration:
-- 1. Adds `preferred_payment_format` to `company_settings` so the UI can
-- pre-select the right format per company.
-- 2. Tracks which format was generated for each salary run, plus the
-- generation timestamp (used by deadline reminders).
-- ------------------------------------------------------------------
-- company_settings.preferred_payment_format
-- ------------------------------------------------------------------
ALTER TABLE public.company_settings
ADD COLUMN IF NOT EXISTS preferred_payment_format text NOT NULL DEFAULT 'bg_lb';
-- Re-apply default + NOT NULL in case ADD COLUMN IF NOT EXISTS skipped them
-- (column existed out-of-band).
ALTER TABLE public.company_settings
ALTER COLUMN preferred_payment_format SET DEFAULT 'bg_lb';
UPDATE public.company_settings
SET preferred_payment_format = 'bg_lb'
WHERE preferred_payment_format IS NULL;
ALTER TABLE public.company_settings
ALTER COLUMN preferred_payment_format SET NOT NULL;
ALTER TABLE public.company_settings
DROP CONSTRAINT IF EXISTS company_settings_preferred_payment_format_check;
ALTER TABLE public.company_settings
ADD CONSTRAINT company_settings_preferred_payment_format_check
CHECK (preferred_payment_format IN ('bg_lb', 'pain001'));
-- ------------------------------------------------------------------
-- salary_runs payment file tracking
-- ------------------------------------------------------------------
ALTER TABLE public.salary_runs
ADD COLUMN IF NOT EXISTS payment_file_format text,
ADD COLUMN IF NOT EXISTS payment_file_generated_at timestamptz;
ALTER TABLE public.salary_runs
DROP CONSTRAINT IF EXISTS salary_runs_payment_file_format_check;
ALTER TABLE public.salary_runs
ADD CONSTRAINT salary_runs_payment_file_format_check
CHECK (payment_file_format IS NULL OR payment_file_format IN ('bg_lb', 'pain001'));
-- Reload PostgREST schema cache.
NOTIFY pgrst, 'reload schema';
@@ -0,0 +1,26 @@
-- AGI tax payment tracking.
--
-- An AGI declaration represents both the filing obligation and the resulting
-- tax liability for the period (skatt + avgifter). We track when the payment
-- file (Bankgirot LB to BG 5050-1055) was generated and when the payment
-- actually cleared the bank.
--
-- This avoids a separate `tax_payment_runs` table — the AGI declaration is
-- already keyed on (company_id, period_year, period_month) and carries the
-- exact amounts owed.
ALTER TABLE public.agi_declarations
ADD COLUMN IF NOT EXISTS tax_payment_file_generated_at timestamptz,
ADD COLUMN IF NOT EXISTS tax_payment_file_format text,
ADD COLUMN IF NOT EXISTS tax_paid_at timestamptz,
ADD COLUMN IF NOT EXISTS tax_payment_journal_entry_id uuid REFERENCES public.journal_entries(id);
ALTER TABLE public.agi_declarations
DROP CONSTRAINT IF EXISTS agi_declarations_tax_payment_format_check;
ALTER TABLE public.agi_declarations
ADD CONSTRAINT agi_declarations_tax_payment_format_check
CHECK (tax_payment_file_format IS NULL OR tax_payment_file_format IN ('bg_lb'));
-- Reload PostgREST schema cache so the columns become visible to the API.
NOTIFY pgrst, 'reload schema';
@@ -0,0 +1,72 @@
-- Salary payslip delivery log.
--
-- Per BFL 7 kap.: delivery confirmation of lönespecifikationer must be
-- retained as part of the audit trail. The send route currently calls Resend
-- and returns the count, but doesn't persist a per-employee record — so we
-- can't answer "did Anna receive her March payslip?" months later.
--
-- This table stores one row per (salary_run, employee, attempt) with the
-- Resend message_id (if known) so a follow-up webhook can update status to
-- delivered/bounced/complained.
CREATE TABLE IF NOT EXISTS public.salary_payslip_deliveries (
id uuid PRIMARY KEY DEFAULT uuid_generate_v4(),
company_id uuid NOT NULL REFERENCES public.companies(id) ON DELETE CASCADE,
salary_run_id uuid NOT NULL REFERENCES public.salary_runs(id) ON DELETE CASCADE,
employee_id uuid NOT NULL REFERENCES public.employees(id) ON DELETE RESTRICT,
user_id uuid NOT NULL REFERENCES auth.users(id) ON DELETE CASCADE,
email_address text NOT NULL,
status text NOT NULL DEFAULT 'sent'
CHECK (status IN ('sent', 'delivered', 'bounced', 'complained', 'failed', 'skipped')),
-- Resend (or other provider) tracking
provider text NOT NULL DEFAULT 'resend',
provider_message_id text,
provider_event jsonb,
error_message text,
sent_at timestamptz NOT NULL DEFAULT now(),
delivered_at timestamptz,
bounced_at timestamptz,
complained_at timestamptz,
created_at timestamptz NOT NULL DEFAULT now(),
updated_at timestamptz NOT NULL DEFAULT now()
);
ALTER TABLE public.salary_payslip_deliveries ENABLE ROW LEVEL SECURITY;
CREATE POLICY "salary_payslip_deliveries_select"
ON public.salary_payslip_deliveries
FOR SELECT
USING (company_id IN (SELECT public.user_company_ids()));
CREATE POLICY "salary_payslip_deliveries_insert"
ON public.salary_payslip_deliveries
FOR INSERT
WITH CHECK (company_id IN (SELECT public.user_company_ids()));
CREATE POLICY "salary_payslip_deliveries_update"
ON public.salary_payslip_deliveries
FOR UPDATE
USING (company_id IN (SELECT public.user_company_ids()));
-- No DELETE policy — BFL 7 kap. requires retention of delivery records.
CREATE INDEX idx_payslip_deliveries_run
ON public.salary_payslip_deliveries (salary_run_id);
CREATE INDEX idx_payslip_deliveries_company
ON public.salary_payslip_deliveries (company_id);
CREATE INDEX idx_payslip_deliveries_provider_msg
ON public.salary_payslip_deliveries (provider_message_id)
WHERE provider_message_id IS NOT NULL;
CREATE TRIGGER salary_payslip_deliveries_updated_at
BEFORE UPDATE ON public.salary_payslip_deliveries
FOR EACH ROW EXECUTE FUNCTION public.update_updated_at_column();
NOTIFY pgrst, 'reload schema';