223 lines
6.2 KiB
TypeScript
223 lines
6.2 KiB
TypeScript
import {
|
|
NormalizedAnalysis,
|
|
SUPPORTED_CONTRACT_TYPES,
|
|
SupportedContractType,
|
|
ContactInfo,
|
|
KeyPerson,
|
|
ExplainabilityItem,
|
|
} from "./analysis.types";
|
|
|
|
function mapContractType(rawType: unknown): SupportedContractType {
|
|
const value = String(rawType ?? "")
|
|
.trim()
|
|
.toUpperCase()
|
|
.replace(/\s+/g, "_");
|
|
|
|
if (SUPPORTED_CONTRACT_TYPES.includes(value as SupportedContractType)) {
|
|
return value as SupportedContractType;
|
|
}
|
|
|
|
const aliases: Record<string, SupportedContractType> = {
|
|
AUTO_INSURANCE: "INSURANCE_AUTO",
|
|
HOME_INSURANCE: "INSURANCE_HOME",
|
|
HEALTH_INSURANCE: "INSURANCE_HEALTH",
|
|
LIFE_INSURANCE: "INSURANCE_LIFE",
|
|
MORTGAGE: "LOAN",
|
|
CREDIT: "LOAN",
|
|
CARD_CREDIT: "CREDIT_CARD",
|
|
};
|
|
|
|
return aliases[value] ?? "OTHER";
|
|
}
|
|
|
|
function toStringOrNull(value: unknown): string | null {
|
|
const normalized = String(value ?? "").trim();
|
|
return normalized.length > 0 ? normalized : null;
|
|
}
|
|
|
|
function normalizeCurrency(value: unknown): string | null {
|
|
const raw = String(value ?? "")
|
|
.trim()
|
|
.toUpperCase();
|
|
if (!raw) return null;
|
|
|
|
const symbolMap: Record<string, string> = {
|
|
"€": "EUR",
|
|
$: "USD",
|
|
"£": "GBP",
|
|
};
|
|
|
|
if (symbolMap[raw]) {
|
|
return symbolMap[raw];
|
|
}
|
|
|
|
// Accept ISO-like 3-letter currencies and common BFSI currencies.
|
|
if (/^[A-Z]{3}$/.test(raw)) {
|
|
return raw;
|
|
}
|
|
|
|
return null;
|
|
}
|
|
|
|
function toDateOrNull(value: unknown): string | null {
|
|
const candidate = String(value ?? "").trim();
|
|
if (!candidate) return null;
|
|
|
|
if (/^\d{4}-\d{2}-\d{2}$/.test(candidate)) {
|
|
return candidate;
|
|
}
|
|
|
|
const parsed = new Date(candidate);
|
|
if (Number.isNaN(parsed.getTime())) return null;
|
|
|
|
return parsed.toISOString().slice(0, 10);
|
|
}
|
|
|
|
function toStringList(value: unknown): string[] {
|
|
if (!Array.isArray(value)) return [];
|
|
return value
|
|
.map((item) => String(item ?? "").trim())
|
|
.filter((item) => item.length > 0)
|
|
.slice(0, 25);
|
|
}
|
|
|
|
function parseContactInfo(input: any): ContactInfo {
|
|
return {
|
|
name: toStringOrNull(input?.name),
|
|
email: toStringOrNull(input?.email),
|
|
phone: toStringOrNull(input?.phone),
|
|
address: toStringOrNull(input?.address),
|
|
role: toStringOrNull(input?.role),
|
|
};
|
|
}
|
|
|
|
function parseKeyPeople(input: any): KeyPerson[] {
|
|
if (!Array.isArray(input)) return [];
|
|
return input.slice(0, 10).map((person) => ({
|
|
name: String(person?.name ?? "").trim() || "Unknown",
|
|
role: toStringOrNull(person?.role),
|
|
email: toStringOrNull(person?.email),
|
|
phone: toStringOrNull(person?.phone),
|
|
}));
|
|
}
|
|
|
|
function parseRelevantDates(input: any): Array<{
|
|
date: string;
|
|
description: string;
|
|
type: "EXPIRATION" | "RENEWAL" | "PAYMENT" | "REVIEW" | "OTHER";
|
|
}> {
|
|
if (!Array.isArray(input)) return [];
|
|
|
|
return input.slice(0, 15).map((dateObj) => {
|
|
const dateStr = toDateOrNull(dateObj?.date);
|
|
const type = String(dateObj?.type ?? "OTHER").toUpperCase();
|
|
const isValidType = [
|
|
"EXPIRATION",
|
|
"RENEWAL",
|
|
"PAYMENT",
|
|
"REVIEW",
|
|
"OTHER",
|
|
].includes(type);
|
|
|
|
return {
|
|
date: dateStr || "0000-01-01",
|
|
description:
|
|
String(dateObj?.description ?? "")
|
|
.trim()
|
|
.slice(0, 200) || "Important date",
|
|
type: (isValidType ? type : "OTHER") as
|
|
| "EXPIRATION"
|
|
| "RENEWAL"
|
|
| "PAYMENT"
|
|
| "REVIEW"
|
|
| "OTHER",
|
|
};
|
|
});
|
|
}
|
|
|
|
function parseExplainability(input: any): ExplainabilityItem[] {
|
|
if (!Array.isArray(input)) return [];
|
|
|
|
return input
|
|
.slice(0, 30)
|
|
.map((item) => {
|
|
const field = String(item?.field ?? "").trim();
|
|
const why = String(item?.why ?? "").trim();
|
|
const sourceSnippet = String(item?.sourceSnippet ?? "").trim();
|
|
|
|
if (!field || !why || !sourceSnippet) return null;
|
|
|
|
const confidenceRaw = Number(item?.sourceHints?.confidence);
|
|
const confidence = Number.isFinite(confidenceRaw)
|
|
? Math.max(0, Math.min(100, Math.round(confidenceRaw)))
|
|
: null;
|
|
|
|
return {
|
|
field: field.slice(0, 80),
|
|
why: why.slice(0, 260),
|
|
sourceSnippet: sourceSnippet.slice(0, 480),
|
|
sourceHints: {
|
|
page: toStringOrNull(item?.sourceHints?.page),
|
|
section: toStringOrNull(item?.sourceHints?.section),
|
|
confidence,
|
|
},
|
|
} as ExplainabilityItem;
|
|
})
|
|
.filter((value): value is ExplainabilityItem => value !== null);
|
|
}
|
|
|
|
export function normalizeAnalysis(input: any): NormalizedAnalysis {
|
|
const title = String(input?.title || "").trim() || "Untitled Contract";
|
|
const summary = String(input?.summary || "").trim();
|
|
const extractedText = String(input?.extractedText || "").trim();
|
|
|
|
if (summary.length < 10) {
|
|
throw new Error("Summary is missing or too short.");
|
|
}
|
|
|
|
if (extractedText.length < 30) {
|
|
throw new Error("Extracted text is missing or too short.");
|
|
}
|
|
|
|
const premiumValue =
|
|
input?.premium === null || input?.premium === undefined
|
|
? null
|
|
: Number(input.premium);
|
|
|
|
const premium =
|
|
premiumValue !== null && Number.isFinite(premiumValue) && premiumValue >= 0
|
|
? Number(premiumValue.toFixed(2))
|
|
: null;
|
|
|
|
const language = toStringOrNull(input?.language) || "en";
|
|
|
|
return {
|
|
title,
|
|
type: mapContractType(input?.type),
|
|
provider: toStringOrNull(input?.provider),
|
|
policyNumber: toStringOrNull(input?.policyNumber),
|
|
startDate: toDateOrNull(input?.startDate),
|
|
endDate: toDateOrNull(input?.endDate),
|
|
premium,
|
|
premiumCurrency: normalizeCurrency(input?.premiumCurrency),
|
|
summary,
|
|
keyPoints: {
|
|
guarantees: toStringList(input?.keyPoints?.guarantees),
|
|
exclusions: toStringList(input?.keyPoints?.exclusions),
|
|
franchise: toStringOrNull(input?.keyPoints?.franchise),
|
|
importantDates: toStringList(input?.keyPoints?.importantDates),
|
|
explainability: parseExplainability(input?.keyPoints?.explainability),
|
|
},
|
|
extractedText: extractedText.slice(0, 12000),
|
|
language,
|
|
keyPeople: parseKeyPeople(input?.keyPeople),
|
|
contactInfo: parseContactInfo(input?.contactInfo),
|
|
importantContacts: Array.isArray(input?.importantContacts)
|
|
? input.importantContacts
|
|
.slice(0, 10)
|
|
.map((c: any) => parseContactInfo(c))
|
|
: [],
|
|
relevantDates: parseRelevantDates(input?.relevantDates),
|
|
};
|
|
}
|