Files
accounted/supabase/migrations/20260512120000_salary_worked_days.sql
T
Mattsson 8f15f98687 Bug/employees creation (#440)
* Fix/employees page salary display logic and labels

* feat: add salary_worked_days table and related functionality

- Implemented the salary_worked_days table to track per-day worked hours for hourly employees.
- Established row-level security (RLS) policies to ensure tenant isolation for salary_worked_days.
- Created unique index on (employee_id, work_date) to enforce uniqueness.
- Added trigger to enforce a 24-hour cap across worked and absence days for the same employee and date.
- Developed tests to validate RLS, uniqueness, and 24-hour cap logic.

* Fix: update hourly_salary calculation and refresh logic in salary run processing
2026-05-12 01:10:01 +02:00

75 lines
3.9 KiB
SQL
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
-- Migration: salary_worked_days — per-day worked hours for hourly employees
--
-- Why this exists: hourly employees (timanställd) need per-day hour tracking
-- so payroll can derive base salary as `hourly_rate × Σ hours`. The previous
-- model relied on a single nullable `salary_run_employees.hours_worked`
-- column, but had no UI to set it after the employee was added to a run.
--
-- Mirrors salary_absence_days deliberately: same calendar UX, same RLS shape,
-- same per-day granularity. Worked time and absence time are separate domain
-- concepts (presence vs frånvaro) so they live in separate tables — combining
-- them via an enum would pollute every absence-side query (AGI Frånvarouppgift,
-- högriskskydd lookups, sjuklöneperiod derivation).
--
-- Half-day mixing is allowed: an employee can have 4h worked + 4h sick on the
-- same date. The 24-hour cap across both tables is enforced by the trigger
-- introduced in 20260512120100_worked_days_absence_conflict_trigger.sql.
CREATE TABLE public.salary_worked_days (
id UUID PRIMARY KEY DEFAULT uuid_generate_v4(),
company_id UUID NOT NULL REFERENCES companies(id) ON DELETE CASCADE,
employee_id UUID NOT NULL REFERENCES employees(id) ON DELETE CASCADE,
-- Optional link to the pay run that already absorbed this day. Null while
-- the user marks worked days before a run exists, or for periods not yet
-- materialized into a salary run.
salary_run_employee_id UUID REFERENCES salary_run_employees(id) ON DELETE SET NULL,
work_date DATE NOT NULL,
-- Hours worked on this date. Defaults to 8.0 for a full scheduled day.
-- Allows partial days (e.g. 4h morning shift) and overtime above a normal
-- workday (up to 24h hard cap; combined with absence the trigger enforces
-- the same 24h limit across both tables).
hours NUMERIC(5, 2) NOT NULL DEFAULT 8.0
CHECK (hours > 0 AND hours <= 24),
notes TEXT,
created_at TIMESTAMPTZ NOT NULL DEFAULT now(),
updated_at TIMESTAMPTZ NOT NULL DEFAULT now()
);
-- One row per employee per date. Unlike absence (which allows multiple types
-- on the same date, e.g. half-day VAB + half-day other), worked time is a
-- single hours value — re-marking a day overwrites the existing row.
CREATE UNIQUE INDEX idx_salary_worked_days_unique
ON public.salary_worked_days (employee_id, work_date);
-- Range queries by employee+date are the dominant access pattern: pay-period
-- aggregation when the calculator sums hours for hourly employees.
CREATE INDEX idx_salary_worked_days_employee_date
ON public.salary_worked_days (employee_id, work_date);
-- Lookup by run, used when the calculator materializes line items.
CREATE INDEX idx_salary_worked_days_run
ON public.salary_worked_days (salary_run_employee_id)
WHERE salary_run_employee_id IS NOT NULL;
-- Company-level scans.
CREATE INDEX idx_salary_worked_days_company_date
ON public.salary_worked_days (company_id, work_date);
ALTER TABLE public.salary_worked_days ENABLE ROW LEVEL SECURITY;
CREATE POLICY "salary_worked_days_select" ON public.salary_worked_days
FOR SELECT USING (company_id IN (SELECT public.user_company_ids()));
CREATE POLICY "salary_worked_days_insert" ON public.salary_worked_days
FOR INSERT WITH CHECK (company_id IN (SELECT public.user_company_ids()));
CREATE POLICY "salary_worked_days_update" ON public.salary_worked_days
FOR UPDATE USING (company_id IN (SELECT public.user_company_ids()))
WITH CHECK (company_id IN (SELECT public.user_company_ids()));
CREATE POLICY "salary_worked_days_delete" ON public.salary_worked_days
FOR DELETE USING (company_id IN (SELECT public.user_company_ids()));
CREATE TRIGGER salary_worked_days_updated_at
BEFORE UPDATE ON public.salary_worked_days
FOR EACH ROW EXECUTE FUNCTION public.update_updated_at_column();
NOTIFY pgrst, 'reload schema';