Hostinger Reach integration UI + ARC Buildium matching, drop Mailchimp

- HostingerReachPage (replaces MailchimpPage): connect Reach via
  reach-connection, per-association segment sync via reach-sync
- ARC Applications: Buildium import review/matching updates
- buildium-import-stage/apply: latest staging + apply changes (already
  deployed to Supabase)
- migrations: hostinger_reach_integration + arc_finalized_lock service
  role (already applied to live DB)
- CI: note that deployment is VPS-side polling (auto-deploy.sh cron)

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
This commit is contained in:
2026-06-11 23:07:30 -04:00
parent 220892203c
commit abd46bcb2b
15 changed files with 1041 additions and 726 deletions
@@ -0,0 +1,23 @@
-- Allow privileged backend contexts (service role / no JWT, e.g. the Buildium import) to update
-- finalized ARC applications, alongside admins. Client writes by non-admins remain blocked by RLS,
-- so this does not weaken the user-facing lock.
CREATE OR REPLACE FUNCTION public.prevent_updates_on_finalized_arc()
RETURNS trigger
LANGUAGE plpgsql
SECURITY DEFINER
SET search_path TO 'public'
AS $function$
BEGIN
IF lower(COALESCE(OLD.status,'')) IN ('approved','denied') THEN
-- auth.uid() IS NULL => no end-user JWT (service role / backend job); admins also exempt.
IF auth.uid() IS NULL OR public.has_role(auth.uid(), 'admin'::public.app_role) THEN
RETURN NEW;
END IF;
RAISE EXCEPTION 'This ARC application has been finalized (approved or denied) and is locked from further changes.'
USING ERRCODE = 'check_violation';
END IF;
RETURN NEW;
END;
$function$;
@@ -0,0 +1,39 @@
-- Hostinger Reach integration: one global API token + per-association segment/sync tracking.
CREATE TABLE IF NOT EXISTS public.hostinger_reach_config (
id uuid PRIMARY KEY DEFAULT gen_random_uuid(),
api_token text NOT NULL,
created_by uuid,
created_at timestamptz NOT NULL DEFAULT now(),
updated_at timestamptz NOT NULL DEFAULT now()
);
CREATE TABLE IF NOT EXISTS public.hostinger_reach_segments (
id uuid PRIMARY KEY DEFAULT gen_random_uuid(),
association_id uuid NOT NULL UNIQUE REFERENCES public.associations(id) ON DELETE CASCADE,
segment_uuid text,
segment_name text,
last_sync_at timestamptz,
last_sync_status text,
last_sync_count integer,
last_sync_error text,
created_at timestamptz NOT NULL DEFAULT now(),
updated_at timestamptz NOT NULL DEFAULT now()
);
ALTER TABLE public.hostinger_reach_config ENABLE ROW LEVEL SECURITY;
ALTER TABLE public.hostinger_reach_segments ENABLE ROW LEVEL SECURITY;
-- Admin-only access (service role bypasses RLS for the edge functions).
CREATE POLICY "Admins manage reach config" ON public.hostinger_reach_config
FOR ALL USING (public.has_role(auth.uid(), 'admin'::public.app_role))
WITH CHECK (public.has_role(auth.uid(), 'admin'::public.app_role));
CREATE POLICY "Admins manage reach segments" ON public.hostinger_reach_segments
FOR ALL USING (public.has_role(auth.uid(), 'admin'::public.app_role))
WITH CHECK (public.has_role(auth.uid(), 'admin'::public.app_role));
CREATE TRIGGER set_reach_config_updated_at BEFORE UPDATE ON public.hostinger_reach_config
FOR EACH ROW EXECUTE FUNCTION public.update_updated_at_column();
CREATE TRIGGER set_reach_segments_updated_at BEFORE UPDATE ON public.hostinger_reach_segments
FOR EACH ROW EXECUTE FUNCTION public.update_updated_at_column();