@fayz-ai/plugin-conversations 0.11.6 → 0.12.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -1,5 +1,8 @@
1
1
  export declare const MIGRATION_000_BASELINE = "-- ============================================================================\n-- plugins/plugin-conversations/src/migrations/000_baseline.sql \u2014 what installing this provisions, as one file.\n--\n-- GENERATED by scripts/emit-unit-baselines.mjs (#338). Do not edit by hand:\n-- change the schema, regenerate, and let scripts/check-chain-equivalence.mjs\n-- prove the result still matches.\n--\n-- This replaced 9 migration files. They are kept, unapplied, in\n-- migrations.archive/ \u2014 a year of decisions is worth reading even when it is no\n-- longer worth replaying.\n--\n-- Object order is fixed rather than dependency-sorted: schemas, types,\n-- sequences, tables, defaults, functions, constraints, views, indexes, foreign\n-- keys, triggers, row security, policies, grants. Dependencies BETWEEN units\n-- are carried by the order packages/db/chain.json declares.\n-- ============================================================================\n\n-- A LANGUAGE sql function binds its body at CREATE time, and 570 functions\n-- sorted by name are not sorted by who calls whom. This is what lets them load\n-- in any order; every body is still compiled on first call, so a genuinely\n-- broken reference surfaces then rather than never. Session-scoped.\nSET check_function_bodies = false;\n\n\n\n-- \u2500\u2500 default privileges: reset \u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\n-- Cleared BEFORE anything is created, so every object below gets the ACL the\n-- dump describes rather than the image's defaults on top of it. Restored at\n-- the end of this file.\n\nDO $dp$ BEGIN\n EXECUTE $stmt$ALTER DEFAULT PRIVILEGES FOR ROLE postgres IN SCHEMA public REVOKE ALL ON SEQUENCES FROM anon, authenticated, service_role, fayz_connector$stmt$;\nEXCEPTION WHEN insufficient_privilege OR undefined_object THEN\n RAISE NOTICE 'baseline: not ours to set \u2014 %', $stmt$ALTER DEFAULT PRIVILEGES FOR ROLE postgres IN SCHEMA public REVOKE ALL ON SEQUENCES FROM anon, authenticated, service_role, fayz_connector$stmt$;\nEND $dp$;\n\nDO $dp$ BEGIN\n EXECUTE $stmt$ALTER DEFAULT PRIVILEGES FOR ROLE supabase_admin IN SCHEMA public REVOKE ALL ON SEQUENCES FROM anon, authenticated, service_role, fayz_connector$stmt$;\nEXCEPTION WHEN insufficient_privilege OR undefined_object THEN\n RAISE NOTICE 'baseline: not ours to set \u2014 %', $stmt$ALTER DEFAULT PRIVILEGES FOR ROLE supabase_admin IN SCHEMA public REVOKE ALL ON SEQUENCES FROM anon, authenticated, service_role, fayz_connector$stmt$;\nEND $dp$;\n\nDO $dp$ BEGIN\n EXECUTE $stmt$ALTER DEFAULT PRIVILEGES FOR ROLE postgres IN SCHEMA public REVOKE ALL ON FUNCTIONS FROM anon, authenticated, service_role, fayz_connector$stmt$;\nEXCEPTION WHEN insufficient_privilege OR undefined_object THEN\n RAISE NOTICE 'baseline: not ours to set \u2014 %', $stmt$ALTER DEFAULT PRIVILEGES FOR ROLE postgres IN SCHEMA public REVOKE ALL ON FUNCTIONS FROM anon, authenticated, service_role, fayz_connector$stmt$;\nEND $dp$;\n\nDO $dp$ BEGIN\n EXECUTE $stmt$ALTER DEFAULT PRIVILEGES FOR ROLE supabase_admin IN SCHEMA public REVOKE ALL ON FUNCTIONS FROM anon, authenticated, service_role, fayz_connector$stmt$;\nEXCEPTION WHEN insufficient_privilege OR undefined_object THEN\n RAISE NOTICE 'baseline: not ours to set \u2014 %', $stmt$ALTER DEFAULT PRIVILEGES FOR ROLE supabase_admin IN SCHEMA public REVOKE ALL ON FUNCTIONS FROM anon, authenticated, service_role, fayz_connector$stmt$;\nEND $dp$;\n\nDO $dp$ BEGIN\n EXECUTE $stmt$ALTER DEFAULT PRIVILEGES FOR ROLE postgres IN SCHEMA public REVOKE ALL ON TABLES FROM anon, authenticated, service_role, fayz_connector$stmt$;\nEXCEPTION WHEN insufficient_privilege OR undefined_object THEN\n RAISE NOTICE 'baseline: not ours to set \u2014 %', $stmt$ALTER DEFAULT PRIVILEGES FOR ROLE postgres IN SCHEMA public REVOKE ALL ON TABLES FROM anon, authenticated, service_role, fayz_connector$stmt$;\nEND $dp$;\n\nDO $dp$ BEGIN\n EXECUTE $stmt$ALTER DEFAULT PRIVILEGES FOR ROLE supabase_admin IN SCHEMA public REVOKE ALL ON TABLES FROM anon, authenticated, service_role, fayz_connector$stmt$;\nEXCEPTION WHEN insufficient_privilege OR undefined_object THEN\n RAISE NOTICE 'baseline: not ours to set \u2014 %', $stmt$ALTER DEFAULT PRIVILEGES FOR ROLE supabase_admin IN SCHEMA public REVOKE ALL ON TABLES FROM anon, authenticated, service_role, fayz_connector$stmt$;\nEND $dp$;\n\n\n-- \u2500\u2500 table \u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\n\nCREATE TABLE public.plg_conversation_messages (\n id uuid DEFAULT gen_random_uuid() NOT NULL,\n tenant_id uuid NOT NULL,\n conversation_id uuid NOT NULL,\n channel text,\n direction text,\n body text NOT NULL,\n author text,\n at timestamp with time zone DEFAULT now(),\n provider_message_id text,\n delivery_status text,\n sender_kind text,\n sender_label text,\n subject_type text,\n subject_id text,\n CONSTRAINT plg_conversation_messages_channel_check CHECK (((channel IS NULL) OR (channel = ANY (ARRAY['sms'::text, 'whatsapp'::text, 'instagram'::text, 'email'::text, 'webchat'::text])))),\n CONSTRAINT plg_conversation_messages_delivery_status_check CHECK (((delivery_status IS NULL) OR (delivery_status = ANY (ARRAY['queued'::text, 'sent'::text, 'delivered'::text, 'read'::text, 'failed'::text, 'expired'::text, 'delivery_timeout'::text, 'cancelled'::text, 'opted_out'::text])))),\n CONSTRAINT plg_conversation_messages_direction_check CHECK ((direction = ANY (ARRAY['inbound'::text, 'outbound'::text]))),\n CONSTRAINT plg_conversation_messages_sender_kind_check CHECK (((sender_kind IS NULL) OR (sender_kind = ANY (ARRAY['user'::text, 'system'::text, 'ai'::text, 'automation'::text]))))\n);\n\nCREATE TABLE public.plg_conversations (\n id uuid DEFAULT gen_random_uuid() NOT NULL,\n tenant_id uuid NOT NULL,\n contact_name text NOT NULL,\n contact_handle text,\n channel text NOT NULL,\n last_message_preview text,\n last_message_at timestamp with time zone DEFAULT now(),\n unread_count integer DEFAULT 0,\n status text DEFAULT 'open'::text,\n assigned_to text,\n accent text,\n tags text[],\n location text,\n note text,\n created_at timestamp with time zone DEFAULT now() NOT NULL,\n updated_at timestamp with time zone DEFAULT now() NOT NULL,\n contact_person_id uuid,\n CONSTRAINT plg_conversations_channel_check CHECK ((channel = ANY (ARRAY['sms'::text, 'whatsapp'::text, 'instagram'::text, 'email'::text, 'webchat'::text]))),\n CONSTRAINT plg_conversations_status_check CHECK ((status = ANY (ARRAY['open'::text, 'snoozed'::text, 'closed'::text])))\n);\n\nCREATE TABLE public.plg_conversations_channels (\n id uuid DEFAULT gen_random_uuid() NOT NULL,\n tenant_id uuid,\n channel text DEFAULT 'whatsapp'::text NOT NULL,\n provider text NOT NULL,\n provider_number_id text NOT NULL,\n phone_e164 text,\n kind text DEFAULT 'dedicated'::text NOT NULL,\n status text DEFAULT 'requested'::text NOT NULL,\n display_name text,\n metadata jsonb DEFAULT '{}'::jsonb NOT NULL,\n created_at timestamp with time zone DEFAULT now() NOT NULL,\n updated_at timestamp with time zone DEFAULT now() NOT NULL,\n CONSTRAINT plg_conversations_channels_channel_check CHECK ((channel = ANY (ARRAY['sms'::text, 'whatsapp'::text, 'instagram'::text, 'email'::text, 'webchat'::text]))),\n CONSTRAINT plg_conversations_channels_kind_check CHECK ((kind = ANY (ARRAY['fallback'::text, 'dedicated'::text]))),\n CONSTRAINT plg_conversations_channels_status_check CHECK ((status = ANY (ARRAY['requested'::text, 'provisioning'::text, 'provisioned'::text, 'verifying'::text, 'active'::text, 'failed'::text, 'released'::text, 'disconnected'::text])))\n);\n\nCREATE TABLE public.plg_conversations_optouts (\n id uuid DEFAULT gen_random_uuid() NOT NULL,\n channel text DEFAULT 'whatsapp'::text NOT NULL,\n phone_e164 text NOT NULL,\n tenant_id uuid,\n provider text NOT NULL,\n reason text,\n created_at timestamp with time zone DEFAULT now() NOT NULL,\n CONSTRAINT plg_conversations_optouts_channel_check CHECK ((channel = ANY (ARRAY['sms'::text, 'whatsapp'::text, 'instagram'::text, 'email'::text, 'webchat'::text])))\n);\n\nCREATE TABLE public.plg_conversations_payment_requests (\n id uuid DEFAULT gen_random_uuid() NOT NULL,\n tenant_id uuid NOT NULL,\n conversation_id uuid,\n message_id uuid,\n subject_type text,\n subject_id text,\n amount_cents integer NOT NULL,\n currency text DEFAULT 'BRL'::text NOT NULL,\n description text,\n provider text NOT NULL,\n status text DEFAULT 'created'::text NOT NULL,\n provider_payment_id text,\n payment_link_url text,\n pix_copy_paste text,\n link_delivered_at timestamp with time zone,\n settled_notified_at timestamp with time zone,\n settled_at timestamp with time zone,\n created_at timestamp with time zone DEFAULT now() NOT NULL,\n updated_at timestamp with time zone DEFAULT now() NOT NULL,\n CONSTRAINT plg_conversations_payment_requests_amount_cents_check CHECK ((amount_cents > 0)),\n CONSTRAINT plg_conversations_payment_requests_status_check CHECK ((status = ANY (ARRAY['created'::text, 'link_generated'::text, 'approval_requested'::text, 'approved'::text, 'paid'::text, 'failed'::text, 'expired'::text, 'cancelled'::text])))\n);\n\nCREATE TABLE public.plg_conversations_webhook_events (\n id uuid DEFAULT gen_random_uuid() NOT NULL,\n provider text NOT NULL,\n event_id text NOT NULL,\n event_type text NOT NULL,\n received_at timestamp with time zone DEFAULT now() NOT NULL\n);\n\n\n-- \u2500\u2500 function \u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\n\nCREATE FUNCTION public.plg_conversations_match_by_phone(p_channel text, p_last10 text, p_last8 text, p_tenant_id uuid DEFAULT NULL::uuid) RETURNS TABLE(id uuid, tenant_id uuid, contact_name text, contact_handle text, contact_person_id uuid)\n LANGUAGE sql STABLE SECURITY DEFINER\n SET search_path TO 'public'\n AS $$\n WITH candidate AS (\n SELECT c.id, c.tenant_id, c.contact_name, c.contact_handle, c.contact_person_id,\n c.last_message_at,\n regexp_replace(COALESCE(c.contact_handle, ''), '[^0-9]', '', 'g') AS digits\n FROM public.plg_conversations c\n WHERE c.channel = p_channel\n AND (p_tenant_id IS NULL OR c.tenant_id = p_tenant_id)\n )\n SELECT id, tenant_id, contact_name, contact_handle, contact_person_id\n FROM candidate\n WHERE length(digits) >= 8\n AND ((p_last10 IS NOT NULL AND right(digits, 10) = p_last10)\n OR (p_last8 IS NOT NULL AND right(digits, 8) = p_last8))\n -- The 10-digit match wins over an 8-digit one even when the 8-digit thread\n -- is newer: a wrong area code is a different person, and a newer wrong\n -- answer is still wrong.\n ORDER BY (p_last10 IS NOT NULL AND right(digits, 10) = p_last10) DESC,\n last_message_at DESC NULLS LAST\n LIMIT 1;\n$$;\n\n\n-- \u2500\u2500 constraint \u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\n\nALTER TABLE ONLY public.plg_conversation_messages\n ADD CONSTRAINT plg_conversation_messages_pkey PRIMARY KEY (id);\n\nALTER TABLE ONLY public.plg_conversations_channels\n ADD CONSTRAINT plg_conversations_channels_pkey PRIMARY KEY (id);\n\nALTER TABLE ONLY public.plg_conversations_optouts\n ADD CONSTRAINT plg_conversations_optouts_pkey PRIMARY KEY (id);\n\nALTER TABLE ONLY public.plg_conversations_payment_requests\n ADD CONSTRAINT plg_conversations_payment_requests_pkey PRIMARY KEY (id);\n\nALTER TABLE ONLY public.plg_conversations\n ADD CONSTRAINT plg_conversations_pkey PRIMARY KEY (id);\n\nALTER TABLE ONLY public.plg_conversations_webhook_events\n ADD CONSTRAINT plg_conversations_webhook_events_pkey PRIMARY KEY (id);\n\n\n-- \u2500\u2500 index \u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\n\nCREATE INDEX idx_plg_conversation_messages_by_subject ON public.plg_conversation_messages USING btree (tenant_id, subject_type, subject_id) WHERE (subject_id IS NOT NULL);\n\nCREATE INDEX idx_plg_conversation_messages_subject ON public.plg_conversation_messages USING btree (conversation_id, at DESC) WHERE (subject_id IS NOT NULL);\n\nCREATE INDEX idx_plg_conversation_messages_thread ON public.plg_conversation_messages USING btree (conversation_id, at);\n\nCREATE INDEX idx_plg_conversations_channels_tenant ON public.plg_conversations_channels USING btree (tenant_id, channel, status) WHERE (tenant_id IS NOT NULL);\n\nCREATE INDEX idx_plg_conversations_handle_suffix ON public.plg_conversations USING btree (channel, \"right\"(regexp_replace(COALESCE(contact_handle, ''::text), '[^0-9]'::text, ''::text, 'g'::text), 8), last_message_at DESC);\n\nCREATE INDEX idx_plg_conversations_optouts_lookup ON public.plg_conversations_optouts USING btree (channel, phone_e164);\n\nCREATE INDEX idx_plg_conversations_payment_requests_subject ON public.plg_conversations_payment_requests USING btree (tenant_id, subject_type, subject_id) WHERE (subject_id IS NOT NULL);\n\nCREATE INDEX idx_plg_conversations_payment_requests_tenant ON public.plg_conversations_payment_requests USING btree (tenant_id, created_at DESC);\n\nCREATE INDEX idx_plg_conversations_person ON public.plg_conversations USING btree (tenant_id, contact_person_id) WHERE (contact_person_id IS NOT NULL);\n\nCREATE INDEX idx_plg_conversations_tenant ON public.plg_conversations USING btree (tenant_id);\n\nCREATE INDEX idx_plg_conversations_tenant_recent ON public.plg_conversations USING btree (tenant_id, last_message_at DESC);\n\nCREATE INDEX idx_plg_conversations_webhook_events_received ON public.plg_conversations_webhook_events USING btree (received_at);\n\nCREATE UNIQUE INDEX uq_plg_conversation_messages_provider_message ON public.plg_conversation_messages USING btree (provider_message_id) WHERE (provider_message_id IS NOT NULL);\n\nCREATE UNIQUE INDEX uq_plg_conversations_channels_fallback ON public.plg_conversations_channels USING btree (channel, provider) WHERE ((tenant_id IS NULL) AND (kind = 'fallback'::text));\n\nCREATE UNIQUE INDEX uq_plg_conversations_channels_number ON public.plg_conversations_channels USING btree (provider, provider_number_id);\n\nCREATE UNIQUE INDEX uq_plg_conversations_optouts_contact ON public.plg_conversations_optouts USING btree (channel, phone_e164, COALESCE(tenant_id, '00000000-0000-0000-0000-000000000000'::uuid));\n\nCREATE UNIQUE INDEX uq_plg_conversations_payment_requests_open_subject ON public.plg_conversations_payment_requests USING btree (tenant_id, subject_type, subject_id) WHERE ((subject_id IS NOT NULL) AND (status = ANY (ARRAY['created'::text, 'link_generated'::text, 'approval_requested'::text, 'approved'::text])));\n\nCREATE UNIQUE INDEX uq_plg_conversations_payment_requests_provider ON public.plg_conversations_payment_requests USING btree (provider, provider_payment_id) WHERE (provider_payment_id IS NOT NULL);\n\nCREATE UNIQUE INDEX uq_plg_conversations_webhook_events_event ON public.plg_conversations_webhook_events USING btree (provider, event_id);\n\n\n-- \u2500\u2500 fk constraint \u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\n\nALTER TABLE ONLY public.plg_conversation_messages\n ADD CONSTRAINT plg_conversation_messages_conversation_id_fkey FOREIGN KEY (conversation_id) REFERENCES public.plg_conversations(id) ON DELETE CASCADE;\n\nALTER TABLE ONLY public.plg_conversation_messages\n ADD CONSTRAINT plg_conversation_messages_tenant_id_fkey FOREIGN KEY (tenant_id) REFERENCES public.tenants(id) ON DELETE CASCADE;\n\nALTER TABLE ONLY public.plg_conversations_channels\n ADD CONSTRAINT plg_conversations_channels_tenant_id_fkey FOREIGN KEY (tenant_id) REFERENCES public.tenants(id) ON DELETE CASCADE;\n\nALTER TABLE ONLY public.plg_conversations\n ADD CONSTRAINT plg_conversations_contact_person_id_fkey FOREIGN KEY (contact_person_id) REFERENCES public.people(id) ON DELETE SET NULL;\n\nALTER TABLE ONLY public.plg_conversations_optouts\n ADD CONSTRAINT plg_conversations_optouts_tenant_id_fkey FOREIGN KEY (tenant_id) REFERENCES public.tenants(id) ON DELETE CASCADE;\n\nALTER TABLE ONLY public.plg_conversations_payment_requests\n ADD CONSTRAINT plg_conversations_payment_requests_conversation_id_fkey FOREIGN KEY (conversation_id) REFERENCES public.plg_conversations(id) ON DELETE SET NULL;\n\nALTER TABLE ONLY public.plg_conversations_payment_requests\n ADD CONSTRAINT plg_conversations_payment_requests_message_id_fkey FOREIGN KEY (message_id) REFERENCES public.plg_conversation_messages(id) ON DELETE SET NULL;\n\nALTER TABLE ONLY public.plg_conversations_payment_requests\n ADD CONSTRAINT plg_conversations_payment_requests_tenant_id_fkey FOREIGN KEY (tenant_id) REFERENCES public.tenants(id) ON DELETE CASCADE;\n\nALTER TABLE ONLY public.plg_conversations\n ADD CONSTRAINT plg_conversations_tenant_id_fkey FOREIGN KEY (tenant_id) REFERENCES public.tenants(id) ON DELETE CASCADE;\n\n\n-- \u2500\u2500 row security \u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\n\nALTER TABLE public.plg_conversation_messages ENABLE ROW LEVEL SECURITY;\n\nALTER TABLE public.plg_conversations ENABLE ROW LEVEL SECURITY;\n\nALTER TABLE public.plg_conversations_channels ENABLE ROW LEVEL SECURITY;\n\nALTER TABLE public.plg_conversations_optouts ENABLE ROW LEVEL SECURITY;\n\nALTER TABLE public.plg_conversations_payment_requests ENABLE ROW LEVEL SECURITY;\n\nALTER TABLE public.plg_conversations_webhook_events ENABLE ROW LEVEL SECURITY;\n\n\n-- \u2500\u2500 policy \u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\n\nCREATE POLICY plg_conversation_messages_delete ON public.plg_conversation_messages FOR DELETE TO authenticated USING ((tenant_id IN ( SELECT public.user_tenant_ids() AS user_tenant_ids)));\n\nCREATE POLICY plg_conversation_messages_insert ON public.plg_conversation_messages FOR INSERT TO authenticated WITH CHECK ((tenant_id IN ( SELECT public.user_tenant_ids() AS user_tenant_ids)));\n\nCREATE POLICY plg_conversation_messages_select ON public.plg_conversation_messages FOR SELECT TO authenticated USING ((tenant_id IN ( SELECT public.user_tenant_ids() AS user_tenant_ids)));\n\nCREATE POLICY plg_conversation_messages_update ON public.plg_conversation_messages FOR UPDATE TO authenticated USING ((tenant_id IN ( SELECT public.user_tenant_ids() AS user_tenant_ids)));\n\nCREATE POLICY plg_conversations_channels_select ON public.plg_conversations_channels FOR SELECT TO authenticated USING (((tenant_id IS NULL) OR (tenant_id IN ( SELECT public.user_tenant_ids() AS user_tenant_ids))));\n\nCREATE POLICY plg_conversations_delete ON public.plg_conversations FOR DELETE TO authenticated USING ((tenant_id IN ( SELECT public.user_tenant_ids() AS user_tenant_ids)));\n\nCREATE POLICY plg_conversations_insert ON public.plg_conversations FOR INSERT TO authenticated WITH CHECK ((tenant_id IN ( SELECT public.user_tenant_ids() AS user_tenant_ids)));\n\nCREATE POLICY plg_conversations_optouts_select ON public.plg_conversations_optouts FOR SELECT TO authenticated USING (((tenant_id IS NULL) OR (tenant_id IN ( SELECT public.user_tenant_ids() AS user_tenant_ids))));\n\nCREATE POLICY plg_conversations_payment_requests_select ON public.plg_conversations_payment_requests FOR SELECT TO authenticated USING ((tenant_id IN ( SELECT public.user_tenant_ids() AS user_tenant_ids)));\n\nCREATE POLICY plg_conversations_select ON public.plg_conversations FOR SELECT TO authenticated USING ((tenant_id IN ( SELECT public.user_tenant_ids() AS user_tenant_ids)));\n\nCREATE POLICY plg_conversations_update ON public.plg_conversations FOR UPDATE TO authenticated USING ((tenant_id IN ( SELECT public.user_tenant_ids() AS user_tenant_ids)));\n\n\n-- \u2500\u2500 acl \u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\n\nREVOKE ALL ON FUNCTION public.plg_conversations_match_by_phone(p_channel text, p_last10 text, p_last8 text, p_tenant_id uuid) FROM PUBLIC;\nGRANT ALL ON FUNCTION public.plg_conversations_match_by_phone(p_channel text, p_last10 text, p_last8 text, p_tenant_id uuid) TO service_role;\n\nGRANT MAINTAIN ON TABLE public.plg_conversation_messages TO anon;\nGRANT ALL ON TABLE public.plg_conversation_messages TO authenticated;\nGRANT ALL ON TABLE public.plg_conversation_messages TO service_role;\n\nGRANT MAINTAIN ON TABLE public.plg_conversations TO anon;\nGRANT ALL ON TABLE public.plg_conversations TO authenticated;\nGRANT ALL ON TABLE public.plg_conversations TO service_role;\n\nGRANT MAINTAIN ON TABLE public.plg_conversations_channels TO anon;\nGRANT ALL ON TABLE public.plg_conversations_channels TO authenticated;\nGRANT ALL ON TABLE public.plg_conversations_channels TO service_role;\n\nGRANT MAINTAIN ON TABLE public.plg_conversations_optouts TO anon;\nGRANT ALL ON TABLE public.plg_conversations_optouts TO authenticated;\nGRANT ALL ON TABLE public.plg_conversations_optouts TO service_role;\n\nGRANT MAINTAIN ON TABLE public.plg_conversations_payment_requests TO anon;\nGRANT ALL ON TABLE public.plg_conversations_payment_requests TO authenticated;\nGRANT ALL ON TABLE public.plg_conversations_payment_requests TO service_role;\n\nGRANT MAINTAIN ON TABLE public.plg_conversations_webhook_events TO anon;\nGRANT ALL ON TABLE public.plg_conversations_webhook_events TO authenticated;\nGRANT ALL ON TABLE public.plg_conversations_webhook_events TO service_role;\n\n\n-- \u2500\u2500 default privileges: restore \u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\n-- Put back, so the NEXT thing installed inherits what the chain had.\n\nDO $dp$ BEGIN\n EXECUTE $stmt$ALTER DEFAULT PRIVILEGES FOR ROLE postgres IN SCHEMA public GRANT ALL ON SEQUENCES TO postgres$stmt$;\nEXCEPTION WHEN insufficient_privilege OR undefined_object THEN\n RAISE NOTICE 'baseline: not ours to set \u2014 %', $stmt$ALTER DEFAULT PRIVILEGES FOR ROLE postgres IN SCHEMA public GRANT ALL ON SEQUENCES TO postgres$stmt$;\nEND $dp$;\n\nDO $dp$ BEGIN\n EXECUTE $stmt$ALTER DEFAULT PRIVILEGES FOR ROLE postgres IN SCHEMA public GRANT ALL ON SEQUENCES TO anon$stmt$;\nEXCEPTION WHEN insufficient_privilege OR undefined_object THEN\n RAISE NOTICE 'baseline: not ours to set \u2014 %', $stmt$ALTER DEFAULT PRIVILEGES FOR ROLE postgres IN SCHEMA public GRANT ALL ON SEQUENCES TO anon$stmt$;\nEND $dp$;\n\nDO $dp$ BEGIN\n EXECUTE $stmt$ALTER DEFAULT PRIVILEGES FOR ROLE postgres IN SCHEMA public GRANT ALL ON SEQUENCES TO authenticated$stmt$;\nEXCEPTION WHEN insufficient_privilege OR undefined_object THEN\n RAISE NOTICE 'baseline: not ours to set \u2014 %', $stmt$ALTER DEFAULT PRIVILEGES FOR ROLE postgres IN SCHEMA public GRANT ALL ON SEQUENCES TO authenticated$stmt$;\nEND $dp$;\n\nDO $dp$ BEGIN\n EXECUTE $stmt$ALTER DEFAULT PRIVILEGES FOR ROLE postgres IN SCHEMA public GRANT ALL ON SEQUENCES TO service_role$stmt$;\nEXCEPTION WHEN insufficient_privilege OR undefined_object THEN\n RAISE NOTICE 'baseline: not ours to set \u2014 %', $stmt$ALTER DEFAULT PRIVILEGES FOR ROLE postgres IN SCHEMA public GRANT ALL ON SEQUENCES TO service_role$stmt$;\nEND $dp$;\n\nDO $dp$ BEGIN\n EXECUTE $stmt$ALTER DEFAULT PRIVILEGES FOR ROLE supabase_admin IN SCHEMA public GRANT ALL ON SEQUENCES TO postgres$stmt$;\nEXCEPTION WHEN insufficient_privilege OR undefined_object THEN\n RAISE NOTICE 'baseline: not ours to set \u2014 %', $stmt$ALTER DEFAULT PRIVILEGES FOR ROLE supabase_admin IN SCHEMA public GRANT ALL ON SEQUENCES TO postgres$stmt$;\nEND $dp$;\n\nDO $dp$ BEGIN\n EXECUTE $stmt$ALTER DEFAULT PRIVILEGES FOR ROLE supabase_admin IN SCHEMA public GRANT ALL ON SEQUENCES TO anon$stmt$;\nEXCEPTION WHEN insufficient_privilege OR undefined_object THEN\n RAISE NOTICE 'baseline: not ours to set \u2014 %', $stmt$ALTER DEFAULT PRIVILEGES FOR ROLE supabase_admin IN SCHEMA public GRANT ALL ON SEQUENCES TO anon$stmt$;\nEND $dp$;\n\nDO $dp$ BEGIN\n EXECUTE $stmt$ALTER DEFAULT PRIVILEGES FOR ROLE supabase_admin IN SCHEMA public GRANT ALL ON SEQUENCES TO authenticated$stmt$;\nEXCEPTION WHEN insufficient_privilege OR undefined_object THEN\n RAISE NOTICE 'baseline: not ours to set \u2014 %', $stmt$ALTER DEFAULT PRIVILEGES FOR ROLE supabase_admin IN SCHEMA public GRANT ALL ON SEQUENCES TO authenticated$stmt$;\nEND $dp$;\n\nDO $dp$ BEGIN\n EXECUTE $stmt$ALTER DEFAULT PRIVILEGES FOR ROLE supabase_admin IN SCHEMA public GRANT ALL ON SEQUENCES TO service_role$stmt$;\nEXCEPTION WHEN insufficient_privilege OR undefined_object THEN\n RAISE NOTICE 'baseline: not ours to set \u2014 %', $stmt$ALTER DEFAULT PRIVILEGES FOR ROLE supabase_admin IN SCHEMA public GRANT ALL ON SEQUENCES TO service_role$stmt$;\nEND $dp$;\n\nDO $dp$ BEGIN\n EXECUTE $stmt$ALTER DEFAULT PRIVILEGES FOR ROLE postgres IN SCHEMA public GRANT ALL ON FUNCTIONS TO postgres$stmt$;\nEXCEPTION WHEN insufficient_privilege OR undefined_object THEN\n RAISE NOTICE 'baseline: not ours to set \u2014 %', $stmt$ALTER DEFAULT PRIVILEGES FOR ROLE postgres IN SCHEMA public GRANT ALL ON FUNCTIONS TO postgres$stmt$;\nEND $dp$;\n\nDO $dp$ BEGIN\n EXECUTE $stmt$ALTER DEFAULT PRIVILEGES FOR ROLE postgres IN SCHEMA public GRANT ALL ON FUNCTIONS TO authenticated$stmt$;\nEXCEPTION WHEN insufficient_privilege OR undefined_object THEN\n RAISE NOTICE 'baseline: not ours to set \u2014 %', $stmt$ALTER DEFAULT PRIVILEGES FOR ROLE postgres IN SCHEMA public GRANT ALL ON FUNCTIONS TO authenticated$stmt$;\nEND $dp$;\n\nDO $dp$ BEGIN\n EXECUTE $stmt$ALTER DEFAULT PRIVILEGES FOR ROLE postgres IN SCHEMA public GRANT ALL ON FUNCTIONS TO service_role$stmt$;\nEXCEPTION WHEN insufficient_privilege OR undefined_object THEN\n RAISE NOTICE 'baseline: not ours to set \u2014 %', $stmt$ALTER DEFAULT PRIVILEGES FOR ROLE postgres IN SCHEMA public GRANT ALL ON FUNCTIONS TO service_role$stmt$;\nEND $dp$;\n\nDO $dp$ BEGIN\n EXECUTE $stmt$ALTER DEFAULT PRIVILEGES FOR ROLE supabase_admin IN SCHEMA public GRANT ALL ON FUNCTIONS TO postgres$stmt$;\nEXCEPTION WHEN insufficient_privilege OR undefined_object THEN\n RAISE NOTICE 'baseline: not ours to set \u2014 %', $stmt$ALTER DEFAULT PRIVILEGES FOR ROLE supabase_admin IN SCHEMA public GRANT ALL ON FUNCTIONS TO postgres$stmt$;\nEND $dp$;\n\nDO $dp$ BEGIN\n EXECUTE $stmt$ALTER DEFAULT PRIVILEGES FOR ROLE supabase_admin IN SCHEMA public GRANT ALL ON FUNCTIONS TO anon$stmt$;\nEXCEPTION WHEN insufficient_privilege OR undefined_object THEN\n RAISE NOTICE 'baseline: not ours to set \u2014 %', $stmt$ALTER DEFAULT PRIVILEGES FOR ROLE supabase_admin IN SCHEMA public GRANT ALL ON FUNCTIONS TO anon$stmt$;\nEND $dp$;\n\nDO $dp$ BEGIN\n EXECUTE $stmt$ALTER DEFAULT PRIVILEGES FOR ROLE supabase_admin IN SCHEMA public GRANT ALL ON FUNCTIONS TO authenticated$stmt$;\nEXCEPTION WHEN insufficient_privilege OR undefined_object THEN\n RAISE NOTICE 'baseline: not ours to set \u2014 %', $stmt$ALTER DEFAULT PRIVILEGES FOR ROLE supabase_admin IN SCHEMA public GRANT ALL ON FUNCTIONS TO authenticated$stmt$;\nEND $dp$;\n\nDO $dp$ BEGIN\n EXECUTE $stmt$ALTER DEFAULT PRIVILEGES FOR ROLE supabase_admin IN SCHEMA public GRANT ALL ON FUNCTIONS TO service_role$stmt$;\nEXCEPTION WHEN insufficient_privilege OR undefined_object THEN\n RAISE NOTICE 'baseline: not ours to set \u2014 %', $stmt$ALTER DEFAULT PRIVILEGES FOR ROLE supabase_admin IN SCHEMA public GRANT ALL ON FUNCTIONS TO service_role$stmt$;\nEND $dp$;\n\nDO $dp$ BEGIN\n EXECUTE $stmt$ALTER DEFAULT PRIVILEGES FOR ROLE postgres IN SCHEMA public GRANT ALL ON TABLES TO postgres$stmt$;\nEXCEPTION WHEN insufficient_privilege OR undefined_object THEN\n RAISE NOTICE 'baseline: not ours to set \u2014 %', $stmt$ALTER DEFAULT PRIVILEGES FOR ROLE postgres IN SCHEMA public GRANT ALL ON TABLES TO postgres$stmt$;\nEND $dp$;\n\nDO $dp$ BEGIN\n EXECUTE $stmt$ALTER DEFAULT PRIVILEGES FOR ROLE postgres IN SCHEMA public GRANT MAINTAIN ON TABLES TO anon$stmt$;\nEXCEPTION WHEN insufficient_privilege OR undefined_object THEN\n RAISE NOTICE 'baseline: not ours to set \u2014 %', $stmt$ALTER DEFAULT PRIVILEGES FOR ROLE postgres IN SCHEMA public GRANT MAINTAIN ON TABLES TO anon$stmt$;\nEND $dp$;\n\nDO $dp$ BEGIN\n EXECUTE $stmt$ALTER DEFAULT PRIVILEGES FOR ROLE postgres IN SCHEMA public GRANT ALL ON TABLES TO authenticated$stmt$;\nEXCEPTION WHEN insufficient_privilege OR undefined_object THEN\n RAISE NOTICE 'baseline: not ours to set \u2014 %', $stmt$ALTER DEFAULT PRIVILEGES FOR ROLE postgres IN SCHEMA public GRANT ALL ON TABLES TO authenticated$stmt$;\nEND $dp$;\n\nDO $dp$ BEGIN\n EXECUTE $stmt$ALTER DEFAULT PRIVILEGES FOR ROLE postgres IN SCHEMA public GRANT ALL ON TABLES TO service_role$stmt$;\nEXCEPTION WHEN insufficient_privilege OR undefined_object THEN\n RAISE NOTICE 'baseline: not ours to set \u2014 %', $stmt$ALTER DEFAULT PRIVILEGES FOR ROLE postgres IN SCHEMA public GRANT ALL ON TABLES TO service_role$stmt$;\nEND $dp$;\n\nDO $dp$ BEGIN\n EXECUTE $stmt$ALTER DEFAULT PRIVILEGES FOR ROLE supabase_admin IN SCHEMA public GRANT ALL ON TABLES TO postgres$stmt$;\nEXCEPTION WHEN insufficient_privilege OR undefined_object THEN\n RAISE NOTICE 'baseline: not ours to set \u2014 %', $stmt$ALTER DEFAULT PRIVILEGES FOR ROLE supabase_admin IN SCHEMA public GRANT ALL ON TABLES TO postgres$stmt$;\nEND $dp$;\n\nDO $dp$ BEGIN\n EXECUTE $stmt$ALTER DEFAULT PRIVILEGES FOR ROLE supabase_admin IN SCHEMA public GRANT ALL ON TABLES TO anon$stmt$;\nEXCEPTION WHEN insufficient_privilege OR undefined_object THEN\n RAISE NOTICE 'baseline: not ours to set \u2014 %', $stmt$ALTER DEFAULT PRIVILEGES FOR ROLE supabase_admin IN SCHEMA public GRANT ALL ON TABLES TO anon$stmt$;\nEND $dp$;\n\nDO $dp$ BEGIN\n EXECUTE $stmt$ALTER DEFAULT PRIVILEGES FOR ROLE supabase_admin IN SCHEMA public GRANT ALL ON TABLES TO authenticated$stmt$;\nEXCEPTION WHEN insufficient_privilege OR undefined_object THEN\n RAISE NOTICE 'baseline: not ours to set \u2014 %', $stmt$ALTER DEFAULT PRIVILEGES FOR ROLE supabase_admin IN SCHEMA public GRANT ALL ON TABLES TO authenticated$stmt$;\nEND $dp$;\n\nDO $dp$ BEGIN\n EXECUTE $stmt$ALTER DEFAULT PRIVILEGES FOR ROLE supabase_admin IN SCHEMA public GRANT ALL ON TABLES TO service_role$stmt$;\nEXCEPTION WHEN insufficient_privilege OR undefined_object THEN\n RAISE NOTICE 'baseline: not ours to set \u2014 %', $stmt$ALTER DEFAULT PRIVILEGES FOR ROLE supabase_admin IN SCHEMA public GRANT ALL ON TABLES TO service_role$stmt$;\nEND $dp$;\n\nDO $dp$ BEGIN\n EXECUTE $stmt$--\n-- PostgreSQL database dump complete\n--$stmt$;\nEXCEPTION WHEN insufficient_privilege OR undefined_object THEN\n RAISE NOTICE 'baseline: not ours to set \u2014 %', $stmt$--\n-- PostgreSQL database dump complete\n--$stmt$;\nEND $dp$;\n";
