Bug/gh issues fiz (#1103)
* refactor: optimize page loading and data fetching * fix: resolve recurring production runtime errors * feat: add MCP company and customer updates * fix: handle year-end tax adjustments * feat: harden annual report compliance * fix: expand invoice logo and font support * fix: sanitize API route error responses * fix: sanitize user-facing error messages * feat: persist onboarding and tax assessment notices * fix: reduce cloud backup audit churn * feat: refine invoice editor layout * fix: show saved tax adjustments in INK2 * fix: complete annual report API mappings * docs: record operational safeguards and decisions * fix: harden annual report review findings * fix: adjust column span for description based on VAT registration * New css class name
This commit is contained in:
@@ -0,0 +1,107 @@
|
||||
-- Exact kvarskatt deadlines and persisted post-company onboarding state.
|
||||
--
|
||||
-- Kvarskatt is not calculated from a guessed 90-day offset. Skatteverket
|
||||
-- states that the exact payment due date is printed on the final tax notice,
|
||||
-- and reassessment decisions can have a materially shorter payment window.
|
||||
-- Store the notice date and its exact due date, then let the deadline
|
||||
-- generator mirror that date without banking-day adjustment.
|
||||
--
|
||||
-- Oasis onboarding state belongs to the company, not localStorage. Existing
|
||||
-- onboarded companies are backfilled as completed and dismissed so only newly
|
||||
-- created companies see the new optional setup surface.
|
||||
|
||||
-- =============================================================================
|
||||
-- 1. Persisted initial setup state
|
||||
-- =============================================================================
|
||||
ALTER TABLE public.company_settings
|
||||
ADD COLUMN IF NOT EXISTS initial_setup_path text NULL,
|
||||
ADD COLUMN IF NOT EXISTS initial_setup_completed_at timestamptz NULL,
|
||||
ADD COLUMN IF NOT EXISTS initial_setup_dismissed_at timestamptz NULL;
|
||||
|
||||
ALTER TABLE public.company_settings
|
||||
DROP CONSTRAINT IF EXISTS company_settings_initial_setup_path_check;
|
||||
|
||||
ALTER TABLE public.company_settings
|
||||
ADD CONSTRAINT company_settings_initial_setup_path_check
|
||||
CHECK (initial_setup_path IS NULL OR initial_setup_path IN ('migration', 'bank', 'fresh'));
|
||||
|
||||
UPDATE public.company_settings
|
||||
SET initial_setup_completed_at = COALESCE(initial_setup_completed_at, now()),
|
||||
initial_setup_dismissed_at = COALESCE(initial_setup_dismissed_at, now())
|
||||
WHERE onboarding_complete = true
|
||||
AND initial_setup_completed_at IS NULL;
|
||||
|
||||
COMMENT ON COLUMN public.company_settings.initial_setup_path IS
|
||||
'Optional first-value path selected after company creation: migration, bank, or fresh.';
|
||||
COMMENT ON COLUMN public.company_settings.initial_setup_completed_at IS
|
||||
'When the selected first-value setup path was completed.';
|
||||
COMMENT ON COLUMN public.company_settings.initial_setup_dismissed_at IS
|
||||
'When the optional dashboard setup checklist was dismissed.';
|
||||
|
||||
-- =============================================================================
|
||||
-- 2. Final tax notices and reassessment decisions
|
||||
-- =============================================================================
|
||||
CREATE TABLE public.tax_assessment_notices (
|
||||
id uuid DEFAULT gen_random_uuid() PRIMARY KEY,
|
||||
company_id uuid NOT NULL REFERENCES public.companies(id) ON DELETE CASCADE,
|
||||
user_id uuid NOT NULL REFERENCES auth.users(id) ON DELETE CASCADE,
|
||||
fiscal_period_id uuid NOT NULL REFERENCES public.fiscal_periods(id) ON DELETE RESTRICT,
|
||||
decision_type text NOT NULL CHECK (decision_type IN ('final', 'reassessment')),
|
||||
decision_date date NOT NULL,
|
||||
payment_due_date date NOT NULL,
|
||||
archived_at timestamptz NULL,
|
||||
created_at timestamptz NOT NULL DEFAULT now(),
|
||||
updated_at timestamptz NOT NULL DEFAULT now(),
|
||||
|
||||
CONSTRAINT tax_assessment_notices_due_after_decision
|
||||
CHECK (payment_due_date >= decision_date),
|
||||
CONSTRAINT tax_assessment_notices_company_period_type_key
|
||||
UNIQUE (company_id, fiscal_period_id, decision_type)
|
||||
);
|
||||
|
||||
ALTER TABLE public.tax_assessment_notices ENABLE ROW LEVEL SECURITY;
|
||||
|
||||
CREATE POLICY "view own-company tax_assessment_notices"
|
||||
ON public.tax_assessment_notices FOR SELECT
|
||||
USING (company_id IN (SELECT user_company_ids()));
|
||||
CREATE POLICY "insert own-company tax_assessment_notices"
|
||||
ON public.tax_assessment_notices FOR INSERT
|
||||
WITH CHECK (
|
||||
company_id IN (SELECT user_company_ids())
|
||||
AND user_id = auth.uid()
|
||||
);
|
||||
CREATE POLICY "update own-company tax_assessment_notices"
|
||||
ON public.tax_assessment_notices FOR UPDATE
|
||||
USING (company_id IN (SELECT user_company_ids()))
|
||||
WITH CHECK (company_id IN (SELECT user_company_ids()));
|
||||
CREATE POLICY "delete own-company tax_assessment_notices"
|
||||
ON public.tax_assessment_notices FOR DELETE
|
||||
USING (company_id IN (SELECT user_company_ids()));
|
||||
|
||||
CREATE INDEX idx_tax_assessment_notices_company
|
||||
ON public.tax_assessment_notices (company_id);
|
||||
CREATE INDEX idx_tax_assessment_notices_due
|
||||
ON public.tax_assessment_notices (company_id, payment_due_date)
|
||||
WHERE archived_at IS NULL;
|
||||
CREATE INDEX idx_tax_assessment_notices_period
|
||||
ON public.tax_assessment_notices (fiscal_period_id);
|
||||
|
||||
CREATE TRIGGER set_updated_at_tax_assessment_notices
|
||||
BEFORE UPDATE ON public.tax_assessment_notices
|
||||
FOR EACH ROW EXECUTE FUNCTION public.update_updated_at_column();
|
||||
|
||||
CREATE TRIGGER audit_tax_assessment_notices
|
||||
AFTER INSERT OR UPDATE OR DELETE ON public.tax_assessment_notices
|
||||
FOR EACH ROW EXECUTE FUNCTION public.write_audit_log();
|
||||
|
||||
-- Relational identity lets edits and archives reconcile the exact generated
|
||||
-- deadline without encoding notice ids into user-facing titles.
|
||||
ALTER TABLE public.deadlines
|
||||
ADD COLUMN IF NOT EXISTS tax_assessment_notice_id uuid NULL
|
||||
REFERENCES public.tax_assessment_notices(id) ON DELETE RESTRICT;
|
||||
|
||||
CREATE INDEX IF NOT EXISTS idx_deadlines_tax_assessment_notice
|
||||
ON public.deadlines (tax_assessment_notice_id)
|
||||
WHERE tax_assessment_notice_id IS NOT NULL;
|
||||
|
||||
NOTIFY pgrst, 'reload schema';
|
||||
@@ -0,0 +1,126 @@
|
||||
-- Persist tax-only adjustments used by the year-end tax provision and INK2.
|
||||
-- These rows do not create journal entries. They explain the bridge from the
|
||||
-- accounting result to the taxable result for one fiscal period.
|
||||
|
||||
CREATE TABLE public.fiscal_period_tax_adjustments (
|
||||
id uuid PRIMARY KEY DEFAULT gen_random_uuid(),
|
||||
company_id uuid NOT NULL REFERENCES public.companies(id) ON DELETE CASCADE,
|
||||
user_id uuid NOT NULL REFERENCES auth.users(id) ON DELETE RESTRICT,
|
||||
fiscal_period_id uuid NOT NULL REFERENCES public.fiscal_periods(id) ON DELETE RESTRICT,
|
||||
adjustment_type text NOT NULL CHECK (
|
||||
adjustment_type IN ('non_deductible_expense', 'non_taxable_income')
|
||||
),
|
||||
source text NOT NULL CHECK (source IN ('detected', 'manual')),
|
||||
source_key text NOT NULL,
|
||||
description text NOT NULL,
|
||||
account_number text NULL CHECK (
|
||||
account_number IS NULL OR account_number ~ '^[0-9]{4}$'
|
||||
),
|
||||
amount numeric(15, 2) NOT NULL CHECK (amount >= 0),
|
||||
included boolean NOT NULL DEFAULT true,
|
||||
created_at timestamptz NOT NULL DEFAULT now(),
|
||||
updated_at timestamptz NOT NULL DEFAULT now(),
|
||||
|
||||
CONSTRAINT fiscal_period_tax_adjustments_source_key
|
||||
UNIQUE (company_id, fiscal_period_id, source_key)
|
||||
);
|
||||
|
||||
ALTER TABLE public.fiscal_period_tax_adjustments ENABLE ROW LEVEL SECURITY;
|
||||
|
||||
CREATE POLICY "view own-company fiscal period tax adjustments"
|
||||
ON public.fiscal_period_tax_adjustments FOR SELECT
|
||||
USING (company_id IN (SELECT public.user_company_ids()));
|
||||
|
||||
CREATE POLICY "insert own-company fiscal period tax adjustments"
|
||||
ON public.fiscal_period_tax_adjustments FOR INSERT
|
||||
WITH CHECK (
|
||||
company_id IN (SELECT public.user_company_ids())
|
||||
AND user_id = auth.uid()
|
||||
);
|
||||
|
||||
CREATE POLICY "update own-company fiscal period tax adjustments"
|
||||
ON public.fiscal_period_tax_adjustments FOR UPDATE
|
||||
USING (company_id IN (SELECT public.user_company_ids()))
|
||||
WITH CHECK (
|
||||
company_id IN (SELECT public.user_company_ids())
|
||||
AND user_id = auth.uid()
|
||||
);
|
||||
|
||||
CREATE POLICY "delete own-company fiscal period tax adjustments"
|
||||
ON public.fiscal_period_tax_adjustments FOR DELETE
|
||||
USING (company_id IN (SELECT public.user_company_ids()));
|
||||
|
||||
CREATE INDEX idx_fiscal_period_tax_adjustments_company
|
||||
ON public.fiscal_period_tax_adjustments (company_id);
|
||||
|
||||
CREATE INDEX idx_fiscal_period_tax_adjustments_period
|
||||
ON public.fiscal_period_tax_adjustments (fiscal_period_id);
|
||||
|
||||
CREATE OR REPLACE FUNCTION public.guard_fiscal_period_tax_adjustment()
|
||||
RETURNS trigger
|
||||
LANGUAGE plpgsql
|
||||
SET search_path = public, pg_temp
|
||||
AS $$
|
||||
DECLARE
|
||||
adjustment_row public.fiscal_period_tax_adjustments%ROWTYPE;
|
||||
period_row public.fiscal_periods%ROWTYPE;
|
||||
BEGIN
|
||||
IF TG_OP = 'UPDATE'
|
||||
AND (
|
||||
OLD.company_id IS DISTINCT FROM NEW.company_id
|
||||
OR OLD.fiscal_period_id IS DISTINCT FROM NEW.fiscal_period_id
|
||||
) THEN
|
||||
RAISE EXCEPTION 'Tax adjustment company and fiscal period are immutable'
|
||||
USING ERRCODE = '23514';
|
||||
END IF;
|
||||
|
||||
IF TG_OP = 'DELETE' THEN
|
||||
adjustment_row := OLD;
|
||||
ELSE
|
||||
adjustment_row := NEW;
|
||||
END IF;
|
||||
|
||||
SELECT * INTO period_row
|
||||
FROM public.fiscal_periods
|
||||
WHERE id = adjustment_row.fiscal_period_id;
|
||||
|
||||
IF NOT FOUND THEN
|
||||
RAISE EXCEPTION 'Fiscal period not found for tax adjustment'
|
||||
USING ERRCODE = '23503';
|
||||
END IF;
|
||||
|
||||
IF period_row.company_id IS DISTINCT FROM adjustment_row.company_id THEN
|
||||
RAISE EXCEPTION 'Tax adjustment company does not match fiscal period company'
|
||||
USING ERRCODE = '23514';
|
||||
END IF;
|
||||
|
||||
IF period_row.is_closed
|
||||
OR period_row.locked_at IS NOT NULL
|
||||
OR period_row.closing_entry_id IS NOT NULL THEN
|
||||
RAISE EXCEPTION 'Fiscal period is locked for tax adjustments'
|
||||
USING ERRCODE = '23514';
|
||||
END IF;
|
||||
|
||||
IF TG_OP = 'DELETE' THEN
|
||||
RETURN OLD;
|
||||
END IF;
|
||||
RETURN NEW;
|
||||
END;
|
||||
$$;
|
||||
|
||||
CREATE TRIGGER guard_fiscal_period_tax_adjustments
|
||||
BEFORE INSERT OR UPDATE OR DELETE ON public.fiscal_period_tax_adjustments
|
||||
FOR EACH ROW EXECUTE FUNCTION public.guard_fiscal_period_tax_adjustment();
|
||||
|
||||
CREATE TRIGGER set_updated_at_fiscal_period_tax_adjustments
|
||||
BEFORE UPDATE ON public.fiscal_period_tax_adjustments
|
||||
FOR EACH ROW EXECUTE FUNCTION public.update_updated_at_column();
|
||||
|
||||
CREATE TRIGGER audit_fiscal_period_tax_adjustments
|
||||
AFTER INSERT OR UPDATE OR DELETE ON public.fiscal_period_tax_adjustments
|
||||
FOR EACH ROW EXECUTE FUNCTION public.write_audit_log();
|
||||
|
||||
COMMENT ON TABLE public.fiscal_period_tax_adjustments IS
|
||||
'Tax-only adjustments bridging accounting result to taxable result for year-end tax and INK2.';
|
||||
|
||||
NOTIFY pgrst, 'reload schema';
|
||||
@@ -0,0 +1,104 @@
|
||||
-- Harden tax notice tenant integrity and preserve every reassessment decision.
|
||||
|
||||
-- A fiscal year can receive more than one reassessment decision. The decision
|
||||
-- date is part of the notice identity so repeated POST requests stay
|
||||
-- idempotent without overwriting an earlier decision.
|
||||
ALTER TABLE public.tax_assessment_notices
|
||||
DROP CONSTRAINT tax_assessment_notices_company_period_type_key;
|
||||
|
||||
ALTER TABLE public.tax_assessment_notices
|
||||
ADD CONSTRAINT tax_assessment_notices_company_period_type_date_key
|
||||
UNIQUE (company_id, fiscal_period_id, decision_type, decision_date);
|
||||
|
||||
-- The creator is attribution, not ownership. Company records must survive a
|
||||
-- user deletion, including when a generated deadline references the notice.
|
||||
ALTER TABLE public.tax_assessment_notices
|
||||
ALTER COLUMN user_id DROP NOT NULL;
|
||||
|
||||
ALTER TABLE public.tax_assessment_notices
|
||||
DROP CONSTRAINT tax_assessment_notices_user_id_fkey;
|
||||
|
||||
ALTER TABLE public.tax_assessment_notices
|
||||
ADD CONSTRAINT tax_assessment_notices_user_id_fkey
|
||||
FOREIGN KEY (user_id) REFERENCES auth.users(id) ON DELETE SET NULL;
|
||||
|
||||
-- Company-owned foreign keys must agree on company_id. The additional unique
|
||||
-- keys are safe because both referenced id columns are already primary keys.
|
||||
ALTER TABLE public.fiscal_periods
|
||||
ADD CONSTRAINT fiscal_periods_id_company_id_key UNIQUE (id, company_id);
|
||||
|
||||
ALTER TABLE public.tax_assessment_notices
|
||||
DROP CONSTRAINT tax_assessment_notices_fiscal_period_id_fkey;
|
||||
|
||||
ALTER TABLE public.tax_assessment_notices
|
||||
ADD CONSTRAINT tax_assessment_notices_fiscal_period_company_fkey
|
||||
FOREIGN KEY (fiscal_period_id, company_id)
|
||||
REFERENCES public.fiscal_periods(id, company_id) ON DELETE RESTRICT;
|
||||
|
||||
ALTER TABLE public.tax_assessment_notices
|
||||
ADD CONSTRAINT tax_assessment_notices_id_company_id_key UNIQUE (id, company_id);
|
||||
|
||||
ALTER TABLE public.deadlines
|
||||
DROP CONSTRAINT deadlines_tax_assessment_notice_id_fkey;
|
||||
|
||||
ALTER TABLE public.deadlines
|
||||
ADD CONSTRAINT deadlines_tax_assessment_notice_company_fkey
|
||||
FOREIGN KEY (tax_assessment_notice_id, company_id)
|
||||
REFERENCES public.tax_assessment_notices(id, company_id) ON DELETE RESTRICT;
|
||||
|
||||
-- Keep creator attribution immutable for authenticated PostgREST callers.
|
||||
CREATE OR REPLACE FUNCTION public.enforce_tax_assessment_notice_attribution()
|
||||
RETURNS trigger
|
||||
LANGUAGE plpgsql
|
||||
SET search_path TO 'public'
|
||||
AS $function$
|
||||
BEGIN
|
||||
IF auth.uid() IS NOT NULL AND NEW.user_id IS DISTINCT FROM OLD.user_id THEN
|
||||
RAISE EXCEPTION 'Tax assessment notice attribution is immutable'
|
||||
USING ERRCODE = '42501';
|
||||
END IF;
|
||||
RETURN NEW;
|
||||
END;
|
||||
$function$;
|
||||
|
||||
CREATE TRIGGER enforce_tax_assessment_notice_attribution
|
||||
BEFORE UPDATE OF user_id ON public.tax_assessment_notices
|
||||
FOR EACH ROW EXECUTE FUNCTION public.enforce_tax_assessment_notice_attribution();
|
||||
|
||||
-- Reads follow the repository-wide membership model. Writes additionally
|
||||
-- require the active company and a non-viewer role, matching other tenant
|
||||
-- tables and preventing direct PostgREST writes from bypassing the API guard.
|
||||
DROP POLICY "insert own-company tax_assessment_notices"
|
||||
ON public.tax_assessment_notices;
|
||||
DROP POLICY "update own-company tax_assessment_notices"
|
||||
ON public.tax_assessment_notices;
|
||||
DROP POLICY "delete own-company tax_assessment_notices"
|
||||
ON public.tax_assessment_notices;
|
||||
|
||||
CREATE POLICY tax_assessment_notices_insert
|
||||
ON public.tax_assessment_notices FOR INSERT TO public
|
||||
WITH CHECK (
|
||||
company_id = public.current_active_company_id()
|
||||
AND public.current_user_can_write()
|
||||
AND user_id = auth.uid()
|
||||
);
|
||||
|
||||
CREATE POLICY tax_assessment_notices_update
|
||||
ON public.tax_assessment_notices FOR UPDATE TO public
|
||||
USING (
|
||||
company_id = public.current_active_company_id()
|
||||
AND public.current_user_can_write()
|
||||
)
|
||||
WITH CHECK (
|
||||
company_id = public.current_active_company_id()
|
||||
AND public.current_user_can_write()
|
||||
);
|
||||
|
||||
CREATE POLICY tax_assessment_notices_delete
|
||||
ON public.tax_assessment_notices FOR DELETE TO public
|
||||
USING (
|
||||
company_id = public.current_active_company_id()
|
||||
AND public.current_user_can_write()
|
||||
);
|
||||
|
||||
NOTIFY pgrst, 'reload schema';
|
||||
@@ -0,0 +1,127 @@
|
||||
-- Repair production error root causes found in the Vercel runtime logs.
|
||||
--
|
||||
-- 1. Register the MCP approval event before processing_history references it.
|
||||
-- 2. Add covering indexes for the two high-volume read paths.
|
||||
-- 3. Aggregate account balances in Postgres instead of transferring every line.
|
||||
-- 4. Page VAT source lines in Postgres instead of loading the full result set.
|
||||
|
||||
INSERT INTO public.processing_event_types (event_type)
|
||||
VALUES ('PendingOperationApproved')
|
||||
ON CONFLICT (event_type) DO NOTHING;
|
||||
|
||||
CREATE INDEX IF NOT EXISTS idx_audit_log_company_created_id
|
||||
ON public.audit_log (company_id, created_at DESC, id DESC);
|
||||
|
||||
CREATE INDEX IF NOT EXISTS idx_journal_entries_company_posted_date_id
|
||||
ON public.journal_entries (company_id, entry_date, id)
|
||||
WHERE status IN ('posted', 'reversed');
|
||||
|
||||
CREATE OR REPLACE FUNCTION public.get_account_period_activity(
|
||||
p_company_id uuid,
|
||||
p_start date,
|
||||
p_end date,
|
||||
p_accounts text[],
|
||||
p_exclude_journal_entry_id uuid DEFAULT NULL
|
||||
)
|
||||
RETURNS TABLE (
|
||||
account_number text,
|
||||
debit numeric,
|
||||
credit numeric
|
||||
)
|
||||
LANGUAGE sql
|
||||
STABLE
|
||||
SECURITY INVOKER
|
||||
SET search_path TO 'public'
|
||||
AS $$
|
||||
SELECT
|
||||
l.account_number,
|
||||
COALESCE(sum(l.debit_amount), 0)::numeric AS debit,
|
||||
COALESCE(sum(l.credit_amount), 0)::numeric AS credit
|
||||
FROM public.journal_entries je
|
||||
JOIN public.journal_entry_lines l ON l.journal_entry_id = je.id
|
||||
WHERE je.company_id = p_company_id
|
||||
AND je.status IN ('posted', 'reversed')
|
||||
AND je.entry_date >= p_start
|
||||
AND je.entry_date <= p_end
|
||||
AND l.account_number = ANY (p_accounts)
|
||||
AND (
|
||||
p_exclude_journal_entry_id IS NULL
|
||||
OR je.id <> p_exclude_journal_entry_id
|
||||
)
|
||||
GROUP BY l.account_number
|
||||
ORDER BY l.account_number;
|
||||
$$;
|
||||
|
||||
REVOKE ALL ON FUNCTION public.get_account_period_activity(uuid, date, date, text[], uuid)
|
||||
FROM PUBLIC, anon;
|
||||
GRANT EXECUTE ON FUNCTION public.get_account_period_activity(uuid, date, date, text[], uuid)
|
||||
TO authenticated, service_role;
|
||||
|
||||
CREATE OR REPLACE FUNCTION public.get_vat_ruta_source_lines(
|
||||
p_company_id uuid,
|
||||
p_start date,
|
||||
p_end date,
|
||||
p_accounts text[],
|
||||
p_cursor_date date DEFAULT NULL,
|
||||
p_cursor_voucher_number integer DEFAULT NULL,
|
||||
p_cursor_entry_id uuid DEFAULT NULL,
|
||||
p_cursor_line_id uuid DEFAULT NULL,
|
||||
p_limit integer DEFAULT 501
|
||||
)
|
||||
RETURNS TABLE (
|
||||
line_id uuid,
|
||||
journal_entry_id uuid,
|
||||
voucher_number integer,
|
||||
voucher_series text,
|
||||
entry_date date,
|
||||
description text,
|
||||
debit_amount numeric,
|
||||
credit_amount numeric
|
||||
)
|
||||
LANGUAGE sql
|
||||
STABLE
|
||||
SECURITY INVOKER
|
||||
SET search_path TO 'public'
|
||||
AS $$
|
||||
SELECT
|
||||
l.id AS line_id,
|
||||
je.id AS journal_entry_id,
|
||||
je.voucher_number,
|
||||
COALESCE(je.voucher_series, 'A') AS voucher_series,
|
||||
je.entry_date,
|
||||
COALESCE(je.description, '') AS description,
|
||||
l.debit_amount,
|
||||
l.credit_amount
|
||||
FROM public.journal_entries je
|
||||
JOIN public.journal_entry_lines l ON l.journal_entry_id = je.id
|
||||
WHERE je.company_id = p_company_id
|
||||
AND je.status IN ('posted', 'reversed')
|
||||
AND je.entry_date >= p_start
|
||||
AND je.entry_date <= p_end
|
||||
AND l.account_number = ANY (p_accounts)
|
||||
AND (
|
||||
p_cursor_date IS NULL
|
||||
OR (
|
||||
je.entry_date,
|
||||
je.voucher_number,
|
||||
je.id,
|
||||
l.id
|
||||
) > (
|
||||
p_cursor_date,
|
||||
p_cursor_voucher_number,
|
||||
COALESCE(p_cursor_entry_id, 'ffffffff-ffff-ffff-ffff-ffffffffffff'::uuid),
|
||||
COALESCE(p_cursor_line_id, 'ffffffff-ffff-ffff-ffff-ffffffffffff'::uuid)
|
||||
)
|
||||
)
|
||||
ORDER BY je.entry_date, je.voucher_number, je.id, l.id
|
||||
LIMIT LEAST(GREATEST(COALESCE(p_limit, 501), 1), 501);
|
||||
$$;
|
||||
|
||||
REVOKE ALL ON FUNCTION public.get_vat_ruta_source_lines(
|
||||
uuid, date, date, text[], date, integer, uuid, uuid, integer
|
||||
) FROM PUBLIC, anon;
|
||||
GRANT EXECUTE ON FUNCTION public.get_vat_ruta_source_lines(
|
||||
uuid, date, date, text[], date, integer, uuid, uuid, integer
|
||||
) TO authenticated, service_role;
|
||||
|
||||
NOTIFY pgrst, 'reload schema';
|
||||
@@ -0,0 +1,69 @@
|
||||
-- Add update_company_settings to the pending_operations operation type CHECK.
|
||||
-- The operation changes payment-routing details shown on future invoices and
|
||||
-- therefore stages at medium risk for explicit approval.
|
||||
|
||||
ALTER TABLE public.pending_operations
|
||||
DROP CONSTRAINT IF EXISTS pending_operations_operation_type_check;
|
||||
|
||||
ALTER TABLE public.pending_operations
|
||||
ADD CONSTRAINT pending_operations_operation_type_check
|
||||
CHECK (operation_type IN (
|
||||
'categorize_transaction',
|
||||
'create_customer',
|
||||
'create_invoice',
|
||||
'mark_invoice_paid',
|
||||
'send_invoice',
|
||||
'mark_invoice_sent',
|
||||
'match_transaction_invoice',
|
||||
'close_period',
|
||||
'lock_period',
|
||||
'unlock_period',
|
||||
'set_opening_balances',
|
||||
'run_year_end',
|
||||
'run_currency_revaluation',
|
||||
'import_sie',
|
||||
'explain_voucher_gap',
|
||||
'uncategorize_transaction',
|
||||
'approve_supplier_invoice',
|
||||
'credit_supplier_invoice',
|
||||
'credit_invoice',
|
||||
'convert_invoice',
|
||||
'create_transaction',
|
||||
'attach_document_to_transaction',
|
||||
'create_voucher',
|
||||
'correct_entry',
|
||||
'reverse_entry',
|
||||
'create_supplier',
|
||||
'create_supplier_invoice_from_inbox',
|
||||
'post_annual_depreciation',
|
||||
'link_invoice_voucher',
|
||||
'undo_sie_import',
|
||||
'match_batch_allocate',
|
||||
'bulk_book_transactions',
|
||||
'create_salary_run',
|
||||
'generate_agi',
|
||||
'link_transaction_journal_entry',
|
||||
'link_supplier_invoice_voucher',
|
||||
'submit_vat_declaration',
|
||||
'submit_agi',
|
||||
'create_article',
|
||||
'update_article',
|
||||
'bulk_book_inbox_items',
|
||||
'create_dimension_value',
|
||||
'retag_line_dimensions',
|
||||
'link_document_to_voucher',
|
||||
'update_payslip_line',
|
||||
'register_absence',
|
||||
'create_employee',
|
||||
'update_employee',
|
||||
'set_employee_opening_balances',
|
||||
'vacation_year_close',
|
||||
'create_account',
|
||||
'update_account',
|
||||
'set_voucher_note',
|
||||
'book_salary_run',
|
||||
'delete_absence',
|
||||
'update_company_settings'
|
||||
)) NOT VALID;
|
||||
|
||||
NOTIFY pgrst, 'reload schema';
|
||||
+6
@@ -0,0 +1,6 @@
|
||||
-- Validate the operation type CHECK re-added in 20260721110750.
|
||||
-- This separate transaction avoids a full-table scan while the preceding
|
||||
-- migration holds its stronger table lock.
|
||||
|
||||
ALTER TABLE public.pending_operations
|
||||
VALIDATE CONSTRAINT pending_operations_operation_type_check;
|
||||
@@ -0,0 +1,70 @@
|
||||
-- Add update_customer to the pending_operations operation type CHECK.
|
||||
-- Customer master-data changes are staged at low risk and remain subject to
|
||||
-- explicit approval unless the company has opted into trusted auto-commit.
|
||||
|
||||
ALTER TABLE public.pending_operations
|
||||
DROP CONSTRAINT IF EXISTS pending_operations_operation_type_check;
|
||||
|
||||
ALTER TABLE public.pending_operations
|
||||
ADD CONSTRAINT pending_operations_operation_type_check
|
||||
CHECK (operation_type IN (
|
||||
'categorize_transaction',
|
||||
'create_customer',
|
||||
'create_invoice',
|
||||
'mark_invoice_paid',
|
||||
'send_invoice',
|
||||
'mark_invoice_sent',
|
||||
'match_transaction_invoice',
|
||||
'close_period',
|
||||
'lock_period',
|
||||
'unlock_period',
|
||||
'set_opening_balances',
|
||||
'run_year_end',
|
||||
'run_currency_revaluation',
|
||||
'import_sie',
|
||||
'explain_voucher_gap',
|
||||
'uncategorize_transaction',
|
||||
'approve_supplier_invoice',
|
||||
'credit_supplier_invoice',
|
||||
'credit_invoice',
|
||||
'convert_invoice',
|
||||
'create_transaction',
|
||||
'attach_document_to_transaction',
|
||||
'create_voucher',
|
||||
'correct_entry',
|
||||
'reverse_entry',
|
||||
'create_supplier',
|
||||
'create_supplier_invoice_from_inbox',
|
||||
'post_annual_depreciation',
|
||||
'link_invoice_voucher',
|
||||
'undo_sie_import',
|
||||
'match_batch_allocate',
|
||||
'bulk_book_transactions',
|
||||
'create_salary_run',
|
||||
'generate_agi',
|
||||
'link_transaction_journal_entry',
|
||||
'link_supplier_invoice_voucher',
|
||||
'submit_vat_declaration',
|
||||
'submit_agi',
|
||||
'create_article',
|
||||
'update_article',
|
||||
'bulk_book_inbox_items',
|
||||
'create_dimension_value',
|
||||
'retag_line_dimensions',
|
||||
'link_document_to_voucher',
|
||||
'update_payslip_line',
|
||||
'register_absence',
|
||||
'create_employee',
|
||||
'update_employee',
|
||||
'set_employee_opening_balances',
|
||||
'vacation_year_close',
|
||||
'create_account',
|
||||
'update_account',
|
||||
'set_voucher_note',
|
||||
'book_salary_run',
|
||||
'delete_absence',
|
||||
'update_company_settings',
|
||||
'update_customer'
|
||||
)) NOT VALID;
|
||||
|
||||
NOTIFY pgrst, 'reload schema';
|
||||
@@ -0,0 +1,6 @@
|
||||
-- Validate the operation type CHECK re-added in 20260721111331.
|
||||
-- This separate transaction avoids a full-table scan while the preceding
|
||||
-- migration holds its stronger table lock.
|
||||
|
||||
ALTER TABLE public.pending_operations
|
||||
VALIDATE CONSTRAINT pending_operations_operation_type_check;
|
||||
@@ -0,0 +1,34 @@
|
||||
-- Keep API-key security and configuration changes in behandlingshistorik, but
|
||||
-- do not create a full audit row for every authenticated API or MCP request.
|
||||
-- validate_and_increment_api_key updates only these four telemetry columns.
|
||||
|
||||
DROP TRIGGER IF EXISTS audit_api_keys_update ON public.api_keys;
|
||||
DROP TRIGGER IF EXISTS audit_api_keys ON public.api_keys;
|
||||
|
||||
-- Preserve the historical trigger name for account-deletion routines that
|
||||
-- temporarily disable audit_api_keys while removing a user.
|
||||
CREATE TRIGGER audit_api_keys
|
||||
AFTER INSERT OR DELETE ON public.api_keys
|
||||
FOR EACH ROW EXECUTE FUNCTION public.write_audit_log();
|
||||
|
||||
CREATE TRIGGER audit_api_keys_update
|
||||
AFTER UPDATE ON public.api_keys
|
||||
FOR EACH ROW
|
||||
WHEN (
|
||||
(to_jsonb(OLD) - ARRAY[
|
||||
'request_count',
|
||||
'rate_limit_window_start',
|
||||
'last_used_at',
|
||||
'updated_at'
|
||||
]::text[])
|
||||
IS DISTINCT FROM
|
||||
(to_jsonb(NEW) - ARRAY[
|
||||
'request_count',
|
||||
'rate_limit_window_start',
|
||||
'last_used_at',
|
||||
'updated_at'
|
||||
]::text[])
|
||||
)
|
||||
EXECUTE FUNCTION public.write_audit_log();
|
||||
|
||||
NOTIFY pgrst, 'reload schema';
|
||||
@@ -0,0 +1,105 @@
|
||||
-- The initial tax-adjustment migration used membership-only write policies.
|
||||
-- Align writes with the global role gate: only a non-viewer member may write,
|
||||
-- and only for the caller's active company.
|
||||
|
||||
ALTER TABLE public.fiscal_period_tax_adjustments
|
||||
ALTER COLUMN id SET DEFAULT uuid_generate_v4();
|
||||
|
||||
-- Serialize adjustment writes with fiscal-period close/lock updates. The row
|
||||
-- lock prevents a concurrent request from saving tax inputs after closing has
|
||||
-- started but before the period status change becomes visible.
|
||||
CREATE OR REPLACE FUNCTION public.guard_fiscal_period_tax_adjustment()
|
||||
RETURNS trigger
|
||||
LANGUAGE plpgsql
|
||||
SET search_path = public, pg_temp
|
||||
AS $$
|
||||
DECLARE
|
||||
adjustment_row public.fiscal_period_tax_adjustments%ROWTYPE;
|
||||
period_row public.fiscal_periods%ROWTYPE;
|
||||
BEGIN
|
||||
IF TG_OP = 'UPDATE'
|
||||
AND (
|
||||
OLD.company_id IS DISTINCT FROM NEW.company_id
|
||||
OR OLD.fiscal_period_id IS DISTINCT FROM NEW.fiscal_period_id
|
||||
) THEN
|
||||
RAISE EXCEPTION 'Tax adjustment company and fiscal period are immutable'
|
||||
USING ERRCODE = '23514';
|
||||
END IF;
|
||||
|
||||
IF TG_OP = 'DELETE' THEN
|
||||
adjustment_row := OLD;
|
||||
ELSE
|
||||
adjustment_row := NEW;
|
||||
END IF;
|
||||
|
||||
SELECT * INTO period_row
|
||||
FROM public.fiscal_periods
|
||||
WHERE id = adjustment_row.fiscal_period_id
|
||||
FOR UPDATE;
|
||||
|
||||
IF NOT FOUND THEN
|
||||
RAISE EXCEPTION 'Fiscal period not found for tax adjustment'
|
||||
USING ERRCODE = '23503';
|
||||
END IF;
|
||||
|
||||
IF period_row.company_id IS DISTINCT FROM adjustment_row.company_id THEN
|
||||
RAISE EXCEPTION 'Tax adjustment company does not match fiscal period company'
|
||||
USING ERRCODE = '23514';
|
||||
END IF;
|
||||
|
||||
IF period_row.is_closed
|
||||
OR period_row.locked_at IS NOT NULL
|
||||
OR period_row.closing_entry_id IS NOT NULL THEN
|
||||
RAISE EXCEPTION 'Fiscal period is locked for tax adjustments'
|
||||
USING ERRCODE = '23514';
|
||||
END IF;
|
||||
|
||||
IF TG_OP = 'DELETE' THEN
|
||||
RETURN OLD;
|
||||
END IF;
|
||||
RETURN NEW;
|
||||
END;
|
||||
$$;
|
||||
|
||||
DROP POLICY IF EXISTS "insert own-company fiscal period tax adjustments"
|
||||
ON public.fiscal_period_tax_adjustments;
|
||||
CREATE POLICY "insert own-company fiscal period tax adjustments"
|
||||
ON public.fiscal_period_tax_adjustments FOR INSERT TO public
|
||||
WITH CHECK (
|
||||
company_id = public.current_active_company_id()
|
||||
AND public.current_user_can_write()
|
||||
AND user_id = auth.uid()
|
||||
);
|
||||
|
||||
DROP POLICY IF EXISTS "update own-company fiscal period tax adjustments"
|
||||
ON public.fiscal_period_tax_adjustments;
|
||||
CREATE POLICY "update own-company fiscal period tax adjustments"
|
||||
ON public.fiscal_period_tax_adjustments FOR UPDATE TO public
|
||||
USING (
|
||||
company_id = public.current_active_company_id()
|
||||
AND public.current_user_can_write()
|
||||
)
|
||||
WITH CHECK (
|
||||
company_id = public.current_active_company_id()
|
||||
AND public.current_user_can_write()
|
||||
AND user_id = auth.uid()
|
||||
);
|
||||
|
||||
DROP POLICY IF EXISTS "delete own-company fiscal period tax adjustments"
|
||||
ON public.fiscal_period_tax_adjustments;
|
||||
CREATE POLICY "delete own-company fiscal period tax adjustments"
|
||||
ON public.fiscal_period_tax_adjustments FOR DELETE TO public
|
||||
USING (
|
||||
company_id = public.current_active_company_id()
|
||||
AND public.current_user_can_write()
|
||||
);
|
||||
|
||||
-- Only the corporate-tax disposition sets source_id to the fiscal period.
|
||||
-- This database invariant closes the read-then-insert race between two POSTs.
|
||||
CREATE UNIQUE INDEX uq_year_end_corporate_tax_per_period
|
||||
ON public.journal_entries (company_id, source_id)
|
||||
WHERE source_type = 'year_end'
|
||||
AND source_id IS NOT NULL
|
||||
AND status IN ('draft', 'posted');
|
||||
|
||||
NOTIFY pgrst, 'reload schema';
|
||||
@@ -0,0 +1,33 @@
|
||||
-- BFL 7 kap. 2 § requires accounting information to be preserved through
|
||||
-- the end of the seventh calendar year after the calendar year in which the
|
||||
-- fiscal year ended. retention_expires_at stores the first date on which the
|
||||
-- statutory minimum retention period has elapsed.
|
||||
|
||||
CREATE OR REPLACE FUNCTION public.set_bfl_retention_expiry()
|
||||
RETURNS trigger
|
||||
LANGUAGE plpgsql
|
||||
SET search_path = public
|
||||
AS $$
|
||||
BEGIN
|
||||
NEW.retention_expires_at := make_date(
|
||||
extract(year FROM NEW.period_end)::integer + 8,
|
||||
1,
|
||||
1
|
||||
);
|
||||
RETURN NEW;
|
||||
END;
|
||||
$$;
|
||||
|
||||
-- PostgreSQL fires triggers with the same timing alphabetically. The zz
|
||||
-- prefix makes this legal correction run after the original migration 017
|
||||
-- trigger without modifying that shipped enforcement migration.
|
||||
CREATE TRIGGER zz_set_bfl_retention_expiry
|
||||
BEFORE INSERT OR UPDATE OF period_end ON public.fiscal_periods
|
||||
FOR EACH ROW EXECUTE FUNCTION public.set_bfl_retention_expiry();
|
||||
|
||||
UPDATE public.fiscal_periods
|
||||
SET retention_expires_at = make_date(
|
||||
extract(year FROM period_end)::integer + 8,
|
||||
1,
|
||||
1
|
||||
);
|
||||
File diff suppressed because it is too large
Load Diff
@@ -0,0 +1,8 @@
|
||||
-- Allow larger company logos while keeping the upload route and storage limit
|
||||
-- aligned at 10 MB. Existing files and public access remain unchanged.
|
||||
|
||||
UPDATE storage.buckets
|
||||
SET file_size_limit = 10485760
|
||||
WHERE id = 'logos';
|
||||
|
||||
NOTIFY pgrst, 'reload schema';
|
||||
@@ -0,0 +1,37 @@
|
||||
-- Add bundled and company-uploaded invoice fonts. Custom font files live in a
|
||||
-- dedicated private bucket. The server embeds each font into the generated PDF,
|
||||
-- so customer font files never need public URLs.
|
||||
|
||||
ALTER TABLE public.company_settings
|
||||
ADD COLUMN IF NOT EXISTS invoice_custom_font_path TEXT NULL,
|
||||
ADD COLUMN IF NOT EXISTS invoice_custom_font_name TEXT NULL;
|
||||
|
||||
ALTER TABLE public.company_settings
|
||||
DROP CONSTRAINT IF EXISTS company_settings_invoice_font_check;
|
||||
ALTER TABLE public.company_settings
|
||||
ADD CONSTRAINT company_settings_invoice_font_check
|
||||
CHECK (
|
||||
invoice_font_family IN (
|
||||
'Helvetica',
|
||||
'Times-Roman',
|
||||
'Courier',
|
||||
'Source Sans 3',
|
||||
'Source Serif 4',
|
||||
'Custom'
|
||||
)
|
||||
);
|
||||
|
||||
INSERT INTO storage.buckets (id, name, public, file_size_limit, allowed_mime_types)
|
||||
VALUES (
|
||||
'invoice-fonts',
|
||||
'invoice-fonts',
|
||||
false,
|
||||
5242880,
|
||||
ARRAY['font/ttf', 'font/woff']
|
||||
)
|
||||
ON CONFLICT (id) DO UPDATE
|
||||
SET public = EXCLUDED.public,
|
||||
file_size_limit = EXCLUDED.file_size_limit,
|
||||
allowed_mime_types = EXCLUDED.allowed_mime_types;
|
||||
|
||||
NOTIFY pgrst, 'reload schema';
|
||||
@@ -0,0 +1,116 @@
|
||||
-- Keep signer roster drafting available to authenticated company members, but
|
||||
-- reserve legally significant signature transitions for the trusted service
|
||||
-- role after the API has verified ownership, version state, and evidence.
|
||||
|
||||
DROP POLICY IF EXISTS "arsredovisning_sigreq_insert"
|
||||
ON public.arsredovisning_signature_requests;
|
||||
|
||||
CREATE POLICY "arsredovisning_sigreq_insert"
|
||||
ON public.arsredovisning_signature_requests
|
||||
FOR INSERT
|
||||
TO authenticated
|
||||
WITH CHECK (
|
||||
company_id IN (SELECT public.user_company_ids())
|
||||
AND user_id = (SELECT auth.uid())
|
||||
AND status = 'pending'
|
||||
AND annual_report_version_id IS NULL
|
||||
AND signing_method IS NULL
|
||||
AND evidence_reference IS NULL
|
||||
AND evidence_recorded_by IS NULL
|
||||
AND evidence_recorded_at IS NULL
|
||||
AND signed_at IS NULL
|
||||
AND bankid_signature_data IS NULL
|
||||
AND signer_personnummer_encrypted IS NULL
|
||||
AND signer_personnummer_hash IS NULL
|
||||
AND EXISTS (
|
||||
SELECT 1
|
||||
FROM public.fiscal_periods fp
|
||||
WHERE fp.id = public.arsredovisning_signature_requests.fiscal_period_id
|
||||
AND fp.company_id = public.arsredovisning_signature_requests.company_id
|
||||
)
|
||||
);
|
||||
|
||||
-- Authenticated users may no longer transition or rewrite a signature row
|
||||
-- directly through PostgREST. The service role bypasses RLS and is used only
|
||||
-- after the API route has completed its ownership and state checks.
|
||||
DROP POLICY IF EXISTS "arsredovisning_sigreq_update"
|
||||
ON public.arsredovisning_signature_requests;
|
||||
|
||||
DROP POLICY IF EXISTS "arsredovisning_sigreq_delete"
|
||||
ON public.arsredovisning_signature_requests;
|
||||
|
||||
CREATE POLICY "arsredovisning_sigreq_delete"
|
||||
ON public.arsredovisning_signature_requests
|
||||
FOR DELETE
|
||||
TO authenticated
|
||||
USING (
|
||||
company_id IN (SELECT public.user_company_ids())
|
||||
AND status = 'pending'
|
||||
AND annual_report_version_id IS NULL
|
||||
AND EXISTS (
|
||||
SELECT 1
|
||||
FROM public.fiscal_periods fp
|
||||
WHERE fp.id = public.arsredovisning_signature_requests.fiscal_period_id
|
||||
AND fp.company_id = public.arsredovisning_signature_requests.company_id
|
||||
)
|
||||
);
|
||||
|
||||
-- Existing signed evidence is retained unchanged. NOT VALID preserves those
|
||||
-- immutable historical rows while enforcing the structured format for every
|
||||
-- new or updated reference.
|
||||
ALTER TABLE public.arsredovisning_signature_requests
|
||||
ADD CONSTRAINT arsredovisning_signature_evidence_reference_format
|
||||
CHECK (
|
||||
evidence_reference IS NULL
|
||||
OR evidence_reference ~ '^(archive|document|receipt):[A-Za-z0-9][A-Za-z0-9._/-]{0,119}$'
|
||||
) NOT VALID;
|
||||
|
||||
COMMENT ON COLUMN public.arsredovisning_signature_requests.evidence_reference IS
|
||||
'Opaque reference only: archive:<id>, document:<id>, or receipt:<id>. Never store free text or personal data.';
|
||||
|
||||
CREATE OR REPLACE FUNCTION public.enforce_annual_report_signature_version_state()
|
||||
RETURNS trigger
|
||||
LANGUAGE plpgsql
|
||||
SET search_path = public
|
||||
AS $$
|
||||
DECLARE
|
||||
linked_version_status text;
|
||||
linked_version_finalized_date date;
|
||||
BEGIN
|
||||
IF NEW.status <> 'signed' THEN
|
||||
RETURN NEW;
|
||||
END IF;
|
||||
IF TG_OP = 'UPDATE' AND OLD.status = 'signed' THEN
|
||||
RETURN NEW;
|
||||
END IF;
|
||||
|
||||
SELECT
|
||||
version.status,
|
||||
(version.finalized_at AT TIME ZONE 'Europe/Stockholm')::date
|
||||
INTO linked_version_status, linked_version_finalized_date
|
||||
FROM public.annual_report_versions version
|
||||
WHERE version.id = NEW.annual_report_version_id;
|
||||
|
||||
IF linked_version_status IS DISTINCT FROM 'ready_for_signature'
|
||||
OR linked_version_finalized_date IS NULL THEN
|
||||
RAISE EXCEPTION 'Annual report version is not ready for signature evidence'
|
||||
USING ERRCODE = 'check_violation';
|
||||
END IF;
|
||||
|
||||
IF (NEW.signed_at AT TIME ZONE 'Europe/Stockholm')::date
|
||||
< linked_version_finalized_date
|
||||
OR (NEW.signed_at AT TIME ZONE 'Europe/Stockholm')::date
|
||||
> (now() AT TIME ZONE 'Europe/Stockholm')::date THEN
|
||||
RAISE EXCEPTION 'Signature date must be between version finalization and today'
|
||||
USING ERRCODE = 'check_violation';
|
||||
END IF;
|
||||
|
||||
RETURN NEW;
|
||||
END;
|
||||
$$;
|
||||
|
||||
CREATE TRIGGER enforce_annual_report_signature_version_state
|
||||
BEFORE INSERT OR UPDATE ON public.arsredovisning_signature_requests
|
||||
FOR EACH ROW EXECUTE FUNCTION public.enforce_annual_report_signature_version_state();
|
||||
|
||||
NOTIFY pgrst, 'reload schema';
|
||||
Reference in New Issue
Block a user