@fayz-ai/plugin-conversations 0.11.2 → 0.11.3

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.
@@ -0,0 +1,17 @@
1
+ import { type OnboardingProgramDef } from '@fayz-ai/core';
2
+ /**
3
+ * Implantação da caixa de entrada — um canal e um hábito.
4
+ *
5
+ * Ordem 30, à frente da base do workspace (50) e do CRM (80), e isso é uma
6
+ * opinião: numa operação que atende, o cliente escreve antes de qualquer outra
7
+ * coisa acontecer. Convidar a equipe e desenhar o funil são trabalho de quem já
8
+ * tem alguém escrevendo; ligar o canal é o que faz aparecer o primeiro.
9
+ *
10
+ * Dois passos, e o segundo não é uma tela. A caixa de entrada é o módulo que
11
+ * mais parece pronto vazio: ela abre, tem filtro por canal, tem busca — e não
12
+ * chega mensagem nenhuma porque ninguém ligou o número. O passo que fecha é o
13
+ * único que prova que o caminho inteiro existe: uma mensagem que entrou e foi
14
+ * respondida daqui.
15
+ */
16
+ export declare function buildConversationsOnboarding(): OnboardingProgramDef;
17
+ //# sourceMappingURL=onboarding.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"onboarding.d.ts","sourceRoot":"","sources":["../../src/lib/onboarding.ts"],"names":[],"mappings":"AAAA,OAAO,EAAiB,KAAK,oBAAoB,EAAE,MAAM,eAAe,CAAA;AAGxE;;;;;;;;;;;;;GAaG;AACH,wBAAgB,4BAA4B,IAAI,oBAAoB,CAwDnE"}
@@ -1 +1 @@
1
- {"version":3,"file":"en.d.ts","sourceRoot":"","sources":["../../src/locales/en.ts"],"names":[],"mappings":"AAAA,eAAO,MAAM,EAAE,EAAE,MAAM,CAAC,MAAM,EAAE,MAAM,CAmErC,CAAA"}
1
+ {"version":3,"file":"en.d.ts","sourceRoot":"","sources":["../../src/locales/en.ts"],"names":[],"mappings":"AAAA,eAAO,MAAM,EAAE,EAAE,MAAM,CAAC,MAAM,EAAE,MAAM,CAkGrC,CAAA"}
@@ -1 +1 @@
1
- {"version":3,"file":"pt-BR.d.ts","sourceRoot":"","sources":["../../src/locales/pt-BR.ts"],"names":[],"mappings":"AAAA,eAAO,MAAM,IAAI,EAAE,MAAM,CAAC,MAAM,EAAE,MAAM,CAmEvC,CAAA"}
1
+ {"version":3,"file":"pt-BR.d.ts","sourceRoot":"","sources":["../../src/locales/pt-BR.ts"],"names":[],"mappings":"AAAA,eAAO,MAAM,IAAI,EAAE,MAAM,CAAC,MAAM,EAAE,MAAM,CAkGvC,CAAA"}
@@ -1,12 +1,5 @@
1
- export declare const MIGRATION_001_CONVERSATIONS = "-- ============================================================================\n-- plugin-conversations 001: omni-channel inbox model (SMS / WhatsApp /\n-- Instagram / Email / Web chat). Prefix: plg_conversations / plg_conversation_messages.\n-- \u00A71 plg_conversations \u2014 one thread per contact+channel\n-- \u00A72 plg_conversation_messages \u2014 inbound/outbound messages within a thread\n-- \u00A73 RLS: authenticated tenant-scoped CRUD on both tables + GRANTs\n--\n-- Column names mirror exactly what supabase.ts's mapConversation / mapMessage\n-- read. Real channel connectors (Twilio, WhatsApp Cloud, Meta, IMAP) deliver\n-- inbound rows here out-of-band; the provider is the read/compose surface.\n-- Idempotent + safe to re-run.\n-- ============================================================================\n\n-- \u00A71 \u2014 conversations (threads)\nCREATE TABLE IF NOT EXISTS public.plg_conversations (\n id uuid PRIMARY KEY DEFAULT gen_random_uuid(),\n tenant_id uuid NOT NULL REFERENCES public.tenants(id) ON DELETE CASCADE,\n contact_name text NOT NULL,\n contact_handle text,\n channel text NOT NULL\n CHECK (channel IN ('sms', 'whatsapp', 'instagram', 'email', 'webchat')),\n last_message_preview text,\n last_message_at timestamptz DEFAULT now(),\n unread_count int DEFAULT 0,\n status text DEFAULT 'open'\n CHECK (status IN ('open', 'snoozed', 'closed')),\n assigned_to text,\n accent text,\n tags text[],\n location text,\n note text,\n created_at timestamptz NOT NULL DEFAULT now(),\n updated_at timestamptz NOT NULL DEFAULT now()\n);\nALTER TABLE public.plg_conversations ENABLE ROW LEVEL SECURITY;\nCREATE INDEX IF NOT EXISTS idx_plg_conversations_tenant ON public.plg_conversations(tenant_id);\nCREATE INDEX IF NOT EXISTS idx_plg_conversations_tenant_recent ON public.plg_conversations(tenant_id, last_message_at DESC);\n\n-- \u00A72 \u2014 messages\nCREATE TABLE IF NOT EXISTS public.plg_conversation_messages (\n id uuid PRIMARY KEY DEFAULT gen_random_uuid(),\n tenant_id uuid NOT NULL REFERENCES public.tenants(id) ON DELETE CASCADE,\n conversation_id uuid NOT NULL REFERENCES public.plg_conversations(id) ON DELETE CASCADE,\n channel text\n CHECK (channel IS NULL OR channel IN ('sms', 'whatsapp', 'instagram', 'email', 'webchat')),\n direction text\n CHECK (direction IN ('inbound', 'outbound')),\n body text NOT NULL,\n author text,\n at timestamptz DEFAULT now()\n);\nALTER TABLE public.plg_conversation_messages ENABLE ROW LEVEL SECURITY;\nCREATE INDEX IF NOT EXISTS idx_plg_conversation_messages_thread ON public.plg_conversation_messages(conversation_id, at);\n\n-- \u00A73 \u2014 RLS: authenticated tenant CRUD (the inbox reads/writes here)\nDROP POLICY IF EXISTS plg_conversations_select ON public.plg_conversations;\nDROP POLICY IF EXISTS plg_conversations_insert ON public.plg_conversations;\nDROP POLICY IF EXISTS plg_conversations_update ON public.plg_conversations;\nDROP POLICY IF EXISTS plg_conversations_delete ON public.plg_conversations;\nCREATE POLICY plg_conversations_select ON public.plg_conversations FOR SELECT TO authenticated USING (tenant_id IN (SELECT public.user_tenant_ids()));\nCREATE POLICY plg_conversations_insert ON public.plg_conversations FOR INSERT TO authenticated WITH CHECK (tenant_id IN (SELECT public.user_tenant_ids()));\nCREATE POLICY plg_conversations_update ON public.plg_conversations FOR UPDATE TO authenticated USING (tenant_id IN (SELECT public.user_tenant_ids()));\nCREATE POLICY plg_conversations_delete ON public.plg_conversations FOR DELETE TO authenticated USING (tenant_id IN (SELECT public.user_tenant_ids()));\nGRANT SELECT, INSERT, UPDATE, DELETE ON public.plg_conversations TO authenticated;\n\nDROP POLICY IF EXISTS plg_conversation_messages_select ON public.plg_conversation_messages;\nDROP POLICY IF EXISTS plg_conversation_messages_insert ON public.plg_conversation_messages;\nDROP POLICY IF EXISTS plg_conversation_messages_update ON public.plg_conversation_messages;\nDROP POLICY IF EXISTS plg_conversation_messages_delete ON public.plg_conversation_messages;\nCREATE POLICY plg_conversation_messages_select ON public.plg_conversation_messages FOR SELECT TO authenticated USING (tenant_id IN (SELECT public.user_tenant_ids()));\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()));\nCREATE POLICY plg_conversation_messages_update ON public.plg_conversation_messages FOR UPDATE TO authenticated USING (tenant_id IN (SELECT public.user_tenant_ids()));\nCREATE POLICY plg_conversation_messages_delete ON public.plg_conversation_messages FOR DELETE TO authenticated USING (tenant_id IN (SELECT public.user_tenant_ids()));\nGRANT SELECT, INSERT, UPDATE, DELETE ON public.plg_conversation_messages TO authenticated;\n";
2
- export declare const MIGRATION_002_CONTACT_PERSON = "-- ============================================================================\n-- plugin-conversations 002: link a thread to a REAL person record.\n--\n-- The compose modal used to take a free-text name + handle, so a conversation\n-- with \"Maria\" had nothing to do with the Maria in the agenda, the CRM or the\n-- financial module. The shared ContactPicker (find-or-create over\n-- public.people) now resolves a person, and this column stores that link.\n--\n-- Nullable on purpose, in both directions of time:\n-- \u2022 rows created before this migration keep working (name/handle only);\n-- \u2022 an inbound message from an unknown number still opens a thread with no\n-- person attached \u2014 the contact panel can offer \"create contact\" later.\n-- ON DELETE SET NULL: deleting a person must never take their history with it.\n-- Idempotent + safe to re-run.\n-- ============================================================================\n\nALTER TABLE public.plg_conversations\n ADD COLUMN IF NOT EXISTS contact_person_id uuid REFERENCES public.people(id) ON DELETE SET NULL;\n\nCREATE INDEX IF NOT EXISTS idx_plg_conversations_person\n ON public.plg_conversations(tenant_id, contact_person_id)\n WHERE contact_person_id IS NOT NULL;\n";
3
- export declare const MIGRATION_003_CHANNELS = "-- ============================================================================\n-- plugin-conversations 003: channel accounts \u2014 WHICH number a message leaves by.\n--\n-- The inbox has always been the read/compose surface for rows a connector\n-- delivered out-of-band. This is the first table that says where those rows\n-- come FROM, and it exists because WhatsApp has two number models at once:\n--\n-- \u2022 kind 'fallback' \u2014 the product's own number, shared by every tenant.\n-- tenant_id IS NULL: it belongs to the software, not to a salon, and a\n-- tenant column with a made-up value there would make every read lie.\n-- \u2022 kind 'dedicated' \u2014 a number rented for one tenant. Once it is `active`\n-- it wins over the fallback; until then the fallback keeps carrying.\n--\n-- Provider-neutral on purpose (`provider`, `provider_number_id`): the sender\n-- model is a WhatsApp fact, not a Tyxter fact, and the second messaging\n-- provider must not need a second table.\n--\n-- Writes are SERVICE-ROLE ONLY. Every row is created by a webhook or by an\n-- edge function acting on the provider's answer \u2014 a member who could edit\n-- `provider_number_id` could point another tenant's replies at their own inbox.\n-- Members read, and the fallback row is readable by all of them because it is\n-- the number their own messages go out on.\n-- Idempotent + safe to re-run.\n-- ============================================================================\n\nCREATE TABLE IF NOT EXISTS public.plg_conversations_channels (\n id uuid PRIMARY KEY DEFAULT gen_random_uuid(),\n -- NULL = the product fallback row. Not a missing value: an owner.\n tenant_id uuid REFERENCES public.tenants(id) ON DELETE CASCADE,\n channel text NOT NULL DEFAULT 'whatsapp'\n CHECK (channel IN ('sms', 'whatsapp', 'instagram', 'email', 'webchat')),\n provider text NOT NULL,\n -- The provider's handle for the number ('pn_\u2026'); what a webhook routes on.\n provider_number_id text NOT NULL,\n phone_e164 text,\n kind text NOT NULL DEFAULT 'dedicated'\n CHECK (kind IN ('fallback', 'dedicated')),\n -- The provider's own lifecycle, kept verbatim so a status webhook never has\n -- to be translated into a vocabulary of ours that means slightly less.\n status text NOT NULL DEFAULT 'requested'\n CHECK (status IN ('requested', 'provisioning', 'provisioned', 'verifying',\n 'active', 'failed', 'released', 'disconnected')),\n display_name text,\n metadata jsonb NOT NULL DEFAULT '{}'::jsonb,\n created_at timestamptz NOT NULL DEFAULT now(),\n updated_at timestamptz NOT NULL DEFAULT now()\n);\nALTER TABLE public.plg_conversations_channels ENABLE ROW LEVEL SECURITY;\n\n-- One row per number at a provider: the webhook resolves the tenant by this\n-- pair, and a duplicate would make that resolution a coin toss.\nCREATE UNIQUE INDEX IF NOT EXISTS uq_plg_conversations_channels_number\n ON public.plg_conversations_channels(provider, provider_number_id);\n\n-- At most one fallback per (channel, provider). Sender resolution falls back to\n-- \"the product's number\", singular \u2014 two of them is an ambiguity no caller can\n-- resolve, and it would surface as messages leaving by the wrong one.\nCREATE UNIQUE INDEX IF NOT EXISTS uq_plg_conversations_channels_fallback\n ON public.plg_conversations_channels(channel, provider)\n WHERE tenant_id IS NULL AND kind = 'fallback';\n\nCREATE INDEX IF NOT EXISTS idx_plg_conversations_channels_tenant\n ON public.plg_conversations_channels(tenant_id, channel, status)\n WHERE tenant_id IS NOT NULL;\n\n-- RLS: members read their own rows AND the product fallback (it is the number\n-- they send from, so hiding it would leave the panel with nothing to say).\n-- No INSERT/UPDATE/DELETE policy and no write GRANT: service role only.\nDROP POLICY IF EXISTS plg_conversations_channels_select ON public.plg_conversations_channels;\nCREATE POLICY plg_conversations_channels_select ON public.plg_conversations_channels\n FOR SELECT TO authenticated\n USING (tenant_id IS NULL OR tenant_id IN (SELECT public.user_tenant_ids()));\nGRANT SELECT ON public.plg_conversations_channels TO authenticated;\n";
4
- export declare const MIGRATION_004_MESSAGE_DELIVERY = "-- ============================================================================\n-- plugin-conversations 004: what the provider did with a message, and who wrote\n-- it. Four columns, ported from the legacy beautyplace inbox.\n--\n-- `direction` + `author` was enough while every row was typed by a person in\n-- this app. It stops being enough the moment a message is sent by a connector:\n--\n-- provider_message_id the provider's handle for the row. A delivery webhook\n-- names only this, so without it a status can never be applied to anything.\n-- UNIQUE (where present) so an at-least-once webhook that redelivers the\n-- same inbound message cannot open a second copy of it in the thread.\n-- delivery_status the last thing the provider said, applied MONOTONICALLY\n-- by the webhook handler \u2014 'read' must not be overwritten by a 'delivered'\n-- that arrived late (they are unordered).\n-- sender_kind person / app / assistant / automation. The backbone of\n-- a shared inbox: legacy proved that a human and an AI writing into the same\n-- thread with no attribution is a thread nobody trusts.\n-- sender_label who exactly, in words \u2014 \"Ana\", \"Lembrete de hor\u00E1rio\".\n--\n-- Nullable throughout: rows written before this migration are not wrong, they\n-- are simply from a time when only people wrote here.\n-- Idempotent + safe to re-run.\n-- ============================================================================\n\nALTER TABLE public.plg_conversation_messages\n ADD COLUMN IF NOT EXISTS provider_message_id text,\n ADD COLUMN IF NOT EXISTS delivery_status text,\n ADD COLUMN IF NOT EXISTS sender_kind text,\n ADD COLUMN IF NOT EXISTS sender_label text;\n\n-- Added apart from the column so a re-run on a pool that already has the column\n-- still installs the constraint (ADD COLUMN IF NOT EXISTS skips its inline ones).\nDO $$\nBEGIN\n IF NOT EXISTS (\n SELECT 1 FROM pg_constraint WHERE conname = 'plg_conversation_messages_delivery_status_check'\n ) THEN\n ALTER TABLE public.plg_conversation_messages\n ADD CONSTRAINT plg_conversation_messages_delivery_status_check\n CHECK (delivery_status IS NULL OR delivery_status IN\n ('queued', 'sent', 'delivered', 'read', 'failed', 'expired'));\n END IF;\n IF NOT EXISTS (\n SELECT 1 FROM pg_constraint WHERE conname = 'plg_conversation_messages_sender_kind_check'\n ) THEN\n ALTER TABLE public.plg_conversation_messages\n ADD CONSTRAINT plg_conversation_messages_sender_kind_check\n CHECK (sender_kind IS NULL OR sender_kind IN ('user', 'system', 'ai', 'automation'));\n END IF;\nEND\n$$;\n\nCREATE UNIQUE INDEX IF NOT EXISTS uq_plg_conversation_messages_provider_message\n ON public.plg_conversation_messages(provider_message_id)\n WHERE provider_message_id IS NOT NULL;\n";
5
- export declare const MIGRATION_005_WEBHOOK_EVENTS = "-- ============================================================================\n-- plugin-conversations 005: the dedupe ledger \u2014 one row per event the provider\n-- ever delivered, and the reason a retry is free.\n--\n-- Tyxter delivers AT LEAST ONCE: 8 attempts over ~32.7 hours, and an attempt\n-- counts as failed on anything that is not a 2xx inside 10 seconds. So the same\n-- `message.received` arrives again whenever our answer was slow, and without\n-- this table the second delivery opens a second message in the thread and\n-- re-applies the button the customer pressed once.\n--\n-- Deliberately NOT keyed on the message: the same message produces several\n-- events (sent, delivered, read) and the same event may name no message at all\n-- (a template approval). The identity that is unique per DELIVERED FACT is the\n-- provider's event id, so that is the key.\n--\n-- The insert is the lock. `ON CONFLICT DO NOTHING \u2026 RETURNING id` returns a row\n-- for the first delivery and nothing for every later one, which is a claim\n-- taken in a single statement \u2014 a SELECT-then-INSERT would let two concurrent\n-- retries both find nothing and both apply.\n--\n-- Service-role only, and not because the rows are secret: a member who could\n-- delete one could make a webhook re-apply, and one who could insert could make\n-- a real delivery be skipped as a duplicate.\n-- Idempotent + safe to re-run.\n-- ============================================================================\n\nCREATE TABLE IF NOT EXISTS public.plg_conversations_webhook_events (\n id uuid PRIMARY KEY DEFAULT gen_random_uuid(),\n provider text NOT NULL,\n -- The provider's id for the fact ('evt_\u2026'), NOT for the message.\n event_id text NOT NULL,\n event_type text NOT NULL,\n received_at timestamptz NOT NULL DEFAULT now()\n);\nALTER TABLE public.plg_conversations_webhook_events ENABLE ROW LEVEL SECURITY;\n\n-- No tenant column on purpose: at claim time the tenant is not known yet \u2014 the\n-- whole point of the row is to stop the SECOND delivery before any of the work\n-- that would resolve one. Scoped by provider so two providers cannot collide on\n-- an id shape neither of them controls.\nCREATE UNIQUE INDEX IF NOT EXISTS uq_plg_conversations_webhook_events_event\n ON public.plg_conversations_webhook_events(provider, event_id);\n\n-- For the sweep that keeps this table from growing forever. Nothing prunes it\n-- yet; retention past the provider's 32.7h retry horizon buys nothing, and an\n-- index the pruner will need is cheaper to add now than to add under load.\nCREATE INDEX IF NOT EXISTS idx_plg_conversations_webhook_events_received\n ON public.plg_conversations_webhook_events(received_at);\n\n-- RLS on with NO policy and no GRANT: service role only, in both directions.\n";
6
- export declare const MIGRATION_006_OPTOUTS = "-- ============================================================================\n-- plugin-conversations 006: who told us to stop.\n--\n-- Tyxter keeps its own consent ledger and refuses a send to an opted-out\n-- contact at its edge. This table is not a copy of that for redundancy's sake \u2014\n-- it exists so the REFUSAL IS OURS. A send that fails at the provider is a\n-- failed send: a queued row, a failed delivery status, a retry, and a line in\n-- an operator's screen saying WhatsApp is broken. A send that never leaves\n-- because this row exists is a suppression, which is a different fact and the\n-- only one a person can act on.\n--\n-- `tenant_id` is NULLABLE and the null carries meaning, the same way it does on\n-- plg_conversations_channels:\n-- \u2022 NULL \u2014 the customer opted out of the PRODUCT's shared fallback number.\n-- They cannot have consented to one salon and not another on a number all\n-- the salons share, so the suppression is product-wide.\n-- \u2022 set \u2014 the customer opted out of that tenant's dedicated number. Their\n-- other conversations are untouched.\n--\n-- Written by the webhook (contact.opted_out / contact.erased) and read by\n-- messaging-send before every send. Nothing else writes it: a member who could\n-- delete a row could resume messaging somebody who asked us to stop, which is\n-- the one mistake in this file with a legal name.\n-- Idempotent + safe to re-run.\n-- ============================================================================\n\nCREATE TABLE IF NOT EXISTS public.plg_conversations_optouts (\n id uuid PRIMARY KEY DEFAULT gen_random_uuid(),\n channel text NOT NULL DEFAULT 'whatsapp'\n CHECK (channel IN ('sms', 'whatsapp', 'instagram', 'email', 'webchat')),\n phone_e164 text NOT NULL,\n -- NULL = the product's shared number. Not a missing value: a scope.\n tenant_id uuid REFERENCES public.tenants(id) ON DELETE CASCADE,\n -- No DEFAULT: the table is provider-neutral, and a column that quietly\n -- defaults to one provider is how the second messaging provider's rows end up\n -- filed under the first one's name. Every writer names it.\n provider text NOT NULL,\n reason text,\n created_at timestamptz NOT NULL DEFAULT now()\n);\nALTER TABLE public.plg_conversations_optouts ENABLE ROW LEVEL SECURITY;\n\n-- NULL-safe uniqueness. A plain UNIQUE(channel, phone_e164, tenant_id) does not\n-- constrain the product-wide rows at all \u2014 in SQL two NULLs are not equal, so\n-- every redelivered contact.opted_out would insert another row and the\n-- suppression read would have to be a DISTINCT. The sentinel uuid is never a\n-- real tenant id, so COALESCE gives the three columns one identity.\nCREATE UNIQUE INDEX IF NOT EXISTS uq_plg_conversations_optouts_contact\n ON public.plg_conversations_optouts(\n channel, phone_e164,\n COALESCE(tenant_id, '00000000-0000-0000-0000-000000000000'::uuid));\n\n-- The read messaging-send makes on every send: \"is this number suppressed for\n-- this tenant, or product-wide?\"\nCREATE INDEX IF NOT EXISTS idx_plg_conversations_optouts_lookup\n ON public.plg_conversations_optouts(channel, phone_e164);\n\n-- RLS: members READ the suppressions that apply to them (their own tenant's and\n-- the product-wide ones), because \"why did this customer stop getting messages\"\n-- is a question the inbox has to be able to answer. Writes are service-role\n-- only \u2014 no INSERT/UPDATE/DELETE policy, no write GRANT.\nDROP POLICY IF EXISTS plg_conversations_optouts_select ON public.plg_conversations_optouts;\nCREATE POLICY plg_conversations_optouts_select ON public.plg_conversations_optouts\n FOR SELECT TO authenticated\n USING (tenant_id IS NULL OR tenant_id IN (SELECT public.user_tenant_ids()));\nGRANT SELECT ON public.plg_conversations_optouts TO authenticated;\n";
7
- export declare const MIGRATION_007_INBOUND_ROUTING = "-- ============================================================================\n-- plugin-conversations 007: the two things the webhook needs that 004 could not\n-- know it would.\n--\n-- \u00A71 delivery_status gains the statuses the provider actually emits.\n-- \u00A72 plg_conversations_match_by_phone \u2014 find the thread a WhatsApp number\n-- belongs to, when the number was typed by a person and delivered by Meta.\n-- Idempotent + safe to re-run.\n-- ============================================================================\n\n-- \u00A71 \u2014 the closed set was one delivery short --------------------------------\n--\n-- 004 wrote the vocabulary from the design. Registering a webhook endpoint at\n-- Tyxter answers with the ENUMERATED list of deliverable events, and it holds\n-- three the CHECK would have refused: `message.delivery_timeout` (the provider\n-- gave up before the recipient's device ever acknowledged), `message.cancelled`\n-- (an accepted message pulled back before it left), and `message.opted_out`\n-- (refused at the provider because the contact withdrew consent \u2014 see 006).\n--\n-- Refusing them is worse than it sounds: the handler's UPDATE fails, the\n-- webhook answers non-2xx, and Tyxter retries the same event eight times over a\n-- day and a half before giving up. The row keeps whatever it said before, which\n-- is 'sent'. An operator reads \"sent\" for a message that was never delivered.\nDO $$\nBEGIN\n IF EXISTS (\n SELECT 1 FROM pg_constraint WHERE conname = 'plg_conversation_messages_delivery_status_check'\n ) THEN\n ALTER TABLE public.plg_conversation_messages\n DROP CONSTRAINT plg_conversation_messages_delivery_status_check;\n END IF;\n ALTER TABLE public.plg_conversation_messages\n ADD CONSTRAINT plg_conversation_messages_delivery_status_check\n CHECK (delivery_status IS NULL OR delivery_status IN\n ('queued', 'sent', 'delivered', 'read',\n 'failed', 'expired', 'delivery_timeout', 'cancelled', 'opted_out'));\nEND\n$$;\n\n-- \u00A72 \u2014 matching a phone number to a thread ----------------------------------\n--\n-- The problem, ported from beautyplace where it cost a duplicate thread per\n-- returning customer: the receptionist typed `(21) 99889-9889`, WhatsApp\n-- delivers `+5521998899889`, and the mobile ninth digit is optional on records\n-- older than 2016. Three spellings of one person, and `=` matches none of them.\n--\n-- The rule is digit-suffix, longest first:\n-- last 10 = DDD + the 8 significant digits. Specific: a collision needs two\n-- customers in the same area code sharing 8 digits.\n-- last 8 = the subscriber digits alone. The FALLBACK, tried only when the\n-- first finds nothing, because it crosses area codes.\n--\n-- SECURITY DEFINER and CROSS-TENANT by construction, which is the part to read\n-- carefully. The product's fallback number is shared by every tenant in the\n-- pool, so an inbound message on it arrives with no tenant attached and finding\n-- one means looking at all of them. That is exactly the read that must never be\n-- reachable by a member, so EXECUTE is granted to service_role ONLY and\n-- explicitly revoked from authenticated and anon. The webhook holds the\n-- service-role key; nothing in the browser can call this.\n-- `p_tenant_id` narrows the search to one tenant, which is what a DEDICATED\n-- number wants: the tenant is already known from the channel row, and the\n-- cross-tenant search could hand back another salon's thread for the same\n-- customer. NULL searches the whole pool, which is the fallback number's case\n-- and the only one that has no other answer.\nCREATE OR REPLACE FUNCTION public.plg_conversations_match_by_phone(\n p_channel text,\n p_last10 text,\n p_last8 text,\n p_tenant_id uuid DEFAULT NULL\n) RETURNS TABLE (\n id uuid,\n tenant_id uuid,\n contact_name text,\n contact_handle text,\n contact_person_id uuid\n)\nLANGUAGE sql\nSTABLE\nSECURITY DEFINER\nSET search_path = public\nAS $fn$\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$fn$;\n\nREVOKE ALL ON FUNCTION public.plg_conversations_match_by_phone(text, text, text, uuid) FROM public;\nREVOKE ALL ON FUNCTION public.plg_conversations_match_by_phone(text, text, text, uuid) FROM anon;\nREVOKE ALL ON FUNCTION public.plg_conversations_match_by_phone(text, text, text, uuid) FROM authenticated;\nGRANT EXECUTE ON FUNCTION public.plg_conversations_match_by_phone(text, text, text, uuid) TO service_role;\n\n-- The suffix is computed per row, so without this the match is a full scan of\n-- every WhatsApp thread in the pool on every inbound message. All three\n-- functions are IMMUTABLE, which is what makes the expression indexable.\nCREATE INDEX IF NOT EXISTS idx_plg_conversations_handle_suffix\n ON public.plg_conversations(\n channel,\n right(regexp_replace(COALESCE(contact_handle, ''), '[^0-9]', '', 'g'), 8),\n last_message_at DESC);\n";
8
- export declare const MIGRATION_008_MESSAGE_SUBJECT = "-- ============================================================================\n-- plugin-conversations 008: what a message was ABOUT.\n--\n-- Two columns, and they exist because of one unproved assumption in the\n-- WhatsApp design. The template send attaches a per-button payload carrying the\n-- booking id (`act:confirm:booking:<uuid>`), and the whole confirm/cancel flow\n-- rests on Meta echoing that payload back when the customer taps. Tyxter's\n-- sandbox accepts the components with a 202 and stores them verbatim \u2014 but it\n-- validates that array not at all (an index of 7 on a two-button template was\n-- accepted the same way), so the 202 proves nothing about Meta. Only a live\n-- WABA can.\n--\n-- If Meta turns out to echo the button's static TEXT instead, the reply arrives\n-- saying \"Confirmar\" and naming nothing. These columns are what it is then\n-- correlated against: the last outbound message on that conversation, and the\n-- subject recorded on it. Which is exactly the seam legacy beautyplace had to\n-- build after the fact \u2014 their keyword matcher confirmed EVERY future\n-- appointment the customer had, and the 48-hour re-enrichment subsystem existed\n-- only to guess which one was meant. Recording the subject at send time costs\n-- two nullable columns and removes the guess.\n--\n-- messaging-send (WP3) already writes them, tolerating 42703 and retrying\n-- without \u2014 so a pool that has not run this migration keeps sending, and one\n-- that has gains the anchor. This is the migration that closes that gap.\n--\n-- Nullable, and deliberately not a foreign key: the subject is polymorphic\n-- ('booking', later 'order', 'invoice'), lives in tables this plugin does not\n-- own, and a message about a deleted booking is still a message somebody sent.\n-- Idempotent + safe to re-run.\n-- ============================================================================\n\nALTER TABLE public.plg_conversation_messages\n ADD COLUMN IF NOT EXISTS subject_type text,\n ADD COLUMN IF NOT EXISTS subject_id text;\n\n-- The correlation read: the most recent outbound message on this thread that\n-- was about something. Partial, because the rows that carry a subject are the\n-- minority and an index over the rest would be paid for on every insert.\nCREATE INDEX IF NOT EXISTS idx_plg_conversation_messages_subject\n ON public.plg_conversation_messages(conversation_id, at DESC)\n WHERE subject_id IS NOT NULL;\n\n-- The other direction, for \"show me everything ever said about this booking\" \u2014\n-- the thread view a booking detail page wants, and the audit answer to \"was the\n-- customer actually told?\".\nCREATE INDEX IF NOT EXISTS idx_plg_conversation_messages_by_subject\n ON public.plg_conversation_messages(tenant_id, subject_type, subject_id)\n WHERE subject_id IS NOT NULL;\n";
9
- export declare const MIGRATION_009_PAYMENT_REQUESTS = "-- ============================================================================\n-- plugin-conversations 009: the charge a customer was asked for inside a\n-- conversation, and the row that stops it being asked for twice.\n--\n-- The flow this table exists for: the customer taps \"Pagar\" on a WhatsApp\n-- confirmation, a Pix is opened at the provider, the copy-and-paste code is\n-- sent back into the same thread, and the money \u2014 or the expiry \u2014 comes back as\n-- a webhook. Four hops, at least one of them delivered more than once.\n--\n-- `id` IS the `external_reference` sent to the provider, and that is the whole\n-- reconciliation story: the provider echoes it on every payment event, so a\n-- delivery names the row without a lookup table, and a payment created by this\n-- pool can never be confused with one created by the merchant's own AbacatePay\n-- account (the redundancy the design accepted on purpose). It is also the\n-- Idempotency-Key of the create call, so a retried create replays the provider's\n-- own first answer instead of opening a second charge.\n--\n-- \u2500\u2500 The two columns that are not data, but claims \u2500\u2500\u2500\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-- `link_delivered_at` and `settled_notified_at` are LOCKS, taken with a\n-- conditional UPDATE \u2026 WHERE \u2026 IS NULL RETURNING. They exist because of a fact\n-- verified against the sandbox on 2026-08-24: ONE Pix becoming available emits\n-- TWO webhook events with two different event ids \u2014\n--\n-- payment.link_generated evt_2448\u2026 occurred_at 16:52:49.640Z\n-- payment.approval_available evt_750b\u2026 occurred_at 16:52:49.639Z\n--\n-- \u2014 carrying identical `data`. The dedupe ledger (005) cannot help: they are\n-- genuinely two events. Without a claim on the ROW the customer receives the\n-- same Pix code twice, one second apart, which reads as a double charge.\n--\n-- \u2500\u2500 Money moves forward only \u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\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-- `status` is applied monotonically by the same rule the delivery statuses use,\n-- because webhooks are unordered: a `link_generated` arriving after a `paid`\n-- must not walk a settled charge back to \"waiting\". The CHECK carries the\n-- provider's own vocabulary verbatim, including `approval_requested` and\n-- `approved`, which the design's list did not have and the OpenAPI does.\n--\n-- Writes are SERVICE-ROLE ONLY. A member who could update `status` could mark a\n-- charge paid; one who could update `amount_cents` could change what a customer\n-- is asked for after the fact. Members read \u2014 \"was this booking paid, and how\n-- much for\" is a question the inbox and the booking page both have to answer.\n-- Idempotent + safe to re-run.\n-- ============================================================================\n\nCREATE TABLE IF NOT EXISTS public.plg_conversations_payment_requests (\n -- Ours, and the provider's `external_reference`, and the create's\n -- Idempotency-Key. One identity, so there is nothing to keep in step.\n id uuid PRIMARY KEY DEFAULT gen_random_uuid(),\n tenant_id uuid NOT NULL REFERENCES public.tenants(id) ON DELETE CASCADE,\n conversation_id uuid REFERENCES public.plg_conversations(id) ON DELETE SET NULL,\n -- The outbound message carrying the Pix code, once it has been sent. NULL\n -- until then, and that is exactly how \"the code has not gone out yet\" is read.\n message_id uuid REFERENCES public.plg_conversation_messages(id) ON DELETE SET NULL,\n -- Polymorphic and deliberately not a foreign key, for the same reason 008's\n -- subject columns are not: 'booking' today, 'order' and 'invoice' later, in\n -- tables this plugin does not own.\n subject_type text,\n subject_id text,\n -- Integers, because the provider takes `amount_brl_centavos` and a float that\n -- rounds to 4499 is a customer charged one centavo less than the salon reads.\n amount_cents integer NOT NULL CHECK (amount_cents > 0),\n currency text NOT NULL DEFAULT 'BRL',\n description text,\n -- Provider-neutral, like every other column in this plugin: the second\n -- payment rail must not need a second table.\n provider text NOT NULL,\n status text NOT NULL DEFAULT 'created'\n CHECK (status IN ('created', 'link_generated', 'approval_requested', 'approved',\n 'paid', 'failed', 'expired', 'cancelled')),\n provider_payment_id text,\n payment_link_url text,\n -- Kept because the customer may lose the message and ask for it again, and\n -- re-reading a code we already have beats opening a second charge.\n pix_copy_paste text,\n -- The claims. See the header.\n link_delivered_at timestamptz,\n settled_notified_at timestamptz,\n settled_at timestamptz,\n created_at timestamptz NOT NULL DEFAULT now(),\n updated_at timestamptz NOT NULL DEFAULT now()\n);\nALTER TABLE public.plg_conversations_payment_requests ENABLE ROW LEVEL SECURITY;\n\n-- The webhook's lookup: an event names the payment, and this finds the row.\n-- UNIQUE where present \u2014 two rows claiming one provider payment would make the\n-- settlement a coin toss.\nCREATE UNIQUE INDEX IF NOT EXISTS uq_plg_conversations_payment_requests_provider\n ON public.plg_conversations_payment_requests(provider, provider_payment_id)\n WHERE provider_payment_id IS NOT NULL;\n\n-- At most ONE live charge per subject. A customer tapping \"Pagar\" twice sends\n-- two `message.received` events with two different event ids, so the dedupe\n-- ledger lets both through \u2014 this is what stops the second one opening a second\n-- Pix for the same appointment. Settled rows are excluded: a booking legitimately\n-- gets a new charge after one expired.\nCREATE UNIQUE INDEX IF NOT EXISTS uq_plg_conversations_payment_requests_open_subject\n ON public.plg_conversations_payment_requests(tenant_id, subject_type, subject_id)\n WHERE subject_id IS NOT NULL\n AND status IN ('created', 'link_generated', 'approval_requested', 'approved');\n\nCREATE INDEX IF NOT EXISTS idx_plg_conversations_payment_requests_tenant\n ON public.plg_conversations_payment_requests(tenant_id, created_at DESC);\n\nCREATE INDEX IF NOT EXISTS idx_plg_conversations_payment_requests_subject\n ON public.plg_conversations_payment_requests(tenant_id, subject_type, subject_id)\n WHERE subject_id IS NOT NULL;\n\n-- RLS: members read their own tenant's charges. No INSERT/UPDATE/DELETE policy\n-- and no write GRANT \u2014 every write here is made by the webhook with the\n-- service-role key, acting on a signed delivery.\nDROP POLICY IF EXISTS plg_conversations_payment_requests_select\n ON public.plg_conversations_payment_requests;\nCREATE POLICY plg_conversations_payment_requests_select\n ON public.plg_conversations_payment_requests\n FOR SELECT TO authenticated\n USING (tenant_id IN (SELECT public.user_tenant_ids()));\nGRANT SELECT ON public.plg_conversations_payment_requests TO authenticated;\n";
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
+ 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";
10
3
  export declare const MIGRATIONS: Array<{
11
4
  id: string;
12
5
  sql: string;
@@ -1 +1 @@
1
- {"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../../src/migrations/index.ts"],"names":[],"mappings":"AAIA,eAAO,MAAM,2BAA2B,gyJA0EvC,CAAA;AAED,eAAO,MAAM,4BAA4B,mvCAsBxC,CAAA;AAED,eAAO,MAAM,sBAAsB,knIAyElC,CAAA;AAED,eAAO,MAAM,8BAA8B,ozFAuD1C,CAAA;AAED,eAAO,MAAM,4BAA4B,8vFAkDxC,CAAA;AAED,eAAO,MAAM,qBAAqB,wwHAkEjC,CAAA;AAED,eAAO,MAAM,6BAA6B,wuLAmHzC,CAAA;AAED,eAAO,MAAM,6BAA6B,8xFAgDzC,CAAA;AAED,eAAO,MAAM,8BAA8B,soOAqH1C,CAAA;AAED,eAAO,MAAM,UAAU,EAAE,KAAK,CAAC;IAAE,EAAE,EAAE,MAAM,CAAC;IAAC,GAAG,EAAE,MAAM,CAAA;CAAE,CAUzD,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,UAAU,EAAE,KAAK,CAAC;IAAE,EAAE,EAAE,MAAM,CAAC;IAAC,GAAG,EAAE,MAAM,CAAA;CAAE,CAGzD,CAAA"}
@@ -0,0 +1,6 @@
1
+ import * as React from 'react';
2
+ export declare function ConversationsSettingsTab(): React.JSX.Element;
3
+ export declare namespace ConversationsSettingsTab {
4
+ var displayName: string;
5
+ }
6
+ //# sourceMappingURL=ConversationsSettings.d.ts.map
@@ -0,0 +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"}
package/dist/types.d.ts CHANGED
@@ -15,6 +15,19 @@ export interface Conversation {
15
15
  channel: Channel;
16
16
  lastMessagePreview: string;
17
17
  lastMessageAt: string;
18
+ /**
19
+ * Who spoke last: `inbound` = the customer, `outbound` = the house.
20
+ *
21
+ * The field the queue is built on — "waiting on us" is `status === 'open'`
22
+ * plus this being `inbound`. `unreadCount` is the older approximation and
23
+ * gets the painful case wrong: a thread somebody opened, read, meant to
24
+ * answer later and forgot has zero unread and a customer still waiting.
25
+ *
26
+ * Optional because a thread predating the column's backfill (or a provider
27
+ * that does not report direction) genuinely does not know — see
28
+ * `isWaitingOnUs`, which falls back rather than guessing.
29
+ */
30
+ lastMessageDirection?: MessageDirection;
18
31
  unreadCount: number;
19
32
  status: ConversationStatus;
20
33
  assignedTo?: string;
@@ -55,4 +68,18 @@ export interface CreateConversationInput {
55
68
  note?: string;
56
69
  }
57
70
  export declare const CHANNEL_LABELS: Record<Channel, string>;
71
+ /**
72
+ * Is this thread waiting on US?
73
+ *
74
+ * One definition, in one place, because the KPI, the queue table and anything
75
+ * that later filters the inbox have to agree — a dashboard that says three and
76
+ * a list that shows one is worse than neither.
77
+ *
78
+ * `lastMessageDirection` is the real answer. `unreadCount` is the fallback for
79
+ * a thread that genuinely does not know its direction (older than migration
80
+ * 001, or a provider that does not report one) — it under-reports rather than
81
+ * over-reports, which is the right way round: a queue that invents work loses
82
+ * trust faster than one that misses an old row.
83
+ */
84
+ export declare function isWaitingOnUs(conversation: Conversation): boolean;
58
85
  //# sourceMappingURL=types.d.ts.map
@@ -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,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"}
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 +1 @@
1
- {"version":3,"file":"NewConversationPanel.d.ts","sourceRoot":"","sources":["../../src/views/NewConversationPanel.tsx"],"names":[],"mappings":"AAAA,OAAO,KAAK,MAAM,OAAO,CAAA;AAmCzB;;;;;;GAMG;AACH,wBAAgB,oBAAoB,CAAC,EACnC,IAAI,EACJ,YAAY,GACb,EAAE;IACD,IAAI,EAAE,OAAO,CAAA;IACb,YAAY,EAAE,CAAC,IAAI,EAAE,OAAO,KAAK,IAAI,CAAA;CACtC,qBAoKA"}
1
+ {"version":3,"file":"NewConversationPanel.d.ts","sourceRoot":"","sources":["../../src/views/NewConversationPanel.tsx"],"names":[],"mappings":"AACA,OAAO,KAAK,MAAM,OAAO,CAAA;AAmCzB;;;;;;GAMG;AACH,wBAAgB,oBAAoB,CAAC,EACnC,IAAI,EACJ,YAAY,GACb,EAAE;IACD,IAAI,EAAE,OAAO,CAAA;IACb,YAAY,EAAE,CAAC,IAAI,EAAE,OAAO,KAAK,IAAI,CAAA;CACtC,qBAoKA"}
@@ -0,0 +1,9 @@
1
+ import type { StoreApi } from 'zustand';
2
+ import type { DashboardWidgetDef } from '@fayz-ai/core';
3
+ import { type ResolvedConversationsConfig } from '../context';
4
+ import type { ConversationsUIState } from '../store';
5
+ export declare function createConversationsDashboardWidgets(ctx: {
6
+ store: StoreApi<ConversationsUIState>;
7
+ config: ResolvedConversationsConfig;
8
+ }): DashboardWidgetDef[];
9
+ //# sourceMappingURL=dashboardWidgets.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"dashboardWidgets.d.ts","sourceRoot":"","sources":["../../src/views/dashboardWidgets.tsx"],"names":[],"mappings":"AACA,OAAO,KAAK,EAAE,QAAQ,EAAE,MAAM,SAAS,CAAA;AACvC,OAAO,KAAK,EAAE,kBAAkB,EAAE,MAAM,eAAe,CAAA;AAMvD,OAAO,EAGL,KAAK,2BAA2B,EACjC,MAAM,YAAY,CAAA;AACnB,OAAO,KAAK,EAAE,oBAAoB,EAAE,MAAM,UAAU,CAAA;AAgLpD,wBAAgB,mCAAmC,CAAC,GAAG,EAAE;IACvD,KAAK,EAAE,QAAQ,CAAC,oBAAoB,CAAC,CAAA;IACrC,MAAM,EAAE,2BAA2B,CAAA;CACpC,GAAG,kBAAkB,EAAE,CAyCvB"}
package/package.json CHANGED
@@ -4,7 +4,7 @@
4
4
  "status": "preview",
5
5
  "dependencies": []
6
6
  },
7
- "version": "0.11.2",
7
+ "version": "0.11.3",
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.18.0",
31
- "@fayz-ai/ui": "^0.18.0",
32
- "@fayz-ai/admin": "^0.18.0"
30
+ "@fayz-ai/core": "^0.19.0",
31
+ "@fayz-ai/ui": "^0.19.0",
32
+ "@fayz-ai/admin": "^0.19.0"
33
33
  },
34
34
  "devDependencies": {
35
35
  "@types/react": "^18.3.0",