evo360-types 1.3.715 → 1.3.719

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -0,0 +1,80 @@
1
+ "use strict";
2
+ Object.defineProperty(exports, "__esModule", { value: true });
3
+ exports.FORWARD_TIMELINE_EVENT_TYPE = exports.FORWARD_STALE_AFTER_MS = exports.FORWARD_EMAIL_SUBJECT_MAX_CHARS = exports.FORWARD_INTRO_MAX_CHARS = exports.FORWARD_MAX_RECIPIENTS = exports.FORWARD_MAX_MESSAGES = exports.FORWARD_ITEMS_COLLECTION = exports.FORWARDS_COLLECTION = void 0;
4
+ exports.resolveForwardContent = resolveForwardContent;
5
+ exports.buildForwardItemId = buildForwardItemId;
6
+ /**
7
+ * feat-282 — encaminhamento de mensagens no Hub Omni.
8
+ *
9
+ * A Cloud API da Meta não tem forward: o encaminhamento é nosso. Cada ação do
10
+ * operador vira um LOTE (`IForwardBatch`) em
11
+ * `tenants/{t}/apps/evo-chat/forwards/{forwardId}`, com um item por
12
+ * (destinatário × mensagem) na subcoleção `items/` e, por destinatário, um item
13
+ * `intro` opcional (texto de apresentação). Cada entrega é uma mensagem NORMAL de
14
+ * saída na thread de destino, com o bloco `forward` (`IMessageForward`) apontando
15
+ * para a origem. A mensagem de origem só ganha `forward_count`.
16
+ */
17
+ // ── Constants ──
18
+ exports.FORWARDS_COLLECTION = "forwards";
19
+ exports.FORWARD_ITEMS_COLLECTION = "items";
20
+ /** Tetos do lote (plan.md D6). Constantes de plataforma, sem config por tenant. */
21
+ exports.FORWARD_MAX_MESSAGES = 20;
22
+ exports.FORWARD_MAX_RECIPIENTS = 10;
23
+ exports.FORWARD_INTRO_MAX_CHARS = 1000;
24
+ /** Teto do assunto (RFC 5322), igual ao do composer de e-mail do Omni. */
25
+ exports.FORWARD_EMAIL_SUBJECT_MAX_CHARS = 998;
26
+ /** Lote `running` sem atualização há mais que isto é mostrado como interrompido. */
27
+ exports.FORWARD_STALE_AFTER_MS = 10 * 60 * 1000;
28
+ /** Evento de timeline gravado na thread de ORIGEM ao criar o lote. */
29
+ exports.FORWARD_TIMELINE_EVENT_TYPE = "messages_forwarded";
30
+ const MEDIA_KINDS = new Set(["image", "video", "audio", "document"]);
31
+ /** Canais que não entram como origem (e-mail, SMS). */
32
+ const UNSUPPORTED_SOURCE_CHANNELS = new Set(["email", "sms"]);
33
+ /**
34
+ * Decide se uma mensagem pode ser encaminhada e como vira conteúdo no destino.
35
+ *
36
+ * `dest = null` é a checagem agnóstica do modo de seleção no front (sem canal
37
+ * escolhido). Regras que dependem do destino: documento não vai para Instagram;
38
+ * áudio e vídeo não vão para e-mail. O teto de tamanho e o MIME aceito como
39
+ * anexo NÃO ficam aqui: dependem do objeto no Storage e da allowlist do provider.
40
+ */
41
+ function resolveForwardContent(m, dest) {
42
+ const direction = String(m.direction ?? "").toLowerCase();
43
+ if (direction !== "in" && direction !== "out")
44
+ return { ok: false, reason: "type_not_supported" };
45
+ if (m.revoked === true)
46
+ return { ok: false, reason: "type_not_supported" };
47
+ if (m.deleted_at !== undefined && m.deleted_at !== null)
48
+ return { ok: false, reason: "type_not_supported" };
49
+ const channelType = String(m.channel_type ?? "").toLowerCase();
50
+ if (UNSUPPORTED_SOURCE_CHANNELS.has(channelType))
51
+ return { ok: false, reason: "type_not_supported" };
52
+ const type = String(m.type ?? "").toLowerCase();
53
+ if (type === "text") {
54
+ const text = (m.body?.text ?? "").trim();
55
+ if (!text)
56
+ return { ok: false, reason: "type_not_supported" };
57
+ return { ok: true, kind: "text", text };
58
+ }
59
+ if (type === "template") {
60
+ const text = (m.template?.rendered?.text ?? "").trim();
61
+ if (!text)
62
+ return { ok: false, reason: "type_not_supported" };
63
+ return { ok: true, kind: "text", text };
64
+ }
65
+ if (MEDIA_KINDS.has(type)) {
66
+ if (dest === "email" && (type === "audio" || type === "video"))
67
+ return { ok: false, reason: "type_not_supported" };
68
+ const uri = (m.media?.gcs_uri ?? "").trim();
69
+ if (!uri)
70
+ return { ok: false, reason: "media_unavailable" };
71
+ if (type === "document" && dest === "instagram")
72
+ return { ok: false, reason: "type_not_supported" };
73
+ return { ok: true, kind: type };
74
+ }
75
+ return { ok: false, reason: "type_not_supported" };
76
+ }
77
+ /** Id determinístico do item: um destinatário aparece uma vez no lote, então é único. */
78
+ function buildForwardItemId(recipientThreadId, messageId) {
79
+ return `${recipientThreadId}__${messageId ?? "intro"}`;
80
+ }
@@ -0,0 +1,303 @@
1
+ import type { IFireDoc } from "../../shared";
2
+ import type { IThreadMessage } from "../thread-message";
3
+
4
+ /**
5
+ * feat-282 — encaminhamento de mensagens no Hub Omni.
6
+ *
7
+ * A Cloud API da Meta não tem forward: o encaminhamento é nosso. Cada ação do
8
+ * operador vira um LOTE (`IForwardBatch`) em
9
+ * `tenants/{t}/apps/evo-chat/forwards/{forwardId}`, com um item por
10
+ * (destinatário × mensagem) na subcoleção `items/` e, por destinatário, um item
11
+ * `intro` opcional (texto de apresentação). Cada entrega é uma mensagem NORMAL de
12
+ * saída na thread de destino, com o bloco `forward` (`IMessageForward`) apontando
13
+ * para a origem. A mensagem de origem só ganha `forward_count`.
14
+ */
15
+
16
+ // ── Constants ──
17
+
18
+ export const FORWARDS_COLLECTION = "forwards" as const;
19
+ export const FORWARD_ITEMS_COLLECTION = "items" as const;
20
+
21
+ /** Tetos do lote (plan.md D6). Constantes de plataforma, sem config por tenant. */
22
+ export const FORWARD_MAX_MESSAGES = 20;
23
+ export const FORWARD_MAX_RECIPIENTS = 10;
24
+ export const FORWARD_INTRO_MAX_CHARS = 1000;
25
+ /** Teto do assunto (RFC 5322), igual ao do composer de e-mail do Omni. */
26
+ export const FORWARD_EMAIL_SUBJECT_MAX_CHARS = 998;
27
+
28
+ /** Lote `running` sem atualização há mais que isto é mostrado como interrompido. */
29
+ export const FORWARD_STALE_AFTER_MS = 10 * 60 * 1000;
30
+
31
+ /** Evento de timeline gravado na thread de ORIGEM ao criar o lote. */
32
+ export const FORWARD_TIMELINE_EVENT_TYPE = "messages_forwarded" as const;
33
+
34
+ // ── Enums ──
35
+
36
+ export type ForwardBatchStatus = "running" | "done" | "done_with_failures";
37
+
38
+ export type ForwardItemStatus = "pending" | "sending" | "sent" | "failed" | "skipped";
39
+
40
+ /** Motivos decididos na pré-validação (item nasce `skipped`, nenhuma chamada ao provedor). */
41
+ export type ForwardSkipReason =
42
+ | "window_closed"
43
+ | "type_not_supported"
44
+ | "media_too_large"
45
+ | "media_unavailable"
46
+ | "channel_not_supported"
47
+ | "attachments_over_limit"
48
+ | "recipient_address_missing"
49
+ | "email_identity_conflict";
50
+
51
+ /** Motivos de falha no envio (item passou por `sending`). */
52
+ export type ForwardFailReason =
53
+ | "provider_error"
54
+ | "window_closed"
55
+ | "send_budget_exceeded"
56
+ | "interrupted";
57
+
58
+ export type ForwardItemReason = ForwardSkipReason | ForwardFailReason;
59
+
60
+ export type ForwardContentKind = "text" | "image" | "video" | "audio" | "document";
61
+
62
+ /** Canais aceitos como DESTINO. E-mail é agregado: um e-mail por destinatário. SMS fica fora. */
63
+ export type ForwardDestinationKind = "whatsapp" | "instagram" | "email";
64
+
65
+ /** `hub-waba`, `hub-instagram` ou o provider do canal de e-mail (`smtp`, `amazon-ses`, ...). */
66
+ export type ForwardRecipientProvider = string;
67
+
68
+ export type ForwardSpecialKind = "doctor" | "secretary" | "employee";
69
+
70
+ // ── Batch ──
71
+
72
+ export interface IForwardActor {
73
+ user_id: string;
74
+ display_name: string;
75
+ }
76
+
77
+ export interface IForwardSource {
78
+ thread_id: string;
79
+ ticket_id: string;
80
+ /** Departamento do ticket de origem: é onde o ticket novo do destino abre (Q4-A). */
81
+ department_id: string | null;
82
+ contact_id: string;
83
+ contact_name: string;
84
+ channel_id: string | null;
85
+ channel_type: string | null;
86
+ }
87
+
88
+ export interface IForwardRecipient {
89
+ thread_id: string;
90
+ contact_id: string;
91
+ contact_name: string;
92
+ channel_id: string;
93
+ channel_type: string;
94
+ provider: ForwardRecipientProvider;
95
+ special_kind?: ForwardSpecialKind | null;
96
+ /** Destino e-mail: endereço resolvido na prévia. O worker usa este, não resolve de novo. */
97
+ email_address?: string | null;
98
+ }
99
+
100
+ export interface IForwardDispatchState {
101
+ claimed_at: Date;
102
+ finished_at?: Date | null;
103
+ }
104
+
105
+ export interface IForwardCounts {
106
+ total: number;
107
+ sent: number;
108
+ failed: number;
109
+ skipped: number;
110
+ }
111
+
112
+ export interface IForwardBatch extends IFireDoc {
113
+ forward_id: string;
114
+ tenant: string;
115
+ created_by: IForwardActor;
116
+ source: IForwardSource;
117
+ /** Ordem cronológica da origem (o backend ordena por `created_at`, não pelo payload). */
118
+ message_ids: string[];
119
+ intro_text: string | null;
120
+ recipients: IForwardRecipient[];
121
+ status: ForwardBatchStatus;
122
+ counts: IForwardCounts;
123
+ /** Destinatários com pelo menos um item `pending` (os que recebem mensagem PubSub). */
124
+ recipients_to_dispatch: number;
125
+ recipients_done: number;
126
+ /** Chave = `thread_id` do destinatário. Claim do worker contra entrega duplicada. */
127
+ dispatch?: Record<string, IForwardDispatchState>;
128
+ finished_at?: Date | null;
129
+ /** Um assunto por lote; gravado só quando há destinatário e-mail. */
130
+ email_subject?: string | null;
131
+ }
132
+
133
+ // ── Item ──
134
+
135
+ export interface IForwardItem {
136
+ /** `${recipient_thread_id}__${message_id}` ou `${recipient_thread_id}__intro`. */
137
+ item_id: string;
138
+ forward_id: string;
139
+ recipient_thread_id: string;
140
+ recipient_index: number;
141
+ /** 0 = intro; 1..N = mensagens na ordem da origem. */
142
+ seq: number;
143
+ kind: "intro" | "message";
144
+ message_id: string | null;
145
+ content_kind: ForwardContentKind;
146
+ status: ForwardItemStatus;
147
+ reason?: ForwardItemReason | null;
148
+ error?: string | null;
149
+ error_code?: number | null;
150
+ dest_message_id?: string | null;
151
+ /** Destino e-mail: o N de `[Anexo N]` no corpo. */
152
+ attachment_index?: number | null;
153
+ dest_notification_message_id?: string | null;
154
+ sent_at?: Date | null;
155
+ updated_at?: Date;
156
+ }
157
+
158
+ // ── Plan (resposta do `dry_run` e do POST) ──
159
+
160
+ export type ForwardWindowState = "open" | "human_agent" | "closed";
161
+
162
+ export interface IForwardPlanItem {
163
+ item_id: string;
164
+ kind: "intro" | "message";
165
+ message_id: string | null;
166
+ status: "pending" | "skipped";
167
+ reason?: ForwardSkipReason;
168
+ attachment?: { index: number | null; filename: string; size_bytes: number | null } | null;
169
+ }
170
+
171
+ export interface IForwardPlanEmailAttachments {
172
+ count: number;
173
+ bytes: number;
174
+ max_count: number;
175
+ max_file_bytes: number;
176
+ max_total_bytes: number;
177
+ }
178
+
179
+ export interface IForwardPlanEmail {
180
+ channel_id: string;
181
+ provider: string;
182
+ from_address: string | null;
183
+ address: string | null;
184
+ address_source: "typed" | "contact" | "lead" | null;
185
+ identity: "own" | "attach" | "conflict" | null;
186
+ identity_owner_name?: string | null;
187
+ /** `fill`: lead sem e-mail, que o worker preenche antes do envio. */
188
+ lead_identity: "match" | "fill" | "conflict" | null;
189
+ /** Só no conflito do lead, para o aviso. */
190
+ lead_email?: string | null;
191
+ subject: string;
192
+ attachments: IForwardPlanEmailAttachments;
193
+ }
194
+
195
+ export interface IForwardPlanRecipient {
196
+ thread_id: string;
197
+ channel_id: string;
198
+ channel_type: string;
199
+ contact_name: string;
200
+ window: ForwardWindowState;
201
+ needs_ticket: boolean;
202
+ items: IForwardPlanItem[];
203
+ email?: IForwardPlanEmail | null;
204
+ }
205
+
206
+ export interface IForwardPlan {
207
+ recipients: IForwardPlanRecipient[];
208
+ counts: { total: number; pending: number; skipped: number };
209
+ }
210
+
211
+ // ── Bloco gravado na mensagem de DESTINO ──
212
+
213
+ export interface IMessageForward {
214
+ forward_id: string;
215
+ /** Ponte para o item do lote (o status-processor reflete falha assíncrona por aqui). */
216
+ item_id: string;
217
+ /** Vazio no item `intro`: o front não mostra selo quando está vazio. */
218
+ source_message_id: string;
219
+ source_thread_id: string;
220
+ source_ticket_id: string;
221
+ source_contact_id: string;
222
+ source_contact_name: string;
223
+ source_channel_type: string | null;
224
+ source_direction: "in" | "out";
225
+ source_created_at: Date;
226
+ forwarded_by: IForwardActor;
227
+ forwarded_at: Date;
228
+ /**
229
+ * Destino e-mail: um e-mail agrega N mensagens. `source_message_id`/`item_id`
230
+ * apontam para a 1ª em ordem cronológica; estes trazem todas, na mesma ordem.
231
+ */
232
+ source_message_ids?: string[];
233
+ item_ids?: string[];
234
+ }
235
+
236
+ // ── Matriz tipo × canal (pura; usada pelo backend na pré-validação e pelo front na seleção) ──
237
+
238
+ export type ForwardableMessage = Pick<
239
+ IThreadMessage,
240
+ "type" | "direction" | "channel_type" | "body" | "media" | "template"
241
+ > & {
242
+ revoked?: unknown;
243
+ deleted_at?: unknown;
244
+ };
245
+
246
+ export type ForwardContentResolution =
247
+ | { ok: true; kind: ForwardContentKind; text?: string }
248
+ | { ok: false; reason: ForwardSkipReason };
249
+
250
+ const MEDIA_KINDS: ReadonlySet<string> = new Set(["image", "video", "audio", "document"]);
251
+
252
+ /** Canais que não entram como origem (e-mail, SMS). */
253
+ const UNSUPPORTED_SOURCE_CHANNELS: ReadonlySet<string> = new Set(["email", "sms"]);
254
+
255
+ /**
256
+ * Decide se uma mensagem pode ser encaminhada e como vira conteúdo no destino.
257
+ *
258
+ * `dest = null` é a checagem agnóstica do modo de seleção no front (sem canal
259
+ * escolhido). Regras que dependem do destino: documento não vai para Instagram;
260
+ * áudio e vídeo não vão para e-mail. O teto de tamanho e o MIME aceito como
261
+ * anexo NÃO ficam aqui: dependem do objeto no Storage e da allowlist do provider.
262
+ */
263
+ export function resolveForwardContent(
264
+ m: ForwardableMessage,
265
+ dest: ForwardDestinationKind | null,
266
+ ): ForwardContentResolution {
267
+ const direction = String(m.direction ?? "").toLowerCase();
268
+ if (direction !== "in" && direction !== "out") return { ok: false, reason: "type_not_supported" };
269
+ if (m.revoked === true) return { ok: false, reason: "type_not_supported" };
270
+ if (m.deleted_at !== undefined && m.deleted_at !== null) return { ok: false, reason: "type_not_supported" };
271
+
272
+ const channelType = String(m.channel_type ?? "").toLowerCase();
273
+ if (UNSUPPORTED_SOURCE_CHANNELS.has(channelType)) return { ok: false, reason: "type_not_supported" };
274
+
275
+ const type = String(m.type ?? "").toLowerCase();
276
+
277
+ if (type === "text") {
278
+ const text = (m.body?.text ?? "").trim();
279
+ if (!text) return { ok: false, reason: "type_not_supported" };
280
+ return { ok: true, kind: "text", text };
281
+ }
282
+
283
+ if (type === "template") {
284
+ const text = (m.template?.rendered?.text ?? "").trim();
285
+ if (!text) return { ok: false, reason: "type_not_supported" };
286
+ return { ok: true, kind: "text", text };
287
+ }
288
+
289
+ if (MEDIA_KINDS.has(type)) {
290
+ if (dest === "email" && (type === "audio" || type === "video")) return { ok: false, reason: "type_not_supported" };
291
+ const uri = (m.media?.gcs_uri ?? "").trim();
292
+ if (!uri) return { ok: false, reason: "media_unavailable" };
293
+ if (type === "document" && dest === "instagram") return { ok: false, reason: "type_not_supported" };
294
+ return { ok: true, kind: type as ForwardContentKind };
295
+ }
296
+
297
+ return { ok: false, reason: "type_not_supported" };
298
+ }
299
+
300
+ /** Id determinístico do item: um destinatário aparece uma vez no lote, então é único. */
301
+ export function buildForwardItemId(recipientThreadId: string, messageId: string | null): string {
302
+ return `${recipientThreadId}__${messageId ?? "intro"}`;
303
+ }
@@ -14,11 +14,14 @@ export * from "./waba-history-import";
14
14
  export * from "./ig-setup";
