evo360-types 1.3.505 → 1.3.510

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -0,0 +1,376 @@
1
+ // ======================================================
2
+ // evo-integrations — bateria de regressão de adapters (feat-123)
3
+ //
4
+ // Porte do script `_regressao-adapters.cjs` para ferramenta de produto no Nexus.
5
+ //
6
+ // A ferramenta é APARTADA: roda por fora, chamando as APIs (`integrations_actions`,
7
+ // `integrations_admin`, `integrations_sync_monitor`) exatamente como o script faz.
8
+ // Quem orquestra a sequência é o FE; quem persiste o resultado é o BE.
9
+ //
10
+ // Este módulo é o contrato COMPARTILHADO entre os dois lados:
11
+ // · o FE monta a seleção a partir do catálogo de passos;
12
+ // · o BE valida a seleção contra o MESMO catálogo e calcula o veredito.
13
+ //
14
+ // O catálogo de defeitos CONHECIDOS não mora aqui — mora no backend do Nexus,
15
+ // porque só ele calcula veredito (o FE apenas renderiza o que recebe).
16
+ // ======================================================
17
+
18
+ import type { IFireDoc } from "../shared";
19
+
20
+ // Path: /platform/evo-integrations/test-runs/{run_id}
21
+ export const TEST_RUNS_COLLECTION = "test-runs";
22
+ // Path: /platform/evo-integrations/test-runs/{run_id}/steps/{step_id}
23
+ export const TEST_RUN_STEPS_COLLECTION = "steps";
24
+
25
+ // ======================================================
26
+ // Suítes
27
+ // ======================================================
28
+
29
+ /**
30
+ * O agrupamento é derivado da DEPENDÊNCIA REAL DE ESTADO, não de estética:
31
+ * o `external_id` devolvido pelo create alimenta get/update/confirm/revert/cancel,
32
+ * e o do patient.create alimenta o round-trip e o delete.
33
+ */
34
+ export type TestSuiteId =
35
+ | "reading" // S1 — dicionários e leitura; independentes, zero efeito colateral
36
+ | "appointment" // S2 — ciclo de vida; ordem estrita
37
+ | "sync" // S3 — depende do cancel da S2
38
+ | "patient" // S4 — ordem estrita; opcional
39
+ | "cleanup"; // Z — limpeza; selecionável (D11)
40
+
41
+ export interface ITestSuiteMeta {
42
+ id: TestSuiteId;
43
+ label: string;
44
+ /** Ordem estrita = o passo N depende do estado produzido pelo N-1. */
45
+ ordered: boolean;
46
+ description: string;
47
+ }
48
+
49
+ export const TEST_SUITES: readonly ITestSuiteMeta[] = [
50
+ {
51
+ id: "reading",
52
+ label: "Dicionários e leitura",
53
+ ordered: false,
54
+ description:
55
+ "Capabilities de consulta. Nenhuma escreve — pode rodar sozinha com efeito colateral zero.",
56
+ },
57
+ {
58
+ id: "appointment",
59
+ label: "Ciclo de vida do agendamento",
60
+ ordered: true,
61
+ description:
62
+ "create → get → update → confirm → revert_confirm → cancel. Cada passo depende do external_id do create.",
63
+ },
64
+ {
65
+ id: "sync",
66
+ label: "Sync não ressuscita",
67
+ ordered: true,
68
+ description:
69
+ "Força o sync de 1 dia e confere que o agendamento cancelado NÃO volta. Exige o cancel da suíte anterior.",
70
+ },
71
+ {
72
+ id: "patient",
73
+ label: "Paciente",
74
+ ordered: true,
75
+ description:
76
+ "create → round-trip (get+update) → delete. O create nunca foi exercitado por nenhuma rodada do script.",
77
+ },
78
+ {
79
+ id: "cleanup",
80
+ label: "Varredura de resíduo",
81
+ ordered: false,
82
+ description:
83
+ "Lê a agenda EXTERNA e cancela o que tiver o marcador. Selecionável — quem roda é o dev de integrações, que responde pelo que escolhe.",
84
+ },
85
+ ] as const;
86
+
87
+ // ======================================================
88
+ // Passos
89
+ // ======================================================
90
+
91
+ export type TestStepId =
92
+ // S1
93
+ | "caps.list"
94
+ | "ui.config"
95
+ | "procedure.list"
96
+ | "status.list"
97
+ | "dicts.extra"
98
+ | "patient.list"
99
+ | "patient.get"
100
+ | "appointment.list"
101
+ // S2
102
+ | "appointment.create"
103
+ | "appointment.get"
104
+ | "appointment.update"
105
+ | "appointment.confirm"
106
+ | "appointment.revert_confirm"
107
+ | "appointment.cancel"
108
+ // S3
109
+ | "sync.force"
110
+ // S4
111
+ | "patient.create"
112
+ | "patient.roundtrip"
113
+ | "patient.delete"
114
+ // Z
115
+ | "residue.sweep";
116
+
117
+ export interface ITestStepMeta {
118
+ id: TestStepId;
119
+ suite: TestSuiteId;
120
+ label: string;
121
+ /**
122
+ * `true` = o passo ESCREVE no PMS de produção.
123
+ *
124
+ * Usado em dois lugares: para o modo somente-leitura e para classificar o custo
125
+ * de REPETIR um passo — repetir leitura é livre, repetir escrita duplica efeito.
126
+ */
127
+ writes: boolean;
128
+ /** Passos que precisam ter passado antes. Vazio = pode rodar sozinho. */
129
+ dependsOn: readonly TestStepId[];
130
+ /** Capability invocada, quando houver. Nem todo passo tem uma. */
131
+ capability?: string;
132
+ }
133
+
134
+ /**
135
+ * Ordem do array = ordem de execução. Não é 1:1 com capability:
136
+ * · `residue.sweep` não invoca capability nenhuma (varre a agenda externa);
137
+ * · `sync.force` chama `integrations_sync_monitor`, não uma capability;
138
+ * · `appointment.list` aparece em dois passos (leitura e varredura);
139
+ * · `patient.roundtrip` combina `patient.get` + `patient.update`.
140
+ */
141
+ export const TEST_STEPS: readonly ITestStepMeta[] = [
142
+ // ── S1 — leitura ────────────────────────────────────────────────────────────
143
+ { id: "caps.list", suite: "reading", label: "capabilities", writes: false, dependsOn: [] },
144
+ { id: "ui.config", suite: "reading", label: "ui-config", writes: false, dependsOn: [] },
145
+ { id: "procedure.list", suite: "reading", label: "procedure.list", writes: false, dependsOn: [], capability: "appointment.procedure.list" },
146
+ { id: "status.list", suite: "reading", label: "status.list", writes: false, dependsOn: [], capability: "appointment.status.list" },
147
+ { id: "dicts.extra", suite: "reading", label: "dicionários extra", writes: false, dependsOn: [] },
148
+ { id: "patient.list", suite: "reading", label: "patient.list", writes: false, dependsOn: [], capability: "patient.list" },
149
+ { id: "patient.get", suite: "reading", label: "patient.get", writes: false, dependsOn: [], capability: "patient.get" },
150
+ { id: "appointment.list", suite: "reading", label: "appointment.list", writes: false, dependsOn: [], capability: "appointment.list" },
151
+
152
+ // ── S2 — ciclo de vida ──────────────────────────────────────────────────────
153
+ { id: "appointment.create", suite: "appointment", label: "create", writes: true, dependsOn: [], capability: "appointment.create" },
154
+ { id: "appointment.get", suite: "appointment", label: "get", writes: false, dependsOn: ["appointment.create"], capability: "appointment.get" },
155
+ { id: "appointment.update", suite: "appointment", label: "update", writes: true, dependsOn: ["appointment.create"], capability: "appointment.update" },
156
+ { id: "appointment.confirm", suite: "appointment", label: "confirm", writes: true, dependsOn: ["appointment.create"], capability: "appointment.confirm" },
157
+ { id: "appointment.revert_confirm", suite: "appointment", label: "revert_confirm", writes: true, dependsOn: ["appointment.confirm"], capability: "appointment.revert_confirm" },
158
+ { id: "appointment.cancel", suite: "appointment", label: "cancel", writes: true, dependsOn: ["appointment.create"], capability: "appointment.cancel" },
159
+
160
+ // ── S3 — sync ───────────────────────────────────────────────────────────────
161
+ { id: "sync.force", suite: "sync", label: "sync (1 dia)", writes: true, dependsOn: ["appointment.cancel"] },
162
+
163
+ // ── S4 — paciente ───────────────────────────────────────────────────────────
164
+ { id: "patient.create", suite: "patient", label: "patient.create", writes: true, dependsOn: [], capability: "patient.create" },
165
+ { id: "patient.roundtrip", suite: "patient", label: "round-trip (get+update)", writes: true, dependsOn: [], capability: "patient.update" },
166
+ { id: "patient.delete", suite: "patient", label: "patient.delete", writes: true, dependsOn: ["patient.create"], capability: "patient.delete" },
167
+
168
+ // ── Z — limpeza ─────────────────────────────────────────────────────────────
169
+ { id: "residue.sweep", suite: "cleanup", label: "varredura de resíduo", writes: true, dependsOn: [] },
170
+ ] as const;
171
+
172
+ /**
173
+ * Suíte padrão (D10) — a LINHA DE BASE COMPARÁVEL: todos os passos.
174
+ *
175
+ * Só uma corrida com exatamente esta seleção é comparável com a anterior. Sem isso
176
+ * o output principal da ferramenta ("nenhum defeito novo") seria infalsificável:
177
+ * bastaria desmarcar o passo que falha.
178
+ *
179
+ * Atenção: a seleção não basta. `comparable` também exige `target.create_patient`
180
+ * ligado — senão `patient.create`/`patient.delete` são pulados e a cobertura real
181
+ * fica menor que a nominal. Quem decide isso é o backend, em `computeComparable`.
182
+ */
183
+ export const STANDARD_SUITE_STEP_IDS: readonly TestStepId[] = TEST_STEPS.map(
184
+ (s) => s.id,
185
+ );
186
+
187
+ export const READ_ONLY_STEP_IDS: readonly TestStepId[] = TEST_STEPS.filter(
188
+ (s) => !s.writes,
189
+ ).map((s) => s.id);
190
+
191
+ // ======================================================
192
+ // Checks
193
+ // ======================================================
194
+
195
+ export type TestCheckStatus = "pass" | "fail" | "skipped";
196
+
197
+ export interface ITestCheck {
198
+ /**
199
+ * Id ESTÁVEL no formato `<step_id>:<slug>` — ex. `appointment.update:sem_divergencia`.
200
+ *
201
+ * É a chave que o catálogo de conhecidos referencia. O script antigo casava por
202
+ * REGEX no texto do label, o que quebrava nos dois sentidos: mudar a redação
203
+ * descatalogava um defeito em silêncio, e um regex largo engolia defeito novo de
204
+ * outra causa. Por isso o id vem antes do texto — o label pode ser reescrito à
205
+ * vontade.
206
+ */
207
+ id: string;
208
+ label: string;
209
+ status: TestCheckStatus;
210
+ detail?: string;
211
+ }
212
+
213
+ // ======================================================
214
+ // Passo executado
215
+ // ======================================================
216
+
217
+ export type TestStepStatus =
218
+ | "pending"
219
+ | "running"
220
+ | "pass"
221
+ | "known" // falhou, mas casa com defeito catalogado — não reprova a corrida
222
+ | "fail" // defeito NOVO
223
+ | "skipped"
224
+ | "error";
225
+
226
+ export interface ITestSchemaValidation {
227
+ ok: boolean;
228
+ errors?: Array<{ path: (string | number)[]; message: string; code?: string }>;
229
+ }
230
+
231
+ /**
232
+ * O wire — o que o BACKEND mandou para o n8n, lido de
233
+ * `integrations_sync_monitor GET /calls?run_id=`.
234
+ *
235
+ * É a camada que interessa a quem mantém os workflows: a resposta da action mostra
236
+ * o que a UI mandou, não o que saiu na linha. `DocumentReference` vivo no payload
237
+ * (a classe de bug que derrubou produção no revert_confirm) só aparece aqui.
238
+ */
239
+ export interface ITestStepWire {
240
+ request_json?: string;
241
+ response_json?: string;
242
+ n8n_execution_id?: string | null;
243
+ n8n_workflow_id?: string | null;
244
+ workflow_url?: string;
245
+ http_status?: number | null;
246
+ timed_out?: boolean | null;
247
+ error_message?: string | null;
248
+ request_truncated?: boolean | null;
249
+ response_truncated?: boolean | null;
250
+ }
251
+
252
+ export interface ITestRunStep extends IFireDoc {
253
+ step_id: TestStepId;
254
+ suite: TestSuiteId;
255
+ order: number;
256
+ status: TestStepStatus;
257
+ started_at?: unknown;
258
+ finished_at?: unknown;
259
+ duration_ms?: number;
260
+
261
+ /** Camada 1 — o que a UI mandou para `integrations_actions` e o que voltou. */
262
+ layer1?: { request?: unknown; response?: unknown };
263
+
264
+ /** Correlação com o wire: `transport_metadata.run_id` da resposta da action. */
265
+ n8n_run_id?: string | null;
266
+
267
+ /** Camada 2 — o que o BE mandou para o n8n. */
268
+ wire?: ITestStepWire | null;
269
+
270
+ schema_validation?: {
271
+ request?: ITestSchemaValidation;
272
+ response?: ITestSchemaValidation;
273
+ } | null;
274
+
275
+ checks: ITestCheck[];
276
+
277
+ /** Preenchido quando `status === 'known'`: qual defeito catalogado casou. */
278
+ known_defects?: Array<{ check_id: string; description: string; ref: string }>;
279
+
280
+ error?: string | null;
281
+ }
282
+
283
+ // ======================================================
284
+ // Corrida
285
+ // ======================================================
286
+
287
+ export type TestRunStatus = "running" | "finished" | "abandoned";
288
+
289
+ /** Dados do paciente novo, quando o usuário liga a criação (D3). */
290
+ export interface ITestNewPatient {
291
+ first_name: string;
292
+ last_name?: string;
293
+ mobile_phone?: string;
294
+ email?: string;
295
+ social_id?: string;
296
+ birth_date?: string;
297
+ }
298
+
299
+ export interface ITestRunTarget {
300
+ /** Paciente de teste JÁ EXISTENTE, usado na leitura e no agendamento. */
301
+ patient_id: string;
302
+ /** Só quando `create_patient` está ligado. */
303
+ new_patient?: ITestNewPatient | null;
304
+ create_patient: boolean;
305
+ /** `YYYY-MM-DD`. Recomendado no passado — é aviso, não trava (D2). */
306
+ slot_date: string;
307
+ /** `HH:MM`. */
308
+ slot_time: string;
309
+ /** Dia povoado para o `appointment.list`; sem isso usa `slot_date`. */
310
+ list_date?: string | null;
311
+ }
312
+
313
+ /**
314
+ * Diagnóstico de segurança — AVISO, não trava (D2).
315
+ *
316
+ * Os dois avisos têm gravidade diferente e a UI precisa refletir isso: data futura
317
+ * só ativa as varreduras D-2/D-0, que dependem de janela; gatilho de evento ligado
318
+ * dispara na hora do create.
319
+ */
320
+ export interface ITestRunPreflight {
321
+ slot_in_past: boolean;
322
+ /** Quais dos gatilhos de evento estão ligados na agenda-alvo. */
323
+ triggers_enabled: string[];
324
+ /**
325
+ * `notification_config.configs` vazio.
326
+ *
327
+ * Distinguir de "desligado" importa: nada dispara nos dois casos, mas aqui é por
328
+ * AUSÊNCIA DE CONFIGURAÇÃO, não por decisão de alguém.
329
+ */
330
+ notification_config_empty: boolean;
331
+ evaluated_at?: unknown;
332
+ }
333
+
334
+ /** O que a corrida criou no PMS — permite enumerar resíduo quando o `Z` é desmarcado (D11). */
335
+ export interface ITestRunInventory {
336
+ appointment_external_ids: string[];
337
+ patient_external_id?: string | null;
338
+ }
339
+
340
+ export interface ITestRunVerdict {
341
+ ok: number;
342
+ known: number;
343
+ new_defects: number;
344
+ }
345
+
346
+ export interface ITestRun extends IFireDoc {
347
+ adapter_id: string;
348
+ tenant: string;
349
+ calendar_id: string;
350
+
351
+ target: ITestRunTarget;
352
+
353
+ selection: {
354
+ preset: "standard" | "read-only" | "custom";
355
+ step_ids: TestStepId[];
356
+ };
357
+
358
+ /** Quantos passos da suíte padrão esta corrida cobriu. */
359
+ coverage: { ran: number; total: number };
360
+ /** `true` só quando a seleção é exatamente a suíte padrão (D10). */
361
+ comparable: boolean;
362
+
363
+ preflight: ITestRunPreflight;
364
+ inventory: ITestRunInventory;
365
+ verdict: ITestRunVerdict;
366
+
367
+ status: TestRunStatus;
368
+
369
+ /**
370
+ * Dono da corrida. Quem orquestra é o FE, então duas abas conduzindo a mesma
371
+ * corrida escreveriam na mesma agenda de produção em paralelo — outro usuário
372
+ * abre em modo leitura até assumir explicitamente.
373
+ */
374
+ created_by: string;
375
+ finished_at?: unknown;
376
+ }
@@ -1,10 +1,36 @@
1
1
  import type { IFireGlobalDoc } from '../shared';