2
2
  export declare const MIGRATION_001_A_CAIXA_SABE_QUEM_FALOU_POR_ULTIMO = "-- ---------------------------------------------------------------------------\n-- 001_a_caixa_sabe_quem_falou_por_ultimo.sql\n--\n-- A conversa guarda o TEXTO e a HORA da \u00FAltima mensagem, e n\u00E3o guarda de quem\n-- ela foi. Sem isso, \"quem est\u00E1 esperando resposta\" \u2014 que \u00E9 a pergunta com que\n-- este produto abre o dia \u2014 n\u00E3o \u00E9 respond\u00EDvel a partir da lista: seria preciso\n-- carregar as mensagens de cada thread para descobrir quem falou por \u00FAltimo.\n--\n-- O painel respondia com `unread_count > 0`, que \u00E9 a aproxima\u00E7\u00E3o honesta que o\n-- shape permitia e n\u00E3o \u00E9 a resposta certa. Ela erra exatamente no caso que mais\n-- d\u00F3i: a thread que algu\u00E9m ABRIU, leu, decidiu responder depois e esqueceu. O\n-- n\u00E3o-lida zera na leitura; o cliente continua esperando.\n--\n-- \u2500\u2500 Por que TRIGGER e n\u00E3o mais uma coluna que cada escritor preenche \u2500\u2500\u2500\u2500\u2500\u2500\u2500\n--\n-- `last_message_preview` e `last_message_at` s\u00E3o escritos hoje em CINCO lugares:\n-- o provider do navegador (criar conversa, enviar), a fun\u00E7\u00E3o `messaging-send` e\n-- o `tyxter-webhook` (duas vezes, contando o handler de pagamento). Uma sexta\n-- coluna com a mesma disciplina \u00E9 uma sexta chance de algu\u00E9m esquecer \u2014 e o\n-- modo de falhar \u00E9 silencioso: a fila simplesmente para de listar algu\u00E9m.\n--\n-- O gatilho tem UM escritor e n\u00E3o pode ser esquecido pelo pr\u00F3ximo chamador. Ele\n-- \u00E9 deliberadamente estreito: cuida s\u00F3 de `last_message_direction`. Trazer o\n-- preview e a hora para c\u00E1 seria a corre\u00E7\u00E3o certa e \u00E9 uma mudan\u00E7a maior, com\n-- cinco chamadores para reconciliar \u2014 fica anotado, n\u00E3o feito aqui.\n-- ---------------------------------------------------------------------------\n\nALTER TABLE public.plg_conversations\n ADD COLUMN IF NOT EXISTS last_message_direction text;\n\nDO $$\nBEGIN\n ALTER TABLE public.plg_conversations\n ADD CONSTRAINT plg_conversations_last_message_direction_check\n CHECK (last_message_direction IS NULL\n OR last_message_direction = ANY (ARRAY['inbound'::text, 'outbound'::text]));\nEXCEPTION WHEN duplicate_object THEN\n NULL;\nEND $$;\n\nCOMMENT ON COLUMN public.plg_conversations.last_message_direction IS\n 'Quem falou por \u00FAltimo: inbound = o cliente, outbound = a casa (001). Mantida por gatilho em plg_conversation_messages, nunca escrita \u00E0 m\u00E3o. NULL = thread anterior a esta migration cujo backfill n\u00E3o achou mensagem nenhuma.';\n\n-- \u2500\u2500 o \u00FAnico escritor \u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\n--\n-- AFTER INSERT: a mensagem j\u00E1 existe quando a conversa \u00E9 atualizada, ent\u00E3o uma\n-- falha aqui n\u00E3o pode desfazer o recebimento \u2014 e recebimento perdido \u00E9 pior que\n-- fila desatualizada.\n--\n-- Sem cl\u00E1usula de ordem: a mensagem que acabou de entrar \u00C9 a \u00FAltima. Comparar\n-- com `last_message_at` para decidir seria correto e traria uma corrida com os\n-- cinco escritores daquela coluna, que \u00E9 justamente o problema que o gatilho\n-- existe para n\u00E3o ter.\nCREATE OR REPLACE FUNCTION public.plg_conversations_stamp_direction()\nRETURNS trigger\nLANGUAGE plpgsql\nSECURITY DEFINER\nSET search_path TO ''\nAS $function$\nBEGIN\n IF NEW.direction IS NULL THEN\n RETURN NULL;\n END IF;\n\n UPDATE public.plg_conversations\n SET last_message_direction = NEW.direction\n WHERE id = NEW.conversation_id;\n\n RETURN NULL;\nEND $function$;\n\nCOMMENT ON FUNCTION public.plg_conversations_stamp_direction() IS\n 'Carimba plg_conversations.last_message_direction a cada mensagem inserida (001). O \u00FAnico escritor da coluna.';\n\nDROP TRIGGER IF EXISTS plg_conversation_messages_stamp_direction ON public.plg_conversation_messages;\nCREATE TRIGGER plg_conversation_messages_stamp_direction\n AFTER INSERT ON public.plg_conversation_messages\n FOR EACH ROW\n EXECUTE FUNCTION public.plg_conversations_stamp_direction();\n\n-- \u2500\u2500 backfill \u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\n--\n-- DISTINCT ON pega uma linha por conversa, a mais recente. `at DESC NULLS LAST`\n-- e depois `id DESC` porque `at` tem default e n\u00E3o NOT NULL: duas mensagens no\n-- mesmo instante (ou ambas sem hora) precisam de um desempate est\u00E1vel, ou o\n-- backfill devolve resultado diferente a cada execu\u00E7\u00E3o.\nUPDATE public.plg_conversations c\n SET last_message_direction = m.direction\n FROM (\n SELECT DISTINCT ON (conversation_id) conversation_id, direction\n FROM public.plg_conversation_messages\n WHERE direction IS NOT NULL\n ORDER BY conversation_id, at DESC NULLS LAST, id DESC\n ) m\n WHERE m.conversation_id = c.id\n AND c.last_message_direction IS DISTINCT FROM m.direction;\n\n-- S\u00F3 as threads que interessam \u00E0 fila, e s\u00F3 quando o cliente falou por \u00FAltimo:\n-- \u00E9 o \u00EDndice que a pergunta \"quem est\u00E1 esperando\" faz, e ele fica pequeno\n-- porque a maioria das conversas de um tenant est\u00E1 encerrada.\nCREATE INDEX IF NOT EXISTS idx_plg_conversations_waiting\n ON public.plg_conversations (tenant_id, last_message_at DESC)\n WHERE status = 'open' AND last_message_direction = 'inbound';\n";
