import { Prisma, PrismaClient } from '@prisma/client'; import { analyticsHash, foldAnalytics, projectAnalytics, COUNT_KEYS, type AnalyticsCall, type AnalyticsEvent, type AnalyticsProjection } from './caller-analytics-model.js'; type Tx = Prisma.TransactionClient; const json = (value: T | string): T => typeof value === 'string' ? JSON.parse(value) as T : value; export class CallerAnalyticsStore { constructor(private readonly db: PrismaClient) {} async consume(event: AnalyticsEvent): Promise<'processed' | 'duplicate'> { // The legacy router puts diagnostic markers in customer_id for unauthenticated traffic. // Such traffic is outside the business population and must not create analysis calls. if (!await this.db.customer.findUnique({ where: { id: event.customerId }, select: { id: true } })) return 'duplicate'; return this.db.$transaction(async tx => { const callKey = analyticsHash(event.callUid); await tx.$executeRaw`INSERT IGNORE INTO caller_analysis_calls (id, payload, updated_at) VALUES (${callKey}, NULL, NOW(3))`; const rows = await tx.$queryRaw>`SELECT payload FROM caller_analysis_calls WHERE id=${callKey} FOR UPDATE`; const eventKey = analyticsHash(event.eventId); const previous = await tx.$queryRaw>`SELECT id FROM caller_analysis_events WHERE id=${eventKey}`; if (previous.length) return 'duplicate'; const old = rows[0].payload ? json(rows[0].payload) : null; const next = foldAnalytics(old, event); const before = new Map((old ? projectAnalytics(old) : []).map(row => [row.id, row])); for (const row of projectAnalytics(next)) { const prior = before.get(row.id); if (prior) await this.bucket(tx, prior, -1); await this.bucket(tx, row, 1); await this.state(tx, row); } await tx.$executeRaw`UPDATE caller_analysis_calls SET payload=${JSON.stringify(next)}, updated_at=NOW(3) WHERE id=${callKey}`; await tx.$executeRaw`INSERT INTO caller_analysis_events (id, call_key, occurred_at, payload, created_at) VALUES (${eventKey}, ${callKey}, ${new Date(event.at)}, ${JSON.stringify(event)}, NOW(3))`; return 'processed'; }, { timeout: 15000 }); } private async state(tx: Tx, row: AnalyticsProjection) { await tx.$executeRaw`INSERT INTO caller_analysis_states (id, call_key, view, started_at, customer_id, customer_gateway_id, vendor_id, vendor_gateway_id, caller, callee, city, carrier, counts, payload, updated_at) VALUES (${row.id}, ${row.callKey}, ${row.view}, ${new Date(row.startedAt)}, ${row.customerId}, ${row.customerGatewayId}, ${row.vendorId}, ${row.vendorGatewayId}, ${row.caller}, ${row.callee}, ${row.city}, ${row.carrier}, ${JSON.stringify(Object.fromEntries(COUNT_KEYS.map(k => [k, row[k]])))}, ${JSON.stringify(row.payload)}, NOW(3)) ON DUPLICATE KEY UPDATE started_at=VALUES(started_at), customer_gateway_id=VALUES(customer_gateway_id), vendor_id=VALUES(vendor_id), vendor_gateway_id=VALUES(vendor_gateway_id), caller=VALUES(caller), callee=VALUES(callee), city=VALUES(city), carrier=VALUES(carrier), counts=VALUES(counts), payload=VALUES(payload), updated_at=NOW(3)`; } private async bucket(tx: Tx, row: AnalyticsProjection, sign: number) { const minute = new Date(Math.floor(row.startedAt / 60000) * 60000); const id = analyticsHash([minute.toISOString(), row.view, row.customerId, row.customerGatewayId, row.vendorId, row.vendorGatewayId, row.caller, row.city, row.carrier]); const delta = JSON.stringify(Object.fromEntries(COUNT_KEYS.map(k => [k, sign * row[k]]))); const additions = Prisma.join(COUNT_KEYS.flatMap(k => [Prisma.sql`${`$.${k}`}`, Prisma.sql`CAST(JSON_UNQUOTE(JSON_EXTRACT(counts, ${`$.${k}`})) AS SIGNED) + ${sign * row[k]}`])); await tx.$executeRaw(Prisma.sql`INSERT INTO caller_analysis_minute_buckets (id, started_at, view, customer_id, customer_gateway_id, vendor_id, vendor_gateway_id, caller, city, carrier, counts) VALUES (${id}, ${minute}, ${row.view}, ${row.customerId}, ${row.customerGatewayId}, ${row.vendorId}, ${row.vendorGatewayId}, ${row.caller}, ${row.city}, ${row.carrier}, ${delta}) ON DUPLICATE KEY UPDATE counts=JSON_OBJECT(${additions})`); } }