evo360-types 1.3.715 → 1.3.717

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.
@@ -278,12 +278,12 @@ export declare const zAppointmentRescheduleSchema: z.ZodObject<{
278
278
  userRef?: any;
279
279
  external_id?: string | null | undefined;
280
280
  user_name?: string | undefined;
281
+ reason?: string | undefined;
281
282
  appointmentSource?: {
282
283
  code: string;
283
284
  name: string;
284
285
  external_id?: string | null | undefined;
285
286
  } | undefined;
286
- reason?: string | undefined;
287
287
  oldStartDate?: Date | undefined;
288
288
  oldEndDate?: Date | undefined;
289
289
  newStartDate?: Date | undefined;
@@ -293,12 +293,12 @@ export declare const zAppointmentRescheduleSchema: z.ZodObject<{
293
293
  userRef?: any;
294
294
  external_id?: string | null | undefined;
295
295
  user_name?: string | undefined;
296
+ reason?: string | undefined;
296
297
  appointmentSource?: {
297
298
  code: string;
298
299
  name: string;
299
300
  external_id?: string | null | undefined;
300
301
  } | undefined;
301
- reason?: string | undefined;
302
302
  oldStartDate?: unknown;
303
303
  oldEndDate?: unknown;
304
304
  newStartDate?: unknown;
@@ -650,12 +650,12 @@ export declare const zAppointmentSchema: z.ZodObject<{
650
650
  userRef?: any;
651
651
  external_id?: string | null | undefined;
652
652
  user_name?: string | undefined;
653
+ reason?: string | undefined;
653
654
  appointmentSource?: {
654
655
  code: string;
655
656
  name: string;
656
657
  external_id?: string | null | undefined;
657
658
  } | undefined;
658
- reason?: string | undefined;
659
659
  oldStartDate?: Date | undefined;
660
660
  oldEndDate?: Date | undefined;
661
661
  newStartDate?: Date | undefined;
@@ -665,12 +665,12 @@ export declare const zAppointmentSchema: z.ZodObject<{
665
665
  userRef?: any;
666
666
  external_id?: string | null | undefined;
667
667
  user_name?: string | undefined;
668
+ reason?: string | undefined;
668
669
  appointmentSource?: {
669
670
  code: string;
670
671
  name: string;
671
672
  external_id?: string | null | undefined;
672
673
  } | undefined;
673
- reason?: string | undefined;
674
674
  oldStartDate?: unknown;
675
675
  oldEndDate?: unknown;
676
676
  newStartDate?: unknown;
@@ -1017,12 +1017,12 @@ export declare const zAppointmentSchema: z.ZodObject<{
1017
1017
  userRef?: any;
1018
1018
  external_id?: string | null | undefined;
1019
1019
  user_name?: string | undefined;
1020
+ reason?: string | undefined;
1020
1021
  appointmentSource?: {
1021
1022
  code: string;
1022
1023
  name: string;
1023
1024
  external_id?: string | null | undefined;
1024
1025
  } | undefined;
1025
- reason?: string | undefined;
1026
1026
  oldStartDate?: Date | undefined;
1027
1027
  oldEndDate?: Date | undefined;
1028
1028
  newStartDate?: Date | undefined;
@@ -1032,12 +1032,12 @@ export declare const zAppointmentSchema: z.ZodObject<{
1032
1032
  userRef?: any;
1033
1033
  external_id?: string | null | undefined;
1034
1034
  user_name?: string | undefined;
1035
+ reason?: string | undefined;
1035
1036
  appointmentSource?: {
1036
1037
  code: string;
1037
1038
  name: string;
1038
1039
  external_id?: string | null | undefined;
1039
1040
  } | undefined;
1040
- reason?: string | undefined;
1041
1041
  oldStartDate?: unknown;
1042
1042
  oldEndDate?: unknown;
1043
1043
  newStartDate?: unknown;
@@ -1384,12 +1384,12 @@ export declare const zAppointmentSchema: z.ZodObject<{
1384
1384
  userRef?: any;
1385
1385
  external_id?: string | null | undefined;
1386
1386
  user_name?: string | undefined;
1387
+ reason?: string | undefined;
1387
1388
  appointmentSource?: {
1388
1389
  code: string;
1389
1390
  name: string;
1390
1391
  external_id?: string | null | undefined;
1391
1392
  } | undefined;
1392
- reason?: string | undefined;
1393
1393
  oldStartDate?: Date | undefined;
1394
1394
  oldEndDate?: Date | undefined;
1395
1395
  newStartDate?: Date | undefined;
@@ -1399,12 +1399,12 @@ export declare const zAppointmentSchema: z.ZodObject<{
1399
1399
  userRef?: any;
1400
1400
  external_id?: string | null | undefined;
1401
1401
  user_name?: string | undefined;
1402
+ reason?: string | undefined;
1402
1403
  appointmentSource?: {
1403
1404
  code: string;
1404
1405
  name: string;
1405
1406
  external_id?: string | null | undefined;
1406
1407
  } | undefined;
1407
- reason?: string | undefined;
1408
1408
  oldStartDate?: unknown;
1409
1409
  oldEndDate?: unknown;
1410
1410
  newStartDate?: unknown;
package/dist/index.d.ts CHANGED
@@ -58,6 +58,7 @@ export * from "./apps/evo-chat/waba-template/zod-schemas";
58
58
  export * from "./apps/evo-chat/waba-setup/zod-schemas";
59
59
  export * from "./apps/evo-chat/ig-setup/zod-schemas";
60
60
  export * from "./apps/evo-chat/quick-reply/zod-schemas";
61
+ export * from "./apps/evo-chat/forward/zod-schemas";
61
62
  export * from "./apps/evo-hub-ia/channel/zod-schemas";
62
63
  export * from "./apps/evo-hub-ia/v1/zod-schemas";
63
64
  export * from "./apps/evo-hub-ia/v1/adapter-tool-registry";
package/dist/index.js CHANGED
@@ -76,6 +76,7 @@ __exportStar(require("./apps/evo-chat/waba-template/zod-schemas"), exports);
76
76
  __exportStar(require("./apps/evo-chat/waba-setup/zod-schemas"), exports);
77
77
  __exportStar(require("./apps/evo-chat/ig-setup/zod-schemas"), exports);
78
78
  __exportStar(require("./apps/evo-chat/quick-reply/zod-schemas"), exports);
79
+ __exportStar(require("./apps/evo-chat/forward/zod-schemas"), exports);
79
80
  __exportStar(require("./apps/evo-hub-ia/channel/zod-schemas"), exports);
80
81
  __exportStar(require("./apps/evo-hub-ia/v1/zod-schemas"), exports);
81
82
  __exportStar(require("./apps/evo-hub-ia/v1/adapter-tool-registry"), exports);
package/dist/index.ts CHANGED
@@ -61,6 +61,7 @@ export * from "./apps/evo-chat/waba-template/zod-schemas";
61
61
  export * from "./apps/evo-chat/waba-setup/zod-schemas";
62
62
  export * from "./apps/evo-chat/ig-setup/zod-schemas";
63
63
  export * from "./apps/evo-chat/quick-reply/zod-schemas";
64
+ export * from "./apps/evo-chat/forward/zod-schemas";
64
65
  export * from "./apps/evo-hub-ia/channel/zod-schemas";
65
66
  export * from "./apps/evo-hub-ia/v1/zod-schemas";
66
67
  export * from "./apps/evo-hub-ia/v1/adapter-tool-registry";
@@ -0,0 +1,168 @@
1
+ import type { IFireDoc } from "../../shared";
2
+ import type { IThreadMessage } from "../thread-message";
3
+ /**
4
+ * feat-282 — encaminhamento de mensagens no Hub Omni.
5
+ *
6
+ * A Cloud API da Meta não tem forward: o encaminhamento é nosso. Cada ação do
7
+ * operador vira um LOTE (`IForwardBatch`) em
8
+ * `tenants/{t}/apps/evo-chat/forwards/{forwardId}`, com um item por
9
+ * (destinatário × mensagem) na subcoleção `items/` e, por destinatário, um item
10
+ * `intro` opcional (texto de apresentação). Cada entrega é uma mensagem NORMAL de
11
+ * saída na thread de destino, com o bloco `forward` (`IMessageForward`) apontando
12
+ * para a origem. A mensagem de origem só ganha `forward_count`.
13
+ */
14
+ export declare const FORWARDS_COLLECTION: "forwards";
15
+ export declare const FORWARD_ITEMS_COLLECTION: "items";
16
+ /** Tetos do lote (plan.md D6). Constantes de plataforma, sem config por tenant. */
17
+ export declare const FORWARD_MAX_MESSAGES = 20;
18
+ export declare const FORWARD_MAX_RECIPIENTS = 10;
19
+ export declare const FORWARD_INTRO_MAX_CHARS = 1000;
20
+ /** Lote `running` sem atualização há mais que isto é mostrado como interrompido. */
21
+ export declare const FORWARD_STALE_AFTER_MS: number;
22
+ /** Evento de timeline gravado na thread de ORIGEM ao criar o lote. */
23
+ export declare const FORWARD_TIMELINE_EVENT_TYPE: "messages_forwarded";
24
+ export type ForwardBatchStatus = "running" | "done" | "done_with_failures";
25
+ export type ForwardItemStatus = "pending" | "sending" | "sent" | "failed" | "skipped";
26
+ /** Motivos decididos na pré-validação (item nasce `skipped`, nenhuma chamada ao provedor). */
27
+ export type ForwardSkipReason = "window_closed" | "type_not_supported" | "media_too_large" | "media_unavailable" | "channel_not_supported";
28
+ /** Motivos de falha no envio (item passou por `sending`). */
29
+ export type ForwardFailReason = "provider_error" | "window_closed" | "send_budget_exceeded" | "interrupted";
30
+ export type ForwardItemReason = ForwardSkipReason | ForwardFailReason;
31
+ export type ForwardContentKind = "text" | "image" | "video" | "audio" | "document";
32
+ /** Canais aceitos como DESTINO na F1. E-mail e SMS ficam para a F2. */
33
+ export type ForwardDestinationKind = "whatsapp" | "instagram";
34
+ export type ForwardRecipientProvider = "hub-waba" | "hub-instagram";
35
+ export type ForwardSpecialKind = "doctor" | "secretary" | "employee";
36
+ export interface IForwardActor {
37
+ user_id: string;
38
+ display_name: string;
39
+ }
40
+ export interface IForwardSource {
41
+ thread_id: string;
42
+ ticket_id: string;
43
+ /** Departamento do ticket de origem: é onde o ticket novo do destino abre (Q4-A). */
44
+ department_id: string | null;
45
+ contact_id: string;
46
+ contact_name: string;
47
+ channel_id: string | null;
48
+ channel_type: string | null;
49
+ }
50
+ export interface IForwardRecipient {
51
+ thread_id: string;
52
+ contact_id: string;
53
+ contact_name: string;
54
+ channel_id: string;
55
+ channel_type: string;
56
+ provider: ForwardRecipientProvider;
57
+ special_kind?: ForwardSpecialKind | null;
58
+ }
59
+ export interface IForwardDispatchState {
60
+ claimed_at: Date;
61
+ finished_at?: Date | null;
62
+ }
63
+ export interface IForwardCounts {
64
+ total: number;
65
+ sent: number;
66
+ failed: number;
67
+ skipped: number;
68
+ }
69
+ export interface IForwardBatch extends IFireDoc {
70
+ forward_id: string;
71
+ tenant: string;
72
+ created_by: IForwardActor;
73
+ source: IForwardSource;
74
+ /** Ordem cronológica da origem (o backend ordena por `created_at`, não pelo payload). */
75
+ message_ids: string[];
76
+ intro_text: string | null;
77
+ recipients: IForwardRecipient[];
78
+ status: ForwardBatchStatus;
79
+ counts: IForwardCounts;
80
+ /** Destinatários com pelo menos um item `pending` (os que recebem mensagem PubSub). */
81
+ recipients_to_dispatch: number;
82
+ recipients_done: number;
83
+ /** Chave = `thread_id` do destinatário. Claim do worker contra entrega duplicada. */
84
+ dispatch?: Record<string, IForwardDispatchState>;
85
+ finished_at?: Date | null;
86
+ }
87
+ export interface IForwardItem {
88
+ /** `${recipient_thread_id}__${message_id}` ou `${recipient_thread_id}__intro`. */
89
+ item_id: string;
90
+ forward_id: string;
91
+ recipient_thread_id: string;
92
+ recipient_index: number;
93
+ /** 0 = intro; 1..N = mensagens na ordem da origem. */
94
+ seq: number;
95
+ kind: "intro" | "message";
96
+ message_id: string | null;
97
+ content_kind: ForwardContentKind;
98
+ status: ForwardItemStatus;
99
+ reason?: ForwardItemReason | null;
100
+ error?: string | null;
101
+ error_code?: number | null;
102
+ dest_message_id?: string | null;
103
+ sent_at?: Date | null;
104
+ updated_at?: Date;
105
+ }
106
+ export type ForwardWindowState = "open" | "human_agent" | "closed";
107
+ export interface IForwardPlanItem {
108
+ item_id: string;
109
+ kind: "intro" | "message";
110
+ message_id: string | null;
111
+ status: "pending" | "skipped";
112
+ reason?: ForwardSkipReason;
113
+ }
114
+ export interface IForwardPlanRecipient {
115
+ thread_id: string;
116
+ channel_id: string;
117
+ channel_type: string;
118
+ contact_name: string;
119
+ window: ForwardWindowState;
120
+ needs_ticket: boolean;
121
+ items: IForwardPlanItem[];
122
+ }
123
+ export interface IForwardPlan {
124
+ recipients: IForwardPlanRecipient[];
125
+ counts: {
126
+ total: number;
127
+ pending: number;
128
+ skipped: number;
129
+ };
130
+ }
131
+ export interface IMessageForward {
132
+ forward_id: string;
133
+ /** Ponte para o item do lote (o status-processor reflete falha assíncrona por aqui). */
134
+ item_id: string;
135
+ /** Vazio no item `intro`: o front não mostra selo quando está vazio. */
136
+ source_message_id: string;
137
+ source_thread_id: string;
138
+ source_ticket_id: string;
139
+ source_contact_id: string;
140
+ source_contact_name: string;
141
+ source_channel_type: string | null;
142
+ source_direction: "in" | "out";
143
+ source_created_at: Date;
144
+ forwarded_by: IForwardActor;
145
+ forwarded_at: Date;
146
+ }
147
+ export type ForwardableMessage = Pick<IThreadMessage, "type" | "direction" | "channel_type" | "body" | "media" | "template"> & {
148
+ revoked?: unknown;
149
+ deleted_at?: unknown;
150
+ };
151
+ export type ForwardContentResolution = {
152
+ ok: true;
153
+ kind: ForwardContentKind;
154
+ text?: string;
155
+ } | {
156
+ ok: false;
157
+ reason: ForwardSkipReason;
158
+ };
159
+ /**
160
+ * Decide se uma mensagem pode ser encaminhada e como vira conteúdo no destino.
161
+ *
162
+ * `dest = null` é a checagem agnóstica do modo de seleção no front (sem canal
163
+ * escolhido). A regra que depende do destino é só uma: documento não vai para
164
+ * Instagram. O teto de tamanho NÃO fica aqui: depende do objeto no Storage.
165
+ */
166
+ export declare function resolveForwardContent(m: ForwardableMessage, dest: ForwardDestinationKind | null): ForwardContentResolution;
167
+ /** Id determinístico do item: um destinatário aparece uma vez no lote, então é único. */
168
+ export declare function buildForwardItemId(recipientThreadId: string, messageId: string | null): string;
@@ -0,0 +1,75 @@
1
+ "use strict";
2
+ Object.defineProperty(exports, "__esModule", { value: true });
3
+ exports.FORWARD_TIMELINE_EVENT_TYPE = exports.FORWARD_STALE_AFTER_MS = 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
+ /** Lote `running` sem atualização há mais que isto é mostrado como interrompido. */
25
+ exports.FORWARD_STALE_AFTER_MS = 10 * 60 * 1000;
26
+ /** Evento de timeline gravado na thread de ORIGEM ao criar o lote. */
27
+ exports.FORWARD_TIMELINE_EVENT_TYPE = "messages_forwarded";
28
+ const MEDIA_KINDS = new Set(["image", "video", "audio", "document"]);
29
+ /** Canais que não entram na F1 nem como origem (e-mail, SMS). */
30
+ const UNSUPPORTED_SOURCE_CHANNELS = new Set(["email", "sms"]);
31
+ /**
32
+ * Decide se uma mensagem pode ser encaminhada e como vira conteúdo no destino.
33
+ *
34
+ * `dest = null` é a checagem agnóstica do modo de seleção no front (sem canal
35
+ * escolhido). A regra que depende do destino é só uma: documento não vai para
36
+ * Instagram. O teto de tamanho NÃO fica aqui: depende do objeto no Storage.
37
+ */
38
+ function resolveForwardContent(m, dest) {
39
+ const direction = String(m.direction ?? "").toLowerCase();
40
+ if (direction !== "in" && direction !== "out")
41
+ return { ok: false, reason: "type_not_supported" };
42
+ if (m.revoked === true)
43
+ return { ok: false, reason: "type_not_supported" };
44
+ if (m.deleted_at !== undefined && m.deleted_at !== null)
45
+ return { ok: false, reason: "type_not_supported" };
46
+ const channelType = String(m.channel_type ?? "").toLowerCase();
47
+ if (UNSUPPORTED_SOURCE_CHANNELS.has(channelType))
48
+ return { ok: false, reason: "type_not_supported" };
49
+ const type = String(m.type ?? "").toLowerCase();
50
+ if (type === "text") {
51
+ const text = (m.body?.text ?? "").trim();
52
+ if (!text)
53
+ return { ok: false, reason: "type_not_supported" };
54
+ return { ok: true, kind: "text", text };
55
+ }
56
+ if (type === "template") {
57
+ const text = (m.template?.rendered?.text ?? "").trim();
58
+ if (!text)
59
+ return { ok: false, reason: "type_not_supported" };
60
+ return { ok: true, kind: "text", text };
61
+ }
62
+ if (MEDIA_KINDS.has(type)) {
63
+ const uri = (m.media?.gcs_uri ?? "").trim();
64
+ if (!uri)
65
+ return { ok: false, reason: "media_unavailable" };
66
+ if (type === "document" && dest === "instagram")
67
+ return { ok: false, reason: "type_not_supported" };
68
+ return { ok: true, kind: type };
69
+ }
70
+ return { ok: false, reason: "type_not_supported" };
71
+ }
72
+ /** Id determinístico do item: um destinatário aparece uma vez no lote, então é único. */
73
+ function buildForwardItemId(recipientThreadId, messageId) {
74
+ return `${recipientThreadId}__${messageId ?? "intro"}`;
75
+ }
@@ -0,0 +1,256 @@
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
+
26
+ /** Lote `running` sem atualização há mais que isto é mostrado como interrompido. */
27
+ export const FORWARD_STALE_AFTER_MS = 10 * 60 * 1000;
28
+
29
+ /** Evento de timeline gravado na thread de ORIGEM ao criar o lote. */
30
+ export const FORWARD_TIMELINE_EVENT_TYPE = "messages_forwarded" as const;
31
+
32
+ // ── Enums ──
33
+
34
+ export type ForwardBatchStatus = "running" | "done" | "done_with_failures";
35
+
36
+ export type ForwardItemStatus = "pending" | "sending" | "sent" | "failed" | "skipped";
37
+
38
+ /** Motivos decididos na pré-validação (item nasce `skipped`, nenhuma chamada ao provedor). */
39
+ export type ForwardSkipReason =
40
+ | "window_closed"
41
+ | "type_not_supported"
42
+ | "media_too_large"
43
+ | "media_unavailable"
44
+ | "channel_not_supported";
45
+
46
+ /** Motivos de falha no envio (item passou por `sending`). */
47
+ export type ForwardFailReason =
48
+ | "provider_error"
49
+ | "window_closed"
50
+ | "send_budget_exceeded"
51
+ | "interrupted";
52
+
53
+ export type ForwardItemReason = ForwardSkipReason | ForwardFailReason;
54
+
55
+ export type ForwardContentKind = "text" | "image" | "video" | "audio" | "document";
56
+
57
+ /** Canais aceitos como DESTINO na F1. E-mail e SMS ficam para a F2. */
58
+ export type ForwardDestinationKind = "whatsapp" | "instagram";
59
+
60
+ export type ForwardRecipientProvider = "hub-waba" | "hub-instagram";
61
+
62
+ export type ForwardSpecialKind = "doctor" | "secretary" | "employee";
63
+
64
+ // ── Batch ──
65
+
66
+ export interface IForwardActor {
67
+ user_id: string;
68
+ display_name: string;
69
+ }
70
+
71
+ export interface IForwardSource {
72
+ thread_id: string;
73
+ ticket_id: string;
74
+ /** Departamento do ticket de origem: é onde o ticket novo do destino abre (Q4-A). */
75
+ department_id: string | null;
76
+ contact_id: string;
77
+ contact_name: string;
78
+ channel_id: string | null;
79
+ channel_type: string | null;
80
+ }
81
+
82
+ export interface IForwardRecipient {
83
+ thread_id: string;
84
+ contact_id: string;
85
+ contact_name: string;
86
+ channel_id: string;
87
+ channel_type: string;
88
+ provider: ForwardRecipientProvider;
89
+ special_kind?: ForwardSpecialKind | null;
90
+ }
91
+
92
+ export interface IForwardDispatchState {
93
+ claimed_at: Date;
94
+ finished_at?: Date | null;
95
+ }
96
+
97
+ export interface IForwardCounts {
98
+ total: number;
99
+ sent: number;
100
+ failed: number;
101
+ skipped: number;
102
+ }
103
+
104
+ export interface IForwardBatch extends IFireDoc {
105
+ forward_id: string;
106
+ tenant: string;
107
+ created_by: IForwardActor;
108
+ source: IForwardSource;
109
+ /** Ordem cronológica da origem (o backend ordena por `created_at`, não pelo payload). */
110
+ message_ids: string[];
111
+ intro_text: string | null;
112
+ recipients: IForwardRecipient[];
113
+ status: ForwardBatchStatus;
114
+ counts: IForwardCounts;
115
+ /** Destinatários com pelo menos um item `pending` (os que recebem mensagem PubSub). */
116
+ recipients_to_dispatch: number;
117
+ recipients_done: number;
118
+ /** Chave = `thread_id` do destinatário. Claim do worker contra entrega duplicada. */
119
+ dispatch?: Record<string, IForwardDispatchState>;
120
+ finished_at?: Date | null;
121
+ }
122
+
123
+ // ── Item ──
124
+
125
+ export interface IForwardItem {
126
+ /** `${recipient_thread_id}__${message_id}` ou `${recipient_thread_id}__intro`. */
127
+ item_id: string;
128
+ forward_id: string;
129
+ recipient_thread_id: string;
130
+ recipient_index: number;
131
+ /** 0 = intro; 1..N = mensagens na ordem da origem. */
132
+ seq: number;
133
+ kind: "intro" | "message";
134
+ message_id: string | null;
135
+ content_kind: ForwardContentKind;
136
+ status: ForwardItemStatus;
137
+ reason?: ForwardItemReason | null;
138
+ error?: string | null;
139
+ error_code?: number | null;
140
+ dest_message_id?: string | null;
141
+ sent_at?: Date | null;
142
+ updated_at?: Date;
143
+ }
144
+
145
+ // ── Plan (resposta do `dry_run` e do POST) ──
146
+
147
+ export type ForwardWindowState = "open" | "human_agent" | "closed";
148
+
149
+ export interface IForwardPlanItem {
150
+ item_id: string;
151
+ kind: "intro" | "message";
152
+ message_id: string | null;
153
+ status: "pending" | "skipped";
154
+ reason?: ForwardSkipReason;
155
+ }
156
+
157
+ export interface IForwardPlanRecipient {
158
+ thread_id: string;
159
+ channel_id: string;
160
+ channel_type: string;
161
+ contact_name: string;
162
+ window: ForwardWindowState;
163
+ needs_ticket: boolean;
164
+ items: IForwardPlanItem[];
165
+ }
166
+
167
+ export interface IForwardPlan {
168
+ recipients: IForwardPlanRecipient[];
169
+ counts: { total: number; pending: number; skipped: number };
170
+ }
171
+
172
+ // ── Bloco gravado na mensagem de DESTINO ──
173
+
174
+ export interface IMessageForward {
175
+ forward_id: string;
176
+ /** Ponte para o item do lote (o status-processor reflete falha assíncrona por aqui). */
177
+ item_id: string;
178
+ /** Vazio no item `intro`: o front não mostra selo quando está vazio. */
179
+ source_message_id: string;
180
+ source_thread_id: string;
181
+ source_ticket_id: string;
182
+ source_contact_id: string;
183
+ source_contact_name: string;
184
+ source_channel_type: string | null;
185
+ source_direction: "in" | "out";
186
+ source_created_at: Date;
187
+ forwarded_by: IForwardActor;
188
+ forwarded_at: Date;
189
+ }
190
+
191
+ // ── Matriz tipo × canal (pura; usada pelo backend na pré-validação e pelo front na seleção) ──
192
+
193
+ export type ForwardableMessage = Pick<
194
+ IThreadMessage,
195
+ "type" | "direction" | "channel_type" | "body" | "media" | "template"
196
+ > & {
197
+ revoked?: unknown;
198
+ deleted_at?: unknown;
199
+ };
200
+
201
+ export type ForwardContentResolution =
202
+ | { ok: true; kind: ForwardContentKind; text?: string }
203
+ | { ok: false; reason: ForwardSkipReason };
204
+
205
+ const MEDIA_KINDS: ReadonlySet<string> = new Set(["image", "video", "audio", "document"]);
206
+
207
+ /** Canais que não entram na F1 nem como origem (e-mail, SMS). */
208
+ const UNSUPPORTED_SOURCE_CHANNELS: ReadonlySet<string> = new Set(["email", "sms"]);
209
+
210
+ /**
211
+ * Decide se uma mensagem pode ser encaminhada e como vira conteúdo no destino.
212
+ *
213
+ * `dest = null` é a checagem agnóstica do modo de seleção no front (sem canal
214
+ * escolhido). A regra que depende do destino é só uma: documento não vai para
215
+ * Instagram. O teto de tamanho NÃO fica aqui: depende do objeto no Storage.
216
+ */
217
+ export function resolveForwardContent(
218
+ m: ForwardableMessage,
219
+ dest: ForwardDestinationKind | null,
220
+ ): ForwardContentResolution {
221
+ const direction = String(m.direction ?? "").toLowerCase();
222
+ if (direction !== "in" && direction !== "out") return { ok: false, reason: "type_not_supported" };
223
+ if (m.revoked === true) return { ok: false, reason: "type_not_supported" };
224
+ if (m.deleted_at !== undefined && m.deleted_at !== null) return { ok: false, reason: "type_not_supported" };
225
+
226
+ const channelType = String(m.channel_type ?? "").toLowerCase();
227
+ if (UNSUPPORTED_SOURCE_CHANNELS.has(channelType)) return { ok: false, reason: "type_not_supported" };
228
+
229
+ const type = String(m.type ?? "").toLowerCase();
230
+
231
+ if (type === "text") {
232
+ const text = (m.body?.text ?? "").trim();
233
+ if (!text) return { ok: false, reason: "type_not_supported" };
234
+ return { ok: true, kind: "text", text };
235
+ }
236
+
237
+ if (type === "template") {
238
+ const text = (m.template?.rendered?.text ?? "").trim();
239
+ if (!text) return { ok: false, reason: "type_not_supported" };
240
+ return { ok: true, kind: "text", text };
241
+ }
242
+
243
+ if (MEDIA_KINDS.has(type)) {
244
+ const uri = (m.media?.gcs_uri ?? "").trim();
245
+ if (!uri) return { ok: false, reason: "media_unavailable" };
246
+ if (type === "document" && dest === "instagram") return { ok: false, reason: "type_not_supported" };
247
+ return { ok: true, kind: type as ForwardContentKind };
248
+ }
249
+
250
+ return { ok: false, reason: "type_not_supported" };
251
+ }
252
+
253
+ /** Id determinístico do item: um destinatário aparece uma vez no lote, então é único. */
254
+ export function buildForwardItemId(recipientThreadId: string, messageId: string | null): string {
255
+ return `${recipientThreadId}__${messageId ?? "intro"}`;
256
+ }
@@ -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
  };