evo360-types 1.3.713 → 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";
@@ -227,7 +227,18 @@ export interface ICampaignChannelCounters {
227
227
  failed?: number;
228
228
  clicked?: number;
229
229
  replied?: number;
230
+ /**
231
+ * Ticket outcome of sends whose policy asked to close the ticket (F6). Recorded by
232
+ * the hub-omni at the moment it decides. `kept_open` is the RULE (patient already had
233
+ * an open ticket — the campaign never ends a live attendance); `close_failed` is a
234
+ * defect (close attempted, did not happen).
235
+ */
236
+ ticket_closed?: number;
237
+ ticket_kept_open?: number;
238
+ ticket_close_failed?: number;
230
239
  }
240
+ /** What happened to the ticket of a campaign send that asked to close it (F6). */
241
+ export type CampaignTicketOutcome = "closed" | "kept_open" | "close_failed";
231
242
  export interface ICampaignCounters {
232
243
  raw?: number;
233
244
  unique?: number;
@@ -330,6 +341,26 @@ export interface ICampaign extends IFireDoc {
330
341
  finished_at?: Date | null;
331
342
  cancel_reason?: string | null;
332
343
  error_reason?: string | null;
344
+ /**
345
+ * Why the campaign is paused, when the SYSTEM paused it (F6 circuit breaker).
346
+ * `null`/absent on an operator pause and cleared on resume — its presence is what
347
+ * tells the screen "the platform stopped this, here is the cause".
348
+ */
349
+ pause_reason?: ICampaignPauseReason | null;
350
+ }
351
+ /**
352
+ * System pause (F6). A provider error that fails EVERY recipient the same way
353
+ * (account payment, account restricted, template paused/disabled, broken media)
354
+ * stops the dispatch instead of burning the rest of the list.
355
+ */
356
+ export interface ICampaignPauseReason {
357
+ kind: "provider_systemic_error";
358
+ channel: CampaignChannel;
359
+ error_code: string;
360
+ error_message?: string | null;
361
+ /** Recipient whose failure tripped the breaker — the operator can open it. */
362
+ recipient_id?: string | null;
363
+ at: Date;
333
364
  }
334
365
  export type CampaignBucketSourceType = "leads" | "patients" | "appointments" | "csv";
335
366
  export interface ICampaignBucketCsvFile {
@@ -417,6 +448,16 @@ export interface ICampaignRecipientChannelState {
417
448
  failed_at?: Date | null;
418
449
  /** Provider error text that accompanies `failed_at`. */
419
450
  error?: string | null;
451
+ /**
452
+ * Provider error CODE that accompanies `failed_at` (F6). Kept apart from `error`
453
+ * because the code is what the operator-facing breakdown groups by and what the
454
+ * circuit breaker decides on; parsing it back out of the text would be guessing.
455
+ * String, not number: providers other than Meta do not promise numeric codes.
456
+ */
457
+ error_code?: string | null;
458
+ /** Ticket outcome when the policy asked to close it (F6) — see ICampaignChannelCounters. */
459
+ ticket_outcome?: CampaignTicketOutcome | null;
460
+ ticket_id?: string | null;
420
461
  }
421
462
  /**
422
463
  * Why a recipient was kept out. Closed union: every value here HAS a writer in
@@ -304,8 +304,20 @@ export interface ICampaignChannelCounters {
304
304
  failed?: number;
305
305
  clicked?: number;
306
306
  replied?: number;
307
+ /**
308
+ * Ticket outcome of sends whose policy asked to close the ticket (F6). Recorded by
309
+ * the hub-omni at the moment it decides. `kept_open` is the RULE (patient already had
310
+ * an open ticket — the campaign never ends a live attendance); `close_failed` is a
311
+ * defect (close attempted, did not happen).
312
+ */
313
+ ticket_closed?: number;
314
+ ticket_kept_open?: number;
315
+ ticket_close_failed?: number;
307
316
  }
308
317
 
318
+ /** What happened to the ticket of a campaign send that asked to close it (F6). */
319
+ export type CampaignTicketOutcome = "closed" | "kept_open" | "close_failed";
320
+
309
321
  export interface ICampaignCounters {
310
322
  raw?: number;
311
323
  unique?: number;
@@ -422,6 +434,27 @@ export interface ICampaign extends IFireDoc {
422
434
  finished_at?: Date | null;
423
435
  cancel_reason?: string | null;
424
436
  error_reason?: string | null;
437
+ /**
438
+ * Why the campaign is paused, when the SYSTEM paused it (F6 circuit breaker).
439
+ * `null`/absent on an operator pause and cleared on resume — its presence is what
440
+ * tells the screen "the platform stopped this, here is the cause".
441
+ */
442
+ pause_reason?: ICampaignPauseReason | null;
443
+ }
444
+
445
+ /**
446
+ * System pause (F6). A provider error that fails EVERY recipient the same way
447
+ * (account payment, account restricted, template paused/disabled, broken media)
448
+ * stops the dispatch instead of burning the rest of the list.
449
+ */
450
+ export interface ICampaignPauseReason {
451
+ kind: "provider_systemic_error";
452
+ channel: CampaignChannel;
453
+ error_code: string;
454
+ error_message?: string | null;
455
+ /** Recipient whose failure tripped the breaker — the operator can open it. */
456
+ recipient_id?: string | null;
457
+ at: Date;
425
458
  }
426
459
 
427
460
  // ----- Bucket ---------------------------------------------------------------
@@ -550,6 +583,16 @@ export interface ICampaignRecipientChannelState {
550
583
  failed_at?: Date | null;
551
584
  /** Provider error text that accompanies `failed_at`. */
552
585
  error?: string | null;
586
+ /**
587
+ * Provider error CODE that accompanies `failed_at` (F6). Kept apart from `error`
588
+ * because the code is what the operator-facing breakdown groups by and what the
589
+ * circuit breaker decides on; parsing it back out of the text would be guessing.
590
+ * String, not number: providers other than Meta do not promise numeric codes.
591
+ */
592
+ error_code?: string | null;
593
+ /** Ticket outcome when the policy asked to close it (F6) — see ICampaignChannelCounters. */
594
+ ticket_outcome?: CampaignTicketOutcome | null;
595
+ ticket_id?: string | null;
553
596
  }
554
597
 
555
598
  /**
@@ -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
+ }