155 lines
4.0 KiB
PL/PgSQL
155 lines
4.0 KiB
PL/PgSQL
-- =====================================================
|
|
-- Atomic Application Workflow
|
|
-- Replaces multi-query approve/reject in api.ts with
|
|
-- single transactional PG functions.
|
|
-- =====================================================
|
|
|
|
-- approve_application: atomically creates a member from an application
|
|
CREATE OR REPLACE FUNCTION public.approve_application(
|
|
p_application_id uuid,
|
|
p_user_id uuid
|
|
)
|
|
RETURNS uuid
|
|
LANGUAGE plpgsql
|
|
SECURITY DEFINER
|
|
SET search_path = ''
|
|
AS $$
|
|
DECLARE
|
|
v_app record;
|
|
v_member_id uuid;
|
|
v_member_number text;
|
|
BEGIN
|
|
-- 1. Fetch and lock the application
|
|
SELECT * INTO v_app
|
|
FROM public.membership_applications
|
|
WHERE id = p_application_id
|
|
FOR UPDATE;
|
|
|
|
IF v_app IS NULL THEN
|
|
RAISE EXCEPTION 'Application % not found', p_application_id
|
|
USING ERRCODE = 'P0002';
|
|
END IF;
|
|
|
|
-- Authorization: caller must have write permission on this account
|
|
IF NOT public.has_permission(auth.uid(), v_app.account_id, 'members.write'::public.app_permissions) THEN
|
|
RAISE EXCEPTION 'Access denied to account %', v_app.account_id
|
|
USING ERRCODE = '42501';
|
|
END IF;
|
|
|
|
IF v_app.status NOT IN ('submitted', 'review') THEN
|
|
RAISE EXCEPTION 'Application is not in a reviewable state (current: %)', v_app.status
|
|
USING ERRCODE = 'P0001';
|
|
END IF;
|
|
|
|
-- 2. Generate next member number
|
|
SELECT LPAD(
|
|
(COALESCE(
|
|
MAX(CASE WHEN member_number ~ '^\d+$' THEN member_number::integer ELSE 0 END),
|
|
0
|
|
) + 1)::text,
|
|
4, '0'
|
|
) INTO v_member_number
|
|
FROM public.members
|
|
WHERE account_id = v_app.account_id;
|
|
|
|
-- 3. Create the member
|
|
INSERT INTO public.members (
|
|
account_id,
|
|
member_number,
|
|
first_name,
|
|
last_name,
|
|
email,
|
|
phone,
|
|
street,
|
|
postal_code,
|
|
city,
|
|
date_of_birth,
|
|
status,
|
|
entry_date,
|
|
created_by,
|
|
updated_by
|
|
) VALUES (
|
|
v_app.account_id,
|
|
v_member_number,
|
|
v_app.first_name,
|
|
v_app.last_name,
|
|
v_app.email,
|
|
v_app.phone,
|
|
v_app.street,
|
|
v_app.postal_code,
|
|
v_app.city,
|
|
v_app.date_of_birth,
|
|
'active'::public.membership_status,
|
|
current_date,
|
|
auth.uid(),
|
|
auth.uid()
|
|
)
|
|
RETURNING id INTO v_member_id;
|
|
|
|
-- 4. Mark application as approved
|
|
UPDATE public.membership_applications
|
|
SET
|
|
status = 'approved'::public.application_status,
|
|
reviewed_by = auth.uid(),
|
|
reviewed_at = now(),
|
|
member_id = v_member_id,
|
|
updated_at = now()
|
|
WHERE id = p_application_id;
|
|
|
|
RETURN v_member_id;
|
|
END;
|
|
$$;
|
|
|
|
GRANT EXECUTE ON FUNCTION public.approve_application(uuid, uuid) TO authenticated;
|
|
GRANT EXECUTE ON FUNCTION public.approve_application(uuid, uuid) TO service_role;
|
|
|
|
-- reject_application: atomically rejects an application with notes
|
|
CREATE OR REPLACE FUNCTION public.reject_application(
|
|
p_application_id uuid,
|
|
p_user_id uuid,
|
|
p_review_notes text DEFAULT NULL
|
|
)
|
|
RETURNS void
|
|
LANGUAGE plpgsql
|
|
SECURITY DEFINER
|
|
SET search_path = ''
|
|
AS $$
|
|
DECLARE
|
|
v_app record;
|
|
BEGIN
|
|
-- Fetch and lock the application
|
|
SELECT * INTO v_app
|
|
FROM public.membership_applications
|
|
WHERE id = p_application_id
|
|
FOR UPDATE;
|
|
|
|
IF v_app IS NULL THEN
|
|
RAISE EXCEPTION 'Application % not found', p_application_id
|
|
USING ERRCODE = 'P0002';
|
|
END IF;
|
|
|
|
-- Authorization: caller must have write permission on this account
|
|
IF NOT public.has_permission(auth.uid(), v_app.account_id, 'members.write'::public.app_permissions) THEN
|
|
RAISE EXCEPTION 'Access denied to account %', v_app.account_id
|
|
USING ERRCODE = '42501';
|
|
END IF;
|
|
|
|
IF v_app.status NOT IN ('submitted', 'review') THEN
|
|
RAISE EXCEPTION 'Application is not in a reviewable state (current: %)', v_app.status
|
|
USING ERRCODE = 'P0001';
|
|
END IF;
|
|
|
|
UPDATE public.membership_applications
|
|
SET
|
|
status = 'rejected'::public.application_status,
|
|
reviewed_by = auth.uid(),
|
|
reviewed_at = now(),
|
|
review_notes = p_review_notes,
|
|
updated_at = now()
|
|
WHERE id = p_application_id;
|
|
END;
|
|
$$;
|
|
|
|
GRANT EXECUTE ON FUNCTION public.reject_application(uuid, uuid, text) TO authenticated;
|
|
GRANT EXECUTE ON FUNCTION public.reject_application(uuid, uuid, text) TO service_role;
|