3
+ export declare const MIGRATION_002_A_CASA_ESCOLHE_POR_ONDE_O_WHATSAPP_SAI = "-- ---------------------------------------------------------------------------\n-- 002_a_casa_escolhe_por_onde_o_whatsapp_sai.sql \u2014 dois provedores de WhatsApp,\n-- a credencial de cada um, e a escolha de qual est\u00E1 valendo.\n--\n-- \u2500\u2500 por que a credencial precisa morar aqui \u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\n--\n-- O `messaging-send` (WP3) pega a chave assim:\n--\n-- const credential = await redeemConnectorCredential({ connectorSlug: 'tyxter', \u2026 })\n--\n-- Quinze linhas que chamam a PLATAFORMA Fayz. Funciona para o Tyxter porque a\n-- chave \u00E9 uma s\u00F3, da FayaLabs, e a plataforma a serve para qualquer inquilino\n-- do projeto. N\u00E3o funciona para o Evolution, e n\u00E3o vai funcionar nunca: o\n-- Evolution \u00E9 auto-hospedado, e cada casa tem a pr\u00F3pria URL e a pr\u00F3pria chave.\n-- Credencial POR INQUILINO n\u00E3o \u00E9 atalho, \u00E9 requisito do segundo provedor.\n--\n-- Um app que n\u00E3o est\u00E1 ligado a projeto Fayz nenhum \u2014 como o FullControl hoje \u2014\n-- tamb\u00E9m n\u00E3o resgata nada. Aqui a chave tem onde ficar nos dois casos.\n--\n-- \u2500\u2500 os dois provedores n\u00E3o s\u00E3o a mesma coisa, e a tabela diz isso \u2500\u2500\u2500\u2500\u2500\u2500\u2500\n--\n-- `tyxter` API oficial (Cloud API, via revenda). Iniciar conversa EXIGE\n-- template aprovado pela Meta; texto livre s\u00F3 dentro de 24h da\n-- \u00FAltima mensagem do cliente. Em troca: entrega confi\u00E1vel, sem\n-- risco de banimento, e a Meta responde por n\u00FAmero e qualidade.\n-- `evolution` WhatsApp Web, auto-hospedado. N\u00E3o tem template nem janela de\n-- 24h \u2014 manda o que quiser. Em troca: a sess\u00E3o morre, o n\u00FAmero\n-- pode ser banido, e ningu\u00E9m responde por isso al\u00E9m de voc\u00EA.\n--\n-- `capabilities` guarda essa diferen\u00E7a como DADO e n\u00E3o como `if` espalhado pela\n-- tela. Um bot\u00E3o de \"submeter template\" num provedor que n\u00E3o tem template n\u00E3o\n-- pode existir; a tela l\u00EA daqui para desabilitar em vez de fingir.\n--\n-- \u2500\u2500 o segredo n\u00E3o passa pelo navegador \u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\n--\n-- A tabela nega SELECT a `authenticated`. Quem l\u00EA \u00E9 o `service_role`, do lado\n-- do servidor. A tela sabe se EXISTE credencial e quais s\u00E3o os campos n\u00E3o\n-- secretos (a URL do Evolution, o nome da inst\u00E2ncia) por uma view; o segredo\n-- ela nunca v\u00EA, nem para reexibir mascarado.\n-- ---------------------------------------------------------------------------\n\n-- \u2500\u2500 quem est\u00E1 valendo, por inquilino \u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\nCREATE TABLE IF NOT EXISTS public.plg_conversations_providers (\n tenant_id uuid NOT NULL REFERENCES public.tenants(id) ON DELETE CASCADE,\n provider text NOT NULL,\n -- N\u00E3o secreto: a URL base do Evolution, o nome da inst\u00E2ncia, o id do n\u00FAmero.\n -- \u00C9 isto que a tela mostra e deixa editar.\n config jsonb NOT NULL DEFAULT '{}'::jsonb,\n -- Secreto. Nenhuma pol\u00EDtica de leitura alcan\u00E7a esta coluna.\n secrets jsonb NOT NULL DEFAULT '{}'::jsonb,\n -- O que este provedor SABE fazer. Lido pela tela para desabilitar o que n\u00E3o\n -- existe, em vez de oferecer um bot\u00E3o que erra.\n capabilities jsonb NOT NULL DEFAULT '{}'::jsonb,\n -- Um s\u00F3 por inquilino fica ativo. O outro continua configurado, pronto para\n -- a troca \u2014 que \u00E9 o pedido: poder alternar sem reconfigurar.\n is_active boolean NOT NULL DEFAULT false,\n status text NOT NULL DEFAULT 'unconfigured',\n status_detail text,\n checked_at timestamptz,\n created_at timestamptz NOT NULL DEFAULT now(),\n updated_at timestamptz NOT NULL DEFAULT now(),\n PRIMARY KEY (tenant_id, provider),\n CONSTRAINT plg_conversations_providers_provider\n CHECK (provider IN ('tyxter', 'evolution')),\n CONSTRAINT plg_conversations_providers_status\n CHECK (status IN ('unconfigured', 'connecting', 'connected', 'error', 'disconnected'))\n);\n\n-- UM ativo por inquilino, garantido pelo banco e n\u00E3o pela tela. Duas telas\n-- abertas em abas diferentes s\u00E3o o caso comum, e \"o \u00FAltimo clique vence\" s\u00F3 \u00E9\n-- verdade se algu\u00E9m impedir o empate.\nCREATE UNIQUE INDEX IF NOT EXISTS plg_conversations_providers_one_active\n ON public.plg_conversations_providers (tenant_id)\n WHERE is_active;\n\nALTER TABLE public.plg_conversations_providers ENABLE ROW LEVEL SECURITY;\nALTER TABLE public.plg_conversations_providers FORCE ROW LEVEL SECURITY;\n\n-- Nenhuma pol\u00EDtica para `authenticated`: o segredo mora numa coluna desta\n-- tabela, e uma pol\u00EDtica de SELECT aqui entregaria a chave ao navegador.\n--\n-- E o REVOKE expl\u00EDcito, porque a aus\u00EAncia de pol\u00EDtica N\u00C3O \u00E9 a \u00FAnica linha de\n-- defesa. O schema `public` deste cluster carrega grants amplos de f\u00E1brica \u2014\n-- medido nesta pr\u00F3pria tabela logo ap\u00F3s cri\u00E1-la: sete privil\u00E9gios j\u00E1\n-- concedidos a `authenticated` sem ningu\u00E9m ter pedido. A RLS for\u00E7ada barra\n-- assim mesmo, mas uma pol\u00EDtica escrita errada num dia ruim \u00E9 tudo o que\n-- separa a chave do navegador. Duas fechaduras.\nREVOKE ALL ON public.plg_conversations_providers FROM PUBLIC, anon, authenticated;\nGRANT SELECT, INSERT, UPDATE, DELETE ON public.plg_conversations_providers TO service_role;\n\n-- \u2500\u2500 o que a tela pode ver \u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\n--\n-- Tudo menos o segredo. `has_credential` responde a \u00FAnica pergunta que a tela\n-- precisa fazer sobre ele: j\u00E1 foi posto?\n-- \u2500\u2500 A \u00DANICA VIEW DESTE REPO QUE N\u00C3O \u00C9 `security_invoker` \u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\n--\n-- A regra da casa \u00E9 `security_invoker = true`: a view enxerga o que o leitor\n-- enxerga, e a RLS da tabela continua valendo. Aqui isso se autoderrota.\n--\n-- `authenticated` N\u00C3O pode ler `plg_conversations_providers` \u2014 \u00E9 onde o segredo\n-- mora, e o REVOKE logo acima \u00E9 deliberado. Uma view invoker sobre uma tabela\n-- que o chamador n\u00E3o alcan\u00E7a devolve `42501 permission denied`, que foi\n-- exatamente o que a tela recebeu no primeiro teste com um JWT de verdade.\n--\n-- Ent\u00E3o esta view \u00E9 definer POR PROJETO, e paga o pre\u00E7o sendo ela mesma a\n-- fronteira: o `WHERE tenant_id IN (user_tenant_ids())` abaixo n\u00E3o \u00E9\n-- decora\u00E7\u00E3o, \u00E9 a RLS desta view. E a coluna do segredo n\u00E3o est\u00E1 na lista do\n-- SELECT \u2014 o que ela n\u00E3o seleciona n\u00E3o existe para quem l\u00EA.\nDROP VIEW IF EXISTS public.v_conversations_providers;\nCREATE VIEW public.v_conversations_providers AS\n SELECT p.tenant_id,\n p.provider,\n p.config,\n p.capabilities,\n p.is_active,\n p.status,\n p.status_detail,\n p.checked_at,\n -- Nunca o valor. S\u00F3 se existe.\n (p.secrets ? 'api_key') AS has_credential,\n p.created_at,\n p.updated_at\n FROM public.plg_conversations_providers p\n WHERE p.tenant_id IN (SELECT public.user_tenant_ids());\n\nGRANT SELECT ON public.v_conversations_providers TO authenticated;\n\nCOMMENT ON VIEW public.v_conversations_providers IS\n 'Os provedores de WhatsApp da casa, SEM o segredo. `has_credential` diz se a chave foi posta; o valor n\u00E3o sai daqui (002).';\n\n-- \u2500\u2500 p\u00F4r a credencial \u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\n--\n-- SECURITY DEFINER porque a tabela nega escrita a `authenticated`: \u00E9 a \u00FAnica\n-- porta, e ela confere o inquilino antes de abrir. `search_path` vazio porque\n-- uma fun\u00E7\u00E3o definer sem isso \u00E9 uma fun\u00E7\u00E3o que o chamador pode redirecionar.\nCREATE OR REPLACE FUNCTION public.conversations_set_provider(\n p_tenant uuid,\n p_provider text,\n p_config jsonb DEFAULT '{}'::jsonb,\n p_api_key text DEFAULT NULL,\n p_capabilities jsonb DEFAULT NULL\n) RETURNS void\nLANGUAGE plpgsql SECURITY DEFINER SET search_path TO ''\nAS $$\nDECLARE v_caps jsonb;\nBEGIN\n IF p_tenant IS NULL OR NOT (p_tenant IN (SELECT public.user_tenant_ids())) THEN\n RAISE EXCEPTION 'conversations_set_provider: n\u00E3o \u00E9 uma casa sua';\n END IF;\n\n -- O que cada provedor sabe fazer \u00E9 conhecimento do produto, n\u00E3o do usu\u00E1rio.\n -- Quem chama pode sobrescrever (um Evolution atr\u00E1s de proxy pode ganhar\n -- capacidades), mas o padr\u00E3o descreve a verdade de cada um.\n v_caps := coalesce(p_capabilities, CASE p_provider\n WHEN 'tyxter' THEN jsonb_build_object(\n 'templates', true, -- e s\u00E3o OBRIGAT\u00D3RIOS para iniciar conversa\n 'requires_template', true,\n 'session_window_hours', 24, -- texto livre s\u00F3 dentro dela\n 'delivery_receipts', true,\n 'qr_pairing', false,\n 'ban_risk', 'low')\n WHEN 'evolution' THEN jsonb_build_object(\n 'templates', false, -- n\u00E3o existem: manda texto direto\n 'requires_template', false,\n 'session_window_hours', null,\n 'delivery_receipts', true,\n 'qr_pairing', true, -- parear \u00E9 ler um QR, n\u00E3o cadastrar na Meta\n 'ban_risk', 'high') -- e \u00E9 por isso que o aquecimento importa\n ELSE '{}'::jsonb END);\n\n INSERT INTO public.plg_conversations_providers AS t\n (tenant_id, provider, config, capabilities, status,\n secrets)\n VALUES\n (p_tenant, p_provider, coalesce(p_config, '{}'::jsonb), v_caps,\n CASE WHEN p_api_key IS NULL THEN 'unconfigured' ELSE 'connecting' END,\n CASE WHEN p_api_key IS NULL THEN '{}'::jsonb\n ELSE jsonb_build_object('api_key', p_api_key) END)\n ON CONFLICT (tenant_id, provider) DO UPDATE\n SET config = coalesce(p_config, t.config),\n capabilities = v_caps,\n -- Chave nula \u00E9 \"n\u00E3o mexe\", n\u00E3o \"apaga\". Salvar a URL do Evolution sem\n -- redigitar a chave \u00E9 o gesto comum, e apag\u00E1-la aqui seria a surpresa.\n secrets = CASE WHEN p_api_key IS NULL THEN t.secrets\n ELSE jsonb_set(t.secrets, '{api_key}', to_jsonb(p_api_key)) END,\n status = CASE WHEN p_api_key IS NULL THEN t.status ELSE 'connecting' END,\n updated_at = now();\nEND $$;\n\nREVOKE ALL ON FUNCTION public.conversations_set_provider(uuid, text, jsonb, text, jsonb) FROM PUBLIC, anon;\nGRANT EXECUTE ON FUNCTION public.conversations_set_provider(uuid, text, jsonb, text, jsonb) TO authenticated, service_role;\n\n-- \u2500\u2500 trocar qual est\u00E1 valendo \u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\nCREATE OR REPLACE FUNCTION public.conversations_activate_provider(\n p_tenant uuid, p_provider text\n) RETURNS void\nLANGUAGE plpgsql SECURITY DEFINER SET search_path TO ''\nAS $$\nBEGIN\n IF p_tenant IS NULL OR NOT (p_tenant IN (SELECT public.user_tenant_ids())) THEN\n RAISE EXCEPTION 'conversations_activate_provider: n\u00E3o \u00E9 uma casa sua';\n END IF;\n\n -- Desligar ANTES de ligar: o \u00EDndice \u00FAnico parcial recusaria os dois ativos,\n -- e numa transa\u00E7\u00E3o s\u00F3 a ordem \u00E9 o que decide entre trocar e falhar.\n UPDATE public.plg_conversations_providers\n SET is_active = false, updated_at = now()\n WHERE tenant_id = p_tenant AND is_active;\n\n UPDATE public.plg_conversations_providers\n SET is_active = true, updated_at = now()\n WHERE tenant_id = p_tenant AND provider = p_provider;\n\n IF NOT FOUND THEN\n RAISE EXCEPTION 'conversations_activate_provider: % n\u00E3o est\u00E1 configurado nesta casa', p_provider;\n END IF;\nEND $$;\n\nREVOKE ALL ON FUNCTION public.conversations_activate_provider(uuid, text) FROM PUBLIC, anon;\nGRANT EXECUTE ON FUNCTION public.conversations_activate_provider(uuid, text) TO authenticated, service_role;\n\n-- \u2500\u2500 o canal aprende de que provedor ele \u00E9 \u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\n--\n-- `plg_conversations_channels.provider` j\u00E1 existe e j\u00E1 guarda 'tyxter'. O que\n-- falta \u00E9 a liga\u00E7\u00E3o com a credencial: um n\u00FAmero do Evolution precisa saber de\n-- QUAL inst\u00E2ncia ele saiu, porque a mesma casa pode ter mais de uma.\nALTER TABLE public.plg_conversations_channels\n ADD COLUMN IF NOT EXISTS instance_key text;\n\nCOMMENT ON COLUMN public.plg_conversations_channels.instance_key IS\n 'A inst\u00E2ncia do provedor de onde este n\u00FAmero sai. Nulo no Tyxter (uma conta por projeto); no Evolution \u00E9 o nome da inst\u00E2ncia (002).';\n";
4
+ export declare const MIGRATION_003_A_CAMPANHA_SAI_NO_RITMO_QUE_O_NUMERO_AGUENTA = "-- ---------------------------------------------------------------------------\n-- 003_a_campanha_sai_no_ritmo_que_o_numero_aguenta.sql \u2014 template, campanha,\n-- log de envio e o ritmo que impede o n\u00FAmero de ser banido.\n--\n-- O desenho n\u00E3o \u00E9 inven\u00E7\u00E3o: saiu de ler Mautic, EspoCRM, Odoo e Twenty, e de\n-- ler a documenta\u00E7\u00E3o da Meta em vez do folclore que circula sobre ela. Onde os\n-- quatro discordam, este arquivo diz qual escolheu e por qu\u00EA.\n--\n-- \u2500\u2500 tr\u00EAs coisas que a Meta documenta e que viram REGRA, n\u00E3o ajuste \u2500\u2500\u2500\u2500\u2500\u2500\n--\n-- \u00B7 131050 (o usu\u00E1rio saiu do marketing) NUNCA se repete. Reenviar \u00E9 outra\n-- infra\u00E7\u00E3o, n\u00E3o outra tentativa.\n-- \u00B7 131048 (bloqueado/marcado como spam) \u00E9 parada dura. \u00C9 o alarme de\n-- qualidade, n\u00E3o um erro de rede.\n-- \u00B7 A janela de 24h s\u00F3 abre por mensagem OU chamada DO CLIENTE. Template\n-- nosso n\u00E3o abre janela nenhuma \u2014 e \u00E9 o erro que mais se v\u00EA em CRM.\n--\n-- O resto \u2014 a rampa de aquecimento, a faixa de atraso entre mensagens \u2014 \u00E9\n-- pr\u00E1tica de comunidade, n\u00E3o documenta\u00E7\u00E3o. Vira PADR\u00C3O EDIT\u00C1VEL: quem opera o\n-- n\u00FAmero sabe mais do que uma tabela escrita hoje.\n--\n-- \u2500\u2500 por que o log de envio tem `rotation` e `claim_token` \u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\n--\n-- `rotation` \u00E9 do Mautic: a chave \u00FAnica \u00E9 (campanha, contato, rota\u00E7\u00E3o), e a\n-- rota\u00E7\u00E3o sobe quando algu\u00E9m REINICIA a campanha de prop\u00F3sito. Sem ela, a \u00FAnica\n-- forma de reenviar \u00E9 apagar o hist\u00F3rico \u2014 e a\u00ED ningu\u00E9m consegue provar o que\n-- foi mandado.\n--\n-- `claim_token` + `claim_expires_at` \u00E9 do Twenty: quem vai enviar ARRENDA a\n-- linha por cinco minutos. Duas inst\u00E2ncias do disparador n\u00E3o mandam a mesma\n-- mensagem duas vezes, e um processo que morre no meio devolve a linha sozinho\n-- quando o arrendamento vence. Os quatro CRMs resolvem isso com \u00EDndice \u00FAnico\n-- mais arrendamento; nenhum usa advisory lock, e nenhum usa outbox.\n-- ---------------------------------------------------------------------------\n\n-- \u2500\u2500 quem nunca mais deve ser contatado \u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\n--\n-- A tabela j\u00E1 existe e j\u00E1 \u00E9 chaveada pelo N\u00DAMERO e n\u00E3o pelo contato \u2014 que \u00E9 o\n-- acerto do Odoo e do Twenty, e o erro do Mautic: n\u00FAmero \u00E9 reatribu\u00EDdo, contato\n-- \u00E9 duplicado, e a supress\u00E3o precisa sobreviver aos dois.\n--\n-- O que falta \u00E9 n\u00E3o apagar nunca. O EspoCRM apaga a linha ao reinscrever e\n-- perde a prova de que algu\u00E9m um dia pediu para sair; o Odoo desliga um\n-- booleano. Seguimos o Odoo.\nALTER TABLE public.plg_conversations_optouts\n ADD COLUMN IF NOT EXISTS active boolean NOT NULL DEFAULT true,\n ADD COLUMN IF NOT EXISTS revoked_at timestamptz,\n ADD COLUMN IF NOT EXISTS source text;\n\nCOMMENT ON COLUMN public.plg_conversations_optouts.active IS\n 'Desligar em vez de apagar: a linha \u00E9 a prova de que a pessoa pediu para sair, e apag\u00E1-la \u00E9 perder a prova (003).';\n\n-- \u2500\u2500 o texto que se manda \u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\n--\n-- Um template aqui N\u00C3O \u00E9 um template da Meta. \u00C9 o texto da casa, com o espelho\n-- do que o provedor respondeu sobre ele. Assim o mesmo registro serve aos dois:\n-- no Tyxter ele carrega o nome e o status da aprova\u00E7\u00E3o; no Evolution, que n\u00E3o\n-- tem o conceito, ele \u00E9 s\u00F3 o texto \u2014 e a campanha n\u00E3o precisa saber a diferen\u00E7a.\nCREATE TABLE IF NOT EXISTS public.plg_conversations_templates (\n id uuid PRIMARY KEY DEFAULT gen_random_uuid(),\n tenant_id uuid NOT NULL REFERENCES public.tenants(id) ON DELETE CASCADE,\n name text NOT NULL,\n language text NOT NULL DEFAULT 'pt_BR',\n category text NOT NULL DEFAULT 'MARKETING',\n body text NOT NULL,\n -- Os nomes das vari\u00E1veis, na ordem em que {{1}}, {{2}} aparecem. A Meta s\u00F3\n -- entende posi\u00E7\u00E3o; gente entende nome. Guardar os dois \u00E9 o que deixa a tela\n -- pedir \"cargo\" em vez de \"vari\u00E1vel 3\".\n variables text[] NOT NULL DEFAULT '{}',\n provider text,\n provider_template_id text,\n -- `draft` enquanto \u00E9 nosso; depois \u00E9 o que o provedor respondeu, verbatim.\n status text NOT NULL DEFAULT 'draft',\n status_detail text,\n submitted_at timestamptz,\n approved_at timestamptz,\n created_at timestamptz NOT NULL DEFAULT now(),\n updated_at timestamptz NOT NULL DEFAULT now()\n);\n\nCREATE UNIQUE INDEX IF NOT EXISTS plg_conversations_templates_name_uq\n ON public.plg_conversations_templates (tenant_id, name, language);\n\nALTER TABLE public.plg_conversations_templates ENABLE ROW LEVEL SECURITY;\nALTER TABLE public.plg_conversations_templates FORCE ROW LEVEL SECURITY;\nDROP POLICY IF EXISTS plg_conversations_templates_rw ON public.plg_conversations_templates;\nCREATE POLICY plg_conversations_templates_rw ON public.plg_conversations_templates\n FOR ALL TO authenticated\n USING (tenant_id IN (SELECT public.user_tenant_ids()))\n WITH CHECK (tenant_id IN (SELECT public.user_tenant_ids()));\nGRANT SELECT, INSERT, UPDATE, DELETE ON public.plg_conversations_templates TO authenticated, service_role;\n\n-- \u2500\u2500 a campanha \u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\nCREATE TABLE IF NOT EXISTS public.plg_conversations_campaigns (\n id uuid PRIMARY KEY DEFAULT gen_random_uuid(),\n tenant_id uuid NOT NULL REFERENCES public.tenants(id) ON DELETE CASCADE,\n name text NOT NULL,\n template_id uuid REFERENCES public.plg_conversations_templates(id) ON DELETE SET NULL,\n channel_id uuid REFERENCES public.plg_conversations_channels(id) ON DELETE SET NULL,\n provider text,\n status text NOT NULL DEFAULT 'draft',\n -- O ritmo. Nasce do padr\u00E3o do provedor e \u00E9 edit\u00E1vel: a rampa \u00E9 pr\u00E1tica de\n -- comunidade, e quem opera o n\u00FAmero sabe mais que esta tabela.\n pacing jsonb NOT NULL DEFAULT '{}'::jsonb,\n -- Por que parou, quando parou sozinha. Ver `crm` nenhum: \u00E9 o alarme de\n -- qualidade da Meta chegando na tela de quem disparou.\n paused_reason text,\n started_at timestamptz,\n finished_at timestamptz,\n created_by uuid,\n created_at timestamptz NOT NULL DEFAULT now(),\n updated_at timestamptz NOT NULL DEFAULT now(),\n CONSTRAINT plg_conversations_campaigns_status\n CHECK (status IN ('draft', 'scheduled', 'running', 'paused', 'done', 'cancelled'))\n);\n\nALTER TABLE public.plg_conversations_campaigns ENABLE ROW LEVEL SECURITY;\nALTER TABLE public.plg_conversations_campaigns FORCE ROW LEVEL SECURITY;\nDROP POLICY IF EXISTS plg_conversations_campaigns_rw ON public.plg_conversations_campaigns;\nCREATE POLICY plg_conversations_campaigns_rw ON public.plg_conversations_campaigns\n FOR ALL TO authenticated\n USING (tenant_id IN (SELECT public.user_tenant_ids()))\n WITH CHECK (tenant_id IN (SELECT public.user_tenant_ids()));\nGRANT SELECT, INSERT, UPDATE, DELETE ON public.plg_conversations_campaigns TO authenticated, service_role;\n\n-- \u2500\u2500 uma linha por destinat\u00E1rio, para sempre \u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\n--\n-- \u00C9 o log de envio e a fila ao mesmo tempo. Vale para os dois provedores, e \u00E9\n-- o \u00FAnico lugar onde se pode PROVAR o que foi mandado, para quem, quando, e\n-- por que algu\u00E9m N\u00C3O foi contatado.\nCREATE TABLE IF NOT EXISTS public.plg_conversations_campaign_targets (\n id uuid PRIMARY KEY DEFAULT gen_random_uuid(),\n tenant_id uuid NOT NULL REFERENCES public.tenants(id) ON DELETE CASCADE,\n campaign_id uuid NOT NULL REFERENCES public.plg_conversations_campaigns(id) ON DELETE CASCADE,\n -- A pessoa do CRM. `SET NULL` e n\u00E3o `CASCADE`: apagar a pessoa n\u00E3o pode\n -- apagar a prova de que uma mensagem foi enviada a ela.\n person_id uuid,\n phone_e164 text NOT NULL,\n -- Ver o cabe\u00E7alho: sobe quando algu\u00E9m REINICIA a campanha de prop\u00F3sito.\n rotation integer NOT NULL DEFAULT 1,\n state text NOT NULL DEFAULT 'queued',\n skip_reason text,\n failure_code text,\n failure_detail text,\n -- O arrendamento. S\u00F3 quem segura o token manda.\n claim_token uuid,\n claim_expires_at timestamptz,\n provider_message_id text,\n variables jsonb NOT NULL DEFAULT '{}'::jsonb,\n queued_at timestamptz NOT NULL DEFAULT now(),\n sent_at timestamptz,\n delivered_at timestamptz,\n read_at timestamptz,\n replied_at timestamptz,\n failed_at timestamptz,\n CONSTRAINT plg_conversations_campaign_targets_state\n CHECK (state IN ('queued', 'sending', 'sent', 'delivered', 'read', 'replied', 'failed', 'skipped')),\n -- O arrendamento existe se e s\u00F3 se est\u00E1 enviando. Duas verifica\u00E7\u00F5es porque\n -- um token sem prazo \u00E9 um token que nunca volta.\n CONSTRAINT plg_conversations_campaign_targets_lease\n CHECK (state <> 'sending' OR claim_expires_at IS NOT NULL),\n CONSTRAINT plg_conversations_campaign_targets_lease_pair\n CHECK ((claim_token IS NULL) = (claim_expires_at IS NULL))\n);\n\n-- Uma vez por campanha, por pessoa, por rodada. \u00C9 esta linha que impede o\n-- disparo duplo \u2014 n\u00E3o um `if` no c\u00F3digo do disparador.\nCREATE UNIQUE INDEX IF NOT EXISTS plg_conversations_campaign_targets_once\n ON public.plg_conversations_campaign_targets (campaign_id, phone_e164, rotation);\n\n-- O webhook do provedor chega mais de uma vez, de prop\u00F3sito (o Evolution tenta\n-- 10 vezes em ~50 min). A idempot\u00EAncia do recebimento \u00E9 este \u00EDndice.\nCREATE UNIQUE INDEX IF NOT EXISTS plg_conversations_campaign_targets_provider_msg\n ON public.plg_conversations_campaign_targets (tenant_id, provider_message_id)\n WHERE provider_message_id IS NOT NULL;\n\nCREATE INDEX IF NOT EXISTS plg_conversations_campaign_targets_pending\n ON public.plg_conversations_campaign_targets (campaign_id)\n WHERE state IN ('queued', 'sending');\n\n-- O ceifeiro: linhas cujo arrendamento venceu voltam para a fila.\nCREATE INDEX IF NOT EXISTS plg_conversations_campaign_targets_reaper\n ON public.plg_conversations_campaign_targets (claim_expires_at)\n WHERE state = 'sending';\n\nALTER TABLE public.plg_conversations_campaign_targets ENABLE ROW LEVEL SECURITY;\nALTER TABLE public.plg_conversations_campaign_targets FORCE ROW LEVEL SECURITY;\nDROP POLICY IF EXISTS plg_conversations_campaign_targets_rw ON public.plg_conversations_campaign_targets;\nCREATE POLICY plg_conversations_campaign_targets_rw ON public.plg_conversations_campaign_targets\n FOR ALL TO authenticated\n USING (tenant_id IN (SELECT public.user_tenant_ids()))\n WITH CHECK (tenant_id IN (SELECT public.user_tenant_ids()));\nGRANT SELECT, INSERT, UPDATE, DELETE ON public.plg_conversations_campaign_targets TO authenticated, service_role;\n\n-- \u2500\u2500 o ritmo que o n\u00FAmero aguenta \u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\n--\n-- Mora no canal e n\u00E3o na campanha: o limite \u00E9 do N\u00DAMERO, e duas campanhas no\n-- mesmo n\u00FAmero somam. Guardar na campanha deixaria cada uma achar que tem a\n-- cota inteira.\nALTER TABLE public.plg_conversations_channels\n ADD COLUMN IF NOT EXISTS pacing jsonb NOT NULL DEFAULT '{}'::jsonb,\n ADD COLUMN IF NOT EXISTS warmup_day integer,\n ADD COLUMN IF NOT EXISTS warmup_started_at timestamptz,\n ADD COLUMN IF NOT EXISTS quality text,\n ADD COLUMN IF NOT EXISTS quality_at timestamptz;\n\nCOMMENT ON COLUMN public.plg_conversations_channels.pacing IS\n 'Ritmo deste n\u00FAmero: por minuto, por hora, por dia, faixa de atraso e janela de hor\u00E1rio. Padr\u00E3o por provedor, edit\u00E1vel \u2014 a rampa \u00E9 pr\u00E1tica de comunidade, n\u00E3o documenta\u00E7\u00E3o da Meta (003).';\n\n-- Os padr\u00F5es, por provedor. N\u00FAmeros da pesquisa; o coment\u00E1rio diz a\n-- proced\u00EAncia de cada um, porque \"por que 20?\" \u00E9 a pergunta que sempre volta.\nCREATE OR REPLACE FUNCTION public.conversations_default_pacing(p_provider text)\nRETURNS jsonb LANGUAGE sql IMMUTABLE AS $$\n SELECT CASE p_provider\n -- Oficial. O teto real \u00E9 o tier da Meta (250/2.000/10.000/100.000), que a\n -- pr\u00F3pria API informa por n\u00FAmero \u2014 ent\u00E3o aqui s\u00F3 entra o que a Meta N\u00C3O\n -- controla: o ritmo instant\u00E2neo. 1 msg / 6 s para o MESMO destinat\u00E1rio \u00E9\n -- documentado; 60/min \u00E9 75% do teto de 80/s por seguran\u00E7a.\n WHEN 'tyxter' THEN jsonb_build_object(\n 'per_minute', 60, 'per_hour', 1000, 'per_day', null,\n 'delay_seconds', jsonb_build_array(1, 3),\n 'same_recipient_seconds', 6,\n 'hours', jsonb_build_object('start', '09:00', 'end', '18:00', 'weekdays_only', true))\n -- N\u00E3o oficial. Aqui n\u00E3o h\u00E1 teto do provedor \u2014 h\u00E1 banimento. Os n\u00FAmeros s\u00E3o\n -- a moda de treze rampas brasileiras publicadas: 10-30/dia na primeira\n -- semana, 200-300/dia no regime. Atraso ALEAT\u00D3RIO entre 15 e 45 s porque\n -- intervalo fixo \u00E9 assinatura de rob\u00F4.\n WHEN 'evolution' THEN jsonb_build_object(\n 'per_minute', 12, 'per_hour', 300, 'per_day', 250,\n 'delay_seconds', jsonb_build_array(15, 45),\n 'same_recipient_seconds', 6,\n 'batch_size', 50, 'batch_cooldown_minutes', 12,\n 'hours', jsonb_build_object('start', '09:00', 'end', '18:00', 'weekdays_only', true))\n ELSE '{}'::jsonb END\n$$;\n\nCOMMENT ON FUNCTION public.conversations_default_pacing(text) IS\n 'O ritmo inicial de cada provedor. Tyxter: o teto \u00E9 da Meta, aqui s\u00F3 o ritmo instant\u00E2neo. Evolution: n\u00E3o h\u00E1 teto, h\u00E1 banimento (003).';\n\n-- A rampa de aquecimento, em dias. Pr\u00E1tica de comunidade, edit\u00E1vel \u2014 e a\n-- fun\u00E7\u00E3o existe para que o padr\u00E3o seja UM lugar e n\u00E3o uma constante copiada.\nCREATE OR REPLACE FUNCTION public.conversations_warmup_allowance(p_day integer)\nRETURNS integer LANGUAGE sql IMMUTABLE AS $$\n SELECT CASE\n WHEN p_day IS NULL THEN NULL -- n\u00FAmero sem aquecimento declarado: sem teto extra\n WHEN p_day <= 3 THEN 20\n WHEN p_day <= 7 THEN 50\n WHEN p_day <= 14 THEN 100\n WHEN p_day <= 21 THEN 200\n ELSE 300\n END\n$$;\n\nCOMMENT ON FUNCTION public.conversations_warmup_allowance(integer) IS\n 'Quantos destinat\u00E1rios NOVOS por dia, por dia de aquecimento. Moda de treze rampas brasileiras publicadas \u2014 comunidade, n\u00E3o Meta. Padr\u00E3o edit\u00E1vel (003).';\n\n-- \u2500\u2500 o que a tela do disparo precisa ver \u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\nDROP VIEW IF EXISTS public.v_conversations_campaign_progress;\nCREATE VIEW public.v_conversations_campaign_progress\nWITH (security_invoker = true) AS\n SELECT c.id AS campaign_id,\n c.tenant_id,\n c.name,\n c.status,\n c.paused_reason,\n c.provider,\n count(t.id) AS total,\n count(*) FILTER (WHERE t.state = 'queued') AS queued,\n count(*) FILTER (WHERE t.state = 'sending') AS sending,\n count(*) FILTER (WHERE t.state IN ('sent','delivered','read','replied')) AS sent,\n count(*) FILTER (WHERE t.state = 'delivered') AS delivered,\n count(*) FILTER (WHERE t.state = 'read') AS read,\n count(*) FILTER (WHERE t.state = 'replied') AS replied,\n count(*) FILTER (WHERE t.state = 'failed') AS failed,\n count(*) FILTER (WHERE t.state = 'skipped') AS skipped,\n -- A taxa de resposta \u00E9 o sinal que decide se o n\u00FAmero sobrevive: a\n -- pr\u00E1tica diz que abaixo de 30% o risco de banimento sobe. Fica na\n -- view porque \u00E9 o n\u00FAmero que precisa estar na tela, n\u00E3o num relat\u00F3rio.\n CASE WHEN count(*) FILTER (WHERE t.state IN ('sent','delivered','read','replied')) > 0\n THEN round(100.0 * count(*) FILTER (WHERE t.state = 'replied')\n / count(*) FILTER (WHERE t.state IN ('sent','delivered','read','replied')), 1)\n END AS reply_rate,\n min(t.sent_at) AS first_sent_at,\n max(t.sent_at) AS last_sent_at\n FROM public.plg_conversations_campaigns c\n LEFT JOIN public.plg_conversations_campaign_targets t ON t.campaign_id = c.id\n WHERE c.tenant_id IN (SELECT public.user_tenant_ids())\n GROUP BY c.id, c.tenant_id, c.name, c.status, c.paused_reason, c.provider;\n\nGRANT SELECT ON public.v_conversations_campaign_progress TO authenticated;\n\n-- \u2500\u2500 arrendar o pr\u00F3ximo lote \u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\n--\n-- `FOR UPDATE SKIP LOCKED` \u00E9 o que deixa dois disparadores trabalharem a mesma\n-- campanha sem pisar um no outro, e sem advisory lock. O arrendamento de cinco\n-- minutos \u00E9 o que devolve a linha quando um deles morre no meio.\n--\n-- A supress\u00E3o \u00E9 conferida AQUI, na mesma transa\u00E7\u00E3o que arrenda. Conferir antes\n-- e mandar depois deixa uma janela em que algu\u00E9m pede para sair e recebe assim\n-- mesmo \u2014 e \u00E9 justamente essa mensagem que vira den\u00FAncia.\nCREATE OR REPLACE FUNCTION public.conversations_claim_targets(\n p_campaign uuid, p_limit integer DEFAULT 10\n) RETURNS TABLE (\n id uuid, phone_e164 text, person_id uuid, variables jsonb, claim_token uuid\n)\nLANGUAGE plpgsql SECURITY DEFINER SET search_path TO ''\nAS $$\nDECLARE v_token uuid := gen_random_uuid(); v_tenant uuid;\nBEGIN\n SELECT c.tenant_id INTO v_tenant\n FROM public.plg_conversations_campaigns c WHERE c.id = p_campaign;\n IF v_tenant IS NULL THEN RAISE EXCEPTION 'campanha n\u00E3o encontrada'; END IF;\n\n -- TR\u00CAS COMANDOS, E N\u00C3O UM. A primeira vers\u00E3o fazia o ceifeiro, a supress\u00E3o e\n -- o arrendamento em CTEs do mesmo UPDATE, e isso \u00E9 uma armadilha: dentro de\n -- um comando s\u00F3, todas as CTEs enxergam o MESMO instant\u00E2neo da tabela, e uma\n -- linha atualizada por duas delas recebe s\u00F3 a primeira \u2014 em sil\u00EAncio. O\n -- suprimido vinha marcado E arrendado, e qual dos dois vencia dependia do\n -- plano que o Postgres escolhesse naquele dia.\n --\n -- Separado, cada passo enxerga o resultado do anterior. \u00C9 mais lento por um\n -- par de milissegundos e \u00E9 a diferen\u00E7a entre uma regra e uma coincid\u00EAncia.\n\n -- 1. O ceifeiro. Quem venceu o arrendamento volta para a fila, sen\u00E3o uma\n -- campanha inteira fica presa num processo que morreu.\n UPDATE public.plg_conversations_campaign_targets t\n SET state = 'queued', claim_token = NULL, claim_expires_at = NULL\n WHERE t.campaign_id = p_campaign AND t.state = 'sending'\n AND t.claim_expires_at < now();\n\n -- 2. Quem pediu para sair NUNCA entra num lote. A linha fica marcada em vez\n -- de sumir: \u00E9 assim que se prova que a decis\u00E3o foi deliberada.\n UPDATE public.plg_conversations_campaign_targets t\n SET state = 'skipped', skip_reason = 'suppressed'\n WHERE t.campaign_id = p_campaign AND t.state = 'queued'\n AND EXISTS (SELECT 1 FROM public.plg_conversations_optouts o\n WHERE o.tenant_id = t.tenant_id\n AND o.phone_e164 = t.phone_e164\n AND o.active);\n\n -- 3. S\u00F3 ent\u00E3o arrenda. `SKIP LOCKED` deixa dois disparadores trabalharem a\n -- mesma campanha sem pisar um no outro, e sem advisory lock.\n RETURN QUERY\n WITH escolhidos AS (\n SELECT t.id FROM public.plg_conversations_campaign_targets t\n WHERE t.campaign_id = p_campaign AND t.state = 'queued'\n ORDER BY t.queued_at\n LIMIT greatest(1, least(coalesce(p_limit, 10), 100))\n FOR UPDATE SKIP LOCKED\n )\n UPDATE public.plg_conversations_campaign_targets t\n SET state = 'sending', claim_token = v_token,\n claim_expires_at = now() + interval '5 minutes'\n FROM escolhidos e\n WHERE t.id = e.id\n RETURNING t.id, t.phone_e164, t.person_id, t.variables, t.claim_token;\nEND $$;\n\nREVOKE ALL ON FUNCTION public.conversations_claim_targets(uuid, integer) FROM PUBLIC, anon;\nGRANT EXECUTE ON FUNCTION public.conversations_claim_targets(uuid, integer) TO service_role;\n";
5
+ export declare const MIGRATION_004_A_TELA_SABE_SE_PODE_FALAR_ANTES_DE_DEIXAR_ESCREVER = "-- ---------------------------------------------------------------------------\n-- 004_a_tela_sabe_se_pode_falar_antes_de_deixar_escrever.sql \u2014 a janela de 24h,\n-- como DADO, para a tela responder antes de algu\u00E9m digitar.\n--\n-- \u2500\u2500 o defeito que isto conserta \u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\n--\n-- Na API oficial do WhatsApp, texto livre s\u00F3 sai dentro de 24h contadas da\n-- \u00FAltima mensagem DO CLIENTE. Fora dela a Meta recusa, e a recusa chega como um\n-- c\u00F3digo no log \u2014 n\u00E3o na tela de quem escreveu. O atendente digita, aperta\n-- enviar, v\u00EA a mensagem aparecer na conversa, e ela nunca chega.\n--\n-- Isso \u00E9 pior que um erro: \u00E9 uma tela que mente. E some inteiro num provedor\n-- n\u00E3o oficial, onde n\u00E3o existe janela nenhuma \u2014 ent\u00E3o o MESMO campo de texto\n-- funciona ou falha calado dependendo de qual provedor est\u00E1 ativo.\n--\n-- A corre\u00E7\u00E3o n\u00E3o \u00E9 um aviso depois. \u00C9 a tela saber ANTES: janela aberta, texto\n-- livre; janela fechada, s\u00F3 template, e o campo diz isso em vez de aceitar.\n--\n-- \u2500\u2500 quem abre a janela \u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\n--\n-- O CLIENTE, e s\u00F3 ele. Mensagem nossa n\u00E3o abre nada \u2014 nem template aprovado.\n-- \u00C9 o erro mais comum de quem implementa isso, e \u00E9 por isso que o c\u00E1lculo\n-- abaixo olha exclusivamente `direction = 'inbound'`.\n--\n-- (Origem: developers.facebook.com/documentation/business-messaging/whatsapp \u2014\n-- \"The user messages you about the product. This opens a 24 hour customer\n-- service window.\" Cliques em an\u00FAncio Click-to-WhatsApp abrem 72h; n\u00E3o\n-- distinguimos ainda, e 24h \u00E9 o limite conservador dos dois.)\n-- ---------------------------------------------------------------------------\n\nDROP VIEW IF EXISTS public.v_conversations_window;\nCREATE VIEW public.v_conversations_window\nWITH (security_invoker = true) AS\n SELECT c.id AS conversation_id,\n c.tenant_id,\n c.channel,\n c.contact_handle,\n c.contact_person_id,\n u.last_inbound_at,\n -- Quando fecha. Nulo = nunca abriu, que \u00E9 o caso de todo lead que\n -- ainda n\u00E3o respondeu \u2014 a maioria de uma lista de feira.\n (u.last_inbound_at + interval '24 hours') AS window_expires_at,\n (u.last_inbound_at IS NOT NULL\n AND u.last_inbound_at > now() - interval '24 hours') AS window_open,\n -- Quanto falta, em minutos. A tela mostra \"faltam 3h\" e n\u00E3o um\n -- carimbo de data: quem est\u00E1 respondendo quer saber se d\u00E1 tempo.\n CASE WHEN u.last_inbound_at IS NOT NULL\n THEN greatest(0, floor(extract(epoch FROM\n (u.last_inbound_at + interval '24 hours') - now()) / 60))::int\n END AS minutes_left,\n -- Quem pediu para n\u00E3o ser mais contatado. Some da tela antes de\n -- qualquer janela: n\u00E3o importa se est\u00E1 aberta.\n EXISTS (SELECT 1 FROM public.plg_conversations_optouts o\n WHERE o.tenant_id = c.tenant_id\n AND o.phone_e164 = regexp_replace(coalesce(c.contact_handle,''), '\\D', '', 'g')\n AND o.active) AS opted_out\n FROM public.plg_conversations c\n LEFT JOIN LATERAL (\n SELECT max(m.at) AS last_inbound_at\n FROM public.plg_conversation_messages m\n WHERE m.conversation_id = c.id\n AND m.direction = 'inbound'\n ) u ON true\n WHERE c.tenant_id IN (SELECT public.user_tenant_ids());\n\nGRANT SELECT ON public.v_conversations_window TO authenticated;\n\nCOMMENT ON VIEW public.v_conversations_window IS\n 'Se d\u00E1 para mandar texto livre para esta conversa agora. A janela de 24h s\u00F3 abre por mensagem DO CLIENTE \u2014 template nosso n\u00E3o abre nada (004).';\n\n-- O \u00EDndice que a LATERAL usa. Sem ele, cada linha da caixa varre as mensagens\n-- da conversa inteira, e a caixa \u00E9 justamente a tela que abre o dia todo.\nCREATE INDEX IF NOT EXISTS plg_conversation_messages_inbound_at\n ON public.plg_conversation_messages (conversation_id, at DESC)\n WHERE direction = 'inbound';\n";
3
6
  export declare const MIGRATIONS: Array<{
4
7
  id: string;
5
8
  sql: string;
@@ -1 +1 @@
1
- {"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../../src/migrations/index.ts"],"names":[],"mappings":"AAIA,eAAO,MAAM,sBAAsB,2rhCAkgBlC,CAAA;AAED,eAAO,MAAM,gDAAgD,i0LAuG5D,CAAA;AAED,eAAO,MAAM,UAAU,EAAE,KAAK,CAAC;IAAE,EAAE,EAAE,MAAM,CAAC;IAAC,GAAG,EAAE,MAAM,CAAA;CAAE,CAGzD,CAAA"}
1
+ {"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../../src/migrations/index.ts"],"names":[],"mappings":"AAIA,eAAO,MAAM,sBAAsB,2rhCAkgBlC,CAAA;AAED,eAAO,MAAM,gDAAgD,i0LAuG5D,CAAA;AAED,eAAO,MAAM,oDAAoD,m5bAuOhE,CAAA;AAED,eAAO,MAAM,0DAA0D,81rBAwWtE,CAAA;AAED,eAAO,MAAM,gEAAgE,+lJA2E5E,CAAA;AAED,eAAO,MAAM,UAAU,EAAE,KAAK,CAAC;IAAE,EAAE,EAAE,MAAM,CAAC;IAAC,GAAG,EAAE,MAAM,CAAA;CAAE,CAMzD,CAAA"}
@@ -1 +1 @@
1
- {"version":3,"file":"ConversationsSettings.d.ts","sourceRoot":"","sources":["../../src/settings/ConversationsSettings.tsx"],"names":[],"mappings":"AAAA,OAAO,KAAK,KAAK,MAAM,OAAO,CAAA;AA6G9B,wBAAgB,wBAAwB,sBAUvC;yBAVe,wBAAwB"}
1
+ {"version":3,"file":"ConversationsSettings.d.ts","sourceRoot":"","sources":["../../src/settings/ConversationsSettings.tsx"],"names":[],"mappings":"AAAA,OAAO,KAAK,KAAK,MAAM,OAAO,CAAA;AAmH9B,wBAAgB,wBAAwB,sBAUvC;yBAVe,wBAAwB"}
@@ -0,0 +1,3 @@
1
+ import React from 'react';
2
+ export declare function WhatsAppProviders(): React.JSX.Element;
3
+ //# sourceMappingURL=WhatsAppProviders.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"WhatsAppProviders.d.ts","sourceRoot":"","sources":["../../src/settings/WhatsAppProviders.tsx"],"names":[],"mappings":"AAAA,OAAO,KAAK,MAAM,OAAO,CAAA;AA4GzB,wBAAgB,iBAAiB,sBAuLhC"}
package/dist/store.d.ts CHANGED
@@ -1,10 +1,21 @@
1
1
  import { type StoreApi } from 'zustand/vanilla';
