evo360-types 1.3.654 → 1.3.659
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-csq/zod-schemas.d.ts +908 -26
- package/dist/apps/evo-csq/zod-schemas.js +54 -2
- package/dist/apps/evo-csq/zod-schemas.ts +58 -0
- package/dist/apps/evo-modules/zod-schemas.d.ts +446 -0
- package/dist/apps/evo-modules/zod-schemas.js +143 -0
- package/dist/apps/evo-modules/zod-schemas.ts +153 -0
- package/dist/apps/hub-automation/zod-schemas.d.ts +66 -0
- package/dist/apps/hub-automation/zod-schemas.js +3 -0
- package/dist/apps/hub-automation/zod-schemas.ts +3 -0
- package/dist/index.d.ts +2 -0
- package/dist/index.js +2 -0
- package/dist/index.ts +2 -0
- package/dist/types/evo-csq/index.d.ts +77 -0
- package/dist/types/evo-csq/index.ts +92 -0
- package/dist/types/evo-modules/activation.d.ts +46 -0
- package/dist/types/evo-modules/activation.js +131 -0
- package/dist/types/evo-modules/activation.ts +150 -0
- package/dist/types/evo-modules/catalog.d.ts +28 -0
- package/dist/types/evo-modules/catalog.js +130 -0
- package/dist/types/evo-modules/catalog.ts +131 -0
- package/dist/types/evo-modules/index.d.ts +227 -0
- package/dist/types/evo-modules/index.js +75 -0
- package/dist/types/evo-modules/index.ts +329 -0
- package/dist/types/evo-telemedicine/fb_collections.js +1 -1
- package/dist/types/evo-telemedicine/fb_collections.ts +1 -1
- package/dist/types/evo-telemedicine/index.d.ts +8 -12
- package/dist/types/evo-telemedicine/index.ts +8 -12
- package/dist/types/evo-tenant/index.ts +3 -1
- package/dist/types/hub-automation/index.d.ts +2 -0
- package/dist/types/hub-automation/index.ts +6 -0
- package/fixtures/tenant-modules/is-module-active.parity.json +364 -0
- package/package.json +7 -4
|
@@ -288,8 +288,17 @@ export interface ICsqClaimAppointmentInformed {
|
|
|
288
288
|
* jornada (o lead cancelou/desistiu, ou foi trocado por outro horario). O
|
|
289
289
|
* codigo nao exige registro na agenda de um claim assim. */
|
|
290
290
|
superseded?: boolean;
|
|
291
|
+
/** feat-206 F3: quem extraiu o claim. Ausente = o juiz (laudos anteriores a
|
|
292
|
+
* F3 e claim so do juiz). `detector` = so o detector de agenda; `both` = os
|
|
293
|
+
* dois (mesma data/hora; a evidencia e a uniao). */
|
|
294
|
+
source?: CsqClaimSource;
|
|
295
|
+
/** feat-206 F3: `YYYY-MM-DD|HH:mm` do horario que substituiu este (o
|
|
296
|
+
* detector viu um horario confirmado depois). */
|
|
297
|
+
superseded_by?: string;
|
|
291
298
|
evidence: ICsqEvidence[];
|
|
292
299
|
}
|
|
300
|
+
/** feat-206 F3: origem de um claim `appointment_informed`. */
|
|
301
|
+
export type CsqClaimSource = "judge" | "detector" | "both";
|
|
293
302
|
/** "Prometi X ate quando". */
|
|
294
303
|
export interface ICsqClaimPromise {
|
|
295
304
|
type: "promise";
|
|
@@ -496,6 +505,71 @@ export interface ICsqTriageDecision {
|
|
|
496
505
|
mode: CsqTriageMode;
|
|
497
506
|
reasons: CsqTriageReason[];
|
|
498
507
|
}
|
|
508
|
+
/** Evento canonico de agenda extraido pelo detector (independente do juiz).
|
|
509
|
+
* `slot_offered` sozinho nao vira claim: oferta nao aceita nao e agendamento
|
|
510
|
+
* informado. */
|
|
511
|
+
export type CsqAppointmentEventKind = "slot_offered" | "slot_accepted" | "appointment_confirmed" | "reschedule_requested" | "appointment_cancelled";
|
|
512
|
+
/** Quem produziu o evento na conversa. */
|
|
513
|
+
export type CsqAppointmentEventActor = "attendant" | "lead" | "hubia" | "automation";
|
|
514
|
+
export interface ICsqAppointmentEvent {
|
|
515
|
+
kind: CsqAppointmentEventKind;
|
|
516
|
+
/** YYYY-MM-DD absoluto, TZ do tenant. */
|
|
517
|
+
date?: string;
|
|
518
|
+
/** HH:mm absoluto, TZ do tenant. */
|
|
519
|
+
time?: string;
|
|
520
|
+
professional?: string;
|
|
521
|
+
patient_name?: string;
|
|
522
|
+
modality?: string;
|
|
523
|
+
actor: CsqAppointmentEventActor;
|
|
524
|
+
/** `message_id`/`ticket_id` do recorte; `ts` sempre do transcript. */
|
|
525
|
+
evidence: ICsqEvidence[];
|
|
526
|
+
/** `YYYY-MM-DD|HH:mm` (hora pode faltar: `YYYY-MM-DD|`) do agendamento que
|
|
527
|
+
* este evento cancela ou remarca. */
|
|
528
|
+
supersedes?: string;
|
|
529
|
+
}
|
|
530
|
+
/** `off`: nao roda; `shadow`: roda e registra em `report.detector`, sem mexer
|
|
531
|
+
* nos claims; `on`: os claims do detector entram na verificacao. */
|
|
532
|
+
export type CsqDetectorMode = "off" | "shadow" | "on";
|
|
533
|
+
/** Como terminou a extracao por LLM do detector. */
|
|
534
|
+
export type CsqDetectorOutcome = "no_signal" | "ok" | "failed" | "skipped";
|
|
535
|
+
/** Uso do LLM do extrator (so quando houve chamada). */
|
|
536
|
+
export interface ICsqDetectorLlm {
|
|
537
|
+
provider: string;
|
|
538
|
+
model: string;
|
|
539
|
+
/** Doc em `llm-connections`; `null` quando sintetica. */
|
|
540
|
+
connection_id: string | null;
|
|
541
|
+
/** `extractor` = `extractor_connection_id`; `judge` = conexao do juiz. */
|
|
542
|
+
connection_source: "extractor" | "judge";
|
|
543
|
+
input_tokens: number;
|
|
544
|
+
/** Parte da entrada servida do cache do provedor (OpenAI: prefixo do system). */
|
|
545
|
+
cache_read_tokens?: number;
|
|
546
|
+
output_tokens: number;
|
|
547
|
+
rounds: number;
|
|
548
|
+
cost_usd_est: number | null;
|
|
549
|
+
duration_ms: number;
|
|
550
|
+
}
|
|
551
|
+
/** Divergencia detector x juiz do laudo (registrada, nao escalada — F4). */
|
|
552
|
+
export interface ICsqDetectorReport {
|
|
553
|
+
mode: CsqDetectorMode;
|
|
554
|
+
outcome: CsqDetectorOutcome;
|
|
555
|
+
/** Mensagens com sinal deterministico (data/hora, confirmacao, desfecho). */
|
|
556
|
+
signals: number;
|
|
557
|
+
/** Mensagens no recorte enviado ao extrator (sinais + vizinhanca). */
|
|
558
|
+
excerpt_messages: number;
|
|
559
|
+
events: number;
|
|
560
|
+
claims_detector: number;
|
|
561
|
+
claims_judge: number;
|
|
562
|
+
only_detector: number;
|
|
563
|
+
only_judge: number;
|
|
564
|
+
superseded_by_detector: number;
|
|
565
|
+
/** Claims do detector no MESMO dia de um claim do juiz, com outra hora: nao
|
|
566
|
+
* entram na verificacao (o juiz manda), so contam. */
|
|
567
|
+
time_mismatch?: number;
|
|
568
|
+
/** Eventos extraidos (com evidencia ja validada). */
|
|
569
|
+
event_list?: ICsqAppointmentEvent[];
|
|
570
|
+
llm?: ICsqDetectorLlm | null;
|
|
571
|
+
error?: string;
|
|
572
|
+
}
|
|
499
573
|
/** Tamanho de um bloco do prompt do juiz. `tokens_est` e estimativa
|
|
500
574
|
* deterministica (chars / 3,6), nao contagem do tokenizer. */
|
|
501
575
|
export interface ICsqPromptBlockUsage {
|
|
@@ -818,4 +892,7 @@ export interface ICsqLeadReport extends IFireDoc {
|
|
|
818
892
|
/** feat-206: presente quando o ULTIMO ticket nao passou pelo juiz. A avaliacao
|
|
819
893
|
* (score, pilares, criterios), `model` e `llm_usage` sao do ultimo juiz. */
|
|
820
894
|
judge_skipped_reasons?: CsqTriageReason[];
|
|
895
|
+
/** feat-206 F3: detector de agenda (contagens e divergencia com o juiz).
|
|
896
|
+
* Ausente quando o detector esta `off` ou o laudo e anterior a F3. */
|
|
897
|
+
detector?: ICsqDetectorReport;
|
|
821
898
|
}
|
|
@@ -501,9 +501,19 @@ export interface ICsqClaimAppointmentInformed {
|
|
|
501
501
|
* jornada (o lead cancelou/desistiu, ou foi trocado por outro horario). O
|
|
502
502
|
* codigo nao exige registro na agenda de um claim assim. */
|
|
503
503
|
superseded?: boolean;
|
|
504
|
+
/** feat-206 F3: quem extraiu o claim. Ausente = o juiz (laudos anteriores a
|
|
505
|
+
* F3 e claim so do juiz). `detector` = so o detector de agenda; `both` = os
|
|
506
|
+
* dois (mesma data/hora; a evidencia e a uniao). */
|
|
507
|
+
source?: CsqClaimSource;
|
|
508
|
+
/** feat-206 F3: `YYYY-MM-DD|HH:mm` do horario que substituiu este (o
|
|
509
|
+
* detector viu um horario confirmado depois). */
|
|
510
|
+
superseded_by?: string;
|
|
504
511
|
evidence: ICsqEvidence[];
|
|
505
512
|
}
|
|
506
513
|
|
|
514
|
+
/** feat-206 F3: origem de um claim `appointment_informed`. */
|
|
515
|
+
export type CsqClaimSource = "judge" | "detector" | "both";
|
|
516
|
+
|
|
507
517
|
/** "Prometi X ate quando". */
|
|
508
518
|
export interface ICsqClaimPromise {
|
|
509
519
|
type: "promise";
|
|
@@ -767,6 +777,85 @@ export interface ICsqTriageDecision {
|
|
|
767
777
|
reasons: CsqTriageReason[];
|
|
768
778
|
}
|
|
769
779
|
|
|
780
|
+
// ───────── Detector de agenda (feat-206 F3) ─────────
|
|
781
|
+
|
|
782
|
+
/** Evento canonico de agenda extraido pelo detector (independente do juiz).
|
|
783
|
+
* `slot_offered` sozinho nao vira claim: oferta nao aceita nao e agendamento
|
|
784
|
+
* informado. */
|
|
785
|
+
export type CsqAppointmentEventKind =
|
|
786
|
+
| "slot_offered"
|
|
787
|
+
| "slot_accepted"
|
|
788
|
+
| "appointment_confirmed"
|
|
789
|
+
| "reschedule_requested"
|
|
790
|
+
| "appointment_cancelled";
|
|
791
|
+
|
|
792
|
+
/** Quem produziu o evento na conversa. */
|
|
793
|
+
export type CsqAppointmentEventActor = "attendant" | "lead" | "hubia" | "automation";
|
|
794
|
+
|
|
795
|
+
export interface ICsqAppointmentEvent {
|
|
796
|
+
kind: CsqAppointmentEventKind;
|
|
797
|
+
/** YYYY-MM-DD absoluto, TZ do tenant. */
|
|
798
|
+
date?: string;
|
|
799
|
+
/** HH:mm absoluto, TZ do tenant. */
|
|
800
|
+
time?: string;
|
|
801
|
+
professional?: string;
|
|
802
|
+
patient_name?: string;
|
|
803
|
+
modality?: string;
|
|
804
|
+
actor: CsqAppointmentEventActor;
|
|
805
|
+
/** `message_id`/`ticket_id` do recorte; `ts` sempre do transcript. */
|
|
806
|
+
evidence: ICsqEvidence[];
|
|
807
|
+
/** `YYYY-MM-DD|HH:mm` (hora pode faltar: `YYYY-MM-DD|`) do agendamento que
|
|
808
|
+
* este evento cancela ou remarca. */
|
|
809
|
+
supersedes?: string;
|
|
810
|
+
}
|
|
811
|
+
|
|
812
|
+
/** `off`: nao roda; `shadow`: roda e registra em `report.detector`, sem mexer
|
|
813
|
+
* nos claims; `on`: os claims do detector entram na verificacao. */
|
|
814
|
+
export type CsqDetectorMode = "off" | "shadow" | "on";
|
|
815
|
+
|
|
816
|
+
/** Como terminou a extracao por LLM do detector. */
|
|
817
|
+
export type CsqDetectorOutcome = "no_signal" | "ok" | "failed" | "skipped";
|
|
818
|
+
|
|
819
|
+
/** Uso do LLM do extrator (so quando houve chamada). */
|
|
820
|
+
export interface ICsqDetectorLlm {
|
|
821
|
+
provider: string;
|
|
822
|
+
model: string;
|
|
823
|
+
/** Doc em `llm-connections`; `null` quando sintetica. */
|
|
824
|
+
connection_id: string | null;
|
|
825
|
+
/** `extractor` = `extractor_connection_id`; `judge` = conexao do juiz. */
|
|
826
|
+
connection_source: "extractor" | "judge";
|
|
827
|
+
input_tokens: number;
|
|
828
|
+
/** Parte da entrada servida do cache do provedor (OpenAI: prefixo do system). */
|
|
829
|
+
cache_read_tokens?: number;
|
|
830
|
+
output_tokens: number;
|
|
831
|
+
rounds: number;
|
|
832
|
+
cost_usd_est: number | null;
|
|
833
|
+
duration_ms: number;
|
|
834
|
+
}
|
|
835
|
+
|
|
836
|
+
/** Divergencia detector x juiz do laudo (registrada, nao escalada — F4). */
|
|
837
|
+
export interface ICsqDetectorReport {
|
|
838
|
+
mode: CsqDetectorMode;
|
|
839
|
+
outcome: CsqDetectorOutcome;
|
|
840
|
+
/** Mensagens com sinal deterministico (data/hora, confirmacao, desfecho). */
|
|
841
|
+
signals: number;
|
|
842
|
+
/** Mensagens no recorte enviado ao extrator (sinais + vizinhanca). */
|
|
843
|
+
excerpt_messages: number;
|
|
844
|
+
events: number;
|
|
845
|
+
claims_detector: number;
|
|
846
|
+
claims_judge: number;
|
|
847
|
+
only_detector: number;
|
|
848
|
+
only_judge: number;
|
|
849
|
+
superseded_by_detector: number;
|
|
850
|
+
/** Claims do detector no MESMO dia de um claim do juiz, com outra hora: nao
|
|
851
|
+
* entram na verificacao (o juiz manda), so contam. */
|
|
852
|
+
time_mismatch?: number;
|
|
853
|
+
/** Eventos extraidos (com evidencia ja validada). */
|
|
854
|
+
event_list?: ICsqAppointmentEvent[];
|
|
855
|
+
llm?: ICsqDetectorLlm | null;
|
|
856
|
+
error?: string;
|
|
857
|
+
}
|
|
858
|
+
|
|
770
859
|
/** Tamanho de um bloco do prompt do juiz. `tokens_est` e estimativa
|
|
771
860
|
* deterministica (chars / 3,6), nao contagem do tokenizer. */
|
|
772
861
|
export interface ICsqPromptBlockUsage {
|
|
@@ -1127,4 +1216,7 @@ export interface ICsqLeadReport extends IFireDoc {
|
|
|
1127
1216
|
/** feat-206: presente quando o ULTIMO ticket nao passou pelo juiz. A avaliacao
|
|
1128
1217
|
* (score, pilares, criterios), `model` e `llm_usage` sao do ultimo juiz. */
|
|
1129
1218
|
judge_skipped_reasons?: CsqTriageReason[];
|
|
1219
|
+
/** feat-206 F3: detector de agenda (contagens e divergencia com o juiz).
|
|
1220
|
+
* Ausente quando o detector esta `off` ou o laudo e anterior a F3. */
|
|
1221
|
+
detector?: ICsqDetectorReport;
|
|
1130
1222
|
}
|
|
@@ -0,0 +1,46 @@
|
|
|
1
|
+
import type { ITenantModuleEntitlement } from "../evo-tenant";
|
|
2
|
+
/**
|
|
3
|
+
* Normaliza qualquer forma de data que chega às superfícies: `Date`, Firestore
|
|
4
|
+
* `Timestamp` (`toDate()`), Timestamp serializado (`{_seconds}` / `{seconds}`),
|
|
5
|
+
* ISO string e epoch em ms. Inválido ⇒ `null` (equivale a "sem data").
|
|
6
|
+
*/
|
|
7
|
+
export declare function moduleDateToMillis(value: unknown): number | null;
|
|
8
|
+
/**
|
|
9
|
+
* Decide se o entitlement autoriza um uso NOVO do módulo. Função pura e ÚNICA:
|
|
10
|
+
* backend (`Core.Tenant.isModuleActive`), web-app e Nexus usam esta; o Flutter
|
|
11
|
+
* espelha e prova pela fixture `fixtures/tenant-modules/is-module-active.parity.json`.
|
|
12
|
+
*
|
|
13
|
+
* Ordem das cláusulas (importa):
|
|
14
|
+
* 1. `override_until` futuro COM `override_by` libera (D6: exceção operacional
|
|
15
|
+
* temporária e auditada) — mesmo suspenso/cancelado/vencido;
|
|
16
|
+
* 2. `enabled !== true` bloqueia;
|
|
17
|
+
* 3. `suspended`/`cancelled` bloqueiam;
|
|
18
|
+
* 4. `effective_at` no futuro bloqueia;
|
|
19
|
+
* 5. `expires_at` no passado (ou agora) bloqueia, para QUALQUER status;
|
|
20
|
+
* 6. só `active` e `trial` liberam.
|
|
21
|
+
*
|
|
22
|
+
* Não tem parâmetro de papel: super admin não ignora entitlement (D6).
|
|
23
|
+
*/
|
|
24
|
+
export declare function isModuleActive(entitlement: ITenantModuleEntitlement | null | undefined, now?: Date): boolean;
|
|
25
|
+
/**
|
|
26
|
+
* Capability ausente, não-booleana (`"true"`, `1`) ou `false` equivale a `false`.
|
|
27
|
+
* Não implica módulo ativo: o gate de UI equivalente ao backend é
|
|
28
|
+
* `isModuleActive(e) && hasModuleCapability(e, cap)`.
|
|
29
|
+
*/
|
|
30
|
+
export declare function hasModuleCapability(entitlement: ITenantModuleEntitlement | null | undefined, capability: string): boolean;
|
|
31
|
+
export interface IModuleContractMismatch {
|
|
32
|
+
module: string;
|
|
33
|
+
metric: string;
|
|
34
|
+
kind: "active_without_allowance" | "allowance_without_module";
|
|
35
|
+
}
|
|
36
|
+
/**
|
|
37
|
+
* Divergências entre franquias do contrato (`allowances`) e módulos do tenant,
|
|
38
|
+
* pelas `allowance_metrics` do catálogo. Só informa (a tela de contrato mostra);
|
|
39
|
+
* nada sincroniza sozinho (Q1).
|
|
40
|
+
*
|
|
41
|
+
* - `active_without_allowance`: módulo ativo e nenhuma métrica dele no contrato
|
|
42
|
+
* (consumo invisível ao alerta e ao portal);
|
|
43
|
+
* - `allowance_without_module`: contrato tem franquia de uma métrica do módulo e
|
|
44
|
+
* o módulo não está ativo.
|
|
45
|
+
*/
|
|
46
|
+
export declare function moduleContractMismatches(allowances: Record<string, unknown> | null | undefined, modules: Record<string, ITenantModuleEntitlement> | null | undefined, now?: Date): IModuleContractMismatch[];
|
|
@@ -0,0 +1,131 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
|
+
exports.moduleDateToMillis = moduleDateToMillis;
|
|
4
|
+
exports.isModuleActive = isModuleActive;
|
|
5
|
+
exports.hasModuleCapability = hasModuleCapability;
|
|
6
|
+
exports.moduleContractMismatches = moduleContractMismatches;
|
|
7
|
+
const catalog_1 = require("./catalog");
|
|
8
|
+
/**
|
|
9
|
+
* Normaliza qualquer forma de data que chega às superfícies: `Date`, Firestore
|
|
10
|
+
* `Timestamp` (`toDate()`), Timestamp serializado (`{_seconds}` / `{seconds}`),
|
|
11
|
+
* ISO string e epoch em ms. Inválido ⇒ `null` (equivale a "sem data").
|
|
12
|
+
*/
|
|
13
|
+
function moduleDateToMillis(value) {
|
|
14
|
+
if (value === null || value === undefined || value === "")
|
|
15
|
+
return null;
|
|
16
|
+
if (value instanceof Date) {
|
|
17
|
+
const t = value.getTime();
|
|
18
|
+
return isNaN(t) ? null : t;
|
|
19
|
+
}
|
|
20
|
+
if (typeof value === "number")
|
|
21
|
+
return isFinite(value) ? value : null;
|
|
22
|
+
if (typeof value === "string") {
|
|
23
|
+
const t = new Date(value).getTime();
|
|
24
|
+
return isNaN(t) ? null : t;
|
|
25
|
+
}
|
|
26
|
+
if (typeof value === "object") {
|
|
27
|
+
const v = value;
|
|
28
|
+
if (typeof v.toDate === "function") {
|
|
29
|
+
try {
|
|
30
|
+
const d = v.toDate();
|
|
31
|
+
if (d instanceof Date) {
|
|
32
|
+
const t = d.getTime();
|
|
33
|
+
return isNaN(t) ? null : t;
|
|
34
|
+
}
|
|
35
|
+
}
|
|
36
|
+
catch {
|
|
37
|
+
return null;
|
|
38
|
+
}
|
|
39
|
+
return null;
|
|
40
|
+
}
|
|
41
|
+
const seconds = typeof v._seconds === "number" ? v._seconds : typeof v.seconds === "number" ? v.seconds : null;
|
|
42
|
+
if (seconds !== null && isFinite(seconds)) {
|
|
43
|
+
const nanos = typeof v._nanoseconds === "number" ? v._nanoseconds : typeof v.nanoseconds === "number" ? v.nanoseconds : 0;
|
|
44
|
+
return seconds * 1000 + Math.floor(nanos / 1e6);
|
|
45
|
+
}
|
|
46
|
+
}
|
|
47
|
+
return null;
|
|
48
|
+
}
|
|
49
|
+
/**
|
|
50
|
+
* Decide se o entitlement autoriza um uso NOVO do módulo. Função pura e ÚNICA:
|
|
51
|
+
* backend (`Core.Tenant.isModuleActive`), web-app e Nexus usam esta; o Flutter
|
|
52
|
+
* espelha e prova pela fixture `fixtures/tenant-modules/is-module-active.parity.json`.
|
|
53
|
+
*
|
|
54
|
+
* Ordem das cláusulas (importa):
|
|
55
|
+
* 1. `override_until` futuro COM `override_by` libera (D6: exceção operacional
|
|
56
|
+
* temporária e auditada) — mesmo suspenso/cancelado/vencido;
|
|
57
|
+
* 2. `enabled !== true` bloqueia;
|
|
58
|
+
* 3. `suspended`/`cancelled` bloqueiam;
|
|
59
|
+
* 4. `effective_at` no futuro bloqueia;
|
|
60
|
+
* 5. `expires_at` no passado (ou agora) bloqueia, para QUALQUER status;
|
|
61
|
+
* 6. só `active` e `trial` liberam.
|
|
62
|
+
*
|
|
63
|
+
* Não tem parâmetro de papel: super admin não ignora entitlement (D6).
|
|
64
|
+
*/
|
|
65
|
+
function isModuleActive(entitlement, now = new Date()) {
|
|
66
|
+
if (!entitlement || typeof entitlement !== "object")
|
|
67
|
+
return false;
|
|
68
|
+
const t = now.getTime();
|
|
69
|
+
const overrideUntil = moduleDateToMillis(entitlement.override_until);
|
|
70
|
+
if (overrideUntil !== null && overrideUntil > t && !!entitlement.override_by)
|
|
71
|
+
return true;
|
|
72
|
+
if (entitlement.enabled !== true)
|
|
73
|
+
return false;
|
|
74
|
+
if (entitlement.status === "suspended" || entitlement.status === "cancelled")
|
|
75
|
+
return false;
|
|
76
|
+
const effectiveAt = moduleDateToMillis(entitlement.effective_at);
|
|
77
|
+
if (effectiveAt !== null && effectiveAt > t)
|
|
78
|
+
return false;
|
|
79
|
+
const expiresAt = moduleDateToMillis(entitlement.expires_at);
|
|
80
|
+
if (expiresAt !== null && expiresAt <= t)
|
|
81
|
+
return false;
|
|
82
|
+
return entitlement.status === "active" || entitlement.status === "trial";
|
|
83
|
+
}
|
|
84
|
+
/**
|
|
85
|
+
* Capability ausente, não-booleana (`"true"`, `1`) ou `false` equivale a `false`.
|
|
86
|
+
* Não implica módulo ativo: o gate de UI equivalente ao backend é
|
|
87
|
+
* `isModuleActive(e) && hasModuleCapability(e, cap)`.
|
|
88
|
+
*/
|
|
89
|
+
function hasModuleCapability(entitlement, capability) {
|
|
90
|
+
if (!entitlement || !entitlement.capabilities)
|
|
91
|
+
return false;
|
|
92
|
+
return entitlement.capabilities[capability] === true;
|
|
93
|
+
}
|
|
94
|
+
/**
|
|
95
|
+
* Divergências entre franquias do contrato (`allowances`) e módulos do tenant,
|
|
96
|
+
* pelas `allowance_metrics` do catálogo. Só informa (a tela de contrato mostra);
|
|
97
|
+
* nada sincroniza sozinho (Q1).
|
|
98
|
+
*
|
|
99
|
+
* - `active_without_allowance`: módulo ativo e nenhuma métrica dele no contrato
|
|
100
|
+
* (consumo invisível ao alerta e ao portal);
|
|
101
|
+
* - `allowance_without_module`: contrato tem franquia de uma métrica do módulo e
|
|
102
|
+
* o módulo não está ativo.
|
|
103
|
+
*/
|
|
104
|
+
function moduleContractMismatches(allowances, modules, now = new Date()) {
|
|
105
|
+
const out = [];
|
|
106
|
+
const allow = allowances && typeof allowances === "object" ? allowances : {};
|
|
107
|
+
const mods = modules && typeof modules === "object" ? modules : {};
|
|
108
|
+
for (const key in catalog_1.PLATFORM_MODULE_CATALOG) {
|
|
109
|
+
if (!Object.prototype.hasOwnProperty.call(catalog_1.PLATFORM_MODULE_CATALOG, key))
|
|
110
|
+
continue;
|
|
111
|
+
const def = catalog_1.PLATFORM_MODULE_CATALOG[key];
|
|
112
|
+
if (def.allowance_metrics.length === 0)
|
|
113
|
+
continue;
|
|
114
|
+
const active = isModuleActive(Object.prototype.hasOwnProperty.call(mods, key) ? mods[key] : null, now);
|
|
115
|
+
const present = [];
|
|
116
|
+
for (const m of def.allowance_metrics) {
|
|
117
|
+
if (Object.prototype.hasOwnProperty.call(allow, m.metric) && allow[m.metric] !== null && allow[m.metric] !== undefined) {
|
|
118
|
+
present.push(m.metric);
|
|
119
|
+
}
|
|
120
|
+
}
|
|
121
|
+
if (active && present.length === 0) {
|
|
122
|
+
out.push({ module: key, metric: def.allowance_metrics[0].metric, kind: "active_without_allowance" });
|
|
123
|
+
}
|
|
124
|
+
if (!active) {
|
|
125
|
+
for (const metric of present) {
|
|
126
|
+
out.push({ module: key, metric, kind: "allowance_without_module" });
|
|
127
|
+
}
|
|
128
|
+
}
|
|
129
|
+
}
|
|
130
|
+
return out;
|
|
131
|
+
}
|
|
@@ -0,0 +1,150 @@
|
|
|
1
|
+
import type { ITenantModuleEntitlement } from "../evo-tenant";
|
|
2
|
+
import { PLATFORM_MODULE_CATALOG } from "./catalog";
|
|
3
|
+
|
|
4
|
+
/**
|
|
5
|
+
* Normaliza qualquer forma de data que chega às superfícies: `Date`, Firestore
|
|
6
|
+
* `Timestamp` (`toDate()`), Timestamp serializado (`{_seconds}` / `{seconds}`),
|
|
7
|
+
* ISO string e epoch em ms. Inválido ⇒ `null` (equivale a "sem data").
|
|
8
|
+
*/
|
|
9
|
+
export function moduleDateToMillis(value: unknown): number | null {
|
|
10
|
+
if (value === null || value === undefined || value === "") return null;
|
|
11
|
+
if (value instanceof Date) {
|
|
12
|
+
const t = value.getTime();
|
|
13
|
+
return isNaN(t) ? null : t;
|
|
14
|
+
}
|
|
15
|
+
if (typeof value === "number") return isFinite(value) ? value : null;
|
|
16
|
+
if (typeof value === "string") {
|
|
17
|
+
const t = new Date(value).getTime();
|
|
18
|
+
return isNaN(t) ? null : t;
|
|
19
|
+
}
|
|
20
|
+
if (typeof value === "object") {
|
|
21
|
+
const v = value as {
|
|
22
|
+
toDate?: unknown;
|
|
23
|
+
_seconds?: unknown;
|
|
24
|
+
seconds?: unknown;
|
|
25
|
+
_nanoseconds?: unknown;
|
|
26
|
+
nanoseconds?: unknown;
|
|
27
|
+
};
|
|
28
|
+
if (typeof v.toDate === "function") {
|
|
29
|
+
try {
|
|
30
|
+
const d = (v.toDate as () => unknown)();
|
|
31
|
+
if (d instanceof Date) {
|
|
32
|
+
const t = d.getTime();
|
|
33
|
+
return isNaN(t) ? null : t;
|
|
34
|
+
}
|
|
35
|
+
} catch {
|
|
36
|
+
return null;
|
|
37
|
+
}
|
|
38
|
+
return null;
|
|
39
|
+
}
|
|
40
|
+
const seconds = typeof v._seconds === "number" ? v._seconds : typeof v.seconds === "number" ? v.seconds : null;
|
|
41
|
+
if (seconds !== null && isFinite(seconds)) {
|
|
42
|
+
const nanos =
|
|
43
|
+
typeof v._nanoseconds === "number" ? v._nanoseconds : typeof v.nanoseconds === "number" ? v.nanoseconds : 0;
|
|
44
|
+
return seconds * 1000 + Math.floor(nanos / 1e6);
|
|
45
|
+
}
|
|
46
|
+
}
|
|
47
|
+
return null;
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
/**
|
|
51
|
+
* Decide se o entitlement autoriza um uso NOVO do módulo. Função pura e ÚNICA:
|
|
52
|
+
* backend (`Core.Tenant.isModuleActive`), web-app e Nexus usam esta; o Flutter
|
|
53
|
+
* espelha e prova pela fixture `fixtures/tenant-modules/is-module-active.parity.json`.
|
|
54
|
+
*
|
|
55
|
+
* Ordem das cláusulas (importa):
|
|
56
|
+
* 1. `override_until` futuro COM `override_by` libera (D6: exceção operacional
|
|
57
|
+
* temporária e auditada) — mesmo suspenso/cancelado/vencido;
|
|
58
|
+
* 2. `enabled !== true` bloqueia;
|
|
59
|
+
* 3. `suspended`/`cancelled` bloqueiam;
|
|
60
|
+
* 4. `effective_at` no futuro bloqueia;
|
|
61
|
+
* 5. `expires_at` no passado (ou agora) bloqueia, para QUALQUER status;
|
|
62
|
+
* 6. só `active` e `trial` liberam.
|
|
63
|
+
*
|
|
64
|
+
* Não tem parâmetro de papel: super admin não ignora entitlement (D6).
|
|
65
|
+
*/
|
|
66
|
+
export function isModuleActive(
|
|
67
|
+
entitlement: ITenantModuleEntitlement | null | undefined,
|
|
68
|
+
now: Date = new Date(),
|
|
69
|
+
): boolean {
|
|
70
|
+
if (!entitlement || typeof entitlement !== "object") return false;
|
|
71
|
+
|
|
72
|
+
const t = now.getTime();
|
|
73
|
+
|
|
74
|
+
const overrideUntil = moduleDateToMillis(entitlement.override_until);
|
|
75
|
+
if (overrideUntil !== null && overrideUntil > t && !!entitlement.override_by) return true;
|
|
76
|
+
|
|
77
|
+
if (entitlement.enabled !== true) return false;
|
|
78
|
+
if (entitlement.status === "suspended" || entitlement.status === "cancelled") return false;
|
|
79
|
+
|
|
80
|
+
const effectiveAt = moduleDateToMillis(entitlement.effective_at);
|
|
81
|
+
if (effectiveAt !== null && effectiveAt > t) return false;
|
|
82
|
+
|
|
83
|
+
const expiresAt = moduleDateToMillis(entitlement.expires_at);
|
|
84
|
+
if (expiresAt !== null && expiresAt <= t) return false;
|
|
85
|
+
|
|
86
|
+
return entitlement.status === "active" || entitlement.status === "trial";
|
|
87
|
+
}
|
|
88
|
+
|
|
89
|
+
/**
|
|
90
|
+
* Capability ausente, não-booleana (`"true"`, `1`) ou `false` equivale a `false`.
|
|
91
|
+
* Não implica módulo ativo: o gate de UI equivalente ao backend é
|
|
92
|
+
* `isModuleActive(e) && hasModuleCapability(e, cap)`.
|
|
93
|
+
*/
|
|
94
|
+
export function hasModuleCapability(
|
|
95
|
+
entitlement: ITenantModuleEntitlement | null | undefined,
|
|
96
|
+
capability: string,
|
|
97
|
+
): boolean {
|
|
98
|
+
if (!entitlement || !entitlement.capabilities) return false;
|
|
99
|
+
return entitlement.capabilities[capability] === true;
|
|
100
|
+
}
|
|
101
|
+
|
|
102
|
+
export interface IModuleContractMismatch {
|
|
103
|
+
module: string;
|
|
104
|
+
metric: string;
|
|
105
|
+
kind: "active_without_allowance" | "allowance_without_module";
|
|
106
|
+
}
|
|
107
|
+
|
|
108
|
+
/**
|
|
109
|
+
* Divergências entre franquias do contrato (`allowances`) e módulos do tenant,
|
|
110
|
+
* pelas `allowance_metrics` do catálogo. Só informa (a tela de contrato mostra);
|
|
111
|
+
* nada sincroniza sozinho (Q1).
|
|
112
|
+
*
|
|
113
|
+
* - `active_without_allowance`: módulo ativo e nenhuma métrica dele no contrato
|
|
114
|
+
* (consumo invisível ao alerta e ao portal);
|
|
115
|
+
* - `allowance_without_module`: contrato tem franquia de uma métrica do módulo e
|
|
116
|
+
* o módulo não está ativo.
|
|
117
|
+
*/
|
|
118
|
+
export function moduleContractMismatches(
|
|
119
|
+
allowances: Record<string, unknown> | null | undefined,
|
|
120
|
+
modules: Record<string, ITenantModuleEntitlement> | null | undefined,
|
|
121
|
+
now: Date = new Date(),
|
|
122
|
+
): IModuleContractMismatch[] {
|
|
123
|
+
const out: IModuleContractMismatch[] = [];
|
|
124
|
+
const allow = allowances && typeof allowances === "object" ? allowances : {};
|
|
125
|
+
const mods = modules && typeof modules === "object" ? modules : {};
|
|
126
|
+
|
|
127
|
+
for (const key in PLATFORM_MODULE_CATALOG) {
|
|
128
|
+
if (!Object.prototype.hasOwnProperty.call(PLATFORM_MODULE_CATALOG, key)) continue;
|
|
129
|
+
const def = PLATFORM_MODULE_CATALOG[key];
|
|
130
|
+
if (def.allowance_metrics.length === 0) continue;
|
|
131
|
+
|
|
132
|
+
const active = isModuleActive(Object.prototype.hasOwnProperty.call(mods, key) ? mods[key] : null, now);
|
|
133
|
+
const present: string[] = [];
|
|
134
|
+
for (const m of def.allowance_metrics) {
|
|
135
|
+
if (Object.prototype.hasOwnProperty.call(allow, m.metric) && allow[m.metric] !== null && allow[m.metric] !== undefined) {
|
|
136
|
+
present.push(m.metric);
|
|
137
|
+
}
|
|
138
|
+
}
|
|
139
|
+
|
|
140
|
+
if (active && present.length === 0) {
|
|
141
|
+
out.push({ module: key, metric: def.allowance_metrics[0].metric, kind: "active_without_allowance" });
|
|
142
|
+
}
|
|
143
|
+
if (!active) {
|
|
144
|
+
for (const metric of present) {
|
|
145
|
+
out.push({ module: key, metric, kind: "allowance_without_module" });
|
|
146
|
+
}
|
|
147
|
+
}
|
|
148
|
+
}
|
|
149
|
+
return out;
|
|
150
|
+
}
|
|
@@ -0,0 +1,28 @@
|
|
|
1
|
+
import type { IPlatformModuleDefinition } from "./index";
|
|
2
|
+
/**
|
|
3
|
+
* Versão do catálogo, gravada em `IModuleProvisioningRun.types_version`.
|
|
4
|
+
* Mudou capability, passo, grupo ou métrica de algum módulo ⇒ incrementar: o
|
|
5
|
+
* run antigo precisa dizer com que catálogo foi planejado.
|
|
6
|
+
*/
|
|
7
|
+
export declare const PLATFORM_MODULE_CATALOG_VERSION = "2026-10-06.1";
|
|
8
|
+
export declare const PLATFORM_MODULE_TELEMEDICINE = "telemedicine";
|
|
9
|
+
export declare const PLATFORM_MODULE_CSQ = "csq";
|
|
10
|
+
/**
|
|
11
|
+
* Catálogo de módulos da plataforma (feat-263). É código, não Firestore: carrega
|
|
12
|
+
* comportamento (passos, grupos, métricas) e precisa de versionamento e tipos.
|
|
13
|
+
*
|
|
14
|
+
* Módulo novo = uma entrada aqui + publish. O estado por tenant continua em
|
|
15
|
+
* `tenants/{t}.modules[key]`.
|
|
16
|
+
*/
|
|
17
|
+
export declare const PLATFORM_MODULE_CATALOG: Readonly<Record<string, IPlatformModuleDefinition>>;
|
|
18
|
+
export declare function getModuleDefinition(key: string | null | undefined): IPlatformModuleDefinition | null;
|
|
19
|
+
/** Definições em ordem estável (ordem de declaração). */
|
|
20
|
+
export declare function listModuleDefinitions(): IPlatformModuleDefinition[];
|
|
21
|
+
/** Chaves do catálogo em ordem de declaração. */
|
|
22
|
+
export declare function listModuleKeys(): string[];
|
|
23
|
+
/** Capabilities default do módulo (mapa novo a cada chamada). */
|
|
24
|
+
export declare function buildModuleDefaultCapabilities(definition: IPlatformModuleDefinition): Record<string, boolean | number | string>;
|
|
25
|
+
/** Módulos que tenant novo recebe habilitados. */
|
|
26
|
+
export declare function listDefaultModulesForNewTenants(): IPlatformModuleDefinition[];
|
|
27
|
+
/** União, sem repetição e em ordem, dos grupos RBAC dos módulos default de tenant novo. */
|
|
28
|
+
export declare function listDefaultTenantRbacGroups(): string[];
|