Files
accounted/supabase/migrations/20260504100000_ai_native_hardening.sql
T
Mattsson bb855d2ddc Add/ai native supp (#385)
* feat(branding): implement dynamic branding in service worker and reports

* feat(auth): enhance API key scopes and add bookkeeping write scope

- Updated transaction write scope description to include additional tools.
- Enhanced reports read scope description to reflect new functionality.
- Introduced bookkeeping write scope with relevant description.
- Updated SCOPE_GROUPS to include bookkeeping domain.
- Modified TOOL_SCOPE_MAP to include new bookkeeping operations.
- Updated validateApiKey function to return api_key_id and api_key_name for better actor attribution.

feat(tests): add unit tests for MCP resource registry

- Created tests for data resources to ensure all required fields are present.
- Added tests for resource query parsing and retrieval.

feat(resources): implement MCP resources for company and accounting data

- Added capabilities resource to expose API key capabilities based on granted scopes.
- Implemented chart of accounts resource to retrieve active BAS chart.
- Created company current resource to fetch active company details.
- Developed active fiscal period resource to check posting eligibility.
- Implemented recent activity resource to fetch latest journal entries, invoices, and transactions.
- Added VAT treatments resource to provide available VAT rates per customer type.

feat(pending-operations): introduce risk tiers for operations

- Added risk level classification for pending operations to determine auto-commit eligibility.
- Implemented functions to classify operation risk levels and identify high-risk operations.

feat(migrations): add actor model and risk tier to pending operations

- Updated pending_operations table to include actor type and risk level columns.
- Enhanced audit_log to mirror actor information for compliance.
- Modified validate_and_increment_api_key function to return actor details.
- Expanded operation types in pending_operations to include new high-risk operations.

* feat: add auto-commit functionality for low-risk pending operations

- Implemented shouldAutoCommit function to determine eligibility for auto-commit based on operation type, actor type, and company settings.
- Created commitPendingOperation function to handle execution of pending operations with consistent status updates.
- Added tests for shouldAutoCommit to cover various scenarios including high-risk operations, user actors, company opt-in status, and monetary thresholds.
- Introduced new columns in company_settings for agent_auto_commit_enabled and agent_auto_commit_max_amount to allow companies to opt-in for auto-commit functionality.
- Added SQL migration to update the database schema for new auto-commit settings.

* feat(idempotency): implement idempotency key handling for safe retries and cleanup

* feat: expand API key scopes and pending operations for bookkeeping

- Added 'suppliers:write' scope to API key scopes for supplier invoice management.
- Updated SCOPE_GROUPS to include the new 'suppliers:write' scope.
- Introduced new pending operation types for bookkeeping: close_period, lock_period, run_year_end, set_opening_balances, run_currency_revaluation, explain_voucher_gap, uncategorize_transaction, approve_supplier_invoice, credit_supplier_invoice, and convert_invoice.
- Implemented corresponding commit functions for the new operations in the pending operations module.
- Enhanced PendingOperation type to include actor model and risk level attributes.
- Added tests for new functionality, ensuring proper behavior and constraints in the database.

* feat: implement unlockPeriod functionality and related tests

* feat: add agent auto-commit settings and related functionality

* feat: add attention resource with comprehensive summary of outstanding tasks

* feat: enhance pending operations with 'committing' status and immutability checks, improve idempotency handling, and add original voucher reference for credit notes
2026-05-04 11:12:29 +02:00

142 lines
6.3 KiB
PL/PgSQL

-- Hardening pass on the ai-native-supp branch:
-- 1. Adds a transient 'committing' status to pending_operations so the
-- commit dispatcher can claim a row atomically (CAS) before running
-- side-effects, eliminating the auto-commit / human-approval race.
-- 2. Adds a DB trigger that prevents UPDATE on pending_operations rows
-- whose previous status was 'committed' or 'rejected' — required for
-- BFL 7 kap. (räkenskapsinformation must be unalterable post-commit).
-- 3. Hardens idempotency_keys: company_id becomes NOT NULL, the unique
-- index includes company_id, and updated_at is added per CLAUDE.md
-- migration rule #2.
-- 4. Replays the schema reload that 20260430120100 forgot.
-- =============================================================================
-- 1. pending_operations: add 'committing' transient status
-- =============================================================================
ALTER TABLE public.pending_operations
DROP CONSTRAINT IF EXISTS pending_operations_status_check;
ALTER TABLE public.pending_operations
ADD CONSTRAINT pending_operations_status_check
CHECK (status IN ('pending', 'committing', 'committed', 'rejected'));
-- =============================================================================
-- 2. pending_operations: immutability after commit/rejection
-- =============================================================================
-- Once a pending_op reaches a terminal state, the params/preview/result must
-- be unchangeable. The dispatcher writes resolved_at + result_data as part of
-- the same UPDATE that flips status, so we only need to block UPDATEs whose
-- OLD.status is already terminal.
CREATE OR REPLACE FUNCTION public.enforce_pending_operations_immutability()
RETURNS TRIGGER AS $$
BEGIN
IF OLD.status IN ('committed', 'rejected') THEN
RAISE EXCEPTION
'pending_operations row % is in terminal state % and cannot be modified (BFL 7 kap.)',
OLD.id, OLD.status
USING ERRCODE = 'check_violation';
END IF;
RETURN NEW;
END;
$$ LANGUAGE plpgsql;
DROP TRIGGER IF EXISTS pending_operations_immutability ON public.pending_operations;
CREATE TRIGGER pending_operations_immutability
BEFORE UPDATE ON public.pending_operations
FOR EACH ROW
EXECUTE FUNCTION public.enforce_pending_operations_immutability();
-- Block DELETE on terminal rows for the same reason.
CREATE OR REPLACE FUNCTION public.enforce_pending_operations_no_delete()
RETURNS TRIGGER AS $$
BEGIN
IF OLD.status IN ('committed', 'rejected') THEN
RAISE EXCEPTION
'pending_operations row % is in terminal state % and cannot be deleted (BFL 7 kap.)',
OLD.id, OLD.status
USING ERRCODE = 'check_violation';
END IF;
RETURN OLD;
END;
$$ LANGUAGE plpgsql;
DROP TRIGGER IF EXISTS pending_operations_no_delete ON public.pending_operations;
CREATE TRIGGER pending_operations_no_delete
BEFORE DELETE ON public.pending_operations
FOR EACH ROW
EXECUTE FUNCTION public.enforce_pending_operations_no_delete();
-- =============================================================================
-- 2b. pending_operations: input fields frozen at insert
-- =============================================================================
-- BFL 7 kap. requires the underlag (basis) for an affärshändelse to be
-- immutable, not just the result. The dispatcher only writes to status /
-- resolved_at / result_data after insert; params, operation_type, and
-- preview_data must never change once the row exists. Without this trigger,
-- a compromised path (or future code refactor) could rewrite the proposal
-- between staging and human approval.
CREATE OR REPLACE FUNCTION public.enforce_pending_operations_input_frozen()
RETURNS TRIGGER AS $$
BEGIN
IF NEW.params IS DISTINCT FROM OLD.params THEN
RAISE EXCEPTION
'pending_operations.params is frozen after insert (BFL 7 kap. underlag-immutability)'
USING ERRCODE = 'check_violation';
END IF;
IF NEW.operation_type IS DISTINCT FROM OLD.operation_type THEN
RAISE EXCEPTION
'pending_operations.operation_type is frozen after insert'
USING ERRCODE = 'check_violation';
END IF;
IF NEW.preview_data IS DISTINCT FROM OLD.preview_data THEN
RAISE EXCEPTION
'pending_operations.preview_data is frozen after insert'
USING ERRCODE = 'check_violation';
END IF;
RETURN NEW;
END;
$$ LANGUAGE plpgsql;
DROP TRIGGER IF EXISTS pending_operations_input_frozen ON public.pending_operations;
CREATE TRIGGER pending_operations_input_frozen
BEFORE UPDATE ON public.pending_operations
FOR EACH ROW
EXECUTE FUNCTION public.enforce_pending_operations_input_frozen();
-- =============================================================================
-- 3. idempotency_keys: company_id NOT NULL + scoped unique index + updated_at
-- =============================================================================
-- The previous migration left company_id nullable and the unique index keyed
-- on (user_id, key) only. For a user owning multiple companies, replaying the
-- same idempotency_key UUID across companies could return a cached response
-- from the wrong company. Scope the cache per (user, company, key) so the
-- replay can never cross tenant boundaries.
-- Defensive: any row created before this migration with a NULL company_id is
-- ambiguous and must be cleared rather than backfilled.
DELETE FROM public.idempotency_keys WHERE company_id IS NULL;
ALTER TABLE public.idempotency_keys
ALTER COLUMN company_id SET NOT NULL;
DROP INDEX IF EXISTS idx_idempotency_keys_user_key;
CREATE UNIQUE INDEX idx_idempotency_keys_user_company_key
ON public.idempotency_keys (user_id, company_id, key);
ALTER TABLE public.idempotency_keys
ADD COLUMN IF NOT EXISTS updated_at TIMESTAMPTZ NOT NULL DEFAULT now();
DROP TRIGGER IF EXISTS idempotency_keys_updated_at ON public.idempotency_keys;
CREATE TRIGGER idempotency_keys_updated_at
BEFORE UPDATE ON public.idempotency_keys
FOR EACH ROW EXECUTE FUNCTION public.update_updated_at_column();
-- =============================================================================
-- 4. PostgREST schema reload
-- =============================================================================
-- Also covers the ALTER on pending_operations from 20260430120100, which
-- forgot to issue this notification.
NOTIFY pgrst, 'reload schema';