15
15
  export * from "./quick-reply";
16
16
  export * from "./search";
17
+ export * from "./forward";
17
18
  export declare const EvoChatPermissions: {
18
19
  readonly Read: "evo_chat_read";
19
20
  readonly Write: "evo_chat_write";
20
21
  readonly InboxManage: "evo_chat_inbox_manage";
21
22
  readonly TicketClose: "evo_chat_ticket_close";
22
23
  readonly TicketTransfer: "evo_chat_ticket_transfer";
24
+ /** feat-254 B2: baixar anexo de e-mail bloqueado pela política (quarentena). Sem papel padrão. */
25
+ readonly QuarantineRead: "evo_chat_quarantine_read";
23
26
  };
24
27
  export type EvoChatPermissions = (typeof EvoChatPermissions)[keyof typeof EvoChatPermissions];
@@ -31,6 +31,7 @@ __exportStar(require("./waba-history-import"), exports);
31
31
  __exportStar(require("./ig-setup"), exports);
32
32
  __exportStar(require("./quick-reply"), exports);
33
33
  __exportStar(require("./search"), exports);
34
+ __exportStar(require("./forward"), exports);
34
35
  // Permissões para o evo-chat
35
36
  exports.EvoChatPermissions = {
36
37
  Read: "evo_chat_read",
@@ -38,4 +39,6 @@ exports.EvoChatPermissions = {
38
39
  InboxManage: "evo_chat_inbox_manage",
39
40
  TicketClose: "evo_chat_ticket_close",
40
41
  TicketTransfer: "evo_chat_ticket_transfer",
42
+ /** feat-254 B2: baixar anexo de e-mail bloqueado pela política (quarentena). Sem papel padrão. */
43
+ QuarantineRead: "evo_chat_quarantine_read",
41
44
  };
@@ -14,6 +14,7 @@ export * from "./waba-history-import";
14
14
  export * from "./ig-setup";
15
15
  export * from "./quick-reply";
16
16
  export * from "./search";
17
+ export * from "./forward";
17
18
 
18
19
  // Permissões para o evo-chat
19
20
  export const EvoChatPermissions = {
@@ -22,6 +23,8 @@ export const EvoChatPermissions = {
22
23
  InboxManage: "evo_chat_inbox_manage",
23
24
  TicketClose: "evo_chat_ticket_close",
24
25
  TicketTransfer: "evo_chat_ticket_transfer",
26
+ /** feat-254 B2: baixar anexo de e-mail bloqueado pela política (quarentena). Sem papel padrão. */
27
+ QuarantineRead: "evo_chat_quarantine_read",
25
28
  } as const;
26
29
 
27
30
  export type EvoChatPermissions =
@@ -1,5 +1,6 @@
1
1
  import { IFireDoc } from "../../shared";
2
2
  import type { IMessageCall } from "../call-session";
3
+ import type { IMessageForward } from "../forward";
3
4
  export type EvoChatMessageType = 'text' | 'image' | 'audio' | 'video' | 'document' | 'sticker' | 'location' | 'contacts' | 'interactive' | 'reaction' | 'template' | 'system' | 'call';
4
5
  export type MessageDirection = 'in' | 'out';
5
6
  export interface IMessageStatus {
@@ -159,6 +160,9 @@ export interface IThreadMessage extends IFireDoc {
159
160
  rendered?: IRenderedTemplateSnapshot;
160
161
  };
161
162
  context?: IMessageContext;
163
+ forward?: IMessageForward;
164
+ /** feat-282: na mensagem de ORIGEM, quantas entregas de encaminhamento já saíram dela. */
165
+ forward_count?: number;
162
166
  call?: IMessageCall;
163
167
  provider_message_id?: string;
164
168
  status?: IMessageStatus;
@@ -1,5 +1,6 @@
1
1
  import { IFireDoc } from "../../shared";
2
2
  import type { IMessageCall } from "../call-session";
3
+ import type { IMessageForward } from "../forward";
3
4
 
4
5
  // ── Message type/direction enums ──
5
6
 
@@ -197,6 +198,11 @@ export interface IThreadMessage extends IFireDoc {
197
198
  };
198
199
  context?: IMessageContext;
199
200
 
201
+ // feat-282: presente na mensagem de DESTINO de um encaminhamento (ver ../forward).
202
+ forward?: IMessageForward;
203
+ /** feat-282: na mensagem de ORIGEM, quantas entregas de encaminhamento já saíram dela. */
204
+ forward_count?: number;
205
+
200
206
  // Call timeline projection (feat-029). Fonte de verdade em ICallSession.
201
207
  call?: IMessageCall;
202
208
 
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "evo360-types",
3
- "version": "1.3.715",
3
+ "version": "1.3.719",
4
4
  "description": "HREVO360 Shared Types",
5
5
  "main": "./dist/index.js",
6
6
  "types": "./dist/index.d.ts",