Files
accounted/supabase/migrations/20260813163034_peppol_delivery_hardening.sql
Mattsson 07e89d9b52 feat(invoices): add Peppol delivery foundation (#1595)
* feat(invoices): add Peppol delivery foundation

* fix(invoices): harden Peppol compliance guards

* fix(api): narrow Peppol document loading

* test(pg): hash Peppol fixture payload

* fix(invoices): address Peppol review findings

* test(pg): isolate Peppol provider events

* test(pg): isolate Peppol submission fixtures
2026-08-13 19:44:32 +02:00

197 lines
6.6 KiB
PL/PgSQL

-- Hardening found by staging advisors and concurrency review.
CREATE INDEX idx_peppol_deliveries_invoice_id
ON public.peppol_deliveries (invoice_id);
CREATE INDEX idx_peppol_deliveries_user_id
ON public.peppol_deliveries (user_id);
CREATE INDEX idx_peppol_delivery_events_company_id
ON public.peppol_delivery_events (company_id);
CREATE INDEX idx_peppol_delivery_evidence_company_id
ON public.peppol_delivery_evidence (company_id);
DROP POLICY "view own-company peppol deliveries" ON public.peppol_deliveries;
CREATE POLICY "view own-company peppol deliveries"
ON public.peppol_deliveries FOR SELECT TO authenticated
USING (company_id IN (SELECT public.user_company_ids()));
DROP POLICY "view own-company peppol delivery events" ON public.peppol_delivery_events;
CREATE POLICY "view own-company peppol delivery events"
ON public.peppol_delivery_events FOR SELECT TO authenticated
USING (company_id IN (SELECT public.user_company_ids()));
DROP POLICY "view own-company peppol delivery evidence" ON public.peppol_delivery_evidence;
CREATE POLICY "view own-company peppol delivery evidence"
ON public.peppol_delivery_evidence FOR SELECT TO authenticated
USING (company_id IN (SELECT public.user_company_ids()));
CREATE OR REPLACE FUNCTION public.enforce_peppol_delivery_immutability()
RETURNS trigger
LANGUAGE plpgsql
SECURITY INVOKER
SET search_path = pg_catalog, public
AS $$
BEGIN
IF TG_OP = 'DELETE' THEN
RAISE EXCEPTION 'Peppol delivery records are retained and cannot be deleted'
USING ERRCODE = '23514';
END IF;
IF current_setting('accounted.peppol_projection_update', true) IS DISTINCT FROM '1' THEN
RAISE EXCEPTION 'Peppol delivery state may only change through its event RPC'
USING ERRCODE = '42501';
END IF;
IF NEW.company_id IS DISTINCT FROM OLD.company_id
OR NEW.user_id IS DISTINCT FROM OLD.user_id
OR NEW.invoice_id IS DISTINCT FROM OLD.invoice_id
OR NEW.idempotency_key IS DISTINCT FROM OLD.idempotency_key
OR NEW.recipient_scheme IS DISTINCT FROM OLD.recipient_scheme
OR NEW.recipient_identifier IS DISTINCT FROM OLD.recipient_identifier
OR NEW.customization_id IS DISTINCT FROM OLD.customization_id
OR NEW.profile_id IS DISTINCT FROM OLD.profile_id
OR NEW.filename IS DISTINCT FROM OLD.filename
OR NEW.xml_payload IS DISTINCT FROM OLD.xml_payload
OR NEW.xml_sha256 IS DISTINCT FROM OLD.xml_sha256
OR NEW.retention_expires_at IS DISTINCT FROM OLD.retention_expires_at
OR NEW.created_at IS DISTINCT FROM OLD.created_at
THEN
RAISE EXCEPTION 'Peppol staged document and tenant identity are immutable'
USING ERRCODE = '23514';
END IF;
IF OLD.provider IS NOT NULL AND NEW.provider IS DISTINCT FROM OLD.provider THEN
RAISE EXCEPTION 'Peppol provider cannot change on an existing delivery'
USING ERRCODE = '23514';
END IF;
IF OLD.provider_tenant_id IS NOT NULL
AND NEW.provider_tenant_id IS DISTINCT FROM OLD.provider_tenant_id
THEN
RAISE EXCEPTION 'Peppol provider tenant cannot change on an existing delivery'
USING ERRCODE = '23514';
END IF;
IF OLD.provider_submission_id IS NOT NULL
AND NEW.provider_submission_id IS DISTINCT FROM OLD.provider_submission_id
THEN
RAISE EXCEPTION 'Peppol provider submission cannot change on an existing delivery'
USING ERRCODE = '23514';
END IF;
RETURN NEW;
END;
$$;
CREATE OR REPLACE FUNCTION public.enforce_peppol_append_only()
RETURNS trigger
LANGUAGE plpgsql
SECURITY INVOKER
SET search_path = pg_catalog, public
AS $$
BEGIN
RAISE EXCEPTION 'Peppol audit records are append-only'
USING ERRCODE = '23514';
END;
$$;
REVOKE ALL ON FUNCTION public.enforce_peppol_delivery_immutability()
FROM PUBLIC, anon, authenticated;
REVOKE ALL ON FUNCTION public.enforce_peppol_append_only()
FROM PUBLIC, anon, authenticated;
CREATE OR REPLACE FUNCTION public.stage_peppol_delivery(
p_company_id uuid,
p_invoice_id uuid,
p_recipient_scheme text,
p_recipient_identifier text,
p_customization_id text,
p_profile_id text,
p_filename text,
p_xml_payload text,
p_xml_sha256 text
)
RETURNS public.peppol_deliveries
LANGUAGE plpgsql
SECURITY DEFINER
SET search_path = pg_catalog, public
AS $$
DECLARE
actor_id uuid := auth.uid();
invoice_date date;
retention_basis date;
staged public.peppol_deliveries%ROWTYPE;
BEGIN
IF actor_id IS NULL OR NOT EXISTS (
SELECT 1
FROM public.company_members AS member
WHERE member.company_id = p_company_id
AND member.user_id = actor_id
AND member.role <> 'viewer'
) THEN
RAISE EXCEPTION 'not authorized to stage Peppol delivery'
USING ERRCODE = '42501';
END IF;
SELECT invoice.invoice_date
INTO invoice_date
FROM public.invoices AS invoice
WHERE invoice.id = p_invoice_id
AND invoice.company_id = p_company_id
AND invoice.invoice_number IS NOT NULL
AND invoice.status <> 'cancelled';
IF invoice_date IS NULL THEN
RAISE EXCEPTION 'invoice not found or not eligible for Peppol staging'
USING ERRCODE = 'P0002';
END IF;
SELECT period.period_end
INTO retention_basis
FROM public.fiscal_periods AS period
WHERE period.company_id = p_company_id
AND invoice_date BETWEEN period.period_start AND period.period_end
ORDER BY period.period_end DESC
LIMIT 1;
retention_basis := COALESCE(retention_basis, invoice_date);
INSERT INTO public.peppol_deliveries (
company_id, user_id, invoice_id,
recipient_scheme, recipient_identifier, customization_id, profile_id,
filename, xml_payload, xml_sha256, retention_expires_at
) VALUES (
p_company_id, actor_id, p_invoice_id,
p_recipient_scheme, p_recipient_identifier, p_customization_id, p_profile_id,
p_filename, p_xml_payload, lower(p_xml_sha256),
(date_trunc('year', retention_basis)::date + interval '8 years')::date
)
ON CONFLICT (company_id, invoice_id, xml_sha256) DO NOTHING
RETURNING * INTO staged;
IF staged.id IS NULL THEN
SELECT * INTO STRICT staged
FROM public.peppol_deliveries AS delivery
WHERE delivery.company_id = p_company_id
AND delivery.invoice_id = p_invoice_id
AND delivery.xml_sha256 = lower(p_xml_sha256);
RETURN staged;
END IF;
INSERT INTO public.peppol_delivery_events (
company_id, delivery_id, source, provider_event_code, normalized_status,
raw_payload, event_sha256, verification_method, occurred_at
) VALUES (
p_company_id, staged.id, 'local', 'staged', 'staged',
jsonb_build_object(
'invoice_id', p_invoice_id,
'idempotency_key', staged.idempotency_key,
'xml_sha256', staged.xml_sha256
),
staged.xml_sha256,
'local',
staged.created_at
);
RETURN staged;
END;
$$;
NOTIFY pgrst, 'reload schema';