@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.
- package/dist/data/mock.d.ts.map +1 -1
- package/dist/data/supabase.d.ts.map +1 -1
- package/dist/index.d.ts +2 -1
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +965 -664
- package/dist/index.js.map +1 -1
- package/dist/lib/onboarding.d.ts +17 -0
- package/dist/lib/onboarding.d.ts.map +1 -0
- package/dist/locales/en.d.ts.map +1 -1
- package/dist/locales/pt-BR.d.ts.map +1 -1
- package/dist/migrations/index.d.ts +2 -9
- package/dist/migrations/index.d.ts.map +1 -1
- package/dist/settings/ConversationsSettings.d.ts +6 -0
- package/dist/settings/ConversationsSettings.d.ts.map +1 -0
- package/dist/types.d.ts +27 -0
- package/dist/types.d.ts.map +1 -1
- package/dist/views/NewConversationPanel.d.ts.map +1 -1
- package/dist/views/dashboardWidgets.d.ts +9 -0
- package/dist/views/dashboardWidgets.d.ts.map +1 -0
- package/package.json +4 -4
package/dist/index.js
CHANGED
|
@@ -1,18 +1,19 @@
|
|
|
1
|
-
import
|
|
2
|
-
import
|
|
1
|
+
import * as React4 from 'react';
|
|
2
|
+
import React4__default, { useState, useRef, useEffect, useCallback, useMemo } from 'react';
|
|
3
|
+
import { createConnectionStore, connectionRuns, connectionStatus, getSupabaseClientOptional, useActiveTenantId, registerTranslations, useTranslation, getActiveTenantId, countByTenant, CONNECTOR_RUNTIME_TOKEN_HEADER, connectorRuntimeToken, errorMessage } from '@fayz-ai/core';
|
|
3
4
|
import { useStore } from 'zustand';
|
|
4
5
|
import { jsxs, jsx, Fragment } from 'react/jsx-runtime';
|
|
5
6
|
import { Loader2, ExternalLink, SquarePen, MessageSquare, Globe, Mail, Instagram, Phone, Search, Inbox, ChevronLeft, Clock, Archive, PanelRight, Send, X, User, MapPin, Tag, StickyNote, Link2 } from 'lucide-react';
|
|
6
|
-
import { Input, Button, toast, PageHeaderActions, cn, Skeleton } from '@fayz-ai/ui';
|
|
7
|
-
import { PermissionGate, useLimitGuard, RightRailPage, ContactPicker, invalidateLimit } from '@fayz-ai/admin';
|
|
7
|
+
import { Input, Button, toast, defineKpiWidget, defineTableWidget, PageHeaderActions, cn, Badge, Skeleton, KpiCard, TableWidget } from '@fayz-ai/ui';
|
|
8
|
+
import { PluginSettingsPanel, SettingsGroup, PermissionGate, useLimitGuard, RightRailPage, ContactPicker, invalidateLimit } from '@fayz-ai/admin';
|
|
8
9
|
import { createStore } from 'zustand/vanilla';
|
|
9
10
|
|
|
10
11
|
// src/index.ts
|
|
11
12
|
var DEFAULT_CONVERSATIONS_CONFIG = {
|
|
12
13
|
contactKind: "contact"
|
|
13
14
|
};
|
|
14
|
-
var StoreContext =
|
|
15
|
-
var ConfigContext =
|
|
15
|
+
var StoreContext = React4__default.createContext(null);
|
|
16
|
+
var ConfigContext = React4__default.createContext(DEFAULT_CONVERSATIONS_CONFIG);
|
|
16
17
|
function ConversationsContextProvider({
|
|
17
18
|
store: store2,
|
|
18
19
|
config = DEFAULT_CONVERSATIONS_CONFIG,
|
|
@@ -21,12 +22,12 @@ function ConversationsContextProvider({
|
|
|
21
22
|
return /* @__PURE__ */ jsx(StoreContext.Provider, { value: store2, children: /* @__PURE__ */ jsx(ConfigContext.Provider, { value: config, children }) });
|
|
22
23
|
}
|
|
23
24
|
function useConversationsStore(selector) {
|
|
24
|
-
const store2 =
|
|
25
|
+
const store2 = React4__default.useContext(StoreContext);
|
|
25
26
|
if (!store2) throw new Error("useConversationsStore must be used within ConversationsPage");
|
|
26
27
|
return useStore(store2, selector);
|
|
27
28
|
}
|
|
28
29
|
function useConversationsConfig() {
|
|
29
|
-
return
|
|
30
|
+
return React4__default.useContext(ConfigContext);
|
|
30
31
|
}
|
|
31
32
|
|
|
32
33
|
// src/types.ts
|
|
@@ -37,6 +38,11 @@ var CHANNEL_LABELS = {
|
|
|
37
38
|
email: "Email",
|
|
38
39
|
webchat: "Web Chat"
|
|
39
40
|
};
|
|
41
|
+
function isWaitingOnUs(conversation) {
|
|
42
|
+
if (conversation.status !== "open") return false;
|
|
43
|
+
if (conversation.lastMessageDirection) return conversation.lastMessageDirection === "inbound";
|
|
44
|
+
return conversation.unreadCount > 0;
|
|
45
|
+
}
|
|
40
46
|
|
|
41
47
|
// src/lib/channel.ts
|
|
42
48
|
var CHANNEL_ICON = {
|
|
@@ -54,10 +60,10 @@ var CHANNEL_ACCENT = {
|
|
|
54
60
|
webchat: { color: "#f59e0b", badge: "bg-[#f59e0b]/12 text-[#b45309] dark:text-[#fcd34d]" }
|
|
55
61
|
};
|
|
56
62
|
function useMediaQuery(query) {
|
|
57
|
-
const [matches, setMatches] =
|
|
63
|
+
const [matches, setMatches] = React4__default.useState(
|
|
58
64
|
() => typeof window !== "undefined" && window.matchMedia(query).matches
|
|
59
65
|
);
|
|
60
|
-
|
|
66
|
+
React4__default.useEffect(() => {
|
|
61
67
|
if (typeof window === "undefined") return;
|
|
62
68
|
const mql = window.matchMedia(query);
|
|
63
69
|
const onChange = () => setMatches(mql.matches);
|
|
@@ -241,9 +247,9 @@ function buildRows(messages) {
|
|
|
241
247
|
function MessageThread({ selected, onTogglePanel, panelOpen, onBack, className }) {
|
|
242
248
|
const t = useTranslation();
|
|
243
249
|
const { messages, sending, send, setStatus } = useConversationsStore((s) => s);
|
|
244
|
-
const [draft, setDraft] =
|
|
245
|
-
const threadRef =
|
|
246
|
-
|
|
250
|
+
const [draft, setDraft] = React4__default.useState("");
|
|
251
|
+
const threadRef = React4__default.useRef(null);
|
|
252
|
+
React4__default.useEffect(() => {
|
|
247
253
|
threadRef.current?.scrollTo({ top: threadRef.current.scrollHeight, behavior: "smooth" });
|
|
248
254
|
}, [messages.length, selected.id]);
|
|
249
255
|
async function handleSend() {
|
|
@@ -253,7 +259,7 @@ function MessageThread({ selected, onTogglePanel, panelOpen, onBack, className }
|
|
|
253
259
|
await send(body);
|
|
254
260
|
}
|
|
255
261
|
const accent = CHANNEL_ACCENT[selected.channel];
|
|
256
|
-
const rows =
|
|
262
|
+
const rows = React4__default.useMemo(() => buildRows(messages), [messages]);
|
|
257
263
|
return /* @__PURE__ */ jsxs("section", { className: cn("flex min-w-0 flex-1 flex-col bg-muted/20", className), children: [
|
|
258
264
|
/* @__PURE__ */ jsxs("div", { className: "flex items-center justify-between gap-2 border-b border-border bg-card px-3 py-2.5 md:px-5", children: [
|
|
259
265
|
/* @__PURE__ */ jsxs("div", { className: "flex min-w-0 items-center gap-2 md:gap-3", children: [
|
|
@@ -422,14 +428,14 @@ function NewConversationPanel({
|
|
|
422
428
|
const create = useConversationsStore((s) => s.create);
|
|
423
429
|
const config = useConversationsConfig();
|
|
424
430
|
const guardConversations = useLimitGuard("conversations_month");
|
|
425
|
-
const [channel, setChannel] =
|
|
426
|
-
const [contact, setContact] =
|
|
427
|
-
const [typedHandle, setTypedHandle] =
|
|
428
|
-
const [creatingContact, setCreatingContact] =
|
|
429
|
-
const [firstMessage, setFirstMessage] =
|
|
430
|
-
const [submitting, setSubmitting] =
|
|
431
|
-
const [pickerKey, setPickerKey] =
|
|
432
|
-
|
|
431
|
+
const [channel, setChannel] = React4__default.useState("whatsapp");
|
|
432
|
+
const [contact, setContact] = React4__default.useState(null);
|
|
433
|
+
const [typedHandle, setTypedHandle] = React4__default.useState("");
|
|
434
|
+
const [creatingContact, setCreatingContact] = React4__default.useState(false);
|
|
435
|
+
const [firstMessage, setFirstMessage] = React4__default.useState("");
|
|
436
|
+
const [submitting, setSubmitting] = React4__default.useState(false);
|
|
437
|
+
const [pickerKey, setPickerKey] = React4__default.useState(0);
|
|
438
|
+
React4__default.useEffect(() => {
|
|
433
439
|
if (open) {
|
|
434
440
|
setChannel("whatsapp");
|
|
435
441
|
setContact(null);
|
|
@@ -460,7 +466,7 @@ function NewConversationPanel({
|
|
|
460
466
|
invalidateLimit("conversations_month");
|
|
461
467
|
onOpenChange(false);
|
|
462
468
|
} catch (err) {
|
|
463
|
-
const message =
|
|
469
|
+
const message = errorMessage(err);
|
|
464
470
|
toast.error(t("conversations.new.createFailed"), { description: message });
|
|
465
471
|
} finally {
|
|
466
472
|
setSubmitting(false);
|
|
@@ -547,10 +553,10 @@ function InboxView() {
|
|
|
547
553
|
const t = useTranslation();
|
|
548
554
|
const { conversations, selectedId, deselect } = useConversationsStore((s) => s);
|
|
549
555
|
const isWidePanel = useMediaQuery("(min-width: 1280px)");
|
|
550
|
-
const [panelOpen, setPanelOpen] =
|
|
551
|
-
const [newOpen, setNewOpen] =
|
|
556
|
+
const [panelOpen, setPanelOpen] = React4__default.useState(false);
|
|
557
|
+
const [newOpen, setNewOpen] = React4__default.useState(false);
|
|
552
558
|
const selected = conversations.find((c) => c.id === selectedId) ?? null;
|
|
553
|
-
|
|
559
|
+
React4__default.useEffect(() => {
|
|
554
560
|
setPanelOpen(isWidePanel);
|
|
555
561
|
}, [isWidePanel]);
|
|
556
562
|
return /* @__PURE__ */ jsxs(Fragment, { children: [
|
|
@@ -605,7 +611,7 @@ function InboxView() {
|
|
|
605
611
|
] });
|
|
606
612
|
}
|
|
607
613
|
function ConversationsPage({ store: store2, config }) {
|
|
608
|
-
|
|
614
|
+
React4__default.useEffect(() => {
|
|
609
615
|
void store2.getState().load();
|
|
610
616
|
}, []);
|
|
611
617
|
return /* @__PURE__ */ jsx(ConversationsContextProvider, { store: store2, config, children: /* @__PURE__ */ jsx(InboxView, {}) });
|
|
@@ -634,6 +640,7 @@ function seed() {
|
|
|
634
640
|
channel: "whatsapp",
|
|
635
641
|
lastMessagePreview: "Perfect, can we book for Friday at 3pm?",
|
|
636
642
|
lastMessageAt: minutesAgo(base, 4),
|
|
643
|
+
lastMessageDirection: "inbound",
|
|
637
644
|
unreadCount: 2,
|
|
638
645
|
status: "open",
|
|
639
646
|
assignedTo: "You",
|
|
@@ -649,6 +656,7 @@ function seed() {
|
|
|
649
656
|
channel: "sms",
|
|
650
657
|
lastMessagePreview: "Got it \u2014 sending the deposit now.",
|
|
651
658
|
lastMessageAt: minutesAgo(base, 22),
|
|
659
|
+
lastMessageDirection: "inbound",
|
|
652
660
|
unreadCount: 0,
|
|
653
661
|
status: "open",
|
|
654
662
|
assignedTo: "You",
|
|
@@ -663,6 +671,7 @@ function seed() {
|
|
|
663
671
|
channel: "instagram",
|
|
664
672
|
lastMessagePreview: "Do you offer balayage on weekends?",
|
|
665
673
|
lastMessageAt: minutesAgo(base, 51),
|
|
674
|
+
lastMessageDirection: "inbound",
|
|
666
675
|
unreadCount: 1,
|
|
667
676
|
status: "open",
|
|
668
677
|
accent: "#ec4899",
|
|
@@ -675,6 +684,9 @@ function seed() {
|
|
|
675
684
|
channel: "email",
|
|
676
685
|
lastMessagePreview: "Re: Proposal \u2014 looks great, one question on pricing\u2026",
|
|
677
686
|
lastMessageAt: minutesAgo(base, 95),
|
|
687
|
+
// Read, never answered — zero unread and a customer still waiting. The
|
|
688
|
+
// exact case `unreadCount` alone gets wrong.
|
|
689
|
+
lastMessageDirection: "inbound",
|
|
678
690
|
unreadCount: 0,
|
|
679
691
|
status: "open",
|
|
680
692
|
assignedTo: "Sofia",
|
|
@@ -690,6 +702,7 @@ function seed() {
|
|
|
690
702
|
channel: "webchat",
|
|
691
703
|
lastMessagePreview: "Is anyone available to chat?",
|
|
692
704
|
lastMessageAt: minutesAgo(base, 140),
|
|
705
|
+
lastMessageDirection: "inbound",
|
|
693
706
|
unreadCount: 0,
|
|
694
707
|
status: "snoozed",
|
|
695
708
|
accent: "#f59e0b",
|
|
@@ -702,6 +715,7 @@ function seed() {
|
|
|
702
715
|
channel: "whatsapp",
|
|
703
716
|
lastMessagePreview: "Thank you! See you next week \u{1F64C}",
|
|
704
717
|
lastMessageAt: minutesAgo(base, 1440),
|
|
718
|
+
lastMessageDirection: "outbound",
|
|
705
719
|
unreadCount: 0,
|
|
706
720
|
status: "closed",
|
|
707
721
|
assignedTo: "You",
|
|
@@ -807,6 +821,10 @@ function createMockConversationsProvider(config) {
|
|
|
807
821
|
channel: input.channel,
|
|
808
822
|
lastMessagePreview: firstMessage ?? "",
|
|
809
823
|
lastMessageAt: now,
|
|
824
|
+
// A thread the house opened is not waiting on the house. Left undefined
|
|
825
|
+
// when there is no first message: nobody has spoken, so there is no
|
|
826
|
+
// last speaker to name, and `isWaitingOnUs` falls back to unread (0).
|
|
827
|
+
lastMessageDirection: firstMessage ? "outbound" : void 0,
|
|
810
828
|
unreadCount: 0,
|
|
811
829
|
status: "open",
|
|
812
830
|
assignedTo: selfAuthor,
|
|
@@ -844,6 +862,7 @@ function createMockConversationsProvider(config) {
|
|
|
844
862
|
if (conv) {
|
|
845
863
|
conv.lastMessagePreview = input.body;
|
|
846
864
|
conv.lastMessageAt = created.at;
|
|
865
|
+
conv.lastMessageDirection = "outbound";
|
|
847
866
|
conv.unreadCount = 0;
|
|
848
867
|
if (conv.status === "closed") conv.status = "open";
|
|
849
868
|
}
|
|
@@ -884,6 +903,10 @@ function mapConversation(r) {
|
|
|
884
903
|
channel: r.channel ?? "sms",
|
|
885
904
|
lastMessagePreview: r.last_message_preview ?? "",
|
|
886
905
|
lastMessageAt: r.last_message_at ?? "",
|
|
906
|
+
// Kept current by the trigger in migration 001, never written from here:
|
|
907
|
+
// the column has one writer on purpose (last_message_preview/at have five,
|
|
908
|
+
// which is the mistake this one was not going to repeat).
|
|
909
|
+
lastMessageDirection: r.last_message_direction ?? void 0,
|
|
887
910
|
unreadCount: Number(r.unread_count ?? 0),
|
|
888
911
|
status: r.status ?? "open",
|
|
889
912
|
assignedTo: r.assigned_to ?? void 0,
|
|
@@ -1190,7 +1213,36 @@ var en = {
|
|
|
1190
1213
|
"conversations.new.cancel": "Cancel",
|
|
1191
1214
|
"conversations.new.create": "Start conversation",
|
|
1192
1215
|
"conversations.new.creating": "Starting\u2026",
|
|
1193
|
-
"conversations.new.createFailed": "Could not create the conversation"
|
|
1216
|
+
"conversations.new.createFailed": "Could not create the conversation",
|
|
1217
|
+
// Dashboard — the queue of people waiting on a reply
|
|
1218
|
+
"conversations.dashboard.waiting": "Waiting on you",
|
|
1219
|
+
"conversations.dashboard.waitingOldest": "Oldest waiting {{time}}",
|
|
1220
|
+
"conversations.dashboard.waitingNone": "Nobody waiting",
|
|
1221
|
+
"conversations.dashboard.openThreads": "Open conversations",
|
|
1222
|
+
"conversations.dashboard.openThreadsSub": "{{count}} waiting on a reply",
|
|
1223
|
+
"conversations.dashboard.needsYou": "Needs you",
|
|
1224
|
+
"conversations.dashboard.needsYouEmpty": "Nobody waiting. Inbox is clear.",
|
|
1225
|
+
"conversations.dashboard.who": "Who",
|
|
1226
|
+
"conversations.dashboard.waited": "Waiting",
|
|
1227
|
+
"conversations.waited.minutes": "{{count}} min",
|
|
1228
|
+
"conversations.waited.hours": "{{count}} h",
|
|
1229
|
+
"conversations.waited.days": "{{count}} d",
|
|
1230
|
+
// Inbox settings
|
|
1231
|
+
"conversations.settings.title": "Conversations",
|
|
1232
|
+
"conversations.settings.channels": "Your numbers",
|
|
1233
|
+
"conversations.settings.channelsHelp": "Where messages come in and go out. Connect and manage channels in the Integrations tab.",
|
|
1234
|
+
"conversations.settings.channelsEmpty": "No channel connected yet \u2014 until then the inbox stays empty. Connect WhatsApp in the Integrations tab.",
|
|
1235
|
+
"conversations.settings.channelsUnavailable": "Could not load your channels right now.",
|
|
1236
|
+
"conversations.settings.channelDedicated": "Your own number",
|
|
1237
|
+
"conversations.settings.channelShared": "Platform number",
|
|
1238
|
+
"conversations.settings.status.active": "Active",
|
|
1239
|
+
"conversations.settings.status.requested": "Requested",
|
|
1240
|
+
"conversations.settings.status.provisioning": "Provisioning",
|
|
1241
|
+
"conversations.settings.status.provisioned": "Provisioned",
|
|
1242
|
+
"conversations.settings.status.verifying": "Verifying",
|
|
1243
|
+
"conversations.settings.status.failed": "Failed",
|
|
1244
|
+
"conversations.settings.status.released": "Released",
|
|
1245
|
+
"conversations.settings.status.disconnected": "Disconnected"
|
|
1194
1246
|
};
|
|
1195
1247
|
|
|
1196
1248
|
// src/locales/pt-BR.ts
|
|
@@ -1253,7 +1305,36 @@ var ptBR = {
|
|
|
1253
1305
|
"conversations.new.cancel": "Cancelar",
|
|
1254
1306
|
"conversations.new.create": "Iniciar conversa",
|
|
1255
1307
|
"conversations.new.creating": "Iniciando\u2026",
|
|
1256
|
-
"conversations.new.createFailed": "N\xE3o foi poss\xEDvel criar a conversa"
|
|
1308
|
+
"conversations.new.createFailed": "N\xE3o foi poss\xEDvel criar a conversa",
|
|
1309
|
+
// Painel — a fila de quem está esperando resposta
|
|
1310
|
+
"conversations.dashboard.waiting": "Esperando resposta",
|
|
1311
|
+
"conversations.dashboard.waitingOldest": "A mais antiga h\xE1 {{time}}",
|
|
1312
|
+
"conversations.dashboard.waitingNone": "Ningu\xE9m na fila",
|
|
1313
|
+
"conversations.dashboard.openThreads": "Conversas abertas",
|
|
1314
|
+
"conversations.dashboard.openThreadsSub": "{{count}} esperando resposta",
|
|
1315
|
+
"conversations.dashboard.needsYou": "Precisa de voc\xEA",
|
|
1316
|
+
"conversations.dashboard.needsYouEmpty": "Ningu\xE9m esperando. Caixa de entrada em dia.",
|
|
1317
|
+
"conversations.dashboard.who": "Quem",
|
|
1318
|
+
"conversations.dashboard.waited": "Esperando h\xE1",
|
|
1319
|
+
"conversations.waited.minutes": "{{count}} min",
|
|
1320
|
+
"conversations.waited.hours": "{{count}} h",
|
|
1321
|
+
"conversations.waited.days": "{{count}} d",
|
|
1322
|
+
// Configurações da caixa de entrada
|
|
1323
|
+
"conversations.settings.title": "Conversas",
|
|
1324
|
+
"conversations.settings.channels": "Seus n\xFAmeros",
|
|
1325
|
+
"conversations.settings.channelsHelp": "Por onde a mensagem entra e sai. Ligue e gerencie os canais na aba Integra\xE7\xF5es.",
|
|
1326
|
+
"conversations.settings.channelsEmpty": "Nenhum canal ligado ainda \u2014 enquanto isso, a caixa de entrada fica vazia. Ligue o WhatsApp na aba Integra\xE7\xF5es.",
|
|
1327
|
+
"conversations.settings.channelsUnavailable": "N\xE3o foi poss\xEDvel consultar seus canais agora.",
|
|
1328
|
+
"conversations.settings.channelDedicated": "N\xFAmero s\xF3 seu",
|
|
1329
|
+
"conversations.settings.channelShared": "N\xFAmero da plataforma",
|
|
1330
|
+
"conversations.settings.status.active": "Ativo",
|
|
1331
|
+
"conversations.settings.status.requested": "Solicitado",
|
|
1332
|
+
"conversations.settings.status.provisioning": "Provisionando",
|
|
1333
|
+
"conversations.settings.status.provisioned": "Provisionado",
|
|
1334
|
+
"conversations.settings.status.verifying": "Verificando",
|
|
1335
|
+
"conversations.settings.status.failed": "Falhou",
|
|
1336
|
+
"conversations.settings.status.released": "Devolvido",
|
|
1337
|
+
"conversations.settings.status.disconnected": "Desconectado"
|
|
1257
1338
|
};
|
|
1258
1339
|
|
|
1259
1340
|
// src/locales/index.ts
|
|
@@ -1261,6 +1342,142 @@ var conversationsLocales = {
|
|
|
1261
1342
|
en,
|
|
1262
1343
|
"pt-BR": ptBR
|
|
1263
1344
|
};
|
|
1345
|
+
function useEnsureConversations() {
|
|
1346
|
+
const load = useConversationsStore((s) => s.load);
|
|
1347
|
+
const loaded = useConversationsStore((s) => s.conversations.length > 0);
|
|
1348
|
+
useEffect(() => {
|
|
1349
|
+
if (!loaded) void load();
|
|
1350
|
+
}, []);
|
|
1351
|
+
}
|
|
1352
|
+
function useWaiting() {
|
|
1353
|
+
useEnsureConversations();
|
|
1354
|
+
const conversations = useConversationsStore((s) => s.conversations);
|
|
1355
|
+
const loading = useConversationsStore((s) => s.loading);
|
|
1356
|
+
return useMemo(() => {
|
|
1357
|
+
const open = conversations.filter((c) => c.status === "open");
|
|
1358
|
+
const waiting = open.filter(isWaitingOnUs).sort((a, b) => Date.parse(a.lastMessageAt) - Date.parse(b.lastMessageAt));
|
|
1359
|
+
return { waiting, open: open.length, loading };
|
|
1360
|
+
}, [conversations, loading]);
|
|
1361
|
+
}
|
|
1362
|
+
function waitedFor(iso, t) {
|
|
1363
|
+
const ms = Date.now() - Date.parse(iso);
|
|
1364
|
+
if (!Number.isFinite(ms) || ms < 0) return "\u2014";
|
|
1365
|
+
const minutes = Math.floor(ms / 6e4);
|
|
1366
|
+
if (minutes < 60) return t("conversations.waited.minutes", { count: String(Math.max(minutes, 1)) });
|
|
1367
|
+
const hours = Math.floor(minutes / 60);
|
|
1368
|
+
if (hours < 24) return t("conversations.waited.hours", { count: String(hours) });
|
|
1369
|
+
return t("conversations.waited.days", { count: String(Math.floor(hours / 24)) });
|
|
1370
|
+
}
|
|
1371
|
+
function WaitingKpi() {
|
|
1372
|
+
const t = useTranslation();
|
|
1373
|
+
const { waiting, loading } = useWaiting();
|
|
1374
|
+
const oldest = waiting[0];
|
|
1375
|
+
return /* @__PURE__ */ jsx(
|
|
1376
|
+
KpiCard,
|
|
1377
|
+
{
|
|
1378
|
+
label: t("conversations.dashboard.waiting"),
|
|
1379
|
+
icon: "MessageCircleWarning",
|
|
1380
|
+
loading: loading && waiting.length === 0,
|
|
1381
|
+
value: String(waiting.length),
|
|
1382
|
+
sub: oldest ? t("conversations.dashboard.waitingOldest", { time: waitedFor(oldest.lastMessageAt, t) }) : t("conversations.dashboard.waitingNone"),
|
|
1383
|
+
invertTrend: true
|
|
1384
|
+
}
|
|
1385
|
+
);
|
|
1386
|
+
}
|
|
1387
|
+
function OpenThreadsKpi() {
|
|
1388
|
+
const t = useTranslation();
|
|
1389
|
+
const { open, waiting, loading } = useWaiting();
|
|
1390
|
+
return /* @__PURE__ */ jsx(
|
|
1391
|
+
KpiCard,
|
|
1392
|
+
{
|
|
1393
|
+
label: t("conversations.dashboard.openThreads"),
|
|
1394
|
+
icon: "MessagesSquare",
|
|
1395
|
+
loading: loading && open === 0,
|
|
1396
|
+
value: String(open),
|
|
1397
|
+
sub: t("conversations.dashboard.openThreadsSub", { count: String(waiting.length) })
|
|
1398
|
+
}
|
|
1399
|
+
);
|
|
1400
|
+
}
|
|
1401
|
+
function WaitingTable() {
|
|
1402
|
+
const t = useTranslation();
|
|
1403
|
+
const { waiting, loading } = useWaiting();
|
|
1404
|
+
const columns = useMemo(() => [
|
|
1405
|
+
{
|
|
1406
|
+
accessorKey: "contactName",
|
|
1407
|
+
header: t("conversations.dashboard.who"),
|
|
1408
|
+
cell: ({ row }) => /* @__PURE__ */ jsx("span", { className: "font-medium", children: row.original.contactName })
|
|
1409
|
+
},
|
|
1410
|
+
{
|
|
1411
|
+
id: "preview",
|
|
1412
|
+
header: "",
|
|
1413
|
+
// The message, not just the name: half of triage is deciding which of
|
|
1414
|
+
// five waiting people is the one worth answering first, and that is
|
|
1415
|
+
// decided by what they said.
|
|
1416
|
+
cell: ({ row }) => /* @__PURE__ */ jsx("span", { className: "line-clamp-1 text-xs text-muted-foreground", children: row.original.lastMessagePreview })
|
|
1417
|
+
},
|
|
1418
|
+
{
|
|
1419
|
+
id: "channel",
|
|
1420
|
+
header: t("conversations.contact.channel"),
|
|
1421
|
+
cell: ({ row }) => /* @__PURE__ */ jsx("span", { className: "text-xs text-muted-foreground", children: t(`conversations.filter.${row.original.channel}`) })
|
|
1422
|
+
},
|
|
1423
|
+
{
|
|
1424
|
+
id: "waited",
|
|
1425
|
+
header: t("conversations.dashboard.waited"),
|
|
1426
|
+
cell: ({ row }) => /* @__PURE__ */ jsx("span", { className: "whitespace-nowrap text-xs font-medium tabular-nums", children: waitedFor(row.original.lastMessageAt, t) })
|
|
1427
|
+
}
|
|
1428
|
+
], [t]);
|
|
1429
|
+
return /* @__PURE__ */ jsx(
|
|
1430
|
+
TableWidget,
|
|
1431
|
+
{
|
|
1432
|
+
title: t("conversations.dashboard.needsYou"),
|
|
1433
|
+
icon: "MessageCircleWarning",
|
|
1434
|
+
columns,
|
|
1435
|
+
data: waiting,
|
|
1436
|
+
loading: loading && waiting.length === 0,
|
|
1437
|
+
emptyMessage: t("conversations.dashboard.needsYouEmpty"),
|
|
1438
|
+
onRowClick: (row) => {
|
|
1439
|
+
window.location.hash = `/conversations?thread=${row.id}`;
|
|
1440
|
+
}
|
|
1441
|
+
}
|
|
1442
|
+
);
|
|
1443
|
+
}
|
|
1444
|
+
function createConversationsDashboardWidgets(ctx) {
|
|
1445
|
+
const withCtx = (Inner) => {
|
|
1446
|
+
const Wrapped = () => /* @__PURE__ */ jsx(ConversationsContextProvider, { store: ctx.store, config: ctx.config, children: /* @__PURE__ */ jsx(Inner, {}) });
|
|
1447
|
+
Wrapped.displayName = `ConversationsWidget(${Inner.displayName ?? Inner.name})`;
|
|
1448
|
+
return Wrapped;
|
|
1449
|
+
};
|
|
1450
|
+
return [
|
|
1451
|
+
// Visible on the home by DEFAULT, unlike most plugin widgets. The rest of
|
|
1452
|
+
// the registry is opt-in because a dashboard of everything is a dashboard
|
|
1453
|
+
// of nothing — but an unanswered customer is the one item on any home
|
|
1454
|
+
// screen that is time-critical, and hiding it behind Customize means it
|
|
1455
|
+
// only reaches the people who already knew to look.
|
|
1456
|
+
defineKpiWidget({
|
|
1457
|
+
id: "conversations.kpi.waiting",
|
|
1458
|
+
title: "conversations.dashboard.waiting",
|
|
1459
|
+
domain: "conversations",
|
|
1460
|
+
defaultOrder: -1,
|
|
1461
|
+
component: withCtx(WaitingKpi)
|
|
1462
|
+
}),
|
|
1463
|
+
defineKpiWidget({
|
|
1464
|
+
id: "conversations.kpi.open",
|
|
1465
|
+
title: "conversations.dashboard.openThreads",
|
|
1466
|
+
domain: "conversations",
|
|
1467
|
+
defaultOrder: 4,
|
|
1468
|
+
defaultVisible: false,
|
|
1469
|
+
component: withCtx(OpenThreadsKpi)
|
|
1470
|
+
}),
|
|
1471
|
+
defineTableWidget({
|
|
1472
|
+
id: "conversations.table.waiting",
|
|
1473
|
+
title: "conversations.dashboard.needsYou",
|
|
1474
|
+
domain: "conversations",
|
|
1475
|
+
span: 2,
|
|
1476
|
+
defaultOrder: 10,
|
|
1477
|
+
component: withCtx(WaitingTable)
|
|
1478
|
+
})
|
|
1479
|
+
];
|
|
1480
|
+
}
|
|
1264
1481
|
function mapChannel(r) {
|
|
1265
1482
|
return {
|
|
1266
1483
|
id: String(r.id),
|
|
@@ -1323,6 +1540,127 @@ function findFallbackChannel(channels) {
|
|
|
1323
1540
|
function canClaimDedicated(channels, tenantId) {
|
|
1324
1541
|
return findDedicatedChannel(channels, tenantId) === null;
|
|
1325
1542
|
}
|
|
1543
|
+
function channelLabel(account, t) {
|
|
1544
|
+
const key = `conversations.filter.${account.channel}`;
|
|
1545
|
+
const translated = t(key);
|
|
1546
|
+
return translated === key ? account.channel : translated;
|
|
1547
|
+
}
|
|
1548
|
+
function ChannelRow({ account }) {
|
|
1549
|
+
const t = useTranslation();
|
|
1550
|
+
const pending = isPendingChannelStatus(account.status);
|
|
1551
|
+
return /* @__PURE__ */ jsxs("div", { className: "flex items-center justify-between gap-3 border-b py-2.5 last:border-b-0", children: [
|
|
1552
|
+
/* @__PURE__ */ jsxs("div", { className: "min-w-0", children: [
|
|
1553
|
+
/* @__PURE__ */ jsx("p", { className: "truncate text-sm font-medium", children: account.phoneE164 ?? account.displayName ?? channelLabel(account, t) }),
|
|
1554
|
+
/* @__PURE__ */ jsxs("p", { className: "text-xs text-muted-foreground", children: [
|
|
1555
|
+
channelLabel(account, t),
|
|
1556
|
+
" \xB7 ",
|
|
1557
|
+
t(
|
|
1558
|
+
account.kind === "dedicated" ? "conversations.settings.channelDedicated" : "conversations.settings.channelShared"
|
|
1559
|
+
)
|
|
1560
|
+
] })
|
|
1561
|
+
] }),
|
|
1562
|
+
/* @__PURE__ */ jsx(Badge, { variant: account.status === "active" ? "default" : pending ? "secondary" : "outline", children: t(`conversations.settings.status.${account.status}`) === `conversations.settings.status.${account.status}` ? account.status : t(`conversations.settings.status.${account.status}`) })
|
|
1563
|
+
] });
|
|
1564
|
+
}
|
|
1565
|
+
function ConversationsGeneralSettings() {
|
|
1566
|
+
const t = useTranslation();
|
|
1567
|
+
const [channels, setChannels] = React4.useState(null);
|
|
1568
|
+
const [failed, setFailed] = React4.useState(false);
|
|
1569
|
+
React4.useEffect(() => {
|
|
1570
|
+
let cancelled = false;
|
|
1571
|
+
listMessagingChannels().then((rows) => {
|
|
1572
|
+
if (!cancelled) setChannels(rows);
|
|
1573
|
+
}).catch(() => {
|
|
1574
|
+
if (!cancelled) {
|
|
1575
|
+
setFailed(true);
|
|
1576
|
+
setChannels([]);
|
|
1577
|
+
}
|
|
1578
|
+
});
|
|
1579
|
+
return () => {
|
|
1580
|
+
cancelled = true;
|
|
1581
|
+
};
|
|
1582
|
+
}, []);
|
|
1583
|
+
return /* @__PURE__ */ jsx("div", { className: "space-y-6", children: /* @__PURE__ */ jsxs(
|
|
1584
|
+
SettingsGroup,
|
|
1585
|
+
{
|
|
1586
|
+
title: t("conversations.settings.channels"),
|
|
1587
|
+
description: t("conversations.settings.channelsHelp"),
|
|
1588
|
+
children: [
|
|
1589
|
+
channels === null && /* @__PURE__ */ jsx("p", { className: "py-2 text-sm text-muted-foreground", children: t("conversations.list.loading") }),
|
|
1590
|
+
channels !== null && failed && /* @__PURE__ */ jsx("p", { className: "py-2 text-sm text-muted-foreground", children: t("conversations.settings.channelsUnavailable") }),
|
|
1591
|
+
channels !== null && !failed && channels.length === 0 && /* @__PURE__ */ jsx("p", { className: "py-2 text-sm text-muted-foreground", children: t("conversations.settings.channelsEmpty") }),
|
|
1592
|
+
channels?.map((account) => /* @__PURE__ */ jsx(ChannelRow, { account }, account.id))
|
|
1593
|
+
]
|
|
1594
|
+
}
|
|
1595
|
+
) });
|
|
1596
|
+
}
|
|
1597
|
+
ConversationsGeneralSettings.displayName = "ConversationsGeneralSettings";
|
|
1598
|
+
function ConversationsSettingsTab() {
|
|
1599
|
+
const t = useTranslation();
|
|
1600
|
+
return /* @__PURE__ */ jsx(
|
|
1601
|
+
PluginSettingsPanel,
|
|
1602
|
+
{
|
|
1603
|
+
title: t("conversations.settings.title"),
|
|
1604
|
+
generalSettings: /* @__PURE__ */ jsx(ConversationsGeneralSettings, {}),
|
|
1605
|
+
hostPluginId: "conversations",
|
|
1606
|
+
routeBase: "/settings/conversations"
|
|
1607
|
+
}
|
|
1608
|
+
);
|
|
1609
|
+
}
|
|
1610
|
+
ConversationsSettingsTab.displayName = "ConversationsSettingsTab";
|
|
1611
|
+
function buildConversationsOnboarding() {
|
|
1612
|
+
return {
|
|
1613
|
+
id: "conversations",
|
|
1614
|
+
title: "Quem fala com voc\xEA",
|
|
1615
|
+
order: 30,
|
|
1616
|
+
role: "frontdesk",
|
|
1617
|
+
instructions: `Voc\xEA est\xE1 ligando a caixa de entrada. O risco aqui n\xE3o \xE9 a pessoa n\xE3o achar a tela \u2014 \xE9 ela achar a tela, ver que est\xE1 vazia e concluir que o produto n\xE3o funciona.
|
|
1618
|
+
|
|
1619
|
+
- Comece pelo CANAL. Sem n\xFAmero ligado n\xE3o entra mensagem nenhuma, e todo o resto do m\xF3dulo \xE9 decora\xE7\xE3o. O caminho \xE9 Configura\xE7\xF5es \u203A Conversas \u203A Integra\xE7\xF5es.
|
|
1620
|
+
- Explique a diferen\xE7a entre o n\xFAmero da plataforma e um n\xFAmero dedicado ANTES de ela escolher: no compartilhado ela j\xE1 come\xE7a a receber hoje; no dedicado sai o nome dela, mas ainda passa por um cadastro na Meta.
|
|
1621
|
+
- O segundo passo \xE9 a prova: pe\xE7a para ela mandar uma mensagem do pr\xF3prio celular para o n\xFAmero e responder daqui. \xC9 o que mostra que ida e volta funcionam.
|
|
1622
|
+
- Se ela j\xE1 tem WhatsApp Business no celular, diga com todas as letras o que muda: as conversas passam a ficar aqui, onde a equipe inteira v\xEA, em vez de num aparelho s\xF3.`,
|
|
1623
|
+
steps: [
|
|
1624
|
+
{
|
|
1625
|
+
key: "channel",
|
|
1626
|
+
title: "Ligue o canal onde seu cliente j\xE1 fala",
|
|
1627
|
+
description: "Sem n\xFAmero conectado, a caixa de entrada abre vazia para sempre.",
|
|
1628
|
+
icon: "MessageCircle",
|
|
1629
|
+
route: "/settings/conversations/_integrations",
|
|
1630
|
+
priority: "urgent",
|
|
1631
|
+
// O passo É a tela: um app que monte o inbox sem a rota de configurações
|
|
1632
|
+
// não tem onde ligar canal nenhum, e a linha ficaria aberta para sempre.
|
|
1633
|
+
requiresRoute: true,
|
|
1634
|
+
agentHint: "Abra Integra\xE7\xF5es e apresente as duas op\xE7\xF5es pelo que elas custam \xE0 pessoa HOJE: o n\xFAmero da plataforma come\xE7a a receber agora, o dedicado sai com o nome dela mas espera o cadastro na Meta.",
|
|
1635
|
+
howTo: "Configura\xE7\xF5es \u203A Conversas \u203A Integra\xE7\xF5es \u203A Tyxter. O n\xFAmero da plataforma j\xE1 est\xE1 ligado e \xE9 compartilhado com outros neg\xF3cios \u2014 d\xE1 para come\xE7ar a receber por ele hoje. Um n\xFAmero dedicado sai s\xF3 com o seu nome, libera os seus pr\xF3prios modelos de mensagem e tem limite de envio pr\xF3prio; ele \xE9 alugado no m\xEAs e ainda passa por um cadastro r\xE1pido na Meta antes do primeiro envio.",
|
|
1636
|
+
celebrate: "Canal ligado \u2014 agora d\xE1 para te acharem \u{1F4F2}",
|
|
1637
|
+
script: [
|
|
1638
|
+
{ kind: "goto", route: "/settings/conversations/_integrations" },
|
|
1639
|
+
{
|
|
1640
|
+
kind: "ask",
|
|
1641
|
+
text: "Estamos nas Integra\xE7\xF5es da caixa de entrada. Mostre o n\xFAmero da plataforma e o dedicado, explique o que muda em cada um e ajude a pessoa a escolher."
|
|
1642
|
+
}
|
|
1643
|
+
],
|
|
1644
|
+
check: async () => await countByTenant(T.channels) > 0
|
|
1645
|
+
},
|
|
1646
|
+
{
|
|
1647
|
+
key: "first-reply",
|
|
1648
|
+
title: "Receba e responda a primeira mensagem",
|
|
1649
|
+
description: "A prova de que a ida e a volta funcionam.",
|
|
1650
|
+
icon: "Send",
|
|
1651
|
+
route: "/conversations",
|
|
1652
|
+
priority: "high",
|
|
1653
|
+
agentHint: "N\xE3o \xE9 passo de cadastro. Pe\xE7a para a pessoa mandar uma mensagem do pr\xF3prio celular para o n\xFAmero ligado e responder aqui de dentro \u2014 e fique com ela at\xE9 a resposta chegar do outro lado.",
|
|
1654
|
+
howTo: "Pegue o seu celular, mande uma mensagem qualquer para o n\xFAmero que voc\xEA acabou de ligar e volte aqui: a conversa aparece em Conversas. Responda por aqui e confira que chegou no celular. \xC9 o teste que prova que a caixa de entrada est\xE1 no ar \u2014 e \xE9 tamb\xE9m como a equipe vai trabalhar todo dia, com a conversa num lugar que todo mundo v\xEA em vez de num aparelho s\xF3.",
|
|
1655
|
+
celebrate: "Ida e volta funcionando \u{1F4AC}",
|
|
1656
|
+
// A conversa existe assim que a primeira mensagem entra, então a
|
|
1657
|
+
// contagem de threads é a checagem — e não a de mensagens, que seria
|
|
1658
|
+
// fechada por um envio que ninguém respondeu.
|
|
1659
|
+
check: async () => await countByTenant(T.conversations) > 0
|
|
1660
|
+
}
|
|
1661
|
+
]
|
|
1662
|
+
};
|
|
1663
|
+
}
|
|
1326
1664
|
var TYXTER_NUMBER_CLAIM_FUNCTION = "tyxter-number-claim";
|
|
1327
1665
|
function client() {
|
|
1328
1666
|
const supabase = getSupabaseClientOptional();
|
|
@@ -1728,433 +2066,186 @@ var tyxterConnectorDef = {
|
|
|
1728
2066
|
};
|
|
1729
2067
|
|
|
1730
2068
|
// src/migrations/index.ts
|
|
1731
|
-
var
|
|
1732
|
-
-- plugin-conversations
|
|
1733
|
-
-- Instagram / Email / Web chat). Prefix: plg_conversations / plg_conversation_messages.
|
|
1734
|
-
-- \xA71 plg_conversations \u2014 one thread per contact+channel
|
|
1735
|
-
-- \xA72 plg_conversation_messages \u2014 inbound/outbound messages within a thread
|
|
1736
|
-
-- \xA73 RLS: authenticated tenant-scoped CRUD on both tables + GRANTs
|
|
2069
|
+
var MIGRATION_000_BASELINE = `-- ============================================================================
|
|
2070
|
+
-- plugins/plugin-conversations/src/migrations/000_baseline.sql \u2014 what installing this provisions, as one file.
|
|
1737
2071
|
--
|
|
1738
|
-
--
|
|
1739
|
-
--
|
|
1740
|
-
--
|
|
1741
|
-
-- Idempotent + safe to re-run.
|
|
1742
|
-
-- ============================================================================
|
|
1743
|
-
|
|
1744
|
-
-- \xA71 \u2014 conversations (threads)
|
|
1745
|
-
CREATE TABLE IF NOT EXISTS public.plg_conversations (
|
|
1746
|
-
id uuid PRIMARY KEY DEFAULT gen_random_uuid(),
|
|
1747
|
-
tenant_id uuid NOT NULL REFERENCES public.tenants(id) ON DELETE CASCADE,
|
|
1748
|
-
contact_name text NOT NULL,
|
|
1749
|
-
contact_handle text,
|
|
1750
|
-
channel text NOT NULL
|
|
1751
|
-
CHECK (channel IN ('sms', 'whatsapp', 'instagram', 'email', 'webchat')),
|
|
1752
|
-
last_message_preview text,
|
|
1753
|
-
last_message_at timestamptz DEFAULT now(),
|
|
1754
|
-
unread_count int DEFAULT 0,
|
|
1755
|
-
status text DEFAULT 'open'
|
|
1756
|
-
CHECK (status IN ('open', 'snoozed', 'closed')),
|
|
1757
|
-
assigned_to text,
|
|
1758
|
-
accent text,
|
|
1759
|
-
tags text[],
|
|
1760
|
-
location text,
|
|
1761
|
-
note text,
|
|
1762
|
-
created_at timestamptz NOT NULL DEFAULT now(),
|
|
1763
|
-
updated_at timestamptz NOT NULL DEFAULT now()
|
|
1764
|
-
);
|
|
1765
|
-
ALTER TABLE public.plg_conversations ENABLE ROW LEVEL SECURITY;
|
|
1766
|
-
CREATE INDEX IF NOT EXISTS idx_plg_conversations_tenant ON public.plg_conversations(tenant_id);
|
|
1767
|
-
CREATE INDEX IF NOT EXISTS idx_plg_conversations_tenant_recent ON public.plg_conversations(tenant_id, last_message_at DESC);
|
|
1768
|
-
|
|
1769
|
-
-- \xA72 \u2014 messages
|
|
1770
|
-
CREATE TABLE IF NOT EXISTS public.plg_conversation_messages (
|
|
1771
|
-
id uuid PRIMARY KEY DEFAULT gen_random_uuid(),
|
|
1772
|
-
tenant_id uuid NOT NULL REFERENCES public.tenants(id) ON DELETE CASCADE,
|
|
1773
|
-
conversation_id uuid NOT NULL REFERENCES public.plg_conversations(id) ON DELETE CASCADE,
|
|
1774
|
-
channel text
|
|
1775
|
-
CHECK (channel IS NULL OR channel IN ('sms', 'whatsapp', 'instagram', 'email', 'webchat')),
|
|
1776
|
-
direction text
|
|
1777
|
-
CHECK (direction IN ('inbound', 'outbound')),
|
|
1778
|
-
body text NOT NULL,
|
|
1779
|
-
author text,
|
|
1780
|
-
at timestamptz DEFAULT now()
|
|
1781
|
-
);
|
|
1782
|
-
ALTER TABLE public.plg_conversation_messages ENABLE ROW LEVEL SECURITY;
|
|
1783
|
-
CREATE INDEX IF NOT EXISTS idx_plg_conversation_messages_thread ON public.plg_conversation_messages(conversation_id, at);
|
|
1784
|
-
|
|
1785
|
-
-- \xA73 \u2014 RLS: authenticated tenant CRUD (the inbox reads/writes here)
|
|
1786
|
-
DROP POLICY IF EXISTS plg_conversations_select ON public.plg_conversations;
|
|
1787
|
-
DROP POLICY IF EXISTS plg_conversations_insert ON public.plg_conversations;
|
|
1788
|
-
DROP POLICY IF EXISTS plg_conversations_update ON public.plg_conversations;
|
|
1789
|
-
DROP POLICY IF EXISTS plg_conversations_delete ON public.plg_conversations;
|
|
1790
|
-
CREATE POLICY plg_conversations_select ON public.plg_conversations FOR SELECT TO authenticated USING (tenant_id IN (SELECT public.user_tenant_ids()));
|
|
1791
|
-
CREATE POLICY plg_conversations_insert ON public.plg_conversations FOR INSERT TO authenticated WITH CHECK (tenant_id IN (SELECT public.user_tenant_ids()));
|
|
1792
|
-
CREATE POLICY plg_conversations_update ON public.plg_conversations FOR UPDATE TO authenticated USING (tenant_id IN (SELECT public.user_tenant_ids()));
|
|
1793
|
-
CREATE POLICY plg_conversations_delete ON public.plg_conversations FOR DELETE TO authenticated USING (tenant_id IN (SELECT public.user_tenant_ids()));
|
|
1794
|
-
GRANT SELECT, INSERT, UPDATE, DELETE ON public.plg_conversations TO authenticated;
|
|
1795
|
-
|
|
1796
|
-
DROP POLICY IF EXISTS plg_conversation_messages_select ON public.plg_conversation_messages;
|
|
1797
|
-
DROP POLICY IF EXISTS plg_conversation_messages_insert ON public.plg_conversation_messages;
|
|
1798
|
-
DROP POLICY IF EXISTS plg_conversation_messages_update ON public.plg_conversation_messages;
|
|
1799
|
-
DROP POLICY IF EXISTS plg_conversation_messages_delete ON public.plg_conversation_messages;
|
|
1800
|
-
CREATE POLICY plg_conversation_messages_select ON public.plg_conversation_messages FOR SELECT TO authenticated USING (tenant_id IN (SELECT public.user_tenant_ids()));
|
|
1801
|
-
CREATE POLICY plg_conversation_messages_insert ON public.plg_conversation_messages FOR INSERT TO authenticated WITH CHECK (tenant_id IN (SELECT public.user_tenant_ids()));
|
|
1802
|
-
CREATE POLICY plg_conversation_messages_update ON public.plg_conversation_messages FOR UPDATE TO authenticated USING (tenant_id IN (SELECT public.user_tenant_ids()));
|
|
1803
|
-
CREATE POLICY plg_conversation_messages_delete ON public.plg_conversation_messages FOR DELETE TO authenticated USING (tenant_id IN (SELECT public.user_tenant_ids()));
|
|
1804
|
-
GRANT SELECT, INSERT, UPDATE, DELETE ON public.plg_conversation_messages TO authenticated;
|
|
1805
|
-
`;
|
|
1806
|
-
var MIGRATION_002_CONTACT_PERSON = `-- ============================================================================
|
|
1807
|
-
-- plugin-conversations 002: link a thread to a REAL person record.
|
|
2072
|
+
-- GENERATED by scripts/emit-unit-baselines.mjs (#338). Do not edit by hand:
|
|
2073
|
+
-- change the schema, regenerate, and let scripts/check-chain-equivalence.mjs
|
|
2074
|
+
-- prove the result still matches.
|
|
1808
2075
|
--
|
|
1809
|
-
--
|
|
1810
|
-
--
|
|
1811
|
-
--
|
|
1812
|
-
-- public.people) now resolves a person, and this column stores that link.
|
|
2076
|
+
-- This replaced 9 migration files. They are kept, unapplied, in
|
|
2077
|
+
-- migrations.archive/ \u2014 a year of decisions is worth reading even when it is no
|
|
2078
|
+
-- longer worth replaying.
|
|
1813
2079
|
--
|
|
1814
|
-
--
|
|
1815
|
-
--
|
|
1816
|
-
--
|
|
1817
|
-
--
|
|
1818
|
-
-- ON DELETE SET NULL: deleting a person must never take their history with it.
|
|
1819
|
-
-- Idempotent + safe to re-run.
|
|
2080
|
+
-- Object order is fixed rather than dependency-sorted: schemas, types,
|
|
2081
|
+
-- sequences, tables, defaults, functions, constraints, views, indexes, foreign
|
|
2082
|
+
-- keys, triggers, row security, policies, grants. Dependencies BETWEEN units
|
|
2083
|
+
-- are carried by the order packages/db/chain.json declares.
|
|
1820
2084
|
-- ============================================================================
|
|
1821
2085
|
|
|
1822
|
-
|
|
1823
|
-
|
|
2086
|
+
-- A LANGUAGE sql function binds its body at CREATE time, and 570 functions
|
|
2087
|
+
-- sorted by name are not sorted by who calls whom. This is what lets them load
|
|
2088
|
+
-- in any order; every body is still compiled on first call, so a genuinely
|
|
2089
|
+
-- broken reference surfaces then rather than never. Session-scoped.
|
|
2090
|
+
SET check_function_bodies = false;
|
|
1824
2091
|
|
|
1825
|
-
CREATE INDEX IF NOT EXISTS idx_plg_conversations_person
|
|
1826
|
-
ON public.plg_conversations(tenant_id, contact_person_id)
|
|
1827
|
-
WHERE contact_person_id IS NOT NULL;
|
|
1828
|
-
`;
|
|
1829
|
-
var MIGRATION_003_CHANNELS = `-- ============================================================================
|
|
1830
|
-
-- plugin-conversations 003: channel accounts \u2014 WHICH number a message leaves by.
|
|
1831
|
-
--
|
|
1832
|
-
-- The inbox has always been the read/compose surface for rows a connector
|
|
1833
|
-
-- delivered out-of-band. This is the first table that says where those rows
|
|
1834
|
-
-- come FROM, and it exists because WhatsApp has two number models at once:
|
|
1835
|
-
--
|
|
1836
|
-
-- \u2022 kind 'fallback' \u2014 the product's own number, shared by every tenant.
|
|
1837
|
-
-- tenant_id IS NULL: it belongs to the software, not to a salon, and a
|
|
1838
|
-
-- tenant column with a made-up value there would make every read lie.
|
|
1839
|
-
-- \u2022 kind 'dedicated' \u2014 a number rented for one tenant. Once it is \`active\`
|
|
1840
|
-
-- it wins over the fallback; until then the fallback keeps carrying.
|
|
1841
|
-
--
|
|
1842
|
-
-- Provider-neutral on purpose (\`provider\`, \`provider_number_id\`): the sender
|
|
1843
|
-
-- model is a WhatsApp fact, not a Tyxter fact, and the second messaging
|
|
1844
|
-
-- provider must not need a second table.
|
|
1845
|
-
--
|
|
1846
|
-
-- Writes are SERVICE-ROLE ONLY. Every row is created by a webhook or by an
|
|
1847
|
-
-- edge function acting on the provider's answer \u2014 a member who could edit
|
|
1848
|
-
-- \`provider_number_id\` could point another tenant's replies at their own inbox.
|
|
1849
|
-
-- Members read, and the fallback row is readable by all of them because it is
|
|
1850
|
-
-- the number their own messages go out on.
|
|
1851
|
-
-- Idempotent + safe to re-run.
|
|
1852
|
-
-- ============================================================================
|
|
1853
2092
|
|
|
1854
|
-
CREATE TABLE IF NOT EXISTS public.plg_conversations_channels (
|
|
1855
|
-
id uuid PRIMARY KEY DEFAULT gen_random_uuid(),
|
|
1856
|
-
-- NULL = the product fallback row. Not a missing value: an owner.
|
|
1857
|
-
tenant_id uuid REFERENCES public.tenants(id) ON DELETE CASCADE,
|
|
1858
|
-
channel text NOT NULL DEFAULT 'whatsapp'
|
|
1859
|
-
CHECK (channel IN ('sms', 'whatsapp', 'instagram', 'email', 'webchat')),
|
|
1860
|
-
provider text NOT NULL,
|
|
1861
|
-
-- The provider's handle for the number ('pn_\u2026'); what a webhook routes on.
|
|
1862
|
-
provider_number_id text NOT NULL,
|
|
1863
|
-
phone_e164 text,
|
|
1864
|
-
kind text NOT NULL DEFAULT 'dedicated'
|
|
1865
|
-
CHECK (kind IN ('fallback', 'dedicated')),
|
|
1866
|
-
-- The provider's own lifecycle, kept verbatim so a status webhook never has
|
|
1867
|
-
-- to be translated into a vocabulary of ours that means slightly less.
|
|
1868
|
-
status text NOT NULL DEFAULT 'requested'
|
|
1869
|
-
CHECK (status IN ('requested', 'provisioning', 'provisioned', 'verifying',
|
|
1870
|
-
'active', 'failed', 'released', 'disconnected')),
|
|
1871
|
-
display_name text,
|
|
1872
|
-
metadata jsonb NOT NULL DEFAULT '{}'::jsonb,
|
|
1873
|
-
created_at timestamptz NOT NULL DEFAULT now(),
|
|
1874
|
-
updated_at timestamptz NOT NULL DEFAULT now()
|
|
1875
|
-
);
|
|
1876
|
-
ALTER TABLE public.plg_conversations_channels ENABLE ROW LEVEL SECURITY;
|
|
1877
2093
|
|
|
1878
|
-
--
|
|
1879
|
-
--
|
|
1880
|
-
|
|
1881
|
-
|
|
1882
|
-
|
|
1883
|
-
-- At most one fallback per (channel, provider). Sender resolution falls back to
|
|
1884
|
-
-- "the product's number", singular \u2014 two of them is an ambiguity no caller can
|
|
1885
|
-
-- resolve, and it would surface as messages leaving by the wrong one.
|
|
1886
|
-
CREATE UNIQUE INDEX IF NOT EXISTS uq_plg_conversations_channels_fallback
|
|
1887
|
-
ON public.plg_conversations_channels(channel, provider)
|
|
1888
|
-
WHERE tenant_id IS NULL AND kind = 'fallback';
|
|
1889
|
-
|
|
1890
|
-
CREATE INDEX IF NOT EXISTS idx_plg_conversations_channels_tenant
|
|
1891
|
-
ON public.plg_conversations_channels(tenant_id, channel, status)
|
|
1892
|
-
WHERE tenant_id IS NOT NULL;
|
|
1893
|
-
|
|
1894
|
-
-- RLS: members read their own rows AND the product fallback (it is the number
|
|
1895
|
-
-- they send from, so hiding it would leave the panel with nothing to say).
|
|
1896
|
-
-- No INSERT/UPDATE/DELETE policy and no write GRANT: service role only.
|
|
1897
|
-
DROP POLICY IF EXISTS plg_conversations_channels_select ON public.plg_conversations_channels;
|
|
1898
|
-
CREATE POLICY plg_conversations_channels_select ON public.plg_conversations_channels
|
|
1899
|
-
FOR SELECT TO authenticated
|
|
1900
|
-
USING (tenant_id IS NULL OR tenant_id IN (SELECT public.user_tenant_ids()));
|
|
1901
|
-
GRANT SELECT ON public.plg_conversations_channels TO authenticated;
|
|
1902
|
-
`;
|
|
1903
|
-
var MIGRATION_004_MESSAGE_DELIVERY = `-- ============================================================================
|
|
1904
|
-
-- plugin-conversations 004: what the provider did with a message, and who wrote
|
|
1905
|
-
-- it. Four columns, ported from the legacy beautyplace inbox.
|
|
1906
|
-
--
|
|
1907
|
-
-- \`direction\` + \`author\` was enough while every row was typed by a person in
|
|
1908
|
-
-- this app. It stops being enough the moment a message is sent by a connector:
|
|
1909
|
-
--
|
|
1910
|
-
-- provider_message_id the provider's handle for the row. A delivery webhook
|
|
1911
|
-
-- names only this, so without it a status can never be applied to anything.
|
|
1912
|
-
-- UNIQUE (where present) so an at-least-once webhook that redelivers the
|
|
1913
|
-
-- same inbound message cannot open a second copy of it in the thread.
|
|
1914
|
-
-- delivery_status the last thing the provider said, applied MONOTONICALLY
|
|
1915
|
-
-- by the webhook handler \u2014 'read' must not be overwritten by a 'delivered'
|
|
1916
|
-
-- that arrived late (they are unordered).
|
|
1917
|
-
-- sender_kind person / app / assistant / automation. The backbone of
|
|
1918
|
-
-- a shared inbox: legacy proved that a human and an AI writing into the same
|
|
1919
|
-
-- thread with no attribution is a thread nobody trusts.
|
|
1920
|
-
-- sender_label who exactly, in words \u2014 "Ana", "Lembrete de hor\xE1rio".
|
|
1921
|
-
--
|
|
1922
|
-
-- Nullable throughout: rows written before this migration are not wrong, they
|
|
1923
|
-
-- are simply from a time when only people wrote here.
|
|
1924
|
-
-- Idempotent + safe to re-run.
|
|
1925
|
-
-- ============================================================================
|
|
2094
|
+
-- \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
|
|
2095
|
+
-- Cleared BEFORE anything is created, so every object below gets the ACL the
|
|
2096
|
+
-- dump describes rather than the image's defaults on top of it. Restored at
|
|
2097
|
+
-- the end of this file.
|
|
1926
2098
|
|
|
1927
|
-
|
|
1928
|
-
|
|
1929
|
-
|
|
1930
|
-
|
|
1931
|
-
|
|
2099
|
+
DO $dp$ BEGIN
|
|
2100
|
+
EXECUTE $stmt$ALTER DEFAULT PRIVILEGES FOR ROLE postgres IN SCHEMA public REVOKE ALL ON SEQUENCES FROM anon, authenticated, service_role, fayz_connector$stmt$;
|
|
2101
|
+
EXCEPTION WHEN insufficient_privilege OR undefined_object THEN
|
|
2102
|
+
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$;
|
|
2103
|
+
END $dp$;
|
|
1932
2104
|
|
|
1933
|
-
|
|
1934
|
-
|
|
1935
|
-
|
|
1936
|
-
|
|
1937
|
-
|
|
1938
|
-
SELECT 1 FROM pg_constraint WHERE conname = 'plg_conversation_messages_delivery_status_check'
|
|
1939
|
-
) THEN
|
|
1940
|
-
ALTER TABLE public.plg_conversation_messages
|
|
1941
|
-
ADD CONSTRAINT plg_conversation_messages_delivery_status_check
|
|
1942
|
-
CHECK (delivery_status IS NULL OR delivery_status IN
|
|
1943
|
-
('queued', 'sent', 'delivered', 'read', 'failed', 'expired'));
|
|
1944
|
-
END IF;
|
|
1945
|
-
IF NOT EXISTS (
|
|
1946
|
-
SELECT 1 FROM pg_constraint WHERE conname = 'plg_conversation_messages_sender_kind_check'
|
|
1947
|
-
) THEN
|
|
1948
|
-
ALTER TABLE public.plg_conversation_messages
|
|
1949
|
-
ADD CONSTRAINT plg_conversation_messages_sender_kind_check
|
|
1950
|
-
CHECK (sender_kind IS NULL OR sender_kind IN ('user', 'system', 'ai', 'automation'));
|
|
1951
|
-
END IF;
|
|
1952
|
-
END
|
|
1953
|
-
$$;
|
|
2105
|
+
DO $dp$ BEGIN
|
|
2106
|
+
EXECUTE $stmt$ALTER DEFAULT PRIVILEGES FOR ROLE supabase_admin IN SCHEMA public REVOKE ALL ON SEQUENCES FROM anon, authenticated, service_role, fayz_connector$stmt$;
|
|
2107
|
+
EXCEPTION WHEN insufficient_privilege OR undefined_object THEN
|
|
2108
|
+
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$;
|
|
2109
|
+
END $dp$;
|
|
1954
2110
|
|
|
1955
|
-
|
|
1956
|
-
ON
|
|
1957
|
-
|
|
1958
|
-
|
|
1959
|
-
|
|
1960
|
-
|
|
1961
|
-
|
|
1962
|
-
|
|
1963
|
-
|
|
1964
|
-
|
|
1965
|
-
|
|
1966
|
-
|
|
1967
|
-
|
|
1968
|
-
|
|
1969
|
-
|
|
1970
|
-
|
|
1971
|
-
|
|
1972
|
-
|
|
1973
|
-
|
|
1974
|
-
|
|
1975
|
-
|
|
1976
|
-
|
|
1977
|
-
|
|
1978
|
-
|
|
1979
|
-
|
|
1980
|
-
--
|
|
1981
|
-
-- a real delivery be skipped as a duplicate.
|
|
1982
|
-
-- Idempotent + safe to re-run.
|
|
1983
|
-
-- ============================================================================
|
|
2111
|
+
DO $dp$ BEGIN
|
|
2112
|
+
EXECUTE $stmt$ALTER DEFAULT PRIVILEGES FOR ROLE postgres IN SCHEMA public REVOKE ALL ON FUNCTIONS FROM anon, authenticated, service_role, fayz_connector$stmt$;
|
|
2113
|
+
EXCEPTION WHEN insufficient_privilege OR undefined_object THEN
|
|
2114
|
+
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$;
|
|
2115
|
+
END $dp$;
|
|
2116
|
+
|
|
2117
|
+
DO $dp$ BEGIN
|
|
2118
|
+
EXECUTE $stmt$ALTER DEFAULT PRIVILEGES FOR ROLE supabase_admin IN SCHEMA public REVOKE ALL ON FUNCTIONS FROM anon, authenticated, service_role, fayz_connector$stmt$;
|
|
2119
|
+
EXCEPTION WHEN insufficient_privilege OR undefined_object THEN
|
|
2120
|
+
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$;
|
|
2121
|
+
END $dp$;
|
|
2122
|
+
|
|
2123
|
+
DO $dp$ BEGIN
|
|
2124
|
+
EXECUTE $stmt$ALTER DEFAULT PRIVILEGES FOR ROLE postgres IN SCHEMA public REVOKE ALL ON TABLES FROM anon, authenticated, service_role, fayz_connector$stmt$;
|
|
2125
|
+
EXCEPTION WHEN insufficient_privilege OR undefined_object THEN
|
|
2126
|
+
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$;
|
|
2127
|
+
END $dp$;
|
|
2128
|
+
|
|
2129
|
+
DO $dp$ BEGIN
|
|
2130
|
+
EXECUTE $stmt$ALTER DEFAULT PRIVILEGES FOR ROLE supabase_admin IN SCHEMA public REVOKE ALL ON TABLES FROM anon, authenticated, service_role, fayz_connector$stmt$;
|
|
2131
|
+
EXCEPTION WHEN insufficient_privilege OR undefined_object THEN
|
|
2132
|
+
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$;
|
|
2133
|
+
END $dp$;
|
|
2134
|
+
|
|
2135
|
+
|
|
2136
|
+
-- \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
|
|
1984
2137
|
|
|
1985
|
-
CREATE TABLE
|
|
1986
|
-
|
|
1987
|
-
|
|
1988
|
-
|
|
1989
|
-
|
|
1990
|
-
|
|
1991
|
-
|
|
2138
|
+
CREATE TABLE public.plg_conversation_messages (
|
|
2139
|
+
id uuid DEFAULT gen_random_uuid() NOT NULL,
|
|
2140
|
+
tenant_id uuid NOT NULL,
|
|
2141
|
+
conversation_id uuid NOT NULL,
|
|
2142
|
+
channel text,
|
|
2143
|
+
direction text,
|
|
2144
|
+
body text NOT NULL,
|
|
2145
|
+
author text,
|
|
2146
|
+
at timestamp with time zone DEFAULT now(),
|
|
2147
|
+
provider_message_id text,
|
|
2148
|
+
delivery_status text,
|
|
2149
|
+
sender_kind text,
|
|
2150
|
+
sender_label text,
|
|
2151
|
+
subject_type text,
|
|
2152
|
+
subject_id text,
|
|
2153
|
+
CONSTRAINT plg_conversation_messages_channel_check CHECK (((channel IS NULL) OR (channel = ANY (ARRAY['sms'::text, 'whatsapp'::text, 'instagram'::text, 'email'::text, 'webchat'::text])))),
|
|
2154
|
+
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])))),
|
|
2155
|
+
CONSTRAINT plg_conversation_messages_direction_check CHECK ((direction = ANY (ARRAY['inbound'::text, 'outbound'::text]))),
|
|
2156
|
+
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]))))
|
|
1992
2157
|
);
|
|
1993
|
-
ALTER TABLE public.plg_conversations_webhook_events ENABLE ROW LEVEL SECURITY;
|
|
1994
2158
|
|
|
1995
|
-
|
|
1996
|
-
|
|
1997
|
-
|
|
1998
|
-
|
|
1999
|
-
|
|
2000
|
-
|
|
2159
|
+
CREATE TABLE public.plg_conversations (
|
|
2160
|
+
id uuid DEFAULT gen_random_uuid() NOT NULL,
|
|
2161
|
+
tenant_id uuid NOT NULL,
|
|
2162
|
+
contact_name text NOT NULL,
|
|
2163
|
+
contact_handle text,
|
|
2164
|
+
channel text NOT NULL,
|
|
2165
|
+
last_message_preview text,
|
|
2166
|
+
last_message_at timestamp with time zone DEFAULT now(),
|
|
2167
|
+
unread_count integer DEFAULT 0,
|
|
2168
|
+
status text DEFAULT 'open'::text,
|
|
2169
|
+
assigned_to text,
|
|
2170
|
+
accent text,
|
|
2171
|
+
tags text[],
|
|
2172
|
+
location text,
|
|
2173
|
+
note text,
|
|
2174
|
+
created_at timestamp with time zone DEFAULT now() NOT NULL,
|
|
2175
|
+
updated_at timestamp with time zone DEFAULT now() NOT NULL,
|
|
2176
|
+
contact_person_id uuid,
|
|
2177
|
+
CONSTRAINT plg_conversations_channel_check CHECK ((channel = ANY (ARRAY['sms'::text, 'whatsapp'::text, 'instagram'::text, 'email'::text, 'webchat'::text]))),
|
|
2178
|
+
CONSTRAINT plg_conversations_status_check CHECK ((status = ANY (ARRAY['open'::text, 'snoozed'::text, 'closed'::text])))
|
|
2179
|
+
);
|
|
2001
2180
|
|
|
2002
|
-
|
|
2003
|
-
|
|
2004
|
-
|
|
2005
|
-
|
|
2006
|
-
|
|
2181
|
+
CREATE TABLE public.plg_conversations_channels (
|
|
2182
|
+
id uuid DEFAULT gen_random_uuid() NOT NULL,
|
|
2183
|
+
tenant_id uuid,
|
|
2184
|
+
channel text DEFAULT 'whatsapp'::text NOT NULL,
|
|
2185
|
+
provider text NOT NULL,
|
|
2186
|
+
provider_number_id text NOT NULL,
|
|
2187
|
+
phone_e164 text,
|
|
2188
|
+
kind text DEFAULT 'dedicated'::text NOT NULL,
|
|
2189
|
+
status text DEFAULT 'requested'::text NOT NULL,
|
|
2190
|
+
display_name text,
|
|
2191
|
+
metadata jsonb DEFAULT '{}'::jsonb NOT NULL,
|
|
2192
|
+
created_at timestamp with time zone DEFAULT now() NOT NULL,
|
|
2193
|
+
updated_at timestamp with time zone DEFAULT now() NOT NULL,
|
|
2194
|
+
CONSTRAINT plg_conversations_channels_channel_check CHECK ((channel = ANY (ARRAY['sms'::text, 'whatsapp'::text, 'instagram'::text, 'email'::text, 'webchat'::text]))),
|
|
2195
|
+
CONSTRAINT plg_conversations_channels_kind_check CHECK ((kind = ANY (ARRAY['fallback'::text, 'dedicated'::text]))),
|
|
2196
|
+
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])))
|
|
2197
|
+
);
|
|
2007
2198
|
|
|
2008
|
-
|
|
2009
|
-
|
|
2010
|
-
|
|
2011
|
-
|
|
2012
|
-
|
|
2013
|
-
|
|
2014
|
-
|
|
2015
|
-
|
|
2016
|
-
|
|
2017
|
-
|
|
2018
|
-
-- because this row exists is a suppression, which is a different fact and the
|
|
2019
|
-
-- only one a person can act on.
|
|
2020
|
-
--
|
|
2021
|
-
-- \`tenant_id\` is NULLABLE and the null carries meaning, the same way it does on
|
|
2022
|
-
-- plg_conversations_channels:
|
|
2023
|
-
-- \u2022 NULL \u2014 the customer opted out of the PRODUCT's shared fallback number.
|
|
2024
|
-
-- They cannot have consented to one salon and not another on a number all
|
|
2025
|
-
-- the salons share, so the suppression is product-wide.
|
|
2026
|
-
-- \u2022 set \u2014 the customer opted out of that tenant's dedicated number. Their
|
|
2027
|
-
-- other conversations are untouched.
|
|
2028
|
-
--
|
|
2029
|
-
-- Written by the webhook (contact.opted_out / contact.erased) and read by
|
|
2030
|
-
-- messaging-send before every send. Nothing else writes it: a member who could
|
|
2031
|
-
-- delete a row could resume messaging somebody who asked us to stop, which is
|
|
2032
|
-
-- the one mistake in this file with a legal name.
|
|
2033
|
-
-- Idempotent + safe to re-run.
|
|
2034
|
-
-- ============================================================================
|
|
2199
|
+
CREATE TABLE public.plg_conversations_optouts (
|
|
2200
|
+
id uuid DEFAULT gen_random_uuid() NOT NULL,
|
|
2201
|
+
channel text DEFAULT 'whatsapp'::text NOT NULL,
|
|
2202
|
+
phone_e164 text NOT NULL,
|
|
2203
|
+
tenant_id uuid,
|
|
2204
|
+
provider text NOT NULL,
|
|
2205
|
+
reason text,
|
|
2206
|
+
created_at timestamp with time zone DEFAULT now() NOT NULL,
|
|
2207
|
+
CONSTRAINT plg_conversations_optouts_channel_check CHECK ((channel = ANY (ARRAY['sms'::text, 'whatsapp'::text, 'instagram'::text, 'email'::text, 'webchat'::text])))
|
|
2208
|
+
);
|
|
2035
2209
|
|
|
2036
|
-
CREATE TABLE
|
|
2037
|
-
|
|
2038
|
-
|
|
2039
|
-
|
|
2040
|
-
|
|
2041
|
-
|
|
2042
|
-
|
|
2043
|
-
|
|
2044
|
-
|
|
2045
|
-
|
|
2046
|
-
|
|
2047
|
-
|
|
2048
|
-
|
|
2210
|
+
CREATE TABLE public.plg_conversations_payment_requests (
|
|
2211
|
+
id uuid DEFAULT gen_random_uuid() NOT NULL,
|
|
2212
|
+
tenant_id uuid NOT NULL,
|
|
2213
|
+
conversation_id uuid,
|
|
2214
|
+
message_id uuid,
|
|
2215
|
+
subject_type text,
|
|
2216
|
+
subject_id text,
|
|
2217
|
+
amount_cents integer NOT NULL,
|
|
2218
|
+
currency text DEFAULT 'BRL'::text NOT NULL,
|
|
2219
|
+
description text,
|
|
2220
|
+
provider text NOT NULL,
|
|
2221
|
+
status text DEFAULT 'created'::text NOT NULL,
|
|
2222
|
+
provider_payment_id text,
|
|
2223
|
+
payment_link_url text,
|
|
2224
|
+
pix_copy_paste text,
|
|
2225
|
+
link_delivered_at timestamp with time zone,
|
|
2226
|
+
settled_notified_at timestamp with time zone,
|
|
2227
|
+
settled_at timestamp with time zone,
|
|
2228
|
+
created_at timestamp with time zone DEFAULT now() NOT NULL,
|
|
2229
|
+
updated_at timestamp with time zone DEFAULT now() NOT NULL,
|
|
2230
|
+
CONSTRAINT plg_conversations_payment_requests_amount_cents_check CHECK ((amount_cents > 0)),
|
|
2231
|
+
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])))
|
|
2049
2232
|
);
|
|
2050
|
-
ALTER TABLE public.plg_conversations_optouts ENABLE ROW LEVEL SECURITY;
|
|
2051
2233
|
|
|
2052
|
-
|
|
2053
|
-
|
|
2054
|
-
|
|
2055
|
-
|
|
2056
|
-
|
|
2057
|
-
|
|
2058
|
-
|
|
2059
|
-
channel, phone_e164,
|
|
2060
|
-
COALESCE(tenant_id, '00000000-0000-0000-0000-000000000000'::uuid));
|
|
2061
|
-
|
|
2062
|
-
-- The read messaging-send makes on every send: "is this number suppressed for
|
|
2063
|
-
-- this tenant, or product-wide?"
|
|
2064
|
-
CREATE INDEX IF NOT EXISTS idx_plg_conversations_optouts_lookup
|
|
2065
|
-
ON public.plg_conversations_optouts(channel, phone_e164);
|
|
2066
|
-
|
|
2067
|
-
-- RLS: members READ the suppressions that apply to them (their own tenant's and
|
|
2068
|
-
-- the product-wide ones), because "why did this customer stop getting messages"
|
|
2069
|
-
-- is a question the inbox has to be able to answer. Writes are service-role
|
|
2070
|
-
-- only \u2014 no INSERT/UPDATE/DELETE policy, no write GRANT.
|
|
2071
|
-
DROP POLICY IF EXISTS plg_conversations_optouts_select ON public.plg_conversations_optouts;
|
|
2072
|
-
CREATE POLICY plg_conversations_optouts_select ON public.plg_conversations_optouts
|
|
2073
|
-
FOR SELECT TO authenticated
|
|
2074
|
-
USING (tenant_id IS NULL OR tenant_id IN (SELECT public.user_tenant_ids()));
|
|
2075
|
-
GRANT SELECT ON public.plg_conversations_optouts TO authenticated;
|
|
2076
|
-
`;
|
|
2077
|
-
var MIGRATION_007_INBOUND_ROUTING = `-- ============================================================================
|
|
2078
|
-
-- plugin-conversations 007: the two things the webhook needs that 004 could not
|
|
2079
|
-
-- know it would.
|
|
2080
|
-
--
|
|
2081
|
-
-- \xA71 delivery_status gains the statuses the provider actually emits.
|
|
2082
|
-
-- \xA72 plg_conversations_match_by_phone \u2014 find the thread a WhatsApp number
|
|
2083
|
-
-- belongs to, when the number was typed by a person and delivered by Meta.
|
|
2084
|
-
-- Idempotent + safe to re-run.
|
|
2085
|
-
-- ============================================================================
|
|
2234
|
+
CREATE TABLE public.plg_conversations_webhook_events (
|
|
2235
|
+
id uuid DEFAULT gen_random_uuid() NOT NULL,
|
|
2236
|
+
provider text NOT NULL,
|
|
2237
|
+
event_id text NOT NULL,
|
|
2238
|
+
event_type text NOT NULL,
|
|
2239
|
+
received_at timestamp with time zone DEFAULT now() NOT NULL
|
|
2240
|
+
);
|
|
2086
2241
|
|
|
2087
|
-
-- \xA71 \u2014 the closed set was one delivery short --------------------------------
|
|
2088
|
-
--
|
|
2089
|
-
-- 004 wrote the vocabulary from the design. Registering a webhook endpoint at
|
|
2090
|
-
-- Tyxter answers with the ENUMERATED list of deliverable events, and it holds
|
|
2091
|
-
-- three the CHECK would have refused: \`message.delivery_timeout\` (the provider
|
|
2092
|
-
-- gave up before the recipient's device ever acknowledged), \`message.cancelled\`
|
|
2093
|
-
-- (an accepted message pulled back before it left), and \`message.opted_out\`
|
|
2094
|
-
-- (refused at the provider because the contact withdrew consent \u2014 see 006).
|
|
2095
|
-
--
|
|
2096
|
-
-- Refusing them is worse than it sounds: the handler's UPDATE fails, the
|
|
2097
|
-
-- webhook answers non-2xx, and Tyxter retries the same event eight times over a
|
|
2098
|
-
-- day and a half before giving up. The row keeps whatever it said before, which
|
|
2099
|
-
-- is 'sent'. An operator reads "sent" for a message that was never delivered.
|
|
2100
|
-
DO $$
|
|
2101
|
-
BEGIN
|
|
2102
|
-
IF EXISTS (
|
|
2103
|
-
SELECT 1 FROM pg_constraint WHERE conname = 'plg_conversation_messages_delivery_status_check'
|
|
2104
|
-
) THEN
|
|
2105
|
-
ALTER TABLE public.plg_conversation_messages
|
|
2106
|
-
DROP CONSTRAINT plg_conversation_messages_delivery_status_check;
|
|
2107
|
-
END IF;
|
|
2108
|
-
ALTER TABLE public.plg_conversation_messages
|
|
2109
|
-
ADD CONSTRAINT plg_conversation_messages_delivery_status_check
|
|
2110
|
-
CHECK (delivery_status IS NULL OR delivery_status IN
|
|
2111
|
-
('queued', 'sent', 'delivered', 'read',
|
|
2112
|
-
'failed', 'expired', 'delivery_timeout', 'cancelled', 'opted_out'));
|
|
2113
|
-
END
|
|
2114
|
-
$$;
|
|
2115
2242
|
|
|
2116
|
-
-- \
|
|
2117
|
-
|
|
2118
|
-
|
|
2119
|
-
|
|
2120
|
-
|
|
2121
|
-
|
|
2122
|
-
--
|
|
2123
|
-
-- The rule is digit-suffix, longest first:
|
|
2124
|
-
-- last 10 = DDD + the 8 significant digits. Specific: a collision needs two
|
|
2125
|
-
-- customers in the same area code sharing 8 digits.
|
|
2126
|
-
-- last 8 = the subscriber digits alone. The FALLBACK, tried only when the
|
|
2127
|
-
-- first finds nothing, because it crosses area codes.
|
|
2128
|
-
--
|
|
2129
|
-
-- SECURITY DEFINER and CROSS-TENANT by construction, which is the part to read
|
|
2130
|
-
-- carefully. The product's fallback number is shared by every tenant in the
|
|
2131
|
-
-- pool, so an inbound message on it arrives with no tenant attached and finding
|
|
2132
|
-
-- one means looking at all of them. That is exactly the read that must never be
|
|
2133
|
-
-- reachable by a member, so EXECUTE is granted to service_role ONLY and
|
|
2134
|
-
-- explicitly revoked from authenticated and anon. The webhook holds the
|
|
2135
|
-
-- service-role key; nothing in the browser can call this.
|
|
2136
|
-
-- \`p_tenant_id\` narrows the search to one tenant, which is what a DEDICATED
|
|
2137
|
-
-- number wants: the tenant is already known from the channel row, and the
|
|
2138
|
-
-- cross-tenant search could hand back another salon's thread for the same
|
|
2139
|
-
-- customer. NULL searches the whole pool, which is the fallback number's case
|
|
2140
|
-
-- and the only one that has no other answer.
|
|
2141
|
-
CREATE OR REPLACE FUNCTION public.plg_conversations_match_by_phone(
|
|
2142
|
-
p_channel text,
|
|
2143
|
-
p_last10 text,
|
|
2144
|
-
p_last8 text,
|
|
2145
|
-
p_tenant_id uuid DEFAULT NULL
|
|
2146
|
-
) RETURNS TABLE (
|
|
2147
|
-
id uuid,
|
|
2148
|
-
tenant_id uuid,
|
|
2149
|
-
contact_name text,
|
|
2150
|
-
contact_handle text,
|
|
2151
|
-
contact_person_id uuid
|
|
2152
|
-
)
|
|
2153
|
-
LANGUAGE sql
|
|
2154
|
-
STABLE
|
|
2155
|
-
SECURITY DEFINER
|
|
2156
|
-
SET search_path = public
|
|
2157
|
-
AS $fn$
|
|
2243
|
+
-- \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
|
|
2244
|
+
|
|
2245
|
+
CREATE 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)
|
|
2246
|
+
LANGUAGE sql STABLE SECURITY DEFINER
|
|
2247
|
+
SET search_path TO 'public'
|
|
2248
|
+
AS $$
|
|
2158
2249
|
WITH candidate AS (
|
|
2159
2250
|
SELECT c.id, c.tenant_id, c.contact_name, c.contact_handle, c.contact_person_id,
|
|
2160
2251
|
c.last_message_at,
|
|
@@ -2174,189 +2265,430 @@ AS $fn$
|
|
|
2174
2265
|
ORDER BY (p_last10 IS NOT NULL AND right(digits, 10) = p_last10) DESC,
|
|
2175
2266
|
last_message_at DESC NULLS LAST
|
|
2176
2267
|
LIMIT 1;
|
|
2177
|
-
|
|
2178
|
-
|
|
2179
|
-
|
|
2180
|
-
|
|
2181
|
-
|
|
2182
|
-
|
|
2183
|
-
|
|
2184
|
-
|
|
2185
|
-
|
|
2186
|
-
|
|
2187
|
-
|
|
2188
|
-
|
|
2189
|
-
|
|
2190
|
-
|
|
2191
|
-
|
|
2192
|
-
|
|
2193
|
-
|
|
2194
|
-
|
|
2195
|
-
|
|
2196
|
-
|
|
2197
|
-
|
|
2198
|
-
|
|
2199
|
-
|
|
2200
|
-
|
|
2201
|
-
--
|
|
2202
|
-
|
|
2203
|
-
|
|
2204
|
-
|
|
2205
|
-
|
|
2206
|
-
|
|
2207
|
-
|
|
2208
|
-
|
|
2209
|
-
|
|
2210
|
-
|
|
2211
|
-
|
|
2212
|
-
|
|
2213
|
-
|
|
2214
|
-
|
|
2215
|
-
|
|
2216
|
-
|
|
2217
|
-
|
|
2218
|
-
|
|
2219
|
-
|
|
2220
|
-
|
|
2221
|
-
|
|
2222
|
-
|
|
2268
|
+
$$;
|
|
2269
|
+
|
|
2270
|
+
|
|
2271
|
+
-- \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
|
|
2272
|
+
|
|
2273
|
+
ALTER TABLE ONLY public.plg_conversation_messages
|
|
2274
|
+
ADD CONSTRAINT plg_conversation_messages_pkey PRIMARY KEY (id);
|
|
2275
|
+
|
|
2276
|
+
ALTER TABLE ONLY public.plg_conversations_channels
|
|
2277
|
+
ADD CONSTRAINT plg_conversations_channels_pkey PRIMARY KEY (id);
|
|
2278
|
+
|
|
2279
|
+
ALTER TABLE ONLY public.plg_conversations_optouts
|
|
2280
|
+
ADD CONSTRAINT plg_conversations_optouts_pkey PRIMARY KEY (id);
|
|
2281
|
+
|
|
2282
|
+
ALTER TABLE ONLY public.plg_conversations_payment_requests
|
|
2283
|
+
ADD CONSTRAINT plg_conversations_payment_requests_pkey PRIMARY KEY (id);
|
|
2284
|
+
|
|
2285
|
+
ALTER TABLE ONLY public.plg_conversations
|
|
2286
|
+
ADD CONSTRAINT plg_conversations_pkey PRIMARY KEY (id);
|
|
2287
|
+
|
|
2288
|
+
ALTER TABLE ONLY public.plg_conversations_webhook_events
|
|
2289
|
+
ADD CONSTRAINT plg_conversations_webhook_events_pkey PRIMARY KEY (id);
|
|
2290
|
+
|
|
2291
|
+
|
|
2292
|
+
-- \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
|
|
2293
|
+
|
|
2294
|
+
CREATE 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);
|
|
2295
|
+
|
|
2296
|
+
CREATE INDEX idx_plg_conversation_messages_subject ON public.plg_conversation_messages USING btree (conversation_id, at DESC) WHERE (subject_id IS NOT NULL);
|
|
2297
|
+
|
|
2298
|
+
CREATE INDEX idx_plg_conversation_messages_thread ON public.plg_conversation_messages USING btree (conversation_id, at);
|
|
2299
|
+
|
|
2300
|
+
CREATE INDEX idx_plg_conversations_channels_tenant ON public.plg_conversations_channels USING btree (tenant_id, channel, status) WHERE (tenant_id IS NOT NULL);
|
|
2301
|
+
|
|
2302
|
+
CREATE 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);
|
|
2303
|
+
|
|
2304
|
+
CREATE INDEX idx_plg_conversations_optouts_lookup ON public.plg_conversations_optouts USING btree (channel, phone_e164);
|
|
2305
|
+
|
|
2306
|
+
CREATE 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);
|
|
2307
|
+
|
|
2308
|
+
CREATE INDEX idx_plg_conversations_payment_requests_tenant ON public.plg_conversations_payment_requests USING btree (tenant_id, created_at DESC);
|
|
2309
|
+
|
|
2310
|
+
CREATE INDEX idx_plg_conversations_person ON public.plg_conversations USING btree (tenant_id, contact_person_id) WHERE (contact_person_id IS NOT NULL);
|
|
2311
|
+
|
|
2312
|
+
CREATE INDEX idx_plg_conversations_tenant ON public.plg_conversations USING btree (tenant_id);
|
|
2313
|
+
|
|
2314
|
+
CREATE INDEX idx_plg_conversations_tenant_recent ON public.plg_conversations USING btree (tenant_id, last_message_at DESC);
|
|
2315
|
+
|
|
2316
|
+
CREATE INDEX idx_plg_conversations_webhook_events_received ON public.plg_conversations_webhook_events USING btree (received_at);
|
|
2317
|
+
|
|
2318
|
+
CREATE 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);
|
|
2319
|
+
|
|
2320
|
+
CREATE 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));
|
|
2321
|
+
|
|
2322
|
+
CREATE UNIQUE INDEX uq_plg_conversations_channels_number ON public.plg_conversations_channels USING btree (provider, provider_number_id);
|
|
2323
|
+
|
|
2324
|
+
CREATE 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));
|
|
2325
|
+
|
|
2326
|
+
CREATE 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])));
|
|
2327
|
+
|
|
2328
|
+
CREATE 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);
|
|
2329
|
+
|
|
2330
|
+
CREATE UNIQUE INDEX uq_plg_conversations_webhook_events_event ON public.plg_conversations_webhook_events USING btree (provider, event_id);
|
|
2331
|
+
|
|
2332
|
+
|
|
2333
|
+
-- \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
|
|
2334
|
+
|
|
2335
|
+
ALTER TABLE ONLY public.plg_conversation_messages
|
|
2336
|
+
ADD CONSTRAINT plg_conversation_messages_conversation_id_fkey FOREIGN KEY (conversation_id) REFERENCES public.plg_conversations(id) ON DELETE CASCADE;
|
|
2337
|
+
|
|
2338
|
+
ALTER TABLE ONLY public.plg_conversation_messages
|
|
2339
|
+
ADD CONSTRAINT plg_conversation_messages_tenant_id_fkey FOREIGN KEY (tenant_id) REFERENCES public.tenants(id) ON DELETE CASCADE;
|
|
2340
|
+
|
|
2341
|
+
ALTER TABLE ONLY public.plg_conversations_channels
|
|
2342
|
+
ADD CONSTRAINT plg_conversations_channels_tenant_id_fkey FOREIGN KEY (tenant_id) REFERENCES public.tenants(id) ON DELETE CASCADE;
|
|
2343
|
+
|
|
2344
|
+
ALTER TABLE ONLY public.plg_conversations
|
|
2345
|
+
ADD CONSTRAINT plg_conversations_contact_person_id_fkey FOREIGN KEY (contact_person_id) REFERENCES public.people(id) ON DELETE SET NULL;
|
|
2346
|
+
|
|
2347
|
+
ALTER TABLE ONLY public.plg_conversations_optouts
|
|
2348
|
+
ADD CONSTRAINT plg_conversations_optouts_tenant_id_fkey FOREIGN KEY (tenant_id) REFERENCES public.tenants(id) ON DELETE CASCADE;
|
|
2349
|
+
|
|
2350
|
+
ALTER TABLE ONLY public.plg_conversations_payment_requests
|
|
2351
|
+
ADD CONSTRAINT plg_conversations_payment_requests_conversation_id_fkey FOREIGN KEY (conversation_id) REFERENCES public.plg_conversations(id) ON DELETE SET NULL;
|
|
2352
|
+
|
|
2353
|
+
ALTER TABLE ONLY public.plg_conversations_payment_requests
|
|
2354
|
+
ADD CONSTRAINT plg_conversations_payment_requests_message_id_fkey FOREIGN KEY (message_id) REFERENCES public.plg_conversation_messages(id) ON DELETE SET NULL;
|
|
2355
|
+
|
|
2356
|
+
ALTER TABLE ONLY public.plg_conversations_payment_requests
|
|
2357
|
+
ADD CONSTRAINT plg_conversations_payment_requests_tenant_id_fkey FOREIGN KEY (tenant_id) REFERENCES public.tenants(id) ON DELETE CASCADE;
|
|
2358
|
+
|
|
2359
|
+
ALTER TABLE ONLY public.plg_conversations
|
|
2360
|
+
ADD CONSTRAINT plg_conversations_tenant_id_fkey FOREIGN KEY (tenant_id) REFERENCES public.tenants(id) ON DELETE CASCADE;
|
|
2361
|
+
|
|
2362
|
+
|
|
2363
|
+
-- \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
|
|
2364
|
+
|
|
2365
|
+
ALTER TABLE public.plg_conversation_messages ENABLE ROW LEVEL SECURITY;
|
|
2366
|
+
|
|
2367
|
+
ALTER TABLE public.plg_conversations ENABLE ROW LEVEL SECURITY;
|
|
2368
|
+
|
|
2369
|
+
ALTER TABLE public.plg_conversations_channels ENABLE ROW LEVEL SECURITY;
|
|
2370
|
+
|
|
2371
|
+
ALTER TABLE public.plg_conversations_optouts ENABLE ROW LEVEL SECURITY;
|
|
2372
|
+
|
|
2373
|
+
ALTER TABLE public.plg_conversations_payment_requests ENABLE ROW LEVEL SECURITY;
|
|
2374
|
+
|
|
2375
|
+
ALTER TABLE public.plg_conversations_webhook_events ENABLE ROW LEVEL SECURITY;
|
|
2376
|
+
|
|
2377
|
+
|
|
2378
|
+
-- \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
|
|
2379
|
+
|
|
2380
|
+
CREATE 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)));
|
|
2381
|
+
|
|
2382
|
+
CREATE 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)));
|
|
2383
|
+
|
|
2384
|
+
CREATE 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)));
|
|
2385
|
+
|
|
2386
|
+
CREATE 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)));
|
|
2387
|
+
|
|
2388
|
+
CREATE 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))));
|
|
2389
|
+
|
|
2390
|
+
CREATE 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)));
|
|
2391
|
+
|
|
2392
|
+
CREATE 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)));
|
|
2393
|
+
|
|
2394
|
+
CREATE 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))));
|
|
2395
|
+
|
|
2396
|
+
CREATE 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)));
|
|
2397
|
+
|
|
2398
|
+
CREATE 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)));
|
|
2399
|
+
|
|
2400
|
+
CREATE 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)));
|
|
2401
|
+
|
|
2402
|
+
|
|
2403
|
+
-- \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
|
|
2404
|
+
|
|
2405
|
+
REVOKE ALL ON FUNCTION public.plg_conversations_match_by_phone(p_channel text, p_last10 text, p_last8 text, p_tenant_id uuid) FROM PUBLIC;
|
|
2406
|
+
GRANT 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;
|
|
2223
2407
|
|
|
2224
|
-
|
|
2225
|
-
|
|
2226
|
-
|
|
2227
|
-
|
|
2228
|
-
|
|
2229
|
-
|
|
2230
|
-
|
|
2231
|
-
|
|
2232
|
-
|
|
2233
|
-
|
|
2234
|
-
|
|
2235
|
-
|
|
2236
|
-
|
|
2237
|
-
|
|
2238
|
-
|
|
2239
|
-
|
|
2240
|
-
|
|
2408
|
+
GRANT MAINTAIN ON TABLE public.plg_conversation_messages TO anon;
|
|
2409
|
+
GRANT ALL ON TABLE public.plg_conversation_messages TO authenticated;
|
|
2410
|
+
GRANT ALL ON TABLE public.plg_conversation_messages TO service_role;
|
|
2411
|
+
|
|
2412
|
+
GRANT MAINTAIN ON TABLE public.plg_conversations TO anon;
|
|
2413
|
+
GRANT ALL ON TABLE public.plg_conversations TO authenticated;
|
|
2414
|
+
GRANT ALL ON TABLE public.plg_conversations TO service_role;
|
|
2415
|
+
|
|
2416
|
+
GRANT MAINTAIN ON TABLE public.plg_conversations_channels TO anon;
|
|
2417
|
+
GRANT ALL ON TABLE public.plg_conversations_channels TO authenticated;
|
|
2418
|
+
GRANT ALL ON TABLE public.plg_conversations_channels TO service_role;
|
|
2419
|
+
|
|
2420
|
+
GRANT MAINTAIN ON TABLE public.plg_conversations_optouts TO anon;
|
|
2421
|
+
GRANT ALL ON TABLE public.plg_conversations_optouts TO authenticated;
|
|
2422
|
+
GRANT ALL ON TABLE public.plg_conversations_optouts TO service_role;
|
|
2423
|
+
|
|
2424
|
+
GRANT MAINTAIN ON TABLE public.plg_conversations_payment_requests TO anon;
|
|
2425
|
+
GRANT ALL ON TABLE public.plg_conversations_payment_requests TO authenticated;
|
|
2426
|
+
GRANT ALL ON TABLE public.plg_conversations_payment_requests TO service_role;
|
|
2427
|
+
|
|
2428
|
+
GRANT MAINTAIN ON TABLE public.plg_conversations_webhook_events TO anon;
|
|
2429
|
+
GRANT ALL ON TABLE public.plg_conversations_webhook_events TO authenticated;
|
|
2430
|
+
GRANT ALL ON TABLE public.plg_conversations_webhook_events TO service_role;
|
|
2431
|
+
|
|
2432
|
+
|
|
2433
|
+
-- \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
|
|
2434
|
+
-- Put back, so the NEXT thing installed inherits what the chain had.
|
|
2435
|
+
|
|
2436
|
+
DO $dp$ BEGIN
|
|
2437
|
+
EXECUTE $stmt$ALTER DEFAULT PRIVILEGES FOR ROLE postgres IN SCHEMA public GRANT ALL ON SEQUENCES TO postgres$stmt$;
|
|
2438
|
+
EXCEPTION WHEN insufficient_privilege OR undefined_object THEN
|
|
2439
|
+
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$;
|
|
2440
|
+
END $dp$;
|
|
2441
|
+
|
|
2442
|
+
DO $dp$ BEGIN
|
|
2443
|
+
EXECUTE $stmt$ALTER DEFAULT PRIVILEGES FOR ROLE postgres IN SCHEMA public GRANT ALL ON SEQUENCES TO anon$stmt$;
|
|
2444
|
+
EXCEPTION WHEN insufficient_privilege OR undefined_object THEN
|
|
2445
|
+
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$;
|
|
2446
|
+
END $dp$;
|
|
2447
|
+
|
|
2448
|
+
DO $dp$ BEGIN
|
|
2449
|
+
EXECUTE $stmt$ALTER DEFAULT PRIVILEGES FOR ROLE postgres IN SCHEMA public GRANT ALL ON SEQUENCES TO authenticated$stmt$;
|
|
2450
|
+
EXCEPTION WHEN insufficient_privilege OR undefined_object THEN
|
|
2451
|
+
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$;
|
|
2452
|
+
END $dp$;
|
|
2453
|
+
|
|
2454
|
+
DO $dp$ BEGIN
|
|
2455
|
+
EXECUTE $stmt$ALTER DEFAULT PRIVILEGES FOR ROLE postgres IN SCHEMA public GRANT ALL ON SEQUENCES TO service_role$stmt$;
|
|
2456
|
+
EXCEPTION WHEN insufficient_privilege OR undefined_object THEN
|
|
2457
|
+
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$;
|
|
2458
|
+
END $dp$;
|
|
2459
|
+
|
|
2460
|
+
DO $dp$ BEGIN
|
|
2461
|
+
EXECUTE $stmt$ALTER DEFAULT PRIVILEGES FOR ROLE supabase_admin IN SCHEMA public GRANT ALL ON SEQUENCES TO postgres$stmt$;
|
|
2462
|
+
EXCEPTION WHEN insufficient_privilege OR undefined_object THEN
|
|
2463
|
+
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$;
|
|
2464
|
+
END $dp$;
|
|
2465
|
+
|
|
2466
|
+
DO $dp$ BEGIN
|
|
2467
|
+
EXECUTE $stmt$ALTER DEFAULT PRIVILEGES FOR ROLE supabase_admin IN SCHEMA public GRANT ALL ON SEQUENCES TO anon$stmt$;
|
|
2468
|
+
EXCEPTION WHEN insufficient_privilege OR undefined_object THEN
|
|
2469
|
+
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$;
|
|
2470
|
+
END $dp$;
|
|
2471
|
+
|
|
2472
|
+
DO $dp$ BEGIN
|
|
2473
|
+
EXECUTE $stmt$ALTER DEFAULT PRIVILEGES FOR ROLE supabase_admin IN SCHEMA public GRANT ALL ON SEQUENCES TO authenticated$stmt$;
|
|
2474
|
+
EXCEPTION WHEN insufficient_privilege OR undefined_object THEN
|
|
2475
|
+
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$;
|
|
2476
|
+
END $dp$;
|
|
2477
|
+
|
|
2478
|
+
DO $dp$ BEGIN
|
|
2479
|
+
EXECUTE $stmt$ALTER DEFAULT PRIVILEGES FOR ROLE supabase_admin IN SCHEMA public GRANT ALL ON SEQUENCES TO service_role$stmt$;
|
|
2480
|
+
EXCEPTION WHEN insufficient_privilege OR undefined_object THEN
|
|
2481
|
+
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$;
|
|
2482
|
+
END $dp$;
|
|
2483
|
+
|
|
2484
|
+
DO $dp$ BEGIN
|
|
2485
|
+
EXECUTE $stmt$ALTER DEFAULT PRIVILEGES FOR ROLE postgres IN SCHEMA public GRANT ALL ON FUNCTIONS TO postgres$stmt$;
|
|
2486
|
+
EXCEPTION WHEN insufficient_privilege OR undefined_object THEN
|
|
2487
|
+
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$;
|
|
2488
|
+
END $dp$;
|
|
2489
|
+
|
|
2490
|
+
DO $dp$ BEGIN
|
|
2491
|
+
EXECUTE $stmt$ALTER DEFAULT PRIVILEGES FOR ROLE postgres IN SCHEMA public GRANT ALL ON FUNCTIONS TO authenticated$stmt$;
|
|
2492
|
+
EXCEPTION WHEN insufficient_privilege OR undefined_object THEN
|
|
2493
|
+
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$;
|
|
2494
|
+
END $dp$;
|
|
2495
|
+
|
|
2496
|
+
DO $dp$ BEGIN
|
|
2497
|
+
EXECUTE $stmt$ALTER DEFAULT PRIVILEGES FOR ROLE postgres IN SCHEMA public GRANT ALL ON FUNCTIONS TO service_role$stmt$;
|
|
2498
|
+
EXCEPTION WHEN insufficient_privilege OR undefined_object THEN
|
|
2499
|
+
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$;
|
|
2500
|
+
END $dp$;
|
|
2501
|
+
|
|
2502
|
+
DO $dp$ BEGIN
|
|
2503
|
+
EXECUTE $stmt$ALTER DEFAULT PRIVILEGES FOR ROLE supabase_admin IN SCHEMA public GRANT ALL ON FUNCTIONS TO postgres$stmt$;
|
|
2504
|
+
EXCEPTION WHEN insufficient_privilege OR undefined_object THEN
|
|
2505
|
+
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$;
|
|
2506
|
+
END $dp$;
|
|
2507
|
+
|
|
2508
|
+
DO $dp$ BEGIN
|
|
2509
|
+
EXECUTE $stmt$ALTER DEFAULT PRIVILEGES FOR ROLE supabase_admin IN SCHEMA public GRANT ALL ON FUNCTIONS TO anon$stmt$;
|
|
2510
|
+
EXCEPTION WHEN insufficient_privilege OR undefined_object THEN
|
|
2511
|
+
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$;
|
|
2512
|
+
END $dp$;
|
|
2513
|
+
|
|
2514
|
+
DO $dp$ BEGIN
|
|
2515
|
+
EXECUTE $stmt$ALTER DEFAULT PRIVILEGES FOR ROLE supabase_admin IN SCHEMA public GRANT ALL ON FUNCTIONS TO authenticated$stmt$;
|
|
2516
|
+
EXCEPTION WHEN insufficient_privilege OR undefined_object THEN
|
|
2517
|
+
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$;
|
|
2518
|
+
END $dp$;
|
|
2519
|
+
|
|
2520
|
+
DO $dp$ BEGIN
|
|
2521
|
+
EXECUTE $stmt$ALTER DEFAULT PRIVILEGES FOR ROLE supabase_admin IN SCHEMA public GRANT ALL ON FUNCTIONS TO service_role$stmt$;
|
|
2522
|
+
EXCEPTION WHEN insufficient_privilege OR undefined_object THEN
|
|
2523
|
+
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$;
|
|
2524
|
+
END $dp$;
|
|
2525
|
+
|
|
2526
|
+
DO $dp$ BEGIN
|
|
2527
|
+
EXECUTE $stmt$ALTER DEFAULT PRIVILEGES FOR ROLE postgres IN SCHEMA public GRANT ALL ON TABLES TO postgres$stmt$;
|
|
2528
|
+
EXCEPTION WHEN insufficient_privilege OR undefined_object THEN
|
|
2529
|
+
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$;
|
|
2530
|
+
END $dp$;
|
|
2531
|
+
|
|
2532
|
+
DO $dp$ BEGIN
|
|
2533
|
+
EXECUTE $stmt$ALTER DEFAULT PRIVILEGES FOR ROLE postgres IN SCHEMA public GRANT MAINTAIN ON TABLES TO anon$stmt$;
|
|
2534
|
+
EXCEPTION WHEN insufficient_privilege OR undefined_object THEN
|
|
2535
|
+
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$;
|
|
2536
|
+
END $dp$;
|
|
2537
|
+
|
|
2538
|
+
DO $dp$ BEGIN
|
|
2539
|
+
EXECUTE $stmt$ALTER DEFAULT PRIVILEGES FOR ROLE postgres IN SCHEMA public GRANT ALL ON TABLES TO authenticated$stmt$;
|
|
2540
|
+
EXCEPTION WHEN insufficient_privilege OR undefined_object THEN
|
|
2541
|
+
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$;
|
|
2542
|
+
END $dp$;
|
|
2543
|
+
|
|
2544
|
+
DO $dp$ BEGIN
|
|
2545
|
+
EXECUTE $stmt$ALTER DEFAULT PRIVILEGES FOR ROLE postgres IN SCHEMA public GRANT ALL ON TABLES TO service_role$stmt$;
|
|
2546
|
+
EXCEPTION WHEN insufficient_privilege OR undefined_object THEN
|
|
2547
|
+
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$;
|
|
2548
|
+
END $dp$;
|
|
2549
|
+
|
|
2550
|
+
DO $dp$ BEGIN
|
|
2551
|
+
EXECUTE $stmt$ALTER DEFAULT PRIVILEGES FOR ROLE supabase_admin IN SCHEMA public GRANT ALL ON TABLES TO postgres$stmt$;
|
|
2552
|
+
EXCEPTION WHEN insufficient_privilege OR undefined_object THEN
|
|
2553
|
+
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$;
|
|
2554
|
+
END $dp$;
|
|
2555
|
+
|
|
2556
|
+
DO $dp$ BEGIN
|
|
2557
|
+
EXECUTE $stmt$ALTER DEFAULT PRIVILEGES FOR ROLE supabase_admin IN SCHEMA public GRANT ALL ON TABLES TO anon$stmt$;
|
|
2558
|
+
EXCEPTION WHEN insufficient_privilege OR undefined_object THEN
|
|
2559
|
+
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$;
|
|
2560
|
+
END $dp$;
|
|
2561
|
+
|
|
2562
|
+
DO $dp$ BEGIN
|
|
2563
|
+
EXECUTE $stmt$ALTER DEFAULT PRIVILEGES FOR ROLE supabase_admin IN SCHEMA public GRANT ALL ON TABLES TO authenticated$stmt$;
|
|
2564
|
+
EXCEPTION WHEN insufficient_privilege OR undefined_object THEN
|
|
2565
|
+
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$;
|
|
2566
|
+
END $dp$;
|
|
2567
|
+
|
|
2568
|
+
DO $dp$ BEGIN
|
|
2569
|
+
EXECUTE $stmt$ALTER DEFAULT PRIVILEGES FOR ROLE supabase_admin IN SCHEMA public GRANT ALL ON TABLES TO service_role$stmt$;
|
|
2570
|
+
EXCEPTION WHEN insufficient_privilege OR undefined_object THEN
|
|
2571
|
+
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$;
|
|
2572
|
+
END $dp$;
|
|
2573
|
+
|
|
2574
|
+
DO $dp$ BEGIN
|
|
2575
|
+
EXECUTE $stmt$--
|
|
2576
|
+
-- PostgreSQL database dump complete
|
|
2577
|
+
--$stmt$;
|
|
2578
|
+
EXCEPTION WHEN insufficient_privilege OR undefined_object THEN
|
|
2579
|
+
RAISE NOTICE 'baseline: not ours to set \u2014 %', $stmt$--
|
|
2580
|
+
-- PostgreSQL database dump complete
|
|
2581
|
+
--$stmt$;
|
|
2582
|
+
END $dp$;
|
|
2241
2583
|
`;
|
|
2242
|
-
var
|
|
2243
|
-
--
|
|
2244
|
-
-- conversation, and the row that stops it being asked for twice.
|
|
2584
|
+
var MIGRATION_001_A_CAIXA_SABE_QUEM_FALOU_POR_ULTIMO = `-- ---------------------------------------------------------------------------
|
|
2585
|
+
-- 001_a_caixa_sabe_quem_falou_por_ultimo.sql
|
|
2245
2586
|
--
|
|
2246
|
-
--
|
|
2247
|
-
--
|
|
2248
|
-
--
|
|
2249
|
-
--
|
|
2587
|
+
-- A conversa guarda o TEXTO e a HORA da \xFAltima mensagem, e n\xE3o guarda de quem
|
|
2588
|
+
-- ela foi. Sem isso, "quem est\xE1 esperando resposta" \u2014 que \xE9 a pergunta com que
|
|
2589
|
+
-- este produto abre o dia \u2014 n\xE3o \xE9 respond\xEDvel a partir da lista: seria preciso
|
|
2590
|
+
-- carregar as mensagens de cada thread para descobrir quem falou por \xFAltimo.
|
|
2250
2591
|
--
|
|
2251
|
-
--
|
|
2252
|
-
--
|
|
2253
|
-
--
|
|
2254
|
-
--
|
|
2255
|
-
-- account (the redundancy the design accepted on purpose). It is also the
|
|
2256
|
-
-- Idempotency-Key of the create call, so a retried create replays the provider's
|
|
2257
|
-
-- own first answer instead of opening a second charge.
|
|
2592
|
+
-- O painel respondia com \`unread_count > 0\`, que \xE9 a aproxima\xE7\xE3o honesta que o
|
|
2593
|
+
-- shape permitia e n\xE3o \xE9 a resposta certa. Ela erra exatamente no caso que mais
|
|
2594
|
+
-- d\xF3i: a thread que algu\xE9m ABRIU, leu, decidiu responder depois e esqueceu. O
|
|
2595
|
+
-- n\xE3o-lida zera na leitura; o cliente continua esperando.
|
|
2258
2596
|
--
|
|
2259
|
-
-- \u2500\u2500
|
|
2260
|
-
-- \`link_delivered_at\` and \`settled_notified_at\` are LOCKS, taken with a
|
|
2261
|
-
-- conditional UPDATE \u2026 WHERE \u2026 IS NULL RETURNING. They exist because of a fact
|
|
2262
|
-
-- verified against the sandbox on 2026-08-24: ONE Pix becoming available emits
|
|
2263
|
-
-- TWO webhook events with two different event ids \u2014
|
|
2597
|
+
-- \u2500\u2500 Por que TRIGGER e n\xE3o mais uma coluna que cada escritor preenche \u2500\u2500\u2500\u2500\u2500\u2500\u2500
|
|
2264
2598
|
--
|
|
2265
|
-
--
|
|
2266
|
-
--
|
|
2599
|
+
-- \`last_message_preview\` e \`last_message_at\` s\xE3o escritos hoje em CINCO lugares:
|
|
2600
|
+
-- o provider do navegador (criar conversa, enviar), a fun\xE7\xE3o \`messaging-send\` e
|
|
2601
|
+
-- o \`tyxter-webhook\` (duas vezes, contando o handler de pagamento). Uma sexta
|
|
2602
|
+
-- coluna com a mesma disciplina \xE9 uma sexta chance de algu\xE9m esquecer \u2014 e o
|
|
2603
|
+
-- modo de falhar \xE9 silencioso: a fila simplesmente para de listar algu\xE9m.
|
|
2267
2604
|
--
|
|
2268
|
-
--
|
|
2269
|
-
--
|
|
2270
|
-
--
|
|
2605
|
+
-- O gatilho tem UM escritor e n\xE3o pode ser esquecido pelo pr\xF3ximo chamador. Ele
|
|
2606
|
+
-- \xE9 deliberadamente estreito: cuida s\xF3 de \`last_message_direction\`. Trazer o
|
|
2607
|
+
-- preview e a hora para c\xE1 seria a corre\xE7\xE3o certa e \xE9 uma mudan\xE7a maior, com
|
|
2608
|
+
-- cinco chamadores para reconciliar \u2014 fica anotado, n\xE3o feito aqui.
|
|
2609
|
+
-- ---------------------------------------------------------------------------
|
|
2610
|
+
|
|
2611
|
+
ALTER TABLE public.plg_conversations
|
|
2612
|
+
ADD COLUMN IF NOT EXISTS last_message_direction text;
|
|
2613
|
+
|
|
2614
|
+
DO $$
|
|
2615
|
+
BEGIN
|
|
2616
|
+
ALTER TABLE public.plg_conversations
|
|
2617
|
+
ADD CONSTRAINT plg_conversations_last_message_direction_check
|
|
2618
|
+
CHECK (last_message_direction IS NULL
|
|
2619
|
+
OR last_message_direction = ANY (ARRAY['inbound'::text, 'outbound'::text]));
|
|
2620
|
+
EXCEPTION WHEN duplicate_object THEN
|
|
2621
|
+
NULL;
|
|
2622
|
+
END $$;
|
|
2623
|
+
|
|
2624
|
+
COMMENT ON COLUMN public.plg_conversations.last_message_direction IS
|
|
2625
|
+
'Quem falou por \xFAltimo: inbound = o cliente, outbound = a casa (001). Mantida por gatilho em plg_conversation_messages, nunca escrita \xE0 m\xE3o. NULL = thread anterior a esta migration cujo backfill n\xE3o achou mensagem nenhuma.';
|
|
2626
|
+
|
|
2627
|
+
-- \u2500\u2500 o \xFAnico 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
|
|
2271
2628
|
--
|
|
2272
|
-
-- \
|
|
2273
|
-
--
|
|
2274
|
-
--
|
|
2275
|
-
-- must not walk a settled charge back to "waiting". The CHECK carries the
|
|
2276
|
-
-- provider's own vocabulary verbatim, including \`approval_requested\` and
|
|
2277
|
-
-- \`approved\`, which the design's list did not have and the OpenAPI does.
|
|
2629
|
+
-- AFTER INSERT: a mensagem j\xE1 existe quando a conversa \xE9 atualizada, ent\xE3o uma
|
|
2630
|
+
-- falha aqui n\xE3o pode desfazer o recebimento \u2014 e recebimento perdido \xE9 pior que
|
|
2631
|
+
-- fila desatualizada.
|
|
2278
2632
|
--
|
|
2279
|
-
--
|
|
2280
|
-
--
|
|
2281
|
-
--
|
|
2282
|
-
--
|
|
2283
|
-
|
|
2284
|
-
|
|
2633
|
+
-- Sem cl\xE1usula de ordem: a mensagem que acabou de entrar \xC9 a \xFAltima. Comparar
|
|
2634
|
+
-- com \`last_message_at\` para decidir seria correto e traria uma corrida com os
|
|
2635
|
+
-- cinco escritores daquela coluna, que \xE9 justamente o problema que o gatilho
|
|
2636
|
+
-- existe para n\xE3o ter.
|
|
2637
|
+
CREATE OR REPLACE FUNCTION public.plg_conversations_stamp_direction()
|
|
2638
|
+
RETURNS trigger
|
|
2639
|
+
LANGUAGE plpgsql
|
|
2640
|
+
SECURITY DEFINER
|
|
2641
|
+
SET search_path TO ''
|
|
2642
|
+
AS $function$
|
|
2643
|
+
BEGIN
|
|
2644
|
+
IF NEW.direction IS NULL THEN
|
|
2645
|
+
RETURN NULL;
|
|
2646
|
+
END IF;
|
|
2285
2647
|
|
|
2286
|
-
|
|
2287
|
-
|
|
2288
|
-
|
|
2289
|
-
|
|
2290
|
-
|
|
2291
|
-
|
|
2292
|
-
|
|
2293
|
-
|
|
2294
|
-
|
|
2295
|
-
|
|
2296
|
-
|
|
2297
|
-
|
|
2298
|
-
|
|
2299
|
-
|
|
2300
|
-
|
|
2301
|
-
-- rounds to 4499 is a customer charged one centavo less than the salon reads.
|
|
2302
|
-
amount_cents integer NOT NULL CHECK (amount_cents > 0),
|
|
2303
|
-
currency text NOT NULL DEFAULT 'BRL',
|
|
2304
|
-
description text,
|
|
2305
|
-
-- Provider-neutral, like every other column in this plugin: the second
|
|
2306
|
-
-- payment rail must not need a second table.
|
|
2307
|
-
provider text NOT NULL,
|
|
2308
|
-
status text NOT NULL DEFAULT 'created'
|
|
2309
|
-
CHECK (status IN ('created', 'link_generated', 'approval_requested', 'approved',
|
|
2310
|
-
'paid', 'failed', 'expired', 'cancelled')),
|
|
2311
|
-
provider_payment_id text,
|
|
2312
|
-
payment_link_url text,
|
|
2313
|
-
-- Kept because the customer may lose the message and ask for it again, and
|
|
2314
|
-
-- re-reading a code we already have beats opening a second charge.
|
|
2315
|
-
pix_copy_paste text,
|
|
2316
|
-
-- The claims. See the header.
|
|
2317
|
-
link_delivered_at timestamptz,
|
|
2318
|
-
settled_notified_at timestamptz,
|
|
2319
|
-
settled_at timestamptz,
|
|
2320
|
-
created_at timestamptz NOT NULL DEFAULT now(),
|
|
2321
|
-
updated_at timestamptz NOT NULL DEFAULT now()
|
|
2322
|
-
);
|
|
2323
|
-
ALTER TABLE public.plg_conversations_payment_requests ENABLE ROW LEVEL SECURITY;
|
|
2648
|
+
UPDATE public.plg_conversations
|
|
2649
|
+
SET last_message_direction = NEW.direction
|
|
2650
|
+
WHERE id = NEW.conversation_id;
|
|
2651
|
+
|
|
2652
|
+
RETURN NULL;
|
|
2653
|
+
END $function$;
|
|
2654
|
+
|
|
2655
|
+
COMMENT ON FUNCTION public.plg_conversations_stamp_direction() IS
|
|
2656
|
+
'Carimba plg_conversations.last_message_direction a cada mensagem inserida (001). O \xFAnico escritor da coluna.';
|
|
2657
|
+
|
|
2658
|
+
DROP TRIGGER IF EXISTS plg_conversation_messages_stamp_direction ON public.plg_conversation_messages;
|
|
2659
|
+
CREATE TRIGGER plg_conversation_messages_stamp_direction
|
|
2660
|
+
AFTER INSERT ON public.plg_conversation_messages
|
|
2661
|
+
FOR EACH ROW
|
|
2662
|
+
EXECUTE FUNCTION public.plg_conversations_stamp_direction();
|
|
2324
2663
|
|
|
2325
|
-
--
|
|
2326
|
-
--
|
|
2327
|
-
--
|
|
2328
|
-
|
|
2329
|
-
|
|
2330
|
-
|
|
2331
|
-
|
|
2332
|
-
|
|
2333
|
-
|
|
2334
|
-
|
|
2335
|
-
|
|
2336
|
-
|
|
2337
|
-
|
|
2338
|
-
|
|
2339
|
-
|
|
2340
|
-
|
|
2341
|
-
|
|
2342
|
-
|
|
2343
|
-
|
|
2344
|
-
|
|
2345
|
-
CREATE INDEX IF NOT EXISTS
|
|
2346
|
-
ON public.
|
|
2347
|
-
WHERE
|
|
2348
|
-
|
|
2349
|
-
-- RLS: members read their own tenant's charges. No INSERT/UPDATE/DELETE policy
|
|
2350
|
-
-- and no write GRANT \u2014 every write here is made by the webhook with the
|
|
2351
|
-
-- service-role key, acting on a signed delivery.
|
|
2352
|
-
DROP POLICY IF EXISTS plg_conversations_payment_requests_select
|
|
2353
|
-
ON public.plg_conversations_payment_requests;
|
|
2354
|
-
CREATE POLICY plg_conversations_payment_requests_select
|
|
2355
|
-
ON public.plg_conversations_payment_requests
|
|
2356
|
-
FOR SELECT TO authenticated
|
|
2357
|
-
USING (tenant_id IN (SELECT public.user_tenant_ids()));
|
|
2358
|
-
GRANT SELECT ON public.plg_conversations_payment_requests TO authenticated;
|
|
2664
|
+
-- \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
|
|
2665
|
+
--
|
|
2666
|
+
-- DISTINCT ON pega uma linha por conversa, a mais recente. \`at DESC NULLS LAST\`
|
|
2667
|
+
-- e depois \`id DESC\` porque \`at\` tem default e n\xE3o NOT NULL: duas mensagens no
|
|
2668
|
+
-- mesmo instante (ou ambas sem hora) precisam de um desempate est\xE1vel, ou o
|
|
2669
|
+
-- backfill devolve resultado diferente a cada execu\xE7\xE3o.
|
|
2670
|
+
UPDATE public.plg_conversations c
|
|
2671
|
+
SET last_message_direction = m.direction
|
|
2672
|
+
FROM (
|
|
2673
|
+
SELECT DISTINCT ON (conversation_id) conversation_id, direction
|
|
2674
|
+
FROM public.plg_conversation_messages
|
|
2675
|
+
WHERE direction IS NOT NULL
|
|
2676
|
+
ORDER BY conversation_id, at DESC NULLS LAST, id DESC
|
|
2677
|
+
) m
|
|
2678
|
+
WHERE m.conversation_id = c.id
|
|
2679
|
+
AND c.last_message_direction IS DISTINCT FROM m.direction;
|
|
2680
|
+
|
|
2681
|
+
-- S\xF3 as threads que interessam \xE0 fila, e s\xF3 quando o cliente falou por \xFAltimo:
|
|
2682
|
+
-- \xE9 o \xEDndice que a pergunta "quem est\xE1 esperando" faz, e ele fica pequeno
|
|
2683
|
+
-- porque a maioria das conversas de um tenant est\xE1 encerrada.
|
|
2684
|
+
CREATE INDEX IF NOT EXISTS idx_plg_conversations_waiting
|
|
2685
|
+
ON public.plg_conversations (tenant_id, last_message_at DESC)
|
|
2686
|
+
WHERE status = 'open' AND last_message_direction = 'inbound';
|
|
2359
2687
|
`;
|
|
2688
|
+
var MIGRATIONS = [
|
|
2689
|
+
{ id: "000_baseline", sql: MIGRATION_000_BASELINE },
|
|
2690
|
+
{ id: "001_a_caixa_sabe_quem_falou_por_ultimo", sql: MIGRATION_001_A_CAIXA_SABE_QUEM_FALOU_POR_ULTIMO }
|
|
2691
|
+
];
|
|
2360
2692
|
|
|
2361
2693
|
// src/index.ts
|
|
2362
2694
|
function createSafeProvider() {
|
|
@@ -2388,7 +2720,8 @@ function createConversationsPlugin(options) {
|
|
|
2388
2720
|
contactLookup: options?.contactLookup,
|
|
2389
2721
|
contactEntityDef: options?.contactEntityDef
|
|
2390
2722
|
};
|
|
2391
|
-
const
|
|
2723
|
+
const dashboardWidgets = createConversationsDashboardWidgets({ store: store2, config });
|
|
2724
|
+
const PageComponent = () => React4__default.createElement(ConversationsPage, { store: store2, config });
|
|
2392
2725
|
PageComponent.displayName = "ConversationsPage";
|
|
2393
2726
|
return {
|
|
2394
2727
|
id: "conversations",
|
|
@@ -2424,6 +2757,7 @@ function createConversationsPlugin(options) {
|
|
|
2424
2757
|
}
|
|
2425
2758
|
],
|
|
2426
2759
|
widgets: [],
|
|
2760
|
+
dashboardWidgets,
|
|
2427
2761
|
events: [
|
|
2428
2762
|
{ name: "conversations.message.received", description: "An inbound message arrived on any channel" },
|
|
2429
2763
|
{ name: "conversations.message.sent", description: "An outbound message was sent" },
|
|
@@ -2493,62 +2827,15 @@ function createConversationsPlugin(options) {
|
|
|
2493
2827
|
permission: { feature: "conversations", action: "create" }
|
|
2494
2828
|
}
|
|
2495
2829
|
],
|
|
2496
|
-
|
|
2497
|
-
|
|
2498
|
-
|
|
2499
|
-
|
|
2500
|
-
|
|
2501
|
-
|
|
2502
|
-
|
|
2503
|
-
|
|
2504
|
-
|
|
2505
|
-
version: "1.1.0",
|
|
2506
|
-
sql: MIGRATION_002_CONTACT_PERSON,
|
|
2507
|
-
description: "Link threads to public.people via contact_person_id (nullable, ON DELETE SET NULL)"
|
|
2508
|
-
},
|
|
2509
|
-
{
|
|
2510
|
-
id: "conversations-003-channels",
|
|
2511
|
-
version: "1.2.0",
|
|
2512
|
-
sql: MIGRATION_003_CHANNELS,
|
|
2513
|
-
description: "Create plg_conversations_channels \u2014 which number a message leaves by (product fallback vs tenant dedicated); member read, service-role writes"
|
|
2514
|
-
},
|
|
2515
|
-
{
|
|
2516
|
-
id: "conversations-004-message-delivery",
|
|
2517
|
-
version: "1.2.0",
|
|
2518
|
-
sql: MIGRATION_004_MESSAGE_DELIVERY,
|
|
2519
|
-
description: "Message provider_message_id + delivery_status + sender attribution (sender_kind / sender_label)"
|
|
2520
|
-
},
|
|
2521
|
-
{
|
|
2522
|
-
id: "conversations-005-webhook-events",
|
|
2523
|
-
version: "1.3.0",
|
|
2524
|
-
sql: MIGRATION_005_WEBHOOK_EVENTS,
|
|
2525
|
-
description: "Create plg_conversations_webhook_events \u2014 the dedupe ledger that makes an at-least-once webhook safe to apply"
|
|
2526
|
-
},
|
|
2527
|
-
{
|
|
2528
|
-
id: "conversations-006-optouts",
|
|
2529
|
-
version: "1.3.0",
|
|
2530
|
-
sql: MIGRATION_006_OPTOUTS,
|
|
2531
|
-
description: "Create plg_conversations_optouts \u2014 consent suppression read by messaging-send before every send"
|
|
2532
|
-
},
|
|
2533
|
-
{
|
|
2534
|
-
id: "conversations-007-inbound-routing",
|
|
2535
|
-
version: "1.3.0",
|
|
2536
|
-
sql: MIGRATION_007_INBOUND_ROUTING,
|
|
2537
|
-
description: "Widen delivery_status to the statuses Tyxter emits + plg_conversations_match_by_phone (BR digit-suffix thread matching, service-role only)"
|
|
2538
|
-
},
|
|
2539
|
-
{
|
|
2540
|
-
id: "conversations-008-message-subject",
|
|
2541
|
-
version: "1.3.0",
|
|
2542
|
-
sql: MIGRATION_008_MESSAGE_SUBJECT,
|
|
2543
|
-
description: "Message subject_type / subject_id \u2014 the correlation anchor a button reply falls back to when the provider echoes the button text instead of our payload"
|
|
2544
|
-
},
|
|
2545
|
-
{
|
|
2546
|
-
id: "conversations-009-payment-requests",
|
|
2547
|
-
version: "1.4.0",
|
|
2548
|
-
sql: MIGRATION_009_PAYMENT_REQUESTS,
|
|
2549
|
-
description: "Create plg_conversations_payment_requests \u2014 the charge opened inside a conversation, with the row-level claims that make one Pix arrive once and a settlement notify once"
|
|
2550
|
-
}
|
|
2551
|
-
],
|
|
2830
|
+
// One baseline per unit since #338 — read from the generated barrel
|
|
2831
|
+
// rather than listed here, so regenerating the SQL never needs a
|
|
2832
|
+
// manifest edit to match.
|
|
2833
|
+
migrations: MIGRATIONS.map((entry) => ({
|
|
2834
|
+
id: `conversations-${entry.id}`,
|
|
2835
|
+
version: "1.0.0",
|
|
2836
|
+
sql: entry.sql,
|
|
2837
|
+
description: "The whole schema this plugin provisions, as one file: every table, view, function, policy and grant, plus the reference rows it seeds. Replaced the per-change chain in the 1.0.0 squash (#338); the files it collapsed are kept, unapplied, in src/migrations.archive/."
|
|
2838
|
+
})),
|
|
2552
2839
|
functions: [
|
|
2553
2840
|
{
|
|
2554
2841
|
slug: "tyxter-webhook",
|
|
@@ -2607,6 +2894,20 @@ function createConversationsPlugin(options) {
|
|
|
2607
2894
|
]
|
|
2608
2895
|
}
|
|
2609
2896
|
],
|
|
2897
|
+
// The settings screen exists so the connector below can be REACHED: the
|
|
2898
|
+
// Integrations tab is drawn by PluginSettingsPanel, so a plugin with a
|
|
2899
|
+
// connector and no settings route ships a connector nobody can open.
|
|
2900
|
+
settings: [
|
|
2901
|
+
{
|
|
2902
|
+
id: "conversations",
|
|
2903
|
+
label: "Conversas",
|
|
2904
|
+
icon: "MessageCircle",
|
|
2905
|
+
component: ConversationsSettingsTab,
|
|
2906
|
+
order: 30,
|
|
2907
|
+
permission: { feature: "conversations", action: "read" }
|
|
2908
|
+
}
|
|
2909
|
+
],
|
|
2910
|
+
onboarding: { program: buildConversationsOnboarding() },
|
|
2610
2911
|
// WhatsApp through Tyxter. Declared here because this plugin IS the inbox
|
|
2611
2912
|
// the messages land in — the connector configures inside Conversas.
|
|
2612
2913
|
connectors: [tyxterConnectorDef],
|
|
@@ -2614,6 +2915,6 @@ function createConversationsPlugin(options) {
|
|
|
2614
2915
|
};
|
|
2615
2916
|
}
|
|
2616
2917
|
|
|
2617
|
-
export { PAYMENT_PREFLIGHT_FUNCTION, TYXTER_CONNECTOR_ID, TYXTER_LATENCY_BUDGET_MS, canOfferPayment, createConversationsPlugin, createMockConversationsProvider, createSupabaseConversationsProvider, listMessagingChannels, openPaymentSetupSession, readPaymentReadiness, tyxterConnectorDef };
|
|
2918
|
+
export { PAYMENT_PREFLIGHT_FUNCTION, TYXTER_CONNECTOR_ID, TYXTER_LATENCY_BUDGET_MS, canOfferPayment, createConversationsPlugin, createMockConversationsProvider, createSupabaseConversationsProvider, isWaitingOnUs, listMessagingChannels, openPaymentSetupSession, readPaymentReadiness, tyxterConnectorDef };
|
|
2618
2919
|
//# sourceMappingURL=index.js.map
|
|
2619
2920
|
//# sourceMappingURL=index.js.map
|