2
2
  export type NexCustomerDocumentType = 'cpf' | 'cnpj';
3
3
  export type NexCustomerStatus = 'active' | 'inactive';
4
+ /**
5
+ * feat-125 — papel de um lead do tenant `hub-medica` em relação a um cliente do
6
+ * Nexus. Enum tipado (e não dicionário) por decisão do time: papel novo é
7
+ * raríssimo, e a tipagem forte vale mais que evitar um release.
8
+ */
9
+ export declare const NexCustomerLeadRoleEnum: {
10
+ readonly Financial: "financial";
11
+ readonly Administrative: "administrative";
12
+ readonly Doctor: "doctor";
13
+ };
14
+ export type NexCustomerLeadRole = (typeof NexCustomerLeadRoleEnum)[keyof typeof NexCustomerLeadRoleEnum];
4
15
  export interface INexCustomerTenantRef {
5
16
  tenant: string;
6
17
  display_name?: string | null;
7
18
  }
19
+ /**
20
+ * feat-125 — vínculo com um lead do CRM do `hub-medica`, com o papel que aquele
21
+ * lead cumpre para este cliente.
22
+ *
23
+ * NÃO confundir com `tenant_refs`, que é outro eixo: lá é "o tenant que este
24
+ * cliente OPERA" (a instância dele na plataforma); aqui é "quem fala com este
25
+ * cliente pela Hub Médica".
26
+ *
27
+ * A chave de unicidade é o PAR (lead_id, role): o mesmo lead pode ser contato
28
+ * financeiro e administrativo do mesmo cliente.
29
+ */
30
+ export interface INexCustomerLeadRef {
31
+ lead_id: string;
32
+ role: NexCustomerLeadRole;
33
+ }
8
34
  export interface INexCustomerAddress {
9
35
  zip?: string | null;
10
36
  street?: string | null;
@@ -31,5 +57,16 @@ export interface INexCustomer extends IFireGlobalDoc {
31
57
  payer_aliases?: string[];
32
58
  address?: INexCustomerAddress | null;
33
59
  fiscal_data?: INexCustomerFiscalData | null;
60
+ /**
61
+ * feat-125 — leads do `hub-medica` que representam este cliente, por papel.
62
+ * Lado AUTORITATIVO do vínculo: é o que o envio de notificação consulta para
63
+ * anexar o `crm_lead` e cair na thread do hub-omni.
64
+ *
65
+ * NUNCA consultar com `array-contains`: o operador exige igualdade do objeto
66
+ * inteiro, então a query quebra em silêncio assim que a entrada ganhar um campo
67
+ * — foi o que travou reusar `tenant_refs` para isto. A busca reversa se faz pelo
68
+ * lado do lead, via `externalLinkKeys array-contains 'nex_customer:{id}'`.
69
+ */
70
+ hm_leads?: INexCustomerLeadRef[];
34
71
  notes?: string | null;
35
72
  }
@@ -1,2 +1,13 @@
1
1
  "use strict";
2
2
  Object.defineProperty(exports, "__esModule", { value: true });
3
+ exports.NexCustomerLeadRoleEnum = void 0;
4
+ /**
5
+ * feat-125 — papel de um lead do tenant `hub-medica` em relação a um cliente do
6
+ * Nexus. Enum tipado (e não dicionário) por decisão do time: papel novo é
7
+ * raríssimo, e a tipagem forte vale mais que evitar um release.
8
+ */
9
+ exports.NexCustomerLeadRoleEnum = {
10
+ Financial: 'financial',
11
+ Administrative: 'administrative',
12
+ Doctor: 'doctor',
13
+ };
@@ -5,6 +5,20 @@ import type { IFireGlobalDoc } from '../shared';
5
5
  export type NexCustomerDocumentType = 'cpf' | 'cnpj';
6
6
  export type NexCustomerStatus = 'active' | 'inactive';
7
7
 
8
+ /**
9
+ * feat-125 — papel de um lead do tenant `hub-medica` em relação a um cliente do
10
+ * Nexus. Enum tipado (e não dicionário) por decisão do time: papel novo é
11
+ * raríssimo, e a tipagem forte vale mais que evitar um release.
12
+ */
13
+ export const NexCustomerLeadRoleEnum = {
14
+ Financial: 'financial',
15
+ Administrative: 'administrative',
16
+ Doctor: 'doctor',
17
+ } as const;
18
+
19
+ export type NexCustomerLeadRole =
20
+ (typeof NexCustomerLeadRoleEnum)[keyof typeof NexCustomerLeadRoleEnum];
21
+
8
22
  // ── Sub-interfaces ──
9
23
 
10
24
  export interface INexCustomerTenantRef {
@@ -12,6 +26,22 @@ export interface INexCustomerTenantRef {
12
26
  display_name?: string | null;
13
27
  }
14
28
 
29
+ /**
30
+ * feat-125 — vínculo com um lead do CRM do `hub-medica`, com o papel que aquele
31
+ * lead cumpre para este cliente.
32
+ *
33
+ * NÃO confundir com `tenant_refs`, que é outro eixo: lá é "o tenant que este
34
+ * cliente OPERA" (a instância dele na plataforma); aqui é "quem fala com este
35
+ * cliente pela Hub Médica".
36
+ *
37
+ * A chave de unicidade é o PAR (lead_id, role): o mesmo lead pode ser contato
38
+ * financeiro e administrativo do mesmo cliente.
39
+ */
40
+ export interface INexCustomerLeadRef {
41
+ lead_id: string;
42
+ role: NexCustomerLeadRole;
43
+ }
44
+
15
45
  export interface INexCustomerAddress {
16
46
  zip?: string | null;
17
47
  street?: string | null;
@@ -58,6 +88,18 @@ export interface INexCustomer extends IFireGlobalDoc {
58
88
  // Fiscal data (PJ)
59
89
  fiscal_data?: INexCustomerFiscalData | null;
60
90
 
91
+ /**
92
+ * feat-125 — leads do `hub-medica` que representam este cliente, por papel.
93
+ * Lado AUTORITATIVO do vínculo: é o que o envio de notificação consulta para
94
+ * anexar o `crm_lead` e cair na thread do hub-omni.
95
+ *
96
+ * NUNCA consultar com `array-contains`: o operador exige igualdade do objeto
97
+ * inteiro, então a query quebra em silêncio assim que a entrada ganhar um campo
98
+ * — foi o que travou reusar `tenant_refs` para isto. A busca reversa se faz pelo
99
+ * lado do lead, via `externalLinkKeys array-contains 'nex_customer:{id}'`.
100
+ */
101
+ hm_leads?: INexCustomerLeadRef[];
102
+
61
103
  // Notes
62
104
  notes?: string | null;
63
105
  }
@@ -19,10 +19,35 @@ export declare const ExternalObjectTypeEnum: {
19
19
  readonly NexCustomer: "nex_customer";
20
20
  };
21
21
  export type ExternalObjectType = (typeof ExternalObjectTypeEnum)[keyof typeof ExternalObjectTypeEnum];
22
+ /**
23
+ * feat-125 — sistema DONO do objeto vinculado, quando ele não vive no tenant.
24
+ *
25
+ * Existe porque o vínculo passou a atravessar a fronteira da plataforma: o lead do
26
+ * `hub-medica` aponta para um cliente do Nexus, que é doc de `core/nexus` e não do
27
+ * tenant. Sem essa marca, quem lê um `externalLink` não tem como saber que não
28
+ * adianta procurar o objeto no próprio tenant.
29
+ */
30
+ export declare const ExternalOwnerSystemEnum: {
31
+ readonly Nexus: "nexus";
32
+ };
33
+ export type ExternalOwnerSystem = (typeof ExternalOwnerSystemEnum)[keyof typeof ExternalOwnerSystemEnum];
22
34
  export interface IExternalLink {
23
35
  type: ExternalObjectType;
24
36
  id: string;
25
37
  label?: string;
26
38
  ref?: FirestoreDocumentReference;
39
+ /**
40
+ * feat-125 — sistema dono do objeto quando ele NÃO vive no tenant.
41
+ *
42
+ * **Ausente = objeto do próprio tenant**, que é o caso de todo vínculo existente
43
+ * até aqui (lead↔paciente, task, invoice, payment-link). Por isso o campo é
44
+ * opcional e nada precisa ser migrado.
45
+ *
46
+ * Consumidores de UI tratam tipo desconhecido por allowlist com fallback (chip
47
+ * não-navegável), então um link externo já degrada bem sem ler este campo — ele
48
+ * serve para quem quiser distinguir "não conheço este tipo" de "este objeto é de
49
+ * outro sistema".
50
+ */
51
+ owner?: ExternalOwnerSystem;
27
52
  [key: string]: unknown;
28
53
  }
@@ -1,6 +1,6 @@
1
1
  "use strict";
2
2
  Object.defineProperty(exports, "__esModule", { value: true });
3
- exports.ExternalObjectTypeEnum = void 0;
3
+ exports.ExternalOwnerSystemEnum = exports.ExternalObjectTypeEnum = void 0;
4
4
  // ----- External object links (for unified UI views across modules)
5
5
  // Promoted from evo-task to shared for reuse in invoices, tasks, and other modules
6
6
  exports.ExternalObjectTypeEnum = {
@@ -22,3 +22,14 @@ exports.ExternalObjectTypeEnum = {
22
22
  NexContract: "nex_contract",
23
23
  NexCustomer: "nex_customer",
24
24
  };
25
+ /**
26
+ * feat-125 — sistema DONO do objeto vinculado, quando ele não vive no tenant.
27
+ *
28
+ * Existe porque o vínculo passou a atravessar a fronteira da plataforma: o lead do
29
+ * `hub-medica` aponta para um cliente do Nexus, que é doc de `core/nexus` e não do
30
+ * tenant. Sem essa marca, quem lê um `externalLink` não tem como saber que não
31
+ * adianta procurar o objeto no próprio tenant.
32
+ */
33
+ exports.ExternalOwnerSystemEnum = {
34
+ Nexus: "nexus",
35
+ };
@@ -26,10 +26,38 @@ export const ExternalObjectTypeEnum = {
26
26
  export type ExternalObjectType =
27
27
  (typeof ExternalObjectTypeEnum)[keyof typeof ExternalObjectTypeEnum];
28
28
 
29
+ /**
30
+ * feat-125 — sistema DONO do objeto vinculado, quando ele não vive no tenant.
31
+ *
32
+ * Existe porque o vínculo passou a atravessar a fronteira da plataforma: o lead do
33
+ * `hub-medica` aponta para um cliente do Nexus, que é doc de `core/nexus` e não do
34
+ * tenant. Sem essa marca, quem lê um `externalLink` não tem como saber que não
35
+ * adianta procurar o objeto no próprio tenant.
36
+ */
37
+ export const ExternalOwnerSystemEnum = {
38
+ Nexus: "nexus",
39
+ } as const;
40
+
41
+ export type ExternalOwnerSystem =
42
+ (typeof ExternalOwnerSystemEnum)[keyof typeof ExternalOwnerSystemEnum];
43
+
29
44
  export interface IExternalLink {
30
45
  type: ExternalObjectType;
31
46
  id: string;
32
47
  label?: string;
33
48
  ref?: FirestoreDocumentReference;
49
+ /**
50
+ * feat-125 — sistema dono do objeto quando ele NÃO vive no tenant.
51
+ *
52
+ * **Ausente = objeto do próprio tenant**, que é o caso de todo vínculo existente
53
+ * até aqui (lead↔paciente, task, invoice, payment-link). Por isso o campo é
54
+ * opcional e nada precisa ser migrado.
55
+ *
56
+ * Consumidores de UI tratam tipo desconhecido por allowlist com fallback (chip
57
+ * não-navegável), então um link externo já degrada bem sem ler este campo — ele
58
+ * serve para quem quiser distinguir "não conheço este tipo" de "este objeto é de
59
+ * outro sistema".
60
+ */
61
+ owner?: ExternalOwnerSystem;
34
62
  [key: string]: unknown;
35
63
  }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "evo360-types",
3
- "version": "1.3.505",
3
+ "version": "1.3.510",
4
4
  "description": "HREVO360 Shared Types",
5
5
  "main": "./dist/index.js",
6
6
  "types": "./dist/index.d.ts",