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.
@@ -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
- /** Canais que não entram como origem (e-mail, SMS). */
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
- export type ForwardContentKind = "text" | "image" | "video" | "audio" | "document";
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
- /** Canais que não entram como origem (e-mail, SMS). */
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.
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "evo360-types",
3
- "version": "1.3.726",
3
+ "version": "1.3.730",
4
4
  "description": "HREVO360 Shared Types",
5
5
  "main": "./dist/index.js",
6
6
  "types": "./dist/index.d.ts",