Terminal
This commit is contained in:
@@ -1,300 +0,0 @@
|
||||
-- Vacation balances and requests — spec section 18.
|
||||
--
|
||||
-- "Full-time, year-round students receive four full weeks" is the default, not
|
||||
-- a constant: section 18 requires the rules to be configurable, so the four
|
||||
-- weeks live in vacation_policies and can be overridden per campus.
|
||||
--
|
||||
-- "Prevents duplicate use" is enforced by an exclusion constraint rather than
|
||||
-- application logic — two approved vacations for one student cannot overlap
|
||||
-- even if two administrators approve simultaneously.
|
||||
|
||||
CREATE EXTENSION IF NOT EXISTS btree_gist;
|
||||
|
||||
-- ============================================================================
|
||||
-- 1. POLICIES
|
||||
-- ============================================================================
|
||||
|
||||
CREATE TABLE IF NOT EXISTS public.vacation_policies (
|
||||
id UUID PRIMARY KEY DEFAULT gen_random_uuid(),
|
||||
name TEXT NOT NULL,
|
||||
-- NULL campus = organization-wide default.
|
||||
campus_id UUID REFERENCES public.campuses(id) ON DELETE CASCADE,
|
||||
|
||||
weeks_per_year NUMERIC(4,2) NOT NULL DEFAULT 4 CHECK (weeks_per_year >= 0),
|
||||
|
||||
-- NULL on either means "applies to all".
|
||||
applies_to_calendar_basis TEXT,
|
||||
applies_to_attendance_basis TEXT,
|
||||
|
||||
partial_weeks_allowed BOOLEAN NOT NULL DEFAULT FALSE,
|
||||
-- Smallest bookable unit in days: 5 = whole weeks only, 1 = single days.
|
||||
min_increment_days SMALLINT NOT NULL DEFAULT 5 CHECK (min_increment_days > 0),
|
||||
|
||||
-- Whether tuition still accrues while a student is on vacation.
|
||||
tuition_due_during_vacation BOOLEAN NOT NULL DEFAULT TRUE,
|
||||
|
||||
-- Where the vacation year turns over.
|
||||
reset_month SMALLINT NOT NULL DEFAULT 8 CHECK (reset_month BETWEEN 1 AND 12),
|
||||
reset_day SMALLINT NOT NULL DEFAULT 1 CHECK (reset_day BETWEEN 1 AND 31),
|
||||
|
||||
is_active BOOLEAN NOT NULL DEFAULT TRUE,
|
||||
priority INTEGER NOT NULL DEFAULT 100,
|
||||
notes TEXT,
|
||||
created_at TIMESTAMPTZ NOT NULL DEFAULT now(),
|
||||
updated_at TIMESTAMPTZ NOT NULL DEFAULT now(),
|
||||
|
||||
CONSTRAINT vp_calendar_valid
|
||||
CHECK (applies_to_calendar_basis IS NULL
|
||||
OR applies_to_calendar_basis IN ('year_round','school_year')),
|
||||
CONSTRAINT vp_attendance_valid
|
||||
CHECK (applies_to_attendance_basis IS NULL
|
||||
OR applies_to_attendance_basis IN ('full_time','part_time'))
|
||||
);
|
||||
|
||||
GRANT SELECT, INSERT, UPDATE, DELETE ON public.vacation_policies TO authenticated;
|
||||
GRANT ALL ON public.vacation_policies TO service_role;
|
||||
ALTER TABLE public.vacation_policies ENABLE ROW LEVEL SECURITY;
|
||||
CREATE INDEX IF NOT EXISTS vp_campus_idx ON public.vacation_policies (campus_id);
|
||||
|
||||
DROP TRIGGER IF EXISTS trg_vp_upd ON public.vacation_policies;
|
||||
CREATE TRIGGER trg_vp_upd BEFORE UPDATE ON public.vacation_policies
|
||||
FOR EACH ROW EXECUTE FUNCTION public.set_updated_at();
|
||||
|
||||
-- The directive's stated default, as data.
|
||||
INSERT INTO public.vacation_policies
|
||||
(name, weeks_per_year, applies_to_calendar_basis, applies_to_attendance_basis, notes)
|
||||
SELECT 'Full-time year-round — four weeks', 4, 'year_round', 'full_time',
|
||||
'Spec section 18 default. Edit rather than replace; students reference the resolved policy.'
|
||||
WHERE NOT EXISTS (
|
||||
SELECT 1 FROM public.vacation_policies WHERE name = 'Full-time year-round — four weeks'
|
||||
);
|
||||
|
||||
-- Most specific active policy for a student.
|
||||
CREATE OR REPLACE FUNCTION public.resolve_vacation_policy(_student UUID)
|
||||
RETURNS public.vacation_policies
|
||||
LANGUAGE SQL STABLE SECURITY DEFINER SET search_path = public
|
||||
AS $$
|
||||
SELECT p.*
|
||||
FROM public.vacation_policies p
|
||||
JOIN public.students s ON s.id = _student
|
||||
WHERE p.is_active
|
||||
AND (p.campus_id IS NULL OR p.campus_id = s.primary_campus_id)
|
||||
AND (p.applies_to_calendar_basis IS NULL OR p.applies_to_calendar_basis = s.calendar_basis)
|
||||
AND (p.applies_to_attendance_basis IS NULL OR p.applies_to_attendance_basis = s.attendance_basis)
|
||||
ORDER BY p.priority DESC, (p.campus_id IS NOT NULL) DESC
|
||||
LIMIT 1
|
||||
$$;
|
||||
|
||||
-- ============================================================================
|
||||
-- 2. REQUESTS
|
||||
-- ============================================================================
|
||||
|
||||
CREATE TABLE IF NOT EXISTS public.vacation_requests (
|
||||
id UUID PRIMARY KEY DEFAULT gen_random_uuid(),
|
||||
student_id UUID NOT NULL REFERENCES public.students(id) ON DELETE CASCADE,
|
||||
policy_year INTEGER NOT NULL,
|
||||
|
||||
start_date DATE NOT NULL,
|
||||
end_date DATE NOT NULL,
|
||||
-- Charged against the balance. Stored rather than derived so an authorized
|
||||
-- partial-week approval can differ from the raw day count.
|
||||
weeks_charged NUMERIC(4,2) NOT NULL DEFAULT 1 CHECK (weeks_charged >= 0),
|
||||
|
||||
status TEXT NOT NULL DEFAULT 'pending',
|
||||
-- Snapshot of the policy at approval, so a later policy edit cannot silently
|
||||
-- change whether an already-taken vacation was billable.
|
||||
tuition_due BOOLEAN NOT NULL DEFAULT TRUE,
|
||||
|
||||
reason TEXT,
|
||||
requested_by UUID REFERENCES auth.users(id) ON DELETE SET NULL,
|
||||
approved_by UUID REFERENCES auth.users(id) ON DELETE SET NULL,
|
||||
approved_at TIMESTAMPTZ,
|
||||
notes TEXT,
|
||||
|
||||
created_at TIMESTAMPTZ NOT NULL DEFAULT now(),
|
||||
updated_at TIMESTAMPTZ NOT NULL DEFAULT now(),
|
||||
|
||||
CONSTRAINT vr_status_valid CHECK (status IN ('pending','approved','denied','cancelled')),
|
||||
CONSTRAINT vr_dates_ordered CHECK (start_date <= end_date),
|
||||
|
||||
-- No student may hold two approved vacations covering the same day.
|
||||
CONSTRAINT vr_no_overlap EXCLUDE USING gist (
|
||||
student_id WITH =,
|
||||
daterange(start_date, end_date, '[]') WITH &&
|
||||
) WHERE (status = 'approved')
|
||||
);
|
||||
|
||||
GRANT SELECT, INSERT, UPDATE, DELETE ON public.vacation_requests TO authenticated;
|
||||
GRANT ALL ON public.vacation_requests TO service_role;
|
||||
ALTER TABLE public.vacation_requests ENABLE ROW LEVEL SECURITY;
|
||||
CREATE INDEX IF NOT EXISTS vr_student_idx ON public.vacation_requests (student_id, start_date DESC);
|
||||
CREATE INDEX IF NOT EXISTS vr_pending_idx ON public.vacation_requests (start_date)
|
||||
WHERE status = 'pending';
|
||||
|
||||
DROP TRIGGER IF EXISTS trg_vr_upd ON public.vacation_requests;
|
||||
CREATE TRIGGER trg_vr_upd BEFORE UPDATE ON public.vacation_requests
|
||||
FOR EACH ROW EXECUTE FUNCTION public.set_updated_at();
|
||||
|
||||
-- ============================================================================
|
||||
-- 3. BALANCES
|
||||
-- ============================================================================
|
||||
-- weeks_used is maintained from approved requests rather than incremented by
|
||||
-- the application, so a denied-then-reapproved request cannot double-count.
|
||||
|
||||
CREATE TABLE IF NOT EXISTS public.vacation_balances (
|
||||
id UUID PRIMARY KEY DEFAULT gen_random_uuid(),
|
||||
student_id UUID NOT NULL REFERENCES public.students(id) ON DELETE CASCADE,
|
||||
policy_year INTEGER NOT NULL,
|
||||
policy_id UUID REFERENCES public.vacation_policies(id) ON DELETE SET NULL,
|
||||
|
||||
weeks_allotted NUMERIC(4,2) NOT NULL DEFAULT 0,
|
||||
weeks_used NUMERIC(4,2) NOT NULL DEFAULT 0,
|
||||
weeks_remaining NUMERIC(5,2) GENERATED ALWAYS AS (weeks_allotted - weeks_used) STORED,
|
||||
|
||||
-- Administrative override of the allotment, with its justification.
|
||||
override_weeks NUMERIC(4,2),
|
||||
override_reason TEXT,
|
||||
overridden_by UUID REFERENCES auth.users(id) ON DELETE SET NULL,
|
||||
|
||||
updated_at TIMESTAMPTZ NOT NULL DEFAULT now(),
|
||||
UNIQUE (student_id, policy_year)
|
||||
);
|
||||
|
||||
GRANT SELECT, INSERT, UPDATE ON public.vacation_balances TO authenticated;
|
||||
GRANT ALL ON public.vacation_balances TO service_role;
|
||||
ALTER TABLE public.vacation_balances ENABLE ROW LEVEL SECURITY;
|
||||
CREATE INDEX IF NOT EXISTS vb_student_idx ON public.vacation_balances (student_id);
|
||||
CREATE INDEX IF NOT EXISTS vb_policy_idx ON public.vacation_balances (policy_id);
|
||||
|
||||
-- Which vacation year a date belongs to, given the policy's reset point.
|
||||
CREATE OR REPLACE FUNCTION public.vacation_year_for(_student UUID, _on DATE)
|
||||
RETURNS INTEGER
|
||||
LANGUAGE plpgsql STABLE SECURITY DEFINER SET search_path = public AS $$
|
||||
DECLARE
|
||||
p public.vacation_policies%ROWTYPE;
|
||||
reset DATE;
|
||||
BEGIN
|
||||
SELECT * INTO p FROM public.resolve_vacation_policy(_student);
|
||||
IF p.id IS NULL THEN RETURN EXTRACT(YEAR FROM _on)::int; END IF;
|
||||
reset := make_date(EXTRACT(YEAR FROM _on)::int, p.reset_month, p.reset_day);
|
||||
RETURN CASE WHEN _on >= reset
|
||||
THEN EXTRACT(YEAR FROM _on)::int
|
||||
ELSE EXTRACT(YEAR FROM _on)::int - 1 END;
|
||||
END;
|
||||
$$;
|
||||
|
||||
CREATE OR REPLACE FUNCTION public.recalc_vacation_balance(_student UUID, _year INTEGER)
|
||||
RETURNS VOID
|
||||
LANGUAGE plpgsql SECURITY DEFINER SET search_path = public AS $$
|
||||
DECLARE
|
||||
p public.vacation_policies%ROWTYPE;
|
||||
used NUMERIC;
|
||||
bal public.vacation_balances%ROWTYPE;
|
||||
BEGIN
|
||||
SELECT * INTO p FROM public.resolve_vacation_policy(_student);
|
||||
|
||||
SELECT COALESCE(SUM(weeks_charged), 0) INTO used
|
||||
FROM public.vacation_requests
|
||||
WHERE student_id = _student AND policy_year = _year AND status = 'approved';
|
||||
|
||||
SELECT * INTO bal FROM public.vacation_balances
|
||||
WHERE student_id = _student AND policy_year = _year;
|
||||
|
||||
INSERT INTO public.vacation_balances
|
||||
(student_id, policy_year, policy_id, weeks_allotted, weeks_used)
|
||||
VALUES (_student, _year, p.id,
|
||||
COALESCE(bal.override_weeks, p.weeks_per_year, 0), used)
|
||||
ON CONFLICT (student_id, policy_year) DO UPDATE
|
||||
SET weeks_used = EXCLUDED.weeks_used,
|
||||
weeks_allotted = COALESCE(public.vacation_balances.override_weeks,
|
||||
EXCLUDED.weeks_allotted),
|
||||
policy_id = EXCLUDED.policy_id,
|
||||
updated_at = now();
|
||||
END;
|
||||
$$;
|
||||
|
||||
CREATE OR REPLACE FUNCTION public.sync_vacation_balance()
|
||||
RETURNS TRIGGER LANGUAGE plpgsql SECURITY DEFINER SET search_path = public AS $$
|
||||
BEGIN
|
||||
IF TG_OP = 'DELETE' THEN
|
||||
PERFORM public.recalc_vacation_balance(OLD.student_id, OLD.policy_year);
|
||||
RETURN OLD;
|
||||
END IF;
|
||||
|
||||
-- Stamp the year and the billability snapshot on the way in.
|
||||
IF TG_OP = 'INSERT' AND NEW.policy_year IS NULL THEN
|
||||
NEW.policy_year := public.vacation_year_for(NEW.student_id, NEW.start_date);
|
||||
END IF;
|
||||
|
||||
PERFORM public.recalc_vacation_balance(NEW.student_id, NEW.policy_year);
|
||||
IF TG_OP = 'UPDATE' AND OLD.policy_year IS DISTINCT FROM NEW.policy_year THEN
|
||||
PERFORM public.recalc_vacation_balance(NEW.student_id, OLD.policy_year);
|
||||
END IF;
|
||||
RETURN NEW;
|
||||
END;
|
||||
$$;
|
||||
|
||||
DROP TRIGGER IF EXISTS trg_vr_balance ON public.vacation_requests;
|
||||
CREATE TRIGGER trg_vr_balance AFTER INSERT OR UPDATE OR DELETE ON public.vacation_requests
|
||||
FOR EACH ROW EXECUTE FUNCTION public.sync_vacation_balance();
|
||||
|
||||
-- Default policy_year and tuition_due from the resolved policy at insert.
|
||||
CREATE OR REPLACE FUNCTION public.default_vacation_request()
|
||||
RETURNS TRIGGER LANGUAGE plpgsql SECURITY DEFINER SET search_path = public AS $$
|
||||
DECLARE
|
||||
p public.vacation_policies%ROWTYPE;
|
||||
BEGIN
|
||||
IF NEW.policy_year IS NULL THEN
|
||||
NEW.policy_year := public.vacation_year_for(NEW.student_id, NEW.start_date);
|
||||
END IF;
|
||||
SELECT * INTO p FROM public.resolve_vacation_policy(NEW.student_id);
|
||||
IF p.id IS NOT NULL AND TG_OP = 'INSERT' THEN
|
||||
NEW.tuition_due := p.tuition_due_during_vacation;
|
||||
IF NOT p.partial_weeks_allowed THEN
|
||||
-- Round part-weeks up to whole weeks when the policy forbids partials.
|
||||
NEW.weeks_charged := CEIL(NEW.weeks_charged);
|
||||
END IF;
|
||||
END IF;
|
||||
RETURN NEW;
|
||||
END;
|
||||
$$;
|
||||
|
||||
DROP TRIGGER IF EXISTS trg_vr_defaults ON public.vacation_requests;
|
||||
CREATE TRIGGER trg_vr_defaults BEFORE INSERT ON public.vacation_requests
|
||||
FOR EACH ROW EXECUTE FUNCTION public.default_vacation_request();
|
||||
|
||||
-- ============================================================================
|
||||
-- 4. POLICIES
|
||||
-- ============================================================================
|
||||
|
||||
DROP POLICY IF EXISTS "vacation policies read" ON public.vacation_policies;
|
||||
CREATE POLICY "vacation policies read" ON public.vacation_policies FOR SELECT TO authenticated
|
||||
USING (TRUE);
|
||||
DROP POLICY IF EXISTS "vacation policies manage" ON public.vacation_policies;
|
||||
CREATE POLICY "vacation policies manage" ON public.vacation_policies FOR ALL TO authenticated
|
||||
USING (public.is_org_admin()) WITH CHECK (public.is_org_admin());
|
||||
|
||||
DROP POLICY IF EXISTS "vacation requests read" ON public.vacation_requests;
|
||||
CREATE POLICY "vacation requests read" ON public.vacation_requests FOR SELECT TO authenticated
|
||||
USING (public.can_access_student(student_id) OR public.is_auditor());
|
||||
|
||||
-- Parents may request; only staff who can manage the student may approve, which
|
||||
-- is why approval fields sit behind the manage policy below.
|
||||
DROP POLICY IF EXISTS "vacation requests insert" ON public.vacation_requests;
|
||||
CREATE POLICY "vacation requests insert" ON public.vacation_requests FOR INSERT TO authenticated
|
||||
WITH CHECK (public.is_parent_of(student_id) OR public.can_manage_student(student_id));
|
||||
|
||||
DROP POLICY IF EXISTS "vacation requests manage" ON public.vacation_requests;
|
||||
CREATE POLICY "vacation requests manage" ON public.vacation_requests FOR UPDATE TO authenticated
|
||||
USING (public.can_manage_student(student_id)) WITH CHECK (public.can_manage_student(student_id));
|
||||
|
||||
DROP POLICY IF EXISTS "vacation requests delete" ON public.vacation_requests;
|
||||
CREATE POLICY "vacation requests delete" ON public.vacation_requests FOR DELETE TO authenticated
|
||||
USING (public.can_manage_student(student_id));
|
||||
|
||||
DROP POLICY IF EXISTS "vacation balances read" ON public.vacation_balances;
|
||||
CREATE POLICY "vacation balances read" ON public.vacation_balances FOR SELECT TO authenticated
|
||||
USING (public.can_access_student(student_id) OR public.is_auditor());
|
||||
DROP POLICY IF EXISTS "vacation balances manage" ON public.vacation_balances;
|
||||
CREATE POLICY "vacation balances manage" ON public.vacation_balances FOR ALL TO authenticated
|
||||
USING (public.can_manage_student(student_id)) WITH CHECK (public.can_manage_student(student_id));
|
||||
Reference in New Issue
Block a user