evo360-types 1.3.726 → 1.3.730
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/apps/evo-chat/forward/zod-schemas.d.ts +86 -9
- package/dist/apps/evo-chat/forward/zod-schemas.js +21 -9
- package/dist/apps/evo-chat/forward/zod-schemas.ts +22 -8
- package/dist/apps/evo-chat/thread-message/zod-schemas.d.ts +273 -0
- package/dist/apps/evo-chat/thread-message/zod-schemas.js +28 -1
- package/dist/apps/evo-chat/thread-message/zod-schemas.ts +29 -0
- package/dist/apps/evo-med/calendar/zod-schemas.d.ts +6 -0
- package/dist/apps/evo-med/calendar/zod-schemas.js +4 -0
- package/dist/apps/evo-med/calendar/zod-schemas.ts +4 -0
- package/dist/apps/evo-notifications/zod-schemas.d.ts +56 -56
- package/dist/types/evo-chat/forward/index.d.ts +67 -3
- package/dist/types/evo-chat/forward/index.js +126 -2
- package/dist/types/evo-chat/forward/index.ts +183 -4
- package/dist/types/evo-chat/thread-message/index.d.ts +35 -0
- package/dist/types/evo-chat/thread-message/index.ts +41 -0
- package/dist/types/evo-med/calendar/index.d.ts +18 -0
- package/dist/types/evo-med/calendar/index.js +6 -1
- package/dist/types/evo-med/calendar/index.ts +21 -0
- package/package.json +1 -1
|
@@ -1,7 +1,10 @@
|
|
|
1
1
|
"use strict";
|
|
2
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;
|
|
3
|
+
exports.FORWARD_TIMELINE_EVENT_TYPE = exports.FORWARD_STALE_AFTER_MS = exports.FORWARD_MAX_PARTS_PER_RECIPIENT = 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
4
|
exports.resolveForwardContent = resolveForwardContent;
|
|
5
|
+
exports.forwardEmailAttachmentSendAs = forwardEmailAttachmentSendAs;
|
|
6
|
+
exports.resolveForwardEmailContent = resolveForwardEmailContent;
|
|
7
|
+
exports.resolveForwardSource = resolveForwardSource;
|
|
5
8
|
exports.buildForwardItemId = buildForwardItemId;
|
|
6
9
|
/**
|
|
7
10
|
* feat-282 — encaminhamento de mensagens no Hub Omni.
|
|
@@ -23,12 +26,22 @@ exports.FORWARD_MAX_RECIPIENTS = 10;
|
|
|
23
26
|
exports.FORWARD_INTRO_MAX_CHARS = 1000;
|
|
24
27
|
/** Teto do assunto (RFC 5322), igual ao do composer de e-mail do Omni. */
|
|
25
28
|
exports.FORWARD_EMAIL_SUBJECT_MAX_CHARS = 998;
|
|
29
|
+
/**
|
|
30
|
+
* Envios por destinatário WhatsApp/Instagram: intro + texto + anexos de cada
|
|
31
|
+
* mensagem. Segura o disjuntor do WhatsApp (40 envios em 6 h por par) quando a
|
|
32
|
+
* origem é e-mail com anexos.
|
|
33
|
+
*/
|
|
34
|
+
exports.FORWARD_MAX_PARTS_PER_RECIPIENT = 30;
|
|
26
35
|
/** Lote `running` sem atualização há mais que isto é mostrado como interrompido. */
|
|
27
36
|
exports.FORWARD_STALE_AFTER_MS = 10 * 60 * 1000;
|
|
28
37
|
/** Evento de timeline gravado na thread de ORIGEM ao criar o lote. */
|
|
29
38
|
exports.FORWARD_TIMELINE_EVENT_TYPE = "messages_forwarded";
|
|
30
39
|
const MEDIA_KINDS = new Set(["image", "video", "audio", "document"]);
|
|
31
|
-
/**
|
|
40
|
+
/**
|
|
41
|
+
* Canais que `resolveForwardContent` não aceita como origem. E-mail entra só por
|
|
42
|
+
* `resolveForwardSource`: abrir este contrato faria código anterior tratar
|
|
43
|
+
* `kind:'email'` como mídia.
|
|
44
|
+
*/
|
|
32
45
|
const UNSUPPORTED_SOURCE_CHANNELS = new Set(["email", "sms"]);
|
|
33
46
|
/**
|
|
34
47
|
* Decide se uma mensagem pode ser encaminhada e como vira conteúdo no destino.
|
|
@@ -74,6 +87,117 @@ function resolveForwardContent(m, dest) {
|
|
|
74
87
|
}
|
|
75
88
|
return { ok: false, reason: "type_not_supported" };
|
|
76
89
|
}
|
|
90
|
+
const WHATSAPP_IMAGE_MIMES = new Set(["image/jpeg", "image/png"]);
|
|
91
|
+
const WHATSAPP_DOCUMENT_MIMES = new Set([
|
|
92
|
+
"application/pdf",
|
|
93
|
+
"text/plain",
|
|
94
|
+
"application/msword",
|
|
95
|
+
"application/vnd.openxmlformats-officedocument.wordprocessingml.document",
|
|
96
|
+
"application/vnd.ms-excel",
|
|
97
|
+
"application/vnd.openxmlformats-officedocument.spreadsheetml.sheet",
|
|
98
|
+
]);
|
|
99
|
+
const INSTAGRAM_IMAGE_MIMES = new Set(["image/jpeg", "image/png", "image/gif"]);
|
|
100
|
+
function normalizeMime(mime) {
|
|
101
|
+
return String(mime ?? "").split(";")[0].trim().toLowerCase();
|
|
102
|
+
}
|
|
103
|
+
/**
|
|
104
|
+
* Como um anexo de e-mail sai no WhatsApp/Instagram. Lista conservadora: o que
|
|
105
|
+
* fica fora (gif e webp no WhatsApp, documento no Instagram) a Meta recusa ou
|
|
106
|
+
* entrega mal.
|
|
107
|
+
*/
|
|
108
|
+
function forwardEmailAttachmentSendAs(mime, dest) {
|
|
109
|
+
const m = normalizeMime(mime);
|
|
110
|
+
if (dest === "instagram")
|
|
111
|
+
return INSTAGRAM_IMAGE_MIMES.has(m) ? "image" : null;
|
|
112
|
+
if (WHATSAPP_IMAGE_MIMES.has(m))
|
|
113
|
+
return "image";
|
|
114
|
+
if (WHATSAPP_DOCUMENT_MIMES.has(m))
|
|
115
|
+
return "document";
|
|
116
|
+
return null;
|
|
117
|
+
}
|
|
118
|
+
function nonEmpty(v) {
|
|
119
|
+
return typeof v === "string" ? v.trim() : "";
|
|
120
|
+
}
|
|
121
|
+
/** O `body.text` do enviado é `${subject}\n${body_text}`: a 1ª linha sai quando é o assunto. */
|
|
122
|
+
function emailBody(m, subject) {
|
|
123
|
+
const bodyText = nonEmpty(m.email?.body_text);
|
|
124
|
+
if (bodyText)
|
|
125
|
+
return bodyText;
|
|
126
|
+
const raw = nonEmpty(m.body?.text);
|
|
127
|
+
if (!subject)
|
|
128
|
+
return raw;
|
|
129
|
+
const breakAt = raw.indexOf("\n");
|
|
130
|
+
const firstLine = (breakAt < 0 ? raw : raw.slice(0, breakAt)).trim();
|
|
131
|
+
if (firstLine !== subject)
|
|
132
|
+
return raw;
|
|
133
|
+
return breakAt < 0 ? "" : raw.slice(breakAt + 1).trim();
|
|
134
|
+
}
|
|
135
|
+
/**
|
|
136
|
+
* Origem e-mail (recebido `type:'text'`, enviado `type:'system'`) para qualquer
|
|
137
|
+
* destino. `html`/`body_html` nunca são lidos. Guarda de bucket/tenant, tamanho,
|
|
138
|
+
* allowlist do e-mail e tetos ficam no backend.
|
|
139
|
+
*/
|
|
140
|
+
function resolveForwardEmailContent(m, dest) {
|
|
141
|
+
const direction = String(m.direction ?? "").toLowerCase();
|
|
142
|
+
if (direction !== "in" && direction !== "out")
|
|
143
|
+
return { ok: false, reason: "type_not_supported" };
|
|
144
|
+
if (m.revoked === true)
|
|
145
|
+
return { ok: false, reason: "type_not_supported" };
|
|
146
|
+
if (m.deleted_at !== undefined && m.deleted_at !== null)
|
|
147
|
+
return { ok: false, reason: "type_not_supported" };
|
|
148
|
+
if (String(m.channel_type ?? "").toLowerCase() !== "email")
|
|
149
|
+
return { ok: false, reason: "type_not_supported" };
|
|
150
|
+
const type = String(m.type ?? "").toLowerCase();
|
|
151
|
+
if (type !== "text" && type !== "system")
|
|
152
|
+
return { ok: false, reason: "type_not_supported" };
|
|
153
|
+
const subject = nonEmpty(m.email?.subject);
|
|
154
|
+
const body = emailBody(m, subject);
|
|
155
|
+
const attachments = [];
|
|
156
|
+
const skipped = {};
|
|
157
|
+
const skip = (r) => {
|
|
158
|
+
skipped[r] = (skipped[r] ?? 0) + 1;
|
|
159
|
+
};
|
|
160
|
+
const list = Array.isArray(m.attachments) ? m.attachments : [];
|
|
161
|
+
list.forEach((a, sourceIndex) => {
|
|
162
|
+
if (!a || typeof a !== "object")
|
|
163
|
+
return skip("media_unavailable");
|
|
164
|
+
if (a.blocked === true || nonEmpty(a.quarantine_uri))
|
|
165
|
+
return skip("quarantined");
|
|
166
|
+
const gcsUri = nonEmpty(a.gcs_uri);
|
|
167
|
+
if (!gcsUri)
|
|
168
|
+
return skip("media_unavailable");
|
|
169
|
+
const mime = normalizeMime(a.mime_type || a.content_type);
|
|
170
|
+
let sendAs = null;
|
|
171
|
+
if (dest === "whatsapp" || dest === "instagram") {
|
|
172
|
+
sendAs = forwardEmailAttachmentSendAs(mime, dest);
|
|
173
|
+
if (!sendAs)
|
|
174
|
+
return skip("type_not_supported");
|
|
175
|
+
}
|
|
176
|
+
const size = typeof a.size_bytes === "number" && Number.isFinite(a.size_bytes) && a.size_bytes >= 0 ? a.size_bytes : null;
|
|
177
|
+
attachments.push({
|
|
178
|
+
source_index: sourceIndex,
|
|
179
|
+
filename: nonEmpty(a.filename),
|
|
180
|
+
mime_type: mime,
|
|
181
|
+
size_bytes: size,
|
|
182
|
+
gcs_uri: gcsUri,
|
|
183
|
+
send_as: sendAs,
|
|
184
|
+
});
|
|
185
|
+
});
|
|
186
|
+
if (!subject && !body && attachments.length === 0) {
|
|
187
|
+
const onlyUnavailable = list.length > 0 && (skipped.quarantined ?? 0) + (skipped.media_unavailable ?? 0) === list.length;
|
|
188
|
+
return { ok: false, reason: onlyUnavailable ? "media_unavailable" : "type_not_supported" };
|
|
189
|
+
}
|
|
190
|
+
return { ok: true, kind: "email", subject, body, attachments, skipped_attachments: skipped };
|
|
191
|
+
}
|
|
192
|
+
/**
|
|
193
|
+
* Ponto de entrada da origem: e-mail vai para `resolveForwardEmailContent`; o
|
|
194
|
+
* resto (SMS incluído, que continua recusado) para `resolveForwardContent`.
|
|
195
|
+
*/
|
|
196
|
+
function resolveForwardSource(m, dest) {
|
|
197
|
+
if (String(m.channel_type ?? "").toLowerCase() === "email")
|
|
198
|
+
return resolveForwardEmailContent(m, dest);
|
|
199
|
+
return resolveForwardContent(m, dest);
|
|
200
|
+
}
|
|
77
201
|
/** Id determinístico do item: um destinatário aparece uma vez no lote, então é único. */
|
|
78
202
|
function buildForwardItemId(recipientThreadId, messageId) {
|
|
79
203
|
return `${recipientThreadId}__${messageId ?? "intro"}`;
|
|
@@ -24,6 +24,12 @@ export const FORWARD_MAX_RECIPIENTS = 10;
|
|
|
24
24
|
export const FORWARD_INTRO_MAX_CHARS = 1000;
|
|
25
25
|
/** Teto do assunto (RFC 5322), igual ao do composer de e-mail do Omni. */
|
|
26
26
|
export const FORWARD_EMAIL_SUBJECT_MAX_CHARS = 998;
|
|
27
|
+
/**
|
|
28
|
+
* Envios por destinatário WhatsApp/Instagram: intro + texto + anexos de cada
|
|
29
|
+
* mensagem. Segura o disjuntor do WhatsApp (40 envios em 6 h por par) quando a
|
|
30
|
+
* origem é e-mail com anexos.
|
|
31
|
+
*/
|
|
32
|
+
export const FORWARD_MAX_PARTS_PER_RECIPIENT = 30;
|
|
27
33
|
|
|
28
34
|
/** Lote `running` sem atualização há mais que isto é mostrado como interrompido. */
|
|
29
35
|
export const FORWARD_STALE_AFTER_MS = 10 * 60 * 1000;
|
|
@@ -46,7 +52,11 @@ export type ForwardSkipReason =
|
|
|
46
52
|
| "channel_not_supported"
|
|
47
53
|
| "attachments_over_limit"
|
|
48
54
|
| "recipient_address_missing"
|
|
49
|
-
| "email_identity_conflict"
|
|
55
|
+
| "email_identity_conflict"
|
|
56
|
+
/** Destinatário é a própria thread de origem e a mensagem já está no canal escolhido. */
|
|
57
|
+
| "same_channel"
|
|
58
|
+
/** A mensagem passaria do `FORWARD_MAX_PARTS_PER_RECIPIENT` (corte cronológico). */
|
|
59
|
+
| "parts_over_limit";
|
|
50
60
|
|
|
51
61
|
/** Motivos de falha no envio (item passou por `sending`). */
|
|
52
62
|
export type ForwardFailReason =
|
|
@@ -57,7 +67,16 @@ export type ForwardFailReason =
|
|
|
57
67
|
|
|
58
68
|
export type ForwardItemReason = ForwardSkipReason | ForwardFailReason;
|
|
59
69
|
|
|
60
|
-
|
|
70
|
+
/** `email`: mensagem de origem e-mail (assunto + corpo + anexos), entregue em `parts`. */
|
|
71
|
+
export type ForwardContentKind = "text" | "image" | "video" | "audio" | "document" | "email";
|
|
72
|
+
|
|
73
|
+
/** Por que um anexo de origem e-mail ficou fora do item (o texto e os outros anexos seguem). */
|
|
74
|
+
export type ForwardAttachmentSkipReason =
|
|
75
|
+
| "quarantined"
|
|
76
|
+
| "type_not_supported"
|
|
77
|
+
| "media_too_large"
|
|
78
|
+
| "media_unavailable"
|
|
79
|
+
| "attachments_over_limit";
|
|
61
80
|
|
|
62
81
|
/** Canais aceitos como DESTINO. E-mail é agregado: um e-mail por destinatário. SMS fica fora. */
|
|
63
82
|
export type ForwardDestinationKind = "whatsapp" | "instagram" | "email";
|
|
@@ -132,6 +151,18 @@ export interface IForwardBatch extends IFireDoc {
|
|
|
132
151
|
|
|
133
152
|
// ── Item ──
|
|
134
153
|
|
|
154
|
+
/** Origem e-mail: o que o item entrega. `has_text` = a parte principal (texto, ou o PDF do e-mail no WhatsApp). */
|
|
155
|
+
export interface IForwardItemParts {
|
|
156
|
+
has_text: boolean;
|
|
157
|
+
/** Posições em `attachments[]` da origem; o worker envia exatamente estas, nesta ordem. */
|
|
158
|
+
attachment_indexes: number[];
|
|
159
|
+
/** `has_text` + `attachment_indexes.length`. */
|
|
160
|
+
total: number;
|
|
161
|
+
/** Só no item gravado: partes aceitas pelo provedor até agora. */
|
|
162
|
+
sent?: number;
|
|
163
|
+
skipped_attachments?: Partial<Record<ForwardAttachmentSkipReason, number>>;
|
|
164
|
+
}
|
|
165
|
+
|
|
135
166
|
export interface IForwardItem {
|
|
136
167
|
/** `${recipient_thread_id}__${message_id}` ou `${recipient_thread_id}__intro`. */
|
|
137
168
|
item_id: string;
|
|
@@ -151,6 +182,7 @@ export interface IForwardItem {
|
|
|
151
182
|
/** Destino e-mail: o N de `[Anexo N]` no corpo. */
|
|
152
183
|
attachment_index?: number | null;
|
|
153
184
|
dest_notification_message_id?: string | null;
|
|
185
|
+
parts?: IForwardItemParts | null;
|
|
154
186
|
sent_at?: Date | null;
|
|
155
187
|
updated_at?: Date;
|
|
156
188
|
}
|
|
@@ -166,6 +198,7 @@ export interface IForwardPlanItem {
|
|
|
166
198
|
status: "pending" | "skipped";
|
|
167
199
|
reason?: ForwardSkipReason;
|
|
168
200
|
attachment?: { index: number | null; filename: string; size_bytes: number | null } | null;
|
|
201
|
+
parts?: IForwardItemParts | null;
|
|
169
202
|
}
|
|
170
203
|
|
|
171
204
|
export interface IForwardPlanEmailAttachments {
|
|
@@ -237,7 +270,7 @@ export interface IMessageForward {
|
|
|
237
270
|
|
|
238
271
|
export type ForwardableMessage = Pick<
|
|
239
272
|
IThreadMessage,
|
|
240
|
-
"type" | "direction" | "channel_type" | "body" | "media" | "template"
|
|
273
|
+
"type" | "direction" | "channel_type" | "channel_id" | "body" | "media" | "template" | "email" | "attachments"
|
|
241
274
|
> & {
|
|
242
275
|
revoked?: unknown;
|
|
243
276
|
deleted_at?: unknown;
|
|
@@ -249,7 +282,11 @@ export type ForwardContentResolution =
|
|
|
249
282
|
|
|
250
283
|
const MEDIA_KINDS: ReadonlySet<string> = new Set(["image", "video", "audio", "document"]);
|
|
251
284
|
|
|
252
|
-
/**
|
|
285
|
+
/**
|
|
286
|
+
* Canais que `resolveForwardContent` não aceita como origem. E-mail entra só por
|
|
287
|
+
* `resolveForwardSource`: abrir este contrato faria código anterior tratar
|
|
288
|
+
* `kind:'email'` como mídia.
|
|
289
|
+
*/
|
|
253
290
|
const UNSUPPORTED_SOURCE_CHANNELS: ReadonlySet<string> = new Set(["email", "sms"]);
|
|
254
291
|
|
|
255
292
|
/**
|
|
@@ -297,6 +334,148 @@ export function resolveForwardContent(
|
|
|
297
334
|
return { ok: false, reason: "type_not_supported" };
|
|
298
335
|
}
|
|
299
336
|
|
|
337
|
+
// ── Origem e-mail ──
|
|
338
|
+
|
|
339
|
+
export interface ForwardEmailSourceAttachment {
|
|
340
|
+
/** Posição em `attachments[]` da mensagem de origem. */
|
|
341
|
+
source_index: number;
|
|
342
|
+
filename: string;
|
|
343
|
+
mime_type: string;
|
|
344
|
+
size_bytes: number | null;
|
|
345
|
+
gcs_uri: string;
|
|
346
|
+
/** Destino e-mail ou `null`: sempre `null` (vai como anexo). */
|
|
347
|
+
send_as: "image" | "document" | null;
|
|
348
|
+
}
|
|
349
|
+
|
|
350
|
+
export type ForwardEmailSourceResolution =
|
|
351
|
+
| {
|
|
352
|
+
ok: true;
|
|
353
|
+
kind: "email";
|
|
354
|
+
subject: string;
|
|
355
|
+
body: string;
|
|
356
|
+
attachments: ForwardEmailSourceAttachment[];
|
|
357
|
+
skipped_attachments: Partial<Record<ForwardAttachmentSkipReason, number>>;
|
|
358
|
+
}
|
|
359
|
+
| { ok: false; reason: ForwardSkipReason };
|
|
360
|
+
|
|
361
|
+
const WHATSAPP_IMAGE_MIMES: ReadonlySet<string> = new Set(["image/jpeg", "image/png"]);
|
|
362
|
+
const WHATSAPP_DOCUMENT_MIMES: ReadonlySet<string> = new Set([
|
|
363
|
+
"application/pdf",
|
|
364
|
+
"text/plain",
|
|
365
|
+
"application/msword",
|
|
366
|
+
"application/vnd.openxmlformats-officedocument.wordprocessingml.document",
|
|
367
|
+
"application/vnd.ms-excel",
|
|
368
|
+
"application/vnd.openxmlformats-officedocument.spreadsheetml.sheet",
|
|
369
|
+
]);
|
|
370
|
+
const INSTAGRAM_IMAGE_MIMES: ReadonlySet<string> = new Set(["image/jpeg", "image/png", "image/gif"]);
|
|
371
|
+
|
|
372
|
+
function normalizeMime(mime: unknown): string {
|
|
373
|
+
return String(mime ?? "").split(";")[0].trim().toLowerCase();
|
|
374
|
+
}
|
|
375
|
+
|
|
376
|
+
/**
|
|
377
|
+
* Como um anexo de e-mail sai no WhatsApp/Instagram. Lista conservadora: o que
|
|
378
|
+
* fica fora (gif e webp no WhatsApp, documento no Instagram) a Meta recusa ou
|
|
379
|
+
* entrega mal.
|
|
380
|
+
*/
|
|
381
|
+
export function forwardEmailAttachmentSendAs(
|
|
382
|
+
mime: string,
|
|
383
|
+
dest: "whatsapp" | "instagram",
|
|
384
|
+
): "image" | "document" | null {
|
|
385
|
+
const m = normalizeMime(mime);
|
|
386
|
+
if (dest === "instagram") return INSTAGRAM_IMAGE_MIMES.has(m) ? "image" : null;
|
|
387
|
+
if (WHATSAPP_IMAGE_MIMES.has(m)) return "image";
|
|
388
|
+
if (WHATSAPP_DOCUMENT_MIMES.has(m)) return "document";
|
|
389
|
+
return null;
|
|
390
|
+
}
|
|
391
|
+
|
|
392
|
+
function nonEmpty(v: unknown): string {
|
|
393
|
+
return typeof v === "string" ? v.trim() : "";
|
|
394
|
+
}
|
|
395
|
+
|
|
396
|
+
/** O `body.text` do enviado é `${subject}\n${body_text}`: a 1ª linha sai quando é o assunto. */
|
|
397
|
+
function emailBody(m: ForwardableMessage, subject: string): string {
|
|
398
|
+
const bodyText = nonEmpty(m.email?.body_text);
|
|
399
|
+
if (bodyText) return bodyText;
|
|
400
|
+
const raw = nonEmpty(m.body?.text);
|
|
401
|
+
if (!subject) return raw;
|
|
402
|
+
const breakAt = raw.indexOf("\n");
|
|
403
|
+
const firstLine = (breakAt < 0 ? raw : raw.slice(0, breakAt)).trim();
|
|
404
|
+
if (firstLine !== subject) return raw;
|
|
405
|
+
return breakAt < 0 ? "" : raw.slice(breakAt + 1).trim();
|
|
406
|
+
}
|
|
407
|
+
|
|
408
|
+
/**
|
|
409
|
+
* Origem e-mail (recebido `type:'text'`, enviado `type:'system'`) para qualquer
|
|
410
|
+
* destino. `html`/`body_html` nunca são lidos. Guarda de bucket/tenant, tamanho,
|
|
411
|
+
* allowlist do e-mail e tetos ficam no backend.
|
|
412
|
+
*/
|
|
413
|
+
export function resolveForwardEmailContent(
|
|
414
|
+
m: ForwardableMessage,
|
|
415
|
+
dest: ForwardDestinationKind | null,
|
|
416
|
+
): ForwardEmailSourceResolution {
|
|
417
|
+
const direction = String(m.direction ?? "").toLowerCase();
|
|
418
|
+
if (direction !== "in" && direction !== "out") return { ok: false, reason: "type_not_supported" };
|
|
419
|
+
if (m.revoked === true) return { ok: false, reason: "type_not_supported" };
|
|
420
|
+
if (m.deleted_at !== undefined && m.deleted_at !== null) return { ok: false, reason: "type_not_supported" };
|
|
421
|
+
|
|
422
|
+
if (String(m.channel_type ?? "").toLowerCase() !== "email") return { ok: false, reason: "type_not_supported" };
|
|
423
|
+
const type = String(m.type ?? "").toLowerCase();
|
|
424
|
+
if (type !== "text" && type !== "system") return { ok: false, reason: "type_not_supported" };
|
|
425
|
+
|
|
426
|
+
const subject = nonEmpty(m.email?.subject);
|
|
427
|
+
const body = emailBody(m, subject);
|
|
428
|
+
|
|
429
|
+
const attachments: ForwardEmailSourceAttachment[] = [];
|
|
430
|
+
const skipped: Partial<Record<ForwardAttachmentSkipReason, number>> = {};
|
|
431
|
+
const skip = (r: ForwardAttachmentSkipReason) => {
|
|
432
|
+
skipped[r] = (skipped[r] ?? 0) + 1;
|
|
433
|
+
};
|
|
434
|
+
|
|
435
|
+
const list = Array.isArray(m.attachments) ? m.attachments : [];
|
|
436
|
+
list.forEach((a, sourceIndex) => {
|
|
437
|
+
if (!a || typeof a !== "object") return skip("media_unavailable");
|
|
438
|
+
if (a.blocked === true || nonEmpty(a.quarantine_uri)) return skip("quarantined");
|
|
439
|
+
const gcsUri = nonEmpty(a.gcs_uri);
|
|
440
|
+
if (!gcsUri) return skip("media_unavailable");
|
|
441
|
+
const mime = normalizeMime(a.mime_type || a.content_type);
|
|
442
|
+
let sendAs: "image" | "document" | null = null;
|
|
443
|
+
if (dest === "whatsapp" || dest === "instagram") {
|
|
444
|
+
sendAs = forwardEmailAttachmentSendAs(mime, dest);
|
|
445
|
+
if (!sendAs) return skip("type_not_supported");
|
|
446
|
+
}
|
|
447
|
+
const size = typeof a.size_bytes === "number" && Number.isFinite(a.size_bytes) && a.size_bytes >= 0 ? a.size_bytes : null;
|
|
448
|
+
attachments.push({
|
|
449
|
+
source_index: sourceIndex,
|
|
450
|
+
filename: nonEmpty(a.filename),
|
|
451
|
+
mime_type: mime,
|
|
452
|
+
size_bytes: size,
|
|
453
|
+
gcs_uri: gcsUri,
|
|
454
|
+
send_as: sendAs,
|
|
455
|
+
});
|
|
456
|
+
});
|
|
457
|
+
|
|
458
|
+
if (!subject && !body && attachments.length === 0) {
|
|
459
|
+
const onlyUnavailable =
|
|
460
|
+
list.length > 0 && (skipped.quarantined ?? 0) + (skipped.media_unavailable ?? 0) === list.length;
|
|
461
|
+
return { ok: false, reason: onlyUnavailable ? "media_unavailable" : "type_not_supported" };
|
|
462
|
+
}
|
|
463
|
+
|
|
464
|
+
return { ok: true, kind: "email", subject, body, attachments, skipped_attachments: skipped };
|
|
465
|
+
}
|
|
466
|
+
|
|
467
|
+
/**
|
|
468
|
+
* Ponto de entrada da origem: e-mail vai para `resolveForwardEmailContent`; o
|
|
469
|
+
* resto (SMS incluído, que continua recusado) para `resolveForwardContent`.
|
|
470
|
+
*/
|
|
471
|
+
export function resolveForwardSource(
|
|
472
|
+
m: ForwardableMessage,
|
|
473
|
+
dest: ForwardDestinationKind | null,
|
|
474
|
+
): ForwardContentResolution | ForwardEmailSourceResolution {
|
|
475
|
+
if (String(m.channel_type ?? "").toLowerCase() === "email") return resolveForwardEmailContent(m, dest);
|
|
476
|
+
return resolveForwardContent(m, dest);
|
|
477
|
+
}
|
|
478
|
+
|
|
300
479
|
/** Id determinístico do item: um destinatário aparece uma vez no lote, então é único. */
|
|
301
480
|
export function buildForwardItemId(recipientThreadId: string, messageId: string | null): string {
|
|
302
481
|
return `${recipientThreadId}__${messageId ?? "intro"}`;
|
|
@@ -124,6 +124,39 @@ export interface IRenderedTemplateSnapshot {
|
|
|
124
124
|
/** Chaves sem valor. O placeholder fica visível no texto. Omitido quando vazio. */
|
|
125
125
|
unresolved?: string[];
|
|
126
126
|
}
|
|
127
|
+
/**
|
|
128
|
+
* Recebido (`type:'text'`): `subject`, `from_address`, `from_name`, `html`, ids de cadeia.
|
|
129
|
+
* Enviado pela clínica (`type:'system'`): `subject`, `recipient_address`, `body_text`, `body_html`, `thread_id`.
|
|
130
|
+
*/
|
|
131
|
+
export interface IThreadMessageEmail {
|
|
132
|
+
subject?: string;
|
|
133
|
+
from_address?: string;
|
|
134
|
+
from_name?: string;
|
|
135
|
+
html?: string;
|
|
136
|
+
message_id?: string;
|
|
137
|
+
in_reply_to?: string;
|
|
138
|
+
references?: string[];
|
|
139
|
+
reply_to_addresses?: string[];
|
|
140
|
+
recipient_address?: string;
|
|
141
|
+
body_text?: string;
|
|
142
|
+
body_html?: string;
|
|
143
|
+
thread_id?: string;
|
|
144
|
+
}
|
|
145
|
+
/**
|
|
146
|
+
* Recebido usa `mime_type`; enviado usa `content_type`. Bloqueado na entrada tem
|
|
147
|
+
* `blocked` + `quarantine_uri` e nenhum `gcs_uri`. O anexo inline legado (ICS) só
|
|
148
|
+
* tem `content_base64`.
|
|
149
|
+
*/
|
|
150
|
+
export interface IThreadMessageEmailAttachment {
|
|
151
|
+
filename?: string;
|
|
152
|
+
mime_type?: string;
|
|
153
|
+
content_type?: string;
|
|
154
|
+
size_bytes?: number;
|
|
155
|
+
gcs_uri?: string;
|
|
156
|
+
blocked?: boolean;
|
|
157
|
+
blocked_reason?: string;
|
|
158
|
+
quarantine_uri?: string;
|
|
159
|
+
}
|
|
127
160
|
export interface IThreadMessage extends IFireDoc {
|
|
128
161
|
model_ver: number;
|
|
129
162
|
thread_id: string;
|
|
@@ -160,6 +193,8 @@ export interface IThreadMessage extends IFireDoc {
|
|
|
160
193
|
rendered?: IRenderedTemplateSnapshot;
|
|
161
194
|
};
|
|
162
195
|
context?: IMessageContext;
|
|
196
|
+
email?: IThreadMessageEmail;
|
|
197
|
+
attachments?: IThreadMessageEmailAttachment[];
|
|
163
198
|
forward?: IMessageForward;
|
|
164
199
|
/** feat-282: na mensagem de ORIGEM, quantas entregas de encaminhamento já saíram dela. */
|
|
165
200
|
forward_count?: number;
|
|
@@ -164,6 +164,43 @@ export interface IRenderedTemplateSnapshot {
|
|
|
164
164
|
unresolved?: string[];
|
|
165
165
|
}
|
|
166
166
|
|
|
167
|
+
// ── Mensagem de e-mail (canal `email`) ──
|
|
168
|
+
|
|
169
|
+
/**
|
|
170
|
+
* Recebido (`type:'text'`): `subject`, `from_address`, `from_name`, `html`, ids de cadeia.
|
|
171
|
+
* Enviado pela clínica (`type:'system'`): `subject`, `recipient_address`, `body_text`, `body_html`, `thread_id`.
|
|
172
|
+
*/
|
|
173
|
+
export interface IThreadMessageEmail {
|
|
174
|
+
subject?: string;
|
|
175
|
+
from_address?: string;
|
|
176
|
+
from_name?: string;
|
|
177
|
+
html?: string;
|
|
178
|
+
message_id?: string;
|
|
179
|
+
in_reply_to?: string;
|
|
180
|
+
references?: string[];
|
|
181
|
+
reply_to_addresses?: string[];
|
|
182
|
+
recipient_address?: string;
|
|
183
|
+
body_text?: string;
|
|
184
|
+
body_html?: string;
|
|
185
|
+
thread_id?: string;
|
|
186
|
+
}
|
|
187
|
+
|
|
188
|
+
/**
|
|
189
|
+
* Recebido usa `mime_type`; enviado usa `content_type`. Bloqueado na entrada tem
|
|
190
|
+
* `blocked` + `quarantine_uri` e nenhum `gcs_uri`. O anexo inline legado (ICS) só
|
|
191
|
+
* tem `content_base64`.
|
|
192
|
+
*/
|
|
193
|
+
export interface IThreadMessageEmailAttachment {
|
|
194
|
+
filename?: string;
|
|
195
|
+
mime_type?: string;
|
|
196
|
+
content_type?: string;
|
|
197
|
+
size_bytes?: number;
|
|
198
|
+
gcs_uri?: string;
|
|
199
|
+
blocked?: boolean;
|
|
200
|
+
blocked_reason?: string;
|
|
201
|
+
quarantine_uri?: string;
|
|
202
|
+
}
|
|
203
|
+
|
|
167
204
|
// ── Thread Message (evo-chat v3) ──
|
|
168
205
|
|
|
169
206
|
export interface IThreadMessage extends IFireDoc {
|
|
@@ -198,6 +235,10 @@ export interface IThreadMessage extends IFireDoc {
|
|
|
198
235
|
};
|
|
199
236
|
context?: IMessageContext;
|
|
200
237
|
|
|
238
|
+
// Canal e-mail.
|
|
239
|
+
email?: IThreadMessageEmail;
|
|
240
|
+
attachments?: IThreadMessageEmailAttachment[];
|
|
241
|
+
|
|
201
242
|
// feat-282: presente na mensagem de DESTINO de um encaminhamento (ver ../forward).
|
|
202
243
|
forward?: IMessageForward;
|
|
203
244
|
/** feat-282: na mensagem de ORIGEM, quantas entregas de encaminhamento já saíram dela. */
|
|
@@ -3,6 +3,7 @@ import type { FirestoreDocumentReference, IFireDoc, ITag } from "../../shared";
|
|
|
3
3
|
import type { IAppointmentCounters } from "../people";
|
|
4
4
|
import type { TelemedicineProvider } from "../appointment";
|
|
5
5
|
import type { IMedCalendarIntegration } from "../../evo-integrations";
|
|
6
|
+
import type { AppointmentType } from "../dic";
|
|
6
7
|
export declare const EvoMedCalendarPermissions: {
|
|
7
8
|
readonly List: "evo_med_calendar_read";
|
|
8
9
|
readonly Get: "evo_med_calendar_read";
|
|
@@ -288,6 +289,12 @@ export interface IMedCalendarVirtualCareSettings {
|
|
|
288
289
|
* Para resolver, faça merge por nível (`{ ...DEFAULTS.recording, ...cfg.recording }`).
|
|
289
290
|
*/
|
|
290
291
|
export declare const CALENDAR_VIRTUAL_CARE_DEFAULTS: IMedCalendarVirtualCareSettings;
|
|
292
|
+
/** feat-287 — forma de pagamento que uma agenda aceita para a HubIA. */
|
|
293
|
+
export declare const HubiaPaymentTypeEnum: {
|
|
294
|
+
readonly Private: "private";
|
|
295
|
+
readonly Insurance: "insurance";
|
|
296
|
+
};
|
|
297
|
+
export type HubiaPaymentType = (typeof HubiaPaymentTypeEnum)[keyof typeof HubiaPaymentTypeEnum];
|
|
291
298
|
export interface IMedCalendar extends IFireDoc {
|
|
292
299
|
calendarId: string;
|
|
293
300
|
color?: string;
|
|
@@ -315,6 +322,17 @@ export interface IMedCalendar extends IFireDoc {
|
|
|
315
322
|
appointmentCounters?: IAppointmentCounters | null;
|
|
316
323
|
available_weekdays?: number[] | null;
|
|
317
324
|
available_for_hubia?: boolean | null;
|
|
325
|
+
/**
|
|
326
|
+
* feat-287 — tipos de atendimento que a HubIA pode oferecer/agendar nesta
|
|
327
|
+
* agenda (`check_schedule_availability.tipo`, `schedule_appointment.tipo`).
|
|
328
|
+
* Vazio ou ausente = aceita todos.
|
|
329
|
+
*/
|
|
330
|
+
hubia_appointment_types?: AppointmentType[] | null;
|
|
331
|
+
/**
|
|
332
|
+
* feat-287 — formas de pagamento aceitas nesta agenda para a HubIA
|
|
333
|
+
* (`check_schedule_availability.pagamento`). Vazio ou ausente = aceita todas.
|
|
334
|
+
*/
|
|
335
|
+
hubia_payment_types?: HubiaPaymentType[] | null;
|
|
318
336
|
integration?: IMedCalendarIntegration | null;
|
|
319
337
|
notification_config?: IMedCalendarNotificationConfig | null;
|
|
320
338
|
/**
|
|
@@ -14,7 +14,7 @@ var __exportStar = (this && this.__exportStar) || function(m, exports) {
|
|
|
14
14
|
for (var p in m) if (p !== "default" && !Object.prototype.hasOwnProperty.call(exports, p)) __createBinding(exports, m, p);
|
|
15
15
|
};
|
|
16
16
|
Object.defineProperty(exports, "__esModule", { value: true });
|
|
17
|
-
exports.CALENDAR_VIRTUAL_CARE_DEFAULTS = exports.VIRTUAL_CARE_MAX_DURATION_MINUTES_MAX = exports.VIRTUAL_CARE_MAX_DURATION_MINUTES_MIN = exports.VIRTUAL_CARE_RETENTION_DAYS_MAX = exports.VIRTUAL_CARE_RETENTION_DAYS_MIN = exports.VirtualCareRecordingModeEnum = exports.MedCalendarNotificationConfigKeys = exports.EvoMedCalendarPermissions = void 0;
|
|
17
|
+
exports.HubiaPaymentTypeEnum = exports.CALENDAR_VIRTUAL_CARE_DEFAULTS = exports.VIRTUAL_CARE_MAX_DURATION_MINUTES_MAX = exports.VIRTUAL_CARE_MAX_DURATION_MINUTES_MIN = exports.VIRTUAL_CARE_RETENTION_DAYS_MAX = exports.VIRTUAL_CARE_RETENTION_DAYS_MIN = exports.VirtualCareRecordingModeEnum = exports.MedCalendarNotificationConfigKeys = exports.EvoMedCalendarPermissions = void 0;
|
|
18
18
|
__exportStar(require("../../evo-people/fb_collections"), exports);
|
|
19
19
|
// Permissões para o calendário
|
|
20
20
|
exports.EvoMedCalendarPermissions = {
|
|
@@ -146,3 +146,8 @@ exports.CALENDAR_VIRTUAL_CARE_DEFAULTS = {
|
|
|
146
146
|
max_duration_minutes: 120,
|
|
147
147
|
end_warning_minutes: 5,
|
|
148
148
|
};
|
|
149
|
+
/** feat-287 — forma de pagamento que uma agenda aceita para a HubIA. */
|
|
150
|
+
exports.HubiaPaymentTypeEnum = {
|
|
151
|
+
Private: "private",
|
|
152
|
+
Insurance: "insurance",
|
|
153
|
+
};
|
|
@@ -3,6 +3,7 @@ import type { FirestoreDocumentReference, IFireDoc, ITag } from "../../shared";
|
|
|
3
3
|
import type { IAppointmentCounters } from "../people";
|
|
4
4
|
import type { TelemedicineProvider } from "../appointment";
|
|
5
5
|
import type { IMedCalendarIntegration } from "../../evo-integrations";
|
|
6
|
+
import type { AppointmentType } from "../dic";
|
|
6
7
|
|
|
7
8
|
// Permissões para o calendário
|
|
8
9
|
export const EvoMedCalendarPermissions = {
|
|
@@ -352,6 +353,15 @@ export const CALENDAR_VIRTUAL_CARE_DEFAULTS: IMedCalendarVirtualCareSettings = {
|
|
|
352
353
|
end_warning_minutes: 5,
|
|
353
354
|
} as const;
|
|
354
355
|
|
|
356
|
+
/** feat-287 — forma de pagamento que uma agenda aceita para a HubIA. */
|
|
357
|
+
export const HubiaPaymentTypeEnum = {
|
|
358
|
+
Private: "private",
|
|
359
|
+
Insurance: "insurance",
|
|
360
|
+
} as const;
|
|
361
|
+
|
|
362
|
+
export type HubiaPaymentType =
|
|
363
|
+
(typeof HubiaPaymentTypeEnum)[keyof typeof HubiaPaymentTypeEnum];
|
|
364
|
+
|
|
355
365
|
// ----- MedCalendarTypes
|
|
356
366
|
export interface IMedCalendar extends IFireDoc {
|
|
357
367
|
calendarId: string;
|
|
@@ -383,6 +393,17 @@ export interface IMedCalendar extends IFireDoc {
|
|
|
383
393
|
available_weekdays?: number[] | null;
|
|
384
394
|
// Se true, a agenda é elegível para oferecer horários via HubIA
|
|
385
395
|
available_for_hubia?: boolean | null;
|
|
396
|
+
/**
|
|
397
|
+
* feat-287 — tipos de atendimento que a HubIA pode oferecer/agendar nesta
|
|
398
|
+
* agenda (`check_schedule_availability.tipo`, `schedule_appointment.tipo`).
|
|
399
|
+
* Vazio ou ausente = aceita todos.
|
|
400
|
+
*/
|
|
401
|
+
hubia_appointment_types?: AppointmentType[] | null;
|
|
402
|
+
/**
|
|
403
|
+
* feat-287 — formas de pagamento aceitas nesta agenda para a HubIA
|
|
404
|
+
* (`check_schedule_availability.pagamento`). Vazio ou ausente = aceita todas.
|
|
405
|
+
*/
|
|
406
|
+
hubia_payment_types?: HubiaPaymentType[] | null;
|
|
386
407
|
// Config de integração (feat-004 evo-integrations). Opcional — calendars legados mantêm n8n_wf/url/ext_urls.
|
|
387
408
|
integration?: IMedCalendarIntegration | null;
|
|
388
409
|
// Config de notificações por-agenda (feat-048). Opcional — agendas sem lembrete não têm o bloco.
|