2
2
  import type { ConversationsProvider } from './data/types';
3
3
  import type { Conversation, Message, Channel, ConversationStatus, CreateConversationInput } from './types';
4
+ /** Quem, por onde, e como chamá-lo. Vem de outro módulo — hoje a ficha do lead
5
+ * no CRM — que sabe a pessoa e não sabe nada sobre conversas. */
6
+ export interface OpenForPersonInput {
7
+ /** `public.people` id. É por ele que a conversa é reconhecida; o telefone é
8
+ * o desempate para threads antigas, criadas antes de haver vínculo. */
9
+ personId?: string;
10
+ name: string;
11
+ handle: string;
12
+ channel: Channel;
13
+ }
4
14
  export interface ConversationsUIState {
5
15
  conversations: Conversation[];
6
16
  messages: Message[];
7
17
  selectedId: string | null;
18
+ refreshMessages: () => Promise<void>;
8
19
  channelFilter: Channel | 'all';
9
20
  search: string;
10
21
  loading: boolean;
@@ -15,6 +26,8 @@ export interface ConversationsUIState {
15
26
  setChannelFilter(channel: Channel | 'all'): Promise<void>;
16
27
  setSearch(search: string): Promise<void>;
17
28
  create(input: CreateConversationInput): Promise<Conversation>;
29
+ /** Abrir a conversa DESTA pessoa neste canal, criando-a se ainda não houver. */
30
+ openForPerson(input: OpenForPersonInput): Promise<Conversation>;
18
31
  send(body: string): Promise<void>;
19
32
  setStatus(status: ConversationStatus): Promise<void>;
20
33
  }
@@ -1 +1 @@
1
- {"version":3,"file":"store.d.ts","sourceRoot":"","sources":["../src/store.ts"],"names":[],"mappings":"AAAA,OAAO,EAAe,KAAK,QAAQ,EAAE,MAAM,iBAAiB,CAAA;AAC5D,OAAO,KAAK,EAAE,qBAAqB,EAAE,MAAM,cAAc,CAAA;AACzD,OAAO,KAAK,EACV,YAAY,EACZ,OAAO,EACP,OAAO,EACP,kBAAkB,EAClB,uBAAuB,EACxB,MAAM,SAAS,CAAA;AAEhB,MAAM,WAAW,oBAAoB;IACnC,aAAa,EAAE,YAAY,EAAE,CAAA;IAC7B,QAAQ,EAAE,OAAO,EAAE,CAAA;IACnB,UAAU,EAAE,MAAM,GAAG,IAAI,CAAA;IACzB,aAAa,EAAE,OAAO,GAAG,KAAK,CAAA;IAC9B,MAAM,EAAE,MAAM,CAAA;IACd,OAAO,EAAE,OAAO,CAAA;IAChB,OAAO,EAAE,OAAO,CAAA;IAEhB,IAAI,IAAI,OAAO,CAAC,IAAI,CAAC,CAAA;IACrB,MAAM,CAAC,EAAE,EAAE,MAAM,GAAG,OAAO,CAAC,IAAI,CAAC,CAAA;IACjC,QAAQ,IAAI,IAAI,CAAA;IAChB,gBAAgB,CAAC,OAAO,EAAE,OAAO,GAAG,KAAK,GAAG,OAAO,CAAC,IAAI,CAAC,CAAA;IACzD,SAAS,CAAC,MAAM,EAAE,MAAM,GAAG,OAAO,CAAC,IAAI,CAAC,CAAA;IACxC,MAAM,CAAC,KAAK,EAAE,uBAAuB,GAAG,OAAO,CAAC,YAAY,CAAC,CAAA;IAC7D,IAAI,CAAC,IAAI,EAAE,MAAM,GAAG,OAAO,CAAC,IAAI,CAAC,CAAA;IACjC,SAAS,CAAC,MAAM,EAAE,kBAAkB,GAAG,OAAO,CAAC,IAAI,CAAC,CAAA;CACrD;AAED,wBAAgB,wBAAwB,CACtC,QAAQ,EAAE,qBAAqB,GAC9B,QAAQ,CAAC,oBAAoB,CAAC,CAwGhC"}
1
+ {"version":3,"file":"store.d.ts","sourceRoot":"","sources":["../src/store.ts"],"names":[],"mappings":"AAAA,OAAO,EAAe,KAAK,QAAQ,EAAE,MAAM,iBAAiB,CAAA;AAC5D,OAAO,KAAK,EAAE,qBAAqB,EAAE,MAAM,cAAc,CAAA;AACzD,OAAO,KAAK,EACV,YAAY,EACZ,OAAO,EACP,OAAO,EACP,kBAAkB,EAClB,uBAAuB,EACxB,MAAM,SAAS,CAAA;AAEhB;kEACkE;AAClE,MAAM,WAAW,kBAAkB;IACjC;4EACwE;IACxE,QAAQ,CAAC,EAAE,MAAM,CAAA;IACjB,IAAI,EAAE,MAAM,CAAA;IACZ,MAAM,EAAE,MAAM,CAAA;IACd,OAAO,EAAE,OAAO,CAAA;CACjB;AAUD,MAAM,WAAW,oBAAoB;IACnC,aAAa,EAAE,YAAY,EAAE,CAAA;IAC7B,QAAQ,EAAE,OAAO,EAAE,CAAA;IACnB,UAAU,EAAE,MAAM,GAAG,IAAI,CAAA;IACzB,eAAe,EAAE,MAAM,OAAO,CAAC,IAAI,CAAC,CAAA;IACpC,aAAa,EAAE,OAAO,GAAG,KAAK,CAAA;IAC9B,MAAM,EAAE,MAAM,CAAA;IACd,OAAO,EAAE,OAAO,CAAA;IAChB,OAAO,EAAE,OAAO,CAAA;IAEhB,IAAI,IAAI,OAAO,CAAC,IAAI,CAAC,CAAA;IACrB,MAAM,CAAC,EAAE,EAAE,MAAM,GAAG,OAAO,CAAC,IAAI,CAAC,CAAA;IACjC,QAAQ,IAAI,IAAI,CAAA;IAChB,gBAAgB,CAAC,OAAO,EAAE,OAAO,GAAG,KAAK,GAAG,OAAO,CAAC,IAAI,CAAC,CAAA;IACzD,SAAS,CAAC,MAAM,EAAE,MAAM,GAAG,OAAO,CAAC,IAAI,CAAC,CAAA;IACxC,MAAM,CAAC,KAAK,EAAE,uBAAuB,GAAG,OAAO,CAAC,YAAY,CAAC,CAAA;IAC7D,gFAAgF;IAChF,aAAa,CAAC,KAAK,EAAE,kBAAkB,GAAG,OAAO,CAAC,YAAY,CAAC,CAAA;IAC/D,IAAI,CAAC,IAAI,EAAE,MAAM,GAAG,OAAO,CAAC,IAAI,CAAC,CAAA;IACjC,SAAS,CAAC,MAAM,EAAE,kBAAkB,GAAG,OAAO,CAAC,IAAI,CAAC,CAAA;CACrD;AAED,wBAAgB,wBAAwB,CACtC,QAAQ,EAAE,qBAAqB,GAC9B,QAAQ,CAAC,oBAAoB,CAAC,CAyKhC"}
package/dist/types.d.ts CHANGED
@@ -45,6 +45,12 @@ export interface Message {
45
45
  body: string;
46
46
  at: string;
47
47
  author: string;
48
+ /** O que aconteceu com ela depois que saiu. Nulo na entrada — ela chegou,
49
+ * não foi entregue por nós. `failed` é o caso que a tela precisa gritar:
50
+ * uma mensagem que nunca saiu não pode parecer uma mensagem enviada. */
51
+ deliveryStatus?: string | null;
52
+ /** O id no provedor. Sem ele, não saiu — e é a chave para buscar o JSON cru. */
53
+ providerMessageId?: string | null;
48
54
  }
49
55
  export interface ListConversationsQuery {
50
56
  channel?: Channel | 'all';
@@ -1 +1 @@
1
- {"version":3,"file":"types.d.ts","sourceRoot":"","sources":["../src/types.ts"],"names":[],"mappings":"AAKA,MAAM,MAAM,OAAO,GAAG,KAAK,GAAG,UAAU,GAAG,WAAW,GAAG,OAAO,GAAG,SAAS,CAAA;AAE5E,MAAM,MAAM,kBAAkB,GAAG,MAAM,GAAG,SAAS,GAAG,QAAQ,CAAA;AAE9D,MAAM,MAAM,gBAAgB,GAAG,SAAS,GAAG,UAAU,CAAA;AAErD,MAAM,WAAW,YAAY;IAC3B,EAAE,EAAE,MAAM,CAAA;IACV,WAAW,EAAE,MAAM,CAAA;IACnB;;;;OAIG;IACH,eAAe,CAAC,EAAE,MAAM,CAAA;IACxB,oDAAoD;IACpD,aAAa,EAAE,MAAM,CAAA;IACrB,OAAO,EAAE,OAAO,CAAA;IAChB,kBAAkB,EAAE,MAAM,CAAA;IAC1B,aAAa,EAAE,MAAM,CAAA;IACrB;;;;;;;;;;;OAWG;IACH,oBAAoB,CAAC,EAAE,gBAAgB,CAAA;IACvC,WAAW,EAAE,MAAM,CAAA;IACnB,MAAM,EAAE,kBAAkB,CAAA;IAC1B,UAAU,CAAC,EAAE,MAAM,CAAA;IACnB,gDAAgD;IAChD,MAAM,EAAE,MAAM,CAAA;IACd,IAAI,EAAE,MAAM,EAAE,CAAA;IAEd,QAAQ,CAAC,EAAE,MAAM,CAAA;IACjB,IAAI,CAAC,EAAE,MAAM,CAAA;CACd;AAED,MAAM,WAAW,OAAO;IACtB,EAAE,EAAE,MAAM,CAAA;IACV,cAAc,EAAE,MAAM,CAAA;IACtB,OAAO,EAAE,OAAO,CAAA;IAChB,SAAS,EAAE,gBAAgB,CAAA;IAC3B,IAAI,EAAE,MAAM,CAAA;IACZ,EAAE,EAAE,MAAM,CAAA;IACV,MAAM,EAAE,MAAM,CAAA;CACf;AAED,MAAM,WAAW,sBAAsB;IACrC,OAAO,CAAC,EAAE,OAAO,GAAG,KAAK,CAAA;IACzB,MAAM,CAAC,EAAE,kBAAkB,GAAG,KAAK,CAAA;IACnC,MAAM,CAAC,EAAE,MAAM,CAAA;CAChB;AAED,MAAM,WAAW,gBAAgB;IAC/B,cAAc,EAAE,MAAM,CAAA;IACtB,IAAI,EAAE,MAAM,CAAA;CACb;AAED,MAAM,WAAW,uBAAuB;IACtC,WAAW,EAAE,MAAM,CAAA;IACnB,+EAA+E;IAC/E,eAAe,CAAC,EAAE,MAAM,CAAA;IACxB,qDAAqD;IACrD,aAAa,CAAC,EAAE,MAAM,CAAA;IACtB,OAAO,EAAE,OAAO,CAAA;IAChB,yEAAyE;IACzE,YAAY,CAAC,EAAE,MAAM,CAAA;IACrB,6DAA6D;IAC7D,IAAI,CAAC,EAAE,MAAM,CAAA;CACd;AAED,eAAO,MAAM,cAAc,EAAE,MAAM,CAAC,OAAO,EAAE,MAAM,CAMlD,CAAA;AAED;;;;;;;;;;;;GAYG;AACH,wBAAgB,aAAa,CAAC,YAAY,EAAE,YAAY,GAAG,OAAO,CAIjE"}
1
+ {"version":3,"file":"types.d.ts","sourceRoot":"","sources":["../src/types.ts"],"names":[],"mappings":"AAKA,MAAM,MAAM,OAAO,GAAG,KAAK,GAAG,UAAU,GAAG,WAAW,GAAG,OAAO,GAAG,SAAS,CAAA;AAE5E,MAAM,MAAM,kBAAkB,GAAG,MAAM,GAAG,SAAS,GAAG,QAAQ,CAAA;AAE9D,MAAM,MAAM,gBAAgB,GAAG,SAAS,GAAG,UAAU,CAAA;AAErD,MAAM,WAAW,YAAY;IAC3B,EAAE,EAAE,MAAM,CAAA;IACV,WAAW,EAAE,MAAM,CAAA;IACnB;;;;OAIG;IACH,eAAe,CAAC,EAAE,MAAM,CAAA;IACxB,oDAAoD;IACpD,aAAa,EAAE,MAAM,CAAA;IACrB,OAAO,EAAE,OAAO,CAAA;IAChB,kBAAkB,EAAE,MAAM,CAAA;IAC1B,aAAa,EAAE,MAAM,CAAA;IACrB;;;;;;;;;;;OAWG;IACH,oBAAoB,CAAC,EAAE,gBAAgB,CAAA;IACvC,WAAW,EAAE,MAAM,CAAA;IACnB,MAAM,EAAE,kBAAkB,CAAA;IAC1B,UAAU,CAAC,EAAE,MAAM,CAAA;IACnB,gDAAgD;IAChD,MAAM,EAAE,MAAM,CAAA;IACd,IAAI,EAAE,MAAM,EAAE,CAAA;IAEd,QAAQ,CAAC,EAAE,MAAM,CAAA;IACjB,IAAI,CAAC,EAAE,MAAM,CAAA;CACd;AAED,MAAM,WAAW,OAAO;IACtB,EAAE,EAAE,MAAM,CAAA;IACV,cAAc,EAAE,MAAM,CAAA;IACtB,OAAO,EAAE,OAAO,CAAA;IAChB,SAAS,EAAE,gBAAgB,CAAA;IAC3B,IAAI,EAAE,MAAM,CAAA;IACZ,EAAE,EAAE,MAAM,CAAA;IACV,MAAM,EAAE,MAAM,CAAA;IACd;;6EAEyE;IACzE,cAAc,CAAC,EAAE,MAAM,GAAG,IAAI,CAAA;IAC9B,gFAAgF;IAChF,iBAAiB,CAAC,EAAE,MAAM,GAAG,IAAI,CAAA;CAClC;AAED,MAAM,WAAW,sBAAsB;IACrC,OAAO,CAAC,EAAE,OAAO,GAAG,KAAK,CAAA;IACzB,MAAM,CAAC,EAAE,kBAAkB,GAAG,KAAK,CAAA;IACnC,MAAM,CAAC,EAAE,MAAM,CAAA;CAChB;AAED,MAAM,WAAW,gBAAgB;IAC/B,cAAc,EAAE,MAAM,CAAA;IACtB,IAAI,EAAE,MAAM,CAAA;CACb;AAED,MAAM,WAAW,uBAAuB;IACtC,WAAW,EAAE,MAAM,CAAA;IACnB,+EAA+E;IAC/E,eAAe,CAAC,EAAE,MAAM,CAAA;IACxB,qDAAqD;IACrD,aAAa,CAAC,EAAE,MAAM,CAAA;IACtB,OAAO,EAAE,OAAO,CAAA;IAChB,yEAAyE;IACzE,YAAY,CAAC,EAAE,MAAM,CAAA;IACrB,6DAA6D;IAC7D,IAAI,CAAC,EAAE,MAAM,CAAA;CACd;AAED,eAAO,MAAM,cAAc,EAAE,MAAM,CAAC,OAAO,EAAE,MAAM,CAMlD,CAAA;AAED;;;;;;;;;;;;GAYG;AACH,wBAAgB,aAAa,CAAC,YAAY,EAAE,YAAY,GAAG,OAAO,CAIjE"}
@@ -0,0 +1,3 @@
1
+ import React from 'react';
2
+ export declare function CampaignsView(): React.JSX.Element;
3
+ //# sourceMappingURL=CampaignsView.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"CampaignsView.d.ts","sourceRoot":"","sources":["../../src/views/CampaignsView.tsx"],"names":[],"mappings":"AAAA,OAAO,KAAK,MAAM,OAAO,CAAA;AAgDzB,wBAAgB,aAAa,sBAuJ5B"}
@@ -1 +1 @@
1
- {"version":3,"file":"ContactPanel.d.ts","sourceRoot":"","sources":["../../src/views/ContactPanel.tsx"],"names":[],"mappings":"AAAA,OAAO,KAAK,MAAM,OAAO,CAAA;AAKzB,OAAO,KAAK,EAAE,YAAY,EAAE,MAAM,UAAU,CAAA;AAkB5C,wBAAgB,YAAY,CAAC,EAAE,OAAO,EAAE,OAAO,EAAE,SAAS,EAAE,EAAE;IAC5D,OAAO,EAAE,YAAY,CAAA;IACrB,OAAO,CAAC,EAAE,MAAM,IAAI,CAAA;IACpB,SAAS,CAAC,EAAE,MAAM,CAAA;CACnB,qBAqEA"}
1
+ {"version":3,"file":"ContactPanel.d.ts","sourceRoot":"","sources":["../../src/views/ContactPanel.tsx"],"names":[],"mappings":"AAAA,OAAO,KAAK,MAAM,OAAO,CAAA;AAMzB,OAAO,KAAK,EAAE,YAAY,EAAE,MAAM,UAAU,CAAA;AAkB5C,wBAAgB,YAAY,CAAC,EAAE,OAAO,EAAE,OAAO,EAAE,SAAS,EAAE,EAAE;IAC5D,OAAO,EAAE,YAAY,CAAA;IACrB,OAAO,CAAC,EAAE,MAAM,IAAI,CAAA;IACpB,SAAS,CAAC,EAAE,MAAM,CAAA;CACnB,qBA0EA"}
@@ -1 +1 @@
1
- {"version":3,"file":"ConversationsPage.d.ts","sourceRoot":"","sources":["../../src/views/ConversationsPage.tsx"],"names":[],"mappings":"AAAA,OAAO,KAAK,MAAM,OAAO,CAAA;AACzB,OAAO,KAAK,EAAE,QAAQ,EAAE,MAAM,iBAAiB,CAAA;AAC/C,OAAO,EAAgC,KAAK,2BAA2B,EAAE,MAAM,YAAY,CAAA;AAC3F,OAAO,KAAK,EAAE,oBAAoB,EAAE,MAAM,UAAU,CAAA;AAGpD,wBAAgB,iBAAiB,CAAC,EAAE,KAAK,EAAE,MAAM,EAAE,EAAE;IACnD,KAAK,EAAE,QAAQ,CAAC,oBAAoB,CAAC,CAAA;IACrC,MAAM,CAAC,EAAE,2BAA2B,CAAA;CACrC,qBAWA"}
1
+ {"version":3,"file":"ConversationsPage.d.ts","sourceRoot":"","sources":["../../src/views/ConversationsPage.tsx"],"names":[],"mappings":"AAAA,OAAO,KAAK,MAAM,OAAO,CAAA;AACzB,OAAO,KAAK,EAAE,QAAQ,EAAE,MAAM,iBAAiB,CAAA;AAC/C,OAAO,EAAgC,KAAK,2BAA2B,EAAE,MAAM,YAAY,CAAA;AAC3F,OAAO,KAAK,EAAE,oBAAoB,EAAE,MAAM,UAAU,CAAA;AAgCpD,wBAAgB,iBAAiB,CAAC,EAAE,KAAK,EAAE,MAAM,EAAE,EAAE;IACnD,KAAK,EAAE,QAAQ,CAAC,oBAAoB,CAAC,CAAA;IACrC,MAAM,CAAC,EAAE,2BAA2B,CAAA;CACrC,qBAgCA"}
@@ -0,0 +1,6 @@
1
+ import React from 'react';
2
+ export declare function LeadCard({ personId, handle }: {
3
+ personId?: string | null;
4
+ handle?: string | null;
5
+ }): React.JSX.Element;
6
+ //# sourceMappingURL=LeadCard.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"LeadCard.d.ts","sourceRoot":"","sources":["../../src/views/LeadCard.tsx"],"names":[],"mappings":"AAAA,OAAO,KAAK,MAAM,OAAO,CAAA;AA2CzB,wBAAgB,QAAQ,CAAC,EAAE,QAAQ,EAAE,MAAM,EAAE,EAAE;IAAE,QAAQ,CAAC,EAAE,MAAM,GAAG,IAAI,CAAC;IAAC,MAAM,CAAC,EAAE,MAAM,GAAG,IAAI,CAAA;CAAE,qBA6GlG"}
@@ -0,0 +1,11 @@
1
+ import React from 'react';
2
+ import type { Message } from '../types';
3
+ export declare function isFailed(m: Message): boolean;
4
+ /** O carimbo ao lado da hora. Some nas de entrada: elas chegaram. */
5
+ export declare function DeliveryMark({ m }: {
6
+ m: Message;
7
+ }): React.JSX.Element | null;
8
+ export declare function MessageMenu({ m }: {
9
+ m: Message;
10
+ }): React.JSX.Element;
11
+ //# sourceMappingURL=MessageInspector.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"MessageInspector.d.ts","sourceRoot":"","sources":["../../src/views/MessageInspector.tsx"],"names":[],"mappings":"AAAA,OAAO,KAAK,MAAM,OAAO,CAAA;AAGzB,OAAO,KAAK,EAAE,OAAO,EAAE,MAAM,UAAU,CAAA;AAqCvC,wBAAgB,QAAQ,CAAC,CAAC,EAAE,OAAO,GAAG,OAAO,CAmB5C;AAED,qEAAqE;AACrE,wBAAgB,YAAY,CAAC,EAAE,CAAC,EAAE,EAAE;IAAE,CAAC,EAAE,OAAO,CAAA;CAAE,4BAWjD;AAED,wBAAgB,WAAW,CAAC,EAAE,CAAC,EAAE,EAAE;IAAE,CAAC,EAAE,OAAO,CAAA;CAAE,qBAkFhD"}
@@ -1 +1 @@
1
- {"version":3,"file":"MessageThread.d.ts","sourceRoot":"","sources":["../../src/views/MessageThread.tsx"],"names":[],"mappings":"AAAA,OAAO,KAAK,MAAM,OAAO,CAAA;AAMzB,OAAO,KAAK,EAAE,YAAY,EAAW,MAAM,UAAU,CAAA;AA2BrD,wBAAgB,aAAa,CAAC,EAAE,QAAQ,EAAE,aAAa,EAAE,SAAS,EAAE,MAAM,EAAE,SAAS,EAAE,EAAE;IACvF,QAAQ,EAAE,YAAY,CAAA;IACtB,aAAa,EAAE,MAAM,IAAI,CAAA;IACzB,SAAS,EAAE,OAAO,CAAA;IAClB,MAAM,CAAC,EAAE,MAAM,IAAI,CAAA;IACnB,SAAS,CAAC,EAAE,MAAM,CAAA;CACnB,qBA+HA"}
1
+ {"version":3,"file":"MessageThread.d.ts","sourceRoot":"","sources":["../../src/views/MessageThread.tsx"],"names":[],"mappings":"AAAA,OAAO,KAAK,MAAM,OAAO,CAAA;AAQzB,OAAO,KAAK,EAAE,YAAY,EAAW,MAAM,UAAU,CAAA;AA2BrD,wBAAgB,aAAa,CAAC,EAAE,QAAQ,EAAE,aAAa,EAAE,SAAS,EAAE,MAAM,EAAE,SAAS,EAAE,EAAE;IACvF,QAAQ,EAAE,YAAY,CAAA;IACtB,aAAa,EAAE,MAAM,IAAI,CAAA;IACzB,SAAS,EAAE,OAAO,CAAA;IAClB,MAAM,CAAC,EAAE,MAAM,IAAI,CAAA;IACnB,SAAS,CAAC,EAAE,MAAM,CAAA;CACnB,qBA0KA"}
@@ -0,0 +1,45 @@
1
+ import React from 'react';
2
+ import { type ProviderRow } from '../data/providers';
3
+ /**
4
+ * Se dá para falar com esta pessoa AGORA, e por qual caminho.
5
+ *
6
+ * ── o defeito que isto existe para matar ────────────────────────────────
7
+ *
8
+ * Na API oficial do WhatsApp, texto livre só sai dentro de 24h contadas da
9
+ * última mensagem DO CLIENTE. Fora dela a Meta recusa — e a recusa chega num
10
+ * log, não na tela. O atendente digita, aperta enviar, vê a mensagem aparecer
11
+ * na conversa, e ela nunca chega ao destinatário.
12
+ *
13
+ * Uma tela que aceita o que não vai entregar é pior que uma tela que recusa.
14
+ *
15
+ * ── e por que isso muda com o provedor ──────────────────────────────────
16
+ *
17
+ * No Evolution não existe janela nenhuma: manda sempre. Então o MESMO campo de
18
+ * texto é seguro num provedor e mentiroso no outro, e qual está valendo é uma
19
+ * configuração que muda numa tela distante daqui. Por isso este gancho lê as
20
+ * duas coisas juntas — a janela da conversa E a capacidade do provedor ativo —
21
+ * e devolve uma resposta só: pode escrever, ou não pode e por quê.
22
+ */
23
+ export type SendMode = {
24
+ kind: 'free';
25
+ until: string | null;
26
+ minutesLeft: number | null;
27
+ } | {
28
+ kind: 'template';
29
+ reason: 'window_closed' | 'never_replied';
30
+ } | {
31
+ kind: 'blocked';
32
+ reason: 'no_provider';
33
+ };
34
+ export interface WindowState {
35
+ loading: boolean;
36
+ mode: SendMode;
37
+ provider: ProviderRow | undefined;
38
+ }
39
+ export declare function useSendWindow(conversationId: string | undefined): WindowState;
40
+ /** Quanto falta, em português de gente: "faltam 3h" e não um carimbo ISO. */
41
+ export declare function humanLeft(minutes: number | null | undefined): string;
42
+ export declare function SendWindowBanner({ state }: {
43
+ state: WindowState;
44
+ }): React.JSX.Element | null;
45
+ //# sourceMappingURL=SendWindow.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"SendWindow.d.ts","sourceRoot":"","sources":["../../src/views/SendWindow.tsx"],"names":[],"mappings":"AAAA,OAAO,KAAK,MAAM,OAAO,CAAA;AAEzB,OAAO,EAAgB,KAAK,WAAW,EAAE,MAAM,mBAAmB,CAAA;AAElE;;;;;;;;;;;;;;;;;;;GAmBG;AAEH,MAAM,MAAM,QAAQ,GAChB;IAAE,IAAI,EAAE,MAAM,CAAC;IAAC,KAAK,EAAE,MAAM,GAAG,IAAI,CAAC;IAAC,WAAW,EAAE,MAAM,GAAG,IAAI,CAAA;CAAE,GAClE;IAAE,IAAI,EAAE,UAAU,CAAC;IAAC,MAAM,EAAE,eAAe,GAAG,eAAe,CAAA;CAAE,GAC/D;IAAE,IAAI,EAAE,SAAS,CAAC;IAAC,MAAM,EAAE,aAAa,CAAA;CAAE,CAAA;AAE9C,MAAM,WAAW,WAAW;IAC1B,OAAO,EAAE,OAAO,CAAA;IAChB,IAAI,EAAE,QAAQ,CAAA;IACd,QAAQ,EAAE,WAAW,GAAG,SAAS,CAAA;CAClC;AASD,wBAAgB,aAAa,CAAC,cAAc,EAAE,MAAM,GAAG,SAAS,GAAG,WAAW,CAwC7E;AAED,6EAA6E;AAC7E,wBAAgB,SAAS,CAAC,OAAO,EAAE,MAAM,GAAG,IAAI,GAAG,SAAS,GAAG,MAAM,CAKpE;AAED,wBAAgB,gBAAgB,CAAC,EAAE,KAAK,EAAE,EAAE;IAAE,KAAK,EAAE,WAAW,CAAA;CAAE,4BAuCjE"}
@@ -0,0 +1,3 @@
1
+ import React from 'react';
2
+ export declare function TemplatesView(): React.JSX.Element;
3
+ //# sourceMappingURL=TemplatesView.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"TemplatesView.d.ts","sourceRoot":"","sources":["../../src/views/TemplatesView.tsx"],"names":[],"mappings":"AAAA,OAAO,KAAK,MAAM,OAAO,CAAA;AAiDzB,wBAAgB,aAAa,sBA6K5B"}
package/package.json CHANGED
@@ -4,7 +4,7 @@
4
4
  "status": "preview",
5
5
  "dependencies": []
6
6
  },
7
- "version": "0.11.6",
7
+ "version": "0.12.0",
8
8
  "description": "[experimental] Fayz SDK — unified conversations / omni-channel inbox plugin",
9
9
  "type": "module",
10
10
  "sideEffects": false,
@@ -27,9 +27,9 @@
27
27
  "lucide-react": ">=0.400.0 <1.0.0"
28
28
  },
29
29
  "dependencies": {
30
- "@fayz-ai/core": "^0.22.0",
31
- "@fayz-ai/ui": "^0.22.0",
32
- "@fayz-ai/admin": "^0.22.0"
30
+ "@fayz-ai/core": "^0.23.0",
31
+ "@fayz-ai/ui": "^0.23.0",
32
+ "@fayz-ai/admin": "^0.23.0"
33
33
  },
34
34
  "devDependencies": {
35
35
  "@types/react": "^18.3.0",