feat: remove AI extensions, restructure settings, and add atomic voucher commits (#157)
Remove AI-dependent extensions (ai-chat, ai-categorization, receipt-ocr, invoice-inbox) and their infrastructure (lib/ai/*, ai-consent, LangChain/ Anthropic/OpenAI deps) to simplify core and reduce bundle size. Restructure monolithic settings page into dedicated sub-pages (company, bookkeeping, invoicing, tax, banking, api, account, team, templates) with shared layout and sidebar navigation. Add atomic commit_journal_entry RPC so voucher number increment and status update happen in a single transaction — prevents burned numbers on constraint failures. Add continuity check report and voucher gap explanation tracking. Co-authored-by: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
This commit is contained in:
co-authored by
Claude Opus 4.6
parent
e89f2c402d
commit
d0b3f21bde
@@ -0,0 +1,40 @@
|
||||
-- Settings restructure: add bookkeeping lock, PDF/invoice settings
|
||||
-- Phase 2: Bookkeeping settings
|
||||
ALTER TABLE public.company_settings
|
||||
ADD COLUMN IF NOT EXISTS bookkeeping_locked_through date,
|
||||
ADD COLUMN IF NOT EXISTS auto_lock_period_days integer;
|
||||
|
||||
-- Phase 3: Invoice/PDF settings
|
||||
ALTER TABLE public.company_settings
|
||||
ADD COLUMN IF NOT EXISTS ore_rounding boolean DEFAULT true,
|
||||
ADD COLUMN IF NOT EXISTS invoice_show_ocr boolean DEFAULT true,
|
||||
ADD COLUMN IF NOT EXISTS invoice_show_bankgiro boolean DEFAULT true,
|
||||
ADD COLUMN IF NOT EXISTS invoice_show_plusgiro boolean DEFAULT true,
|
||||
ADD COLUMN IF NOT EXISTS invoice_late_fee_text text,
|
||||
ADD COLUMN IF NOT EXISTS invoice_credit_terms_text text;
|
||||
|
||||
-- Enforce company-wide bookkeeping lock date on journal entries
|
||||
CREATE OR REPLACE FUNCTION public.enforce_company_lock_date()
|
||||
RETURNS TRIGGER AS $$
|
||||
DECLARE
|
||||
lock_date date;
|
||||
BEGIN
|
||||
SELECT bookkeeping_locked_through INTO lock_date
|
||||
FROM public.company_settings
|
||||
WHERE company_id = NEW.company_id;
|
||||
|
||||
IF lock_date IS NOT NULL AND NEW.entry_date <= lock_date THEN
|
||||
RAISE EXCEPTION 'Bokföringen är låst t.o.m. %. Kan inte skapa verifikation med datum %.',
|
||||
lock_date, NEW.entry_date;
|
||||
END IF;
|
||||
|
||||
RETURN NEW;
|
||||
END;
|
||||
$$ LANGUAGE plpgsql;
|
||||
|
||||
-- Apply trigger only on insert and update of entry_date
|
||||
DROP TRIGGER IF EXISTS trg_enforce_company_lock_date ON public.journal_entries;
|
||||
CREATE TRIGGER trg_enforce_company_lock_date
|
||||
BEFORE INSERT OR UPDATE OF entry_date ON public.journal_entries
|
||||
FOR EACH ROW
|
||||
EXECUTE FUNCTION public.enforce_company_lock_date();
|
||||
@@ -0,0 +1,4 @@
|
||||
-- Add continuity_verified column to fiscal_periods
|
||||
-- NULL = not checked, true = IB/UB match verified, false = discrepancy detected
|
||||
ALTER TABLE public.fiscal_periods
|
||||
ADD COLUMN continuity_verified boolean;
|
||||
@@ -0,0 +1,6 @@
|
||||
-- Partial unique index on journal_entries to prevent duplicate voucher numbers.
|
||||
-- Drafts use voucher_number = 0 (integer NOT NULL), so WHERE > 0 excludes them.
|
||||
-- This enforces BFL 5:7 at the database level — application logic alone is insufficient.
|
||||
CREATE UNIQUE INDEX IF NOT EXISTS uq_journal_entries_voucher_number
|
||||
ON public.journal_entries (company_id, fiscal_period_id, voucher_series, voucher_number)
|
||||
WHERE voucher_number > 0;
|
||||
@@ -0,0 +1,55 @@
|
||||
-- Voucher gap explanations table (BFNAR 2013:2 punkt 5.8)
|
||||
-- Stores documented reasons for missing verifikationsnummer.
|
||||
-- Unexplained gaps block year-end closing.
|
||||
CREATE TABLE public.voucher_gap_explanations (
|
||||
id uuid PRIMARY KEY DEFAULT uuid_generate_v4(),
|
||||
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 CASCADE,
|
||||
voucher_series text NOT NULL DEFAULT 'A',
|
||||
gap_start integer NOT NULL,
|
||||
gap_end integer NOT NULL,
|
||||
explanation text NOT NULL CHECK (char_length(explanation) <= 500),
|
||||
created_at timestamptz NOT NULL DEFAULT now(),
|
||||
updated_at timestamptz NOT NULL DEFAULT now(),
|
||||
|
||||
UNIQUE (company_id, fiscal_period_id, voucher_series, gap_start, gap_end)
|
||||
);
|
||||
|
||||
ALTER TABLE public.voucher_gap_explanations ENABLE ROW LEVEL SECURITY;
|
||||
|
||||
-- SELECT: any company member can read explanations
|
||||
CREATE POLICY "voucher_gap_explanations_select"
|
||||
ON public.voucher_gap_explanations FOR SELECT
|
||||
USING (company_id IN (SELECT public.user_company_ids()));
|
||||
|
||||
-- INSERT: only owner/admin can create explanations (bokföringsansvarig)
|
||||
CREATE POLICY "voucher_gap_explanations_insert"
|
||||
ON public.voucher_gap_explanations FOR INSERT
|
||||
WITH CHECK (
|
||||
company_id IN (SELECT public.user_company_ids())
|
||||
AND EXISTS (
|
||||
SELECT 1 FROM public.team_members tm
|
||||
JOIN public.companies c ON c.team_id = tm.team_id
|
||||
WHERE c.id = company_id
|
||||
AND tm.user_id = auth.uid()
|
||||
AND tm.role IN ('owner', 'admin')
|
||||
)
|
||||
);
|
||||
|
||||
-- UPDATE: only owner/admin can update explanations
|
||||
CREATE POLICY "voucher_gap_explanations_update"
|
||||
ON public.voucher_gap_explanations FOR UPDATE
|
||||
USING (
|
||||
company_id IN (SELECT public.user_company_ids())
|
||||
AND EXISTS (
|
||||
SELECT 1 FROM public.team_members tm
|
||||
JOIN public.companies c ON c.team_id = tm.team_id
|
||||
WHERE c.id = company_id
|
||||
AND tm.user_id = auth.uid()
|
||||
AND tm.role IN ('owner', 'admin')
|
||||
)
|
||||
);
|
||||
|
||||
CREATE TRIGGER set_updated_at BEFORE UPDATE ON public.voucher_gap_explanations
|
||||
FOR EACH ROW EXECUTE FUNCTION public.update_updated_at_column();
|
||||
@@ -0,0 +1,54 @@
|
||||
-- Atomic journal entry commit: assigns voucher number and posts entry in one transaction.
|
||||
-- Prevents burned voucher numbers from failed commits (the counter increment rolls back
|
||||
-- if the status update or balance trigger fails).
|
||||
--
|
||||
-- Only used for the draft→posted transition (commitEntry). reverseEntry and storno
|
||||
-- use a different flow (INSERT with voucher number) and still call next_voucher_number.
|
||||
CREATE OR REPLACE FUNCTION public.commit_journal_entry(
|
||||
p_company_id uuid,
|
||||
p_entry_id uuid
|
||||
)
|
||||
RETURNS TABLE (voucher_number integer)
|
||||
LANGUAGE plpgsql
|
||||
SECURITY DEFINER
|
||||
AS $$
|
||||
DECLARE
|
||||
v_next integer;
|
||||
v_fiscal_period_id uuid;
|
||||
v_series text;
|
||||
BEGIN
|
||||
-- Fetch and lock the draft entry
|
||||
SELECT je.fiscal_period_id, COALESCE(je.voucher_series, 'A')
|
||||
INTO v_fiscal_period_id, v_series
|
||||
FROM public.journal_entries je
|
||||
WHERE je.id = p_entry_id
|
||||
AND je.company_id = p_company_id
|
||||
AND je.status = 'draft'
|
||||
FOR UPDATE;
|
||||
|
||||
IF NOT FOUND THEN
|
||||
RAISE EXCEPTION 'Draft journal entry not found: %', p_entry_id;
|
||||
END IF;
|
||||
|
||||
-- Increment voucher sequence (atomic via INSERT ON CONFLICT)
|
||||
INSERT INTO public.voucher_sequences (company_id, user_id, fiscal_period_id, voucher_series, last_number)
|
||||
VALUES (p_company_id, auth.uid(), v_fiscal_period_id, v_series, 1)
|
||||
ON CONFLICT (company_id, fiscal_period_id, voucher_series)
|
||||
DO UPDATE SET
|
||||
last_number = public.voucher_sequences.last_number + 1,
|
||||
updated_at = now()
|
||||
RETURNING last_number INTO v_next;
|
||||
|
||||
-- Update entry to posted with the assigned voucher number.
|
||||
-- If the balance trigger (check_balance_on_post) rejects the UPDATE,
|
||||
-- the entire transaction rolls back — including the sequence increment.
|
||||
-- No burned number, no gap.
|
||||
UPDATE public.journal_entries
|
||||
SET voucher_number = v_next,
|
||||
status = 'posted'
|
||||
WHERE id = p_entry_id
|
||||
AND company_id = p_company_id;
|
||||
|
||||
RETURN QUERY SELECT v_next;
|
||||
END;
|
||||
$$;
|
||||
Reference in New Issue
Block a user