Spaces:
Sleeping
Sleeping
File size: 7,024 Bytes
8e6c6e4 e11f6a9 8e6c6e4 e11f6a9 8e6c6e4 e11f6a9 8e6c6e4 | 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 127 128 129 130 131 132 133 134 135 136 137 138 139 140 141 142 143 144 145 146 147 148 149 150 151 152 153 154 155 156 157 158 159 160 161 162 163 164 165 166 167 168 169 170 171 172 173 174 175 176 177 178 179 180 181 182 183 184 185 186 187 188 | // βββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββ
// MEMEX β Data Transform Utilities
// βββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββ
import { GraphNode, GraphEdge, EntityNodeType, EdgeType } from './types';
// ββ Scenario β Cytoscape Graph Elements ββββββββββββββββββββββ
interface ScenarioData {
customer_profiles?: Record<string, Record<string, unknown>>;
network_graph?: Record<string, { connections?: Array<Record<string, unknown>>; entity_name?: string }>;
transactions?: Array<Record<string, unknown>>;
watchlist_results?: Record<string, Record<string, unknown>>;
}
function inferNodeType(profile: Record<string, unknown>): EntityNodeType {
const type = (profile.type as string || '').toLowerCase();
const name = (profile.name as string || '').toLowerCase();
const notes = (profile.notes as string || '').toLowerCase();
if (type === 'individual' || profile.date_of_birth || profile.occupation || profile.nationality) return 'person';
if (notes.includes('shell') || notes.includes('no employees') || notes.includes('no commercial')) return 'shell';
if (type === 'business' || profile.business_type || profile.directors) return 'company';
if (name.includes('account') || name.includes('acc-')) return 'account';
return 'company';
}
function inferRisk(profile: Record<string, unknown>, watchlist?: Record<string, Record<string, unknown>>): number {
let score = 30;
const riskRating = (profile.risk_rating as string || '').toLowerCase();
if (riskRating === 'high') score += 40;
else if (riskRating === 'medium') score += 20;
if (profile.pep_status) score += 25;
const name = profile.name as string || '';
if (watchlist && watchlist[name]?.hit) score += 20;
const notes = (profile.notes as string || '').toLowerCase();
if (notes.includes('shell') || notes.includes('no employees')) score += 15;
if (notes.includes('shared') || notes.includes('nominee')) score += 10;
return Math.min(score, 100);
}
export function scenarioToGraph(scenario: ScenarioData): { nodes: GraphNode[]; edges: GraphEdge[] } {
const nodes: GraphNode[] = [];
const edges: GraphEdge[] = [];
const nodeIds = new Set<string>();
const profiles = scenario.customer_profiles || {};
const network = scenario.network_graph || {};
const watchlist = scenario.watchlist_results || {};
// Build nodes from profiles
for (const [id, profile] of Object.entries(profiles)) {
if (nodeIds.has(id)) continue;
nodeIds.add(id);
nodes.push({
id,
label: (profile.name as string) || id,
type: inferNodeType(profile),
risk: inferRisk(profile, watchlist),
jurisdiction: (profile.nationality as string) || (profile.jurisdiction as string),
flagged: !!(watchlist[(profile.name as string)]?.hit || watchlist[id]?.hit),
pep: !!profile.pep_status,
meta: profile,
});
}
// Build edges from network graph
const edgeIds = new Set<string>();
for (const [entityId, node] of Object.entries(network)) {
if (!node.connections) continue;
// Ensure source node exists
if (!nodeIds.has(entityId)) {
nodeIds.add(entityId);
nodes.push({
id: entityId,
label: entityId, // fallback
type: entityId.startsWith('CUST') ? 'person' : 'company',
risk: 40,
flagged: false,
pep: false,
});
}
for (const conn of node.connections) {
const targetId = (conn.entity_id as string) || (conn.entity as string) || '';
if (!targetId) continue;
const edgeId = `e-${entityId}-${targetId}`;
const reverseId = `e-${targetId}-${entityId}`;
if (edgeIds.has(edgeId) || edgeIds.has(reverseId)) continue;
edgeIds.add(edgeId);
// Ensure target node exists
if (!nodeIds.has(targetId)) {
nodeIds.add(targetId);
const targetName = (conn.entity_name as string) || targetId;
const isPep = !!(conn.pep || (conn.director as string || '').includes('PEP'));
nodes.push({
id: targetId,
label: targetName,
type: isPep ? 'person' : 'company',
risk: isPep ? 80 : 40,
flagged: isPep,
pep: isPep,
});
}
const rel = (conn.relationship as string || '').toLowerCase();
let edgeType: EdgeType = 'association';
if (rel.includes('wire') || rel.includes('transfer') || rel.includes('inbound') || rel.includes('outbound')) edgeType = 'transaction';
else if (rel.includes('director') || rel.includes('owns') || rel.includes('owner')) edgeType = 'ownership';
else if (rel.includes('shared') || rel.includes('address')) edgeType = 'suspicious';
edges.push({
id: edgeId,
source: entityId,
target: targetId,
type: edgeType,
label: conn.relationship as string,
amount: conn.amount as number | undefined,
suspicious: edgeType === 'suspicious' || !!(conn.note as string || '').includes('SHARED'),
});
}
}
// Build edges from transactions
const txns = scenario.transactions || [];
for (const txn of txns) {
const from = (txn.from as string) || (txn.sender_id as string) || '';
const to = (txn.to as string) || (txn.receiver_id as string) || '';
if (!from || !to) continue;
const edgeId = `txn-${txn.transaction_id}`;
if (edgeIds.has(edgeId)) continue;
edgeIds.add(edgeId);
// Ensure nodes exist for external entities
for (const [eid, ename] of [[from, from], [to, to]]) {
if (!nodeIds.has(eid)) {
nodeIds.add(eid);
nodes.push({
id: eid,
label: ename,
type: (ename as string).includes('ENT') ? 'company' : 'asset',
risk: 20,
});
}
}
edges.push({
id: edgeId,
source: from,
target: to,
type: 'transaction',
label: `$${formatCompact(txn.amount as number)}`,
amount: txn.amount as number,
suspicious: (txn.amount as number) > 100000,
});
}
return { nodes, edges };
}
function formatCompact(n: number): string {
if (!n) return '0';
if (n >= 1e9) return `${(n / 1e9).toFixed(1)}B`;
if (n >= 1e6) return `${(n / 1e6).toFixed(0)}M`;
if (n >= 1e3) return `${(n / 1e3).toFixed(0)}K`;
return String(n);
}
// ββ Risk Color βββββββββββββββββββββββββββββββββββββββββββββββ
export function riskToColor(score: number): string {
if (score >= 80) return '#E11D48';
if (score >= 60) return '#EA580C';
if (score >= 40) return '#EAB308';
return '#22C55E';
}
export function nodeSize(risk: number): number {
return 24 + (risk / 100) * 24;
}
|