* fix: reconcile annual reports with final closing entries * test: cover annual report depreciation and VAT balances * Merge remote-tracking branch 'origin/main' into fix/usr-fdbck-ch * fix: show exact invoice delivery details * fix: use currency account in invoice emails * fix: address invoice delivery review feedback * fix: harden invoice delivery and payment accounts * test: assert RLS-denied zero-row updates * fix: close remaining invoice compliance gaps * fix: harden invoice archive authorization * fix: close invoice delivery review findings * fix: verify delivery finalization results * fix: cap combined invoice email recipients * fix: close final invoice compliance findings * fix: prevent stale payment account saves * test: prove invoice delivery isolation * fix: close invoice privacy review findings * test: normalize delivery retention dates
662 lines
20 KiB
PL/PgSQL
662 lines
20 KiB
PL/PgSQL
-- Keep exact invoice email payloads visible only to the sending user.
|
|
-- Other company members consume a data-minimized SECURITY DEFINER summary.
|
|
|
|
DROP POLICY IF EXISTS invoice_deliveries_select ON public.invoice_deliveries;
|
|
|
|
CREATE POLICY invoice_deliveries_select
|
|
ON public.invoice_deliveries FOR SELECT TO authenticated
|
|
USING (
|
|
company_id = public.current_active_company_id()
|
|
AND user_id = auth.uid()
|
|
);
|
|
|
|
CREATE OR REPLACE FUNCTION public.list_invoice_delivery_summaries(
|
|
p_company_id uuid,
|
|
p_invoice_id uuid
|
|
)
|
|
RETURNS TABLE (
|
|
id uuid,
|
|
channel text,
|
|
status text,
|
|
to_addresses text[],
|
|
cc_addresses text[],
|
|
provider text,
|
|
error_code text,
|
|
document_attachment_id uuid,
|
|
sent_at timestamptz,
|
|
failed_at timestamptz,
|
|
created_at timestamptz
|
|
)
|
|
LANGUAGE plpgsql
|
|
STABLE
|
|
SECURITY DEFINER
|
|
SET search_path = pg_catalog, public
|
|
AS $$
|
|
BEGIN
|
|
IF auth.uid() IS NULL
|
|
OR p_company_id IS DISTINCT FROM public.current_active_company_id()
|
|
OR NOT EXISTS (
|
|
SELECT 1
|
|
FROM public.company_members cm
|
|
WHERE cm.company_id = p_company_id
|
|
AND cm.user_id = auth.uid()
|
|
)
|
|
THEN
|
|
RAISE EXCEPTION 'not authorized to list invoice delivery summaries'
|
|
USING ERRCODE = '42501';
|
|
END IF;
|
|
|
|
RETURN QUERY
|
|
SELECT
|
|
d.id,
|
|
d.channel,
|
|
d.status,
|
|
ARRAY(
|
|
SELECT CASE
|
|
WHEN recipient.address ~ '^[^@]+@[^@]+$'
|
|
THEN '***@' || split_part(recipient.address, '@', 2)
|
|
ELSE '***'
|
|
END
|
|
FROM unnest(d.to_addresses) WITH ORDINALITY AS recipient(address, position)
|
|
ORDER BY recipient.position
|
|
),
|
|
ARRAY(
|
|
SELECT CASE
|
|
WHEN recipient.address ~ '^[^@]+@[^@]+$'
|
|
THEN '***@' || split_part(recipient.address, '@', 2)
|
|
ELSE '***'
|
|
END
|
|
FROM unnest(d.cc_addresses) WITH ORDINALITY AS recipient(address, position)
|
|
ORDER BY recipient.position
|
|
),
|
|
d.provider,
|
|
d.error_code,
|
|
d.document_attachment_id,
|
|
d.sent_at,
|
|
d.failed_at,
|
|
d.created_at
|
|
FROM public.invoice_deliveries d
|
|
WHERE d.company_id = p_company_id
|
|
AND d.invoice_id = p_invoice_id
|
|
AND d.status <> 'preparing'
|
|
ORDER BY d.created_at DESC;
|
|
END;
|
|
$$;
|
|
|
|
REVOKE ALL ON FUNCTION public.list_invoice_delivery_summaries(uuid, uuid) FROM PUBLIC;
|
|
GRANT EXECUTE ON FUNCTION public.list_invoice_delivery_summaries(uuid, uuid) TO authenticated;
|
|
|
|
COMMENT ON FUNCTION public.list_invoice_delivery_summaries(uuid, uuid) IS
|
|
'Returns active-company invoice delivery status with masked To and CC addresses. Exact payload and BCC remain server-side.';
|
|
|
|
-- Fixed CC and BCC settings can redirect every future invoice email. Keep
|
|
-- these fields owner/admin controlled even when PostgREST is called directly.
|
|
CREATE OR REPLACE FUNCTION public.enforce_invoice_email_recipient_settings_admin()
|
|
RETURNS trigger
|
|
LANGUAGE plpgsql
|
|
SECURITY DEFINER
|
|
SET search_path = pg_catalog, public
|
|
AS $$
|
|
BEGIN
|
|
IF TG_OP = 'UPDATE'
|
|
AND NEW.invoice_email_cc_addresses IS NOT DISTINCT FROM OLD.invoice_email_cc_addresses
|
|
AND NEW.invoice_email_bcc_addresses IS NOT DISTINCT FROM OLD.invoice_email_bcc_addresses
|
|
THEN
|
|
RETURN NEW;
|
|
END IF;
|
|
|
|
IF TG_OP = 'INSERT'
|
|
AND NEW.invoice_email_cc_addresses IS NULL
|
|
AND NEW.invoice_email_bcc_addresses IS NULL
|
|
THEN
|
|
RETURN NEW;
|
|
END IF;
|
|
|
|
IF auth.role() = 'service_role' OR auth.uid() IS NULL THEN
|
|
RETURN NEW;
|
|
END IF;
|
|
|
|
IF NOT EXISTS (
|
|
SELECT 1
|
|
FROM public.company_members cm
|
|
WHERE cm.company_id = NEW.company_id
|
|
AND cm.user_id = auth.uid()
|
|
AND cm.role IN ('owner', 'admin')
|
|
) THEN
|
|
RAISE EXCEPTION 'owner or admin role required to change invoice email recipients'
|
|
USING ERRCODE = '42501';
|
|
END IF;
|
|
|
|
RETURN NEW;
|
|
END;
|
|
$$;
|
|
|
|
DROP TRIGGER IF EXISTS enforce_invoice_email_recipient_settings_admin
|
|
ON public.company_settings;
|
|
CREATE TRIGGER enforce_invoice_email_recipient_settings_admin
|
|
BEFORE INSERT OR UPDATE OF invoice_email_cc_addresses, invoice_email_bcc_addresses
|
|
ON public.company_settings
|
|
FOR EACH ROW
|
|
EXECUTE FUNCTION public.enforce_invoice_email_recipient_settings_admin();
|
|
|
|
-- Direct authenticated delivery writes can forge evidence. All delivery state
|
|
-- changes now go through service-role-only RPCs that bind the row to a verified
|
|
-- company member supplied by the server route.
|
|
DROP POLICY IF EXISTS invoice_deliveries_insert ON public.invoice_deliveries;
|
|
DROP POLICY IF EXISTS invoice_deliveries_update ON public.invoice_deliveries;
|
|
|
|
CREATE OR REPLACE FUNCTION public.authorize_invoice_delivery_service_actor(
|
|
p_company_id uuid,
|
|
p_actor_user_id uuid
|
|
)
|
|
RETURNS uuid
|
|
LANGUAGE plpgsql
|
|
STABLE
|
|
SECURITY DEFINER
|
|
SET search_path = pg_catalog, public
|
|
AS $$
|
|
BEGIN
|
|
IF auth.role() IS DISTINCT FROM 'service_role' THEN
|
|
RAISE EXCEPTION 'invoice delivery writes require a server-controlled service role'
|
|
USING ERRCODE = '42501';
|
|
END IF;
|
|
|
|
IF p_actor_user_id IS NULL OR NOT EXISTS (
|
|
SELECT 1
|
|
FROM public.company_members cm
|
|
WHERE cm.company_id = p_company_id
|
|
AND cm.user_id = p_actor_user_id
|
|
AND cm.role IN ('owner', 'admin', 'member')
|
|
) THEN
|
|
RAISE EXCEPTION 'invoice delivery actor is not a writable company member'
|
|
USING ERRCODE = '42501';
|
|
END IF;
|
|
|
|
RETURN p_actor_user_id;
|
|
END;
|
|
$$;
|
|
|
|
REVOKE ALL ON FUNCTION public.authorize_invoice_delivery_service_actor(uuid, uuid)
|
|
FROM PUBLIC, anon, authenticated;
|
|
|
|
-- A preparing row contains no recipient or message payload. It may be removed
|
|
-- after a bounded timeout so another authorized sender can recover from a
|
|
-- crashed render without deleting accounting evidence.
|
|
CREATE OR REPLACE FUNCTION public.enforce_invoice_delivery_immutability()
|
|
RETURNS trigger
|
|
LANGUAGE plpgsql
|
|
SECURITY DEFINER
|
|
SET search_path = pg_catalog, public
|
|
AS $$
|
|
BEGIN
|
|
IF TG_OP = 'DELETE' THEN
|
|
IF OLD.status = 'preparing'
|
|
AND OLD.created_at <= now() - interval '15 minutes'
|
|
THEN
|
|
RETURN OLD;
|
|
END IF;
|
|
|
|
INSERT INTO public.audit_log (
|
|
user_id,
|
|
company_id,
|
|
action,
|
|
table_name,
|
|
record_id,
|
|
actor_id,
|
|
old_state,
|
|
description
|
|
) VALUES (
|
|
OLD.user_id,
|
|
OLD.company_id,
|
|
'SECURITY_EVENT',
|
|
'invoice_deliveries',
|
|
OLD.id,
|
|
auth.uid(),
|
|
public.invoice_delivery_audit_state(OLD),
|
|
'Blocked deletion of immutable invoice delivery history.'
|
|
);
|
|
RETURN NULL;
|
|
END IF;
|
|
|
|
IF OLD.status = 'preparing' THEN
|
|
IF NEW.status <> 'pending'
|
|
OR 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.channel IS DISTINCT FROM OLD.channel
|
|
OR NEW.provider IS NOT NULL
|
|
OR NEW.provider_message_id IS NOT NULL
|
|
OR NEW.error_code IS NOT NULL
|
|
OR NEW.sent_at IS NOT NULL
|
|
OR NEW.failed_at IS NOT NULL
|
|
OR NEW.retention_expires_at IS DISTINCT FROM OLD.retention_expires_at
|
|
OR NEW.pii_redacted_at IS NOT NULL
|
|
OR NEW.created_at IS DISTINCT FROM OLD.created_at
|
|
THEN
|
|
RAISE EXCEPTION 'preparing invoice delivery may only capture its pending payload'
|
|
USING ERRCODE = '23514';
|
|
END IF;
|
|
RETURN NEW;
|
|
END IF;
|
|
|
|
IF OLD.status = 'pending' THEN
|
|
IF NEW.status NOT IN ('sent', 'failed') THEN
|
|
RAISE EXCEPTION 'pending invoice delivery may only transition to sent or failed'
|
|
USING ERRCODE = '23514';
|
|
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.channel IS DISTINCT FROM OLD.channel
|
|
OR NEW.to_addresses IS DISTINCT FROM OLD.to_addresses
|
|
OR NEW.cc_addresses IS DISTINCT FROM OLD.cc_addresses
|
|
OR NEW.bcc_addresses IS DISTINCT FROM OLD.bcc_addresses
|
|
OR NEW.reply_to IS DISTINCT FROM OLD.reply_to
|
|
OR NEW.from_name IS DISTINCT FROM OLD.from_name
|
|
OR NEW.subject IS DISTINCT FROM OLD.subject
|
|
OR NEW.body_text IS DISTINCT FROM OLD.body_text
|
|
OR NEW.body_html IS DISTINCT FROM OLD.body_html
|
|
OR NEW.attachment_filename IS DISTINCT FROM OLD.attachment_filename
|
|
OR NEW.attachment_content_type IS DISTINCT FROM OLD.attachment_content_type
|
|
OR NEW.attachment_sha256 IS DISTINCT FROM OLD.attachment_sha256
|
|
OR NEW.retention_expires_at IS DISTINCT FROM OLD.retention_expires_at
|
|
OR NEW.pii_redacted_at IS DISTINCT FROM OLD.pii_redacted_at
|
|
OR NEW.created_at IS DISTINCT FROM OLD.created_at
|
|
OR (
|
|
NEW.status = 'sent'
|
|
AND NEW.document_attachment_id IS DISTINCT FROM OLD.document_attachment_id
|
|
)
|
|
OR (
|
|
NEW.status = 'failed'
|
|
AND NEW.document_attachment_id IS NOT NULL
|
|
)
|
|
THEN
|
|
RAISE EXCEPTION 'invoice delivery payload is immutable'
|
|
USING ERRCODE = '23514';
|
|
END IF;
|
|
RETURN NEW;
|
|
END IF;
|
|
|
|
IF OLD.status IN ('sent', 'failed')
|
|
AND OLD.pii_redacted_at IS NULL
|
|
AND CURRENT_DATE >= OLD.retention_expires_at
|
|
AND NEW.pii_redacted_at IS NOT NULL
|
|
AND NEW.company_id IS NOT DISTINCT FROM OLD.company_id
|
|
AND NEW.user_id IS NOT DISTINCT FROM OLD.user_id
|
|
AND NEW.invoice_id IS NOT DISTINCT FROM OLD.invoice_id
|
|
AND NEW.channel IS NOT DISTINCT FROM OLD.channel
|
|
AND NEW.status IS NOT DISTINCT FROM OLD.status
|
|
AND cardinality(NEW.to_addresses) = 0
|
|
AND cardinality(NEW.cc_addresses) = 0
|
|
AND cardinality(NEW.bcc_addresses) = 0
|
|
AND NEW.reply_to IS NULL
|
|
AND NEW.from_name IS NULL
|
|
AND NEW.subject IS NULL
|
|
AND NEW.body_text IS NULL
|
|
AND NEW.body_html IS NULL
|
|
AND NEW.provider IS NOT DISTINCT FROM OLD.provider
|
|
AND NEW.provider_message_id IS NULL
|
|
AND NEW.error_code IS NOT DISTINCT FROM OLD.error_code
|
|
AND NEW.document_attachment_id IS NOT DISTINCT FROM OLD.document_attachment_id
|
|
AND NEW.attachment_filename IS NULL
|
|
AND NEW.attachment_content_type IS NOT DISTINCT FROM OLD.attachment_content_type
|
|
AND NEW.attachment_sha256 IS NULL
|
|
AND NEW.sent_at IS NOT DISTINCT FROM OLD.sent_at
|
|
AND NEW.failed_at IS NOT DISTINCT FROM OLD.failed_at
|
|
AND NEW.retention_expires_at IS NOT DISTINCT FROM OLD.retention_expires_at
|
|
AND NEW.created_at IS NOT DISTINCT FROM OLD.created_at
|
|
THEN
|
|
RETURN NEW;
|
|
END IF;
|
|
|
|
RAISE EXCEPTION 'terminal invoice delivery (%) is immutable', OLD.status
|
|
USING ERRCODE = '23514';
|
|
END;
|
|
$$;
|
|
|
|
CREATE OR REPLACE FUNCTION public.reserve_invoice_delivery(
|
|
p_company_id uuid,
|
|
p_invoice_id uuid,
|
|
p_actor_user_id uuid
|
|
)
|
|
RETURNS uuid
|
|
LANGUAGE plpgsql
|
|
SECURITY DEFINER
|
|
SET search_path = pg_catalog, public
|
|
AS $$
|
|
DECLARE
|
|
actor_id uuid;
|
|
delivery_id uuid;
|
|
existing_actor_id uuid;
|
|
existing_created_at timestamptz;
|
|
BEGIN
|
|
actor_id := public.authorize_invoice_delivery_service_actor(
|
|
p_company_id,
|
|
p_actor_user_id
|
|
);
|
|
|
|
IF NOT EXISTS (
|
|
SELECT 1 FROM public.invoices i
|
|
WHERE i.id = p_invoice_id AND i.company_id = p_company_id
|
|
) THEN
|
|
RAISE EXCEPTION 'invoice not found for delivery reservation'
|
|
USING ERRCODE = 'P0002';
|
|
END IF;
|
|
|
|
LOOP
|
|
BEGIN
|
|
INSERT INTO public.invoice_deliveries (
|
|
company_id, user_id, invoice_id, channel, status
|
|
) VALUES (
|
|
p_company_id, actor_id, p_invoice_id, 'email', 'preparing'
|
|
)
|
|
RETURNING id INTO delivery_id;
|
|
RETURN delivery_id;
|
|
EXCEPTION WHEN unique_violation THEN
|
|
SELECT d.id, d.user_id, d.created_at
|
|
INTO delivery_id, existing_actor_id, existing_created_at
|
|
FROM public.invoice_deliveries d
|
|
WHERE d.company_id = p_company_id
|
|
AND d.invoice_id = p_invoice_id
|
|
AND d.status = 'preparing'
|
|
FOR UPDATE;
|
|
|
|
IF delivery_id IS NULL THEN
|
|
CONTINUE;
|
|
END IF;
|
|
IF existing_actor_id = actor_id THEN
|
|
RETURN delivery_id;
|
|
END IF;
|
|
IF existing_created_at > now() - interval '15 minutes' THEN
|
|
RAISE EXCEPTION 'invoice delivery is already being prepared'
|
|
USING ERRCODE = '55P03';
|
|
END IF;
|
|
|
|
DELETE FROM public.invoice_deliveries d WHERE d.id = delivery_id;
|
|
delivery_id := NULL;
|
|
END;
|
|
END LOOP;
|
|
END;
|
|
$$;
|
|
|
|
CREATE OR REPLACE FUNCTION public.capture_invoice_delivery_payload(
|
|
p_delivery_id uuid,
|
|
p_company_id uuid,
|
|
p_invoice_id uuid,
|
|
p_actor_user_id uuid,
|
|
p_to_addresses text[],
|
|
p_cc_addresses text[],
|
|
p_bcc_addresses text[],
|
|
p_reply_to text,
|
|
p_from_name text,
|
|
p_subject text,
|
|
p_body_text text,
|
|
p_body_html text,
|
|
p_document_attachment_id uuid,
|
|
p_attachment_filename text,
|
|
p_attachment_content_type text,
|
|
p_attachment_sha256 text
|
|
)
|
|
RETURNS uuid
|
|
LANGUAGE plpgsql
|
|
SECURITY DEFINER
|
|
SET search_path = pg_catalog, public
|
|
AS $$
|
|
DECLARE
|
|
actor_id uuid;
|
|
captured_id uuid;
|
|
BEGIN
|
|
actor_id := public.authorize_invoice_delivery_service_actor(
|
|
p_company_id,
|
|
p_actor_user_id
|
|
);
|
|
|
|
UPDATE public.invoice_deliveries
|
|
SET status = 'pending',
|
|
to_addresses = p_to_addresses,
|
|
cc_addresses = p_cc_addresses,
|
|
bcc_addresses = p_bcc_addresses,
|
|
reply_to = p_reply_to,
|
|
from_name = p_from_name,
|
|
subject = p_subject,
|
|
body_text = p_body_text,
|
|
body_html = p_body_html,
|
|
document_attachment_id = p_document_attachment_id,
|
|
attachment_filename = p_attachment_filename,
|
|
attachment_content_type = p_attachment_content_type,
|
|
attachment_sha256 = p_attachment_sha256
|
|
WHERE id = p_delivery_id
|
|
AND company_id = p_company_id
|
|
AND invoice_id = p_invoice_id
|
|
AND user_id = actor_id
|
|
AND status = 'preparing'
|
|
RETURNING id INTO captured_id;
|
|
|
|
IF captured_id IS NULL THEN
|
|
RAISE EXCEPTION 'invoice delivery reservation cannot capture payload'
|
|
USING ERRCODE = '23514';
|
|
END IF;
|
|
RETURN captured_id;
|
|
END;
|
|
$$;
|
|
|
|
CREATE OR REPLACE FUNCTION public.finalize_invoice_delivery(
|
|
p_delivery_id uuid,
|
|
p_company_id uuid,
|
|
p_actor_user_id uuid,
|
|
p_status text,
|
|
p_provider text,
|
|
p_provider_message_id text,
|
|
p_error_code text
|
|
)
|
|
RETURNS uuid
|
|
LANGUAGE plpgsql
|
|
SECURITY DEFINER
|
|
SET search_path = pg_catalog, public
|
|
AS $$
|
|
DECLARE
|
|
actor_id uuid;
|
|
finalized_id uuid;
|
|
BEGIN
|
|
actor_id := public.authorize_invoice_delivery_service_actor(
|
|
p_company_id,
|
|
p_actor_user_id
|
|
);
|
|
|
|
IF p_status = 'sent' THEN
|
|
UPDATE public.invoice_deliveries
|
|
SET status = 'sent',
|
|
provider = p_provider,
|
|
provider_message_id = p_provider_message_id,
|
|
error_code = NULL,
|
|
sent_at = now(),
|
|
failed_at = NULL
|
|
WHERE id = p_delivery_id
|
|
AND company_id = p_company_id
|
|
AND user_id = actor_id
|
|
AND status = 'pending'
|
|
RETURNING id INTO finalized_id;
|
|
ELSIF p_status = 'failed' THEN
|
|
UPDATE public.invoice_deliveries
|
|
SET status = 'failed',
|
|
provider = p_provider,
|
|
provider_message_id = NULL,
|
|
error_code = COALESCE(p_error_code, 'provider_failed'),
|
|
document_attachment_id = NULL,
|
|
sent_at = NULL,
|
|
failed_at = now()
|
|
WHERE id = p_delivery_id
|
|
AND company_id = p_company_id
|
|
AND user_id = actor_id
|
|
AND status = 'pending'
|
|
RETURNING id INTO finalized_id;
|
|
ELSE
|
|
RAISE EXCEPTION 'invoice delivery terminal status must be sent or failed'
|
|
USING ERRCODE = '22023';
|
|
END IF;
|
|
|
|
IF finalized_id IS NULL THEN
|
|
RAISE EXCEPTION 'invoice delivery cannot be finalized'
|
|
USING ERRCODE = '23514';
|
|
END IF;
|
|
RETURN finalized_id;
|
|
END;
|
|
$$;
|
|
|
|
CREATE OR REPLACE FUNCTION public.record_manual_invoice_delivery(
|
|
p_company_id uuid,
|
|
p_invoice_id uuid,
|
|
p_actor_user_id uuid,
|
|
p_sent_at timestamptz DEFAULT NULL
|
|
)
|
|
RETURNS jsonb
|
|
LANGUAGE plpgsql
|
|
SECURITY DEFINER
|
|
SET search_path = pg_catalog, public
|
|
AS $$
|
|
DECLARE
|
|
actor_id uuid;
|
|
delivery public.invoice_deliveries%ROWTYPE;
|
|
BEGIN
|
|
actor_id := public.authorize_invoice_delivery_service_actor(
|
|
p_company_id,
|
|
p_actor_user_id
|
|
);
|
|
|
|
IF NOT EXISTS (
|
|
SELECT 1 FROM public.invoices i
|
|
WHERE i.id = p_invoice_id
|
|
AND i.company_id = p_company_id
|
|
AND i.status IN ('sent', 'overdue', 'partially_paid', 'paid', 'credited')
|
|
) THEN
|
|
RAISE EXCEPTION 'invoice is not in a manually sent state'
|
|
USING ERRCODE = '23514';
|
|
END IF;
|
|
|
|
INSERT INTO public.invoice_deliveries (
|
|
company_id, user_id, invoice_id, channel, status, sent_at
|
|
) VALUES (
|
|
p_company_id, actor_id, p_invoice_id, 'manual', 'marked_sent',
|
|
COALESCE(p_sent_at, now())
|
|
)
|
|
RETURNING * INTO delivery;
|
|
|
|
RETURN to_jsonb(delivery);
|
|
END;
|
|
$$;
|
|
|
|
-- Deferred booking needs only the archived document id, not the email payload.
|
|
CREATE OR REPLACE FUNCTION public.latest_sent_invoice_delivery_document(
|
|
p_company_id uuid,
|
|
p_invoice_id uuid
|
|
)
|
|
RETURNS uuid
|
|
LANGUAGE plpgsql
|
|
STABLE
|
|
SECURITY DEFINER
|
|
SET search_path = pg_catalog, public
|
|
AS $$
|
|
DECLARE
|
|
document_id uuid;
|
|
BEGIN
|
|
IF auth.uid() IS NULL
|
|
OR p_company_id IS DISTINCT FROM public.current_active_company_id()
|
|
OR NOT EXISTS (
|
|
SELECT 1 FROM public.company_members cm
|
|
WHERE cm.company_id = p_company_id AND cm.user_id = auth.uid()
|
|
)
|
|
THEN
|
|
RAISE EXCEPTION 'not authorized to find delivered invoice document'
|
|
USING ERRCODE = '42501';
|
|
END IF;
|
|
|
|
SELECT d.document_attachment_id
|
|
INTO document_id
|
|
FROM public.invoice_deliveries d
|
|
WHERE d.company_id = p_company_id
|
|
AND d.invoice_id = p_invoice_id
|
|
AND d.status = 'sent'
|
|
AND d.document_attachment_id IS NOT NULL
|
|
ORDER BY d.sent_at DESC
|
|
LIMIT 1;
|
|
|
|
RETURN document_id;
|
|
END;
|
|
$$;
|
|
|
|
-- Full statutory exports are an explicit need-to-know exception to routine
|
|
-- sender-only payload access. Authenticated callers must be owner/admin; server
|
|
-- service paths are already authorized before invoking the exporter.
|
|
CREATE OR REPLACE FUNCTION public.export_invoice_delivery_evidence(
|
|
p_company_id uuid
|
|
)
|
|
RETURNS SETOF public.invoice_deliveries
|
|
LANGUAGE plpgsql
|
|
STABLE
|
|
SECURITY DEFINER
|
|
SET search_path = pg_catalog, public
|
|
AS $$
|
|
BEGIN
|
|
IF auth.role() IS DISTINCT FROM 'service_role' AND (
|
|
auth.uid() IS NULL
|
|
OR p_company_id IS DISTINCT FROM public.current_active_company_id()
|
|
OR NOT EXISTS (
|
|
SELECT 1 FROM public.company_members cm
|
|
WHERE cm.company_id = p_company_id
|
|
AND cm.user_id = auth.uid()
|
|
AND cm.role IN ('owner', 'admin')
|
|
)
|
|
) THEN
|
|
RAISE EXCEPTION 'owner or admin role required to export invoice delivery evidence'
|
|
USING ERRCODE = '42501';
|
|
END IF;
|
|
|
|
RETURN QUERY
|
|
SELECT d.*
|
|
FROM public.invoice_deliveries d
|
|
WHERE d.company_id = p_company_id;
|
|
END;
|
|
$$;
|
|
|
|
REVOKE ALL ON FUNCTION public.reserve_invoice_delivery(uuid, uuid, uuid)
|
|
FROM PUBLIC, anon, authenticated;
|
|
REVOKE ALL ON FUNCTION public.capture_invoice_delivery_payload(
|
|
uuid, uuid, uuid, uuid, text[], text[], text[], text, text, text, text, text,
|
|
uuid, text, text, text
|
|
) FROM PUBLIC, anon, authenticated;
|
|
REVOKE ALL ON FUNCTION public.finalize_invoice_delivery(
|
|
uuid, uuid, uuid, text, text, text, text
|
|
) FROM PUBLIC, anon, authenticated;
|
|
REVOKE ALL ON FUNCTION public.record_manual_invoice_delivery(uuid, uuid, uuid, timestamptz)
|
|
FROM PUBLIC, anon, authenticated;
|
|
|
|
GRANT EXECUTE ON FUNCTION public.reserve_invoice_delivery(uuid, uuid, uuid)
|
|
TO service_role;
|
|
GRANT EXECUTE ON FUNCTION public.capture_invoice_delivery_payload(
|
|
uuid, uuid, uuid, uuid, text[], text[], text[], text, text, text, text, text,
|
|
uuid, text, text, text
|
|
) TO service_role;
|
|
GRANT EXECUTE ON FUNCTION public.finalize_invoice_delivery(
|
|
uuid, uuid, uuid, text, text, text, text
|
|
) TO service_role;
|
|
GRANT EXECUTE ON FUNCTION public.record_manual_invoice_delivery(uuid, uuid, uuid, timestamptz)
|
|
TO service_role;
|
|
|
|
REVOKE ALL ON FUNCTION public.latest_sent_invoice_delivery_document(uuid, uuid)
|
|
FROM PUBLIC;
|
|
GRANT EXECUTE ON FUNCTION public.latest_sent_invoice_delivery_document(uuid, uuid)
|
|
TO authenticated, service_role;
|
|
REVOKE ALL ON FUNCTION public.export_invoice_delivery_evidence(uuid) FROM PUBLIC;
|
|
GRANT EXECUTE ON FUNCTION public.export_invoice_delivery_evidence(uuid)
|
|
TO authenticated, service_role;
|
|
|
|
COMMENT ON FUNCTION public.reserve_invoice_delivery(uuid, uuid, uuid) IS
|
|
'Service-only reservation for an invoice email delivery. Reclaims payload-free reservations after 15 minutes.';
|
|
COMMENT ON FUNCTION public.latest_sent_invoice_delivery_document(uuid, uuid) IS
|
|
'Returns only the latest sent delivery document id for active-company deferred booking.';
|
|
COMMENT ON FUNCTION public.export_invoice_delivery_evidence(uuid) IS
|
|
'Returns exact company delivery evidence only to owner/admin audit exports and service-role server paths.';
|
|
|
|
NOTIFY pgrst, 'reload schema';
|