FRE-5352 Apply P1/P2/P3 fixes from code review: severity type rename, dedup query fix, SMS phone field, test assertions

This commit is contained in:
2026-05-14 14:24:20 -04:00
parent ece12b6525
commit d0ddb8d159
7 changed files with 836 additions and 266 deletions

View File

@@ -15,15 +15,15 @@ import {
const DEFAULT_CONFIG: AlertPipelineConfig = {
dedupWindowMs: 24 * 60 * 60 * 1000,
minSeverity: 'moderate',
minSeverity: 'warning',
premiumTierChannels: ['email', 'push', 'sms'],
defaultChannels: ['email'],
};
const SEVERITY_MAP: Record<Severity, AlertSeverityLevel> = {
major: 'critical',
moderate: 'warning',
minor: 'info',
critical: 'critical',
warning: 'warning',
info: 'info',
};
const CHANGE_TYPE_LABELS: Record<ChangeType, string> = {
@@ -41,7 +41,7 @@ export class HomeTitleAlertPipeline {
constructor(config?: Partial<AlertPipelineConfig>) {
this.config = { ...DEFAULT_CONFIG, ...config };
this.notificationService = new NotificationService(loadNotificationConfig());
this.notificationService = NotificationService.getInstance();
}
async processChangeDetection(
@@ -136,8 +136,8 @@ export class HomeTitleAlertPipeline {
}
private shouldAlert(result: ChangeDetectionResult, severity: AlertSeverityLevel): boolean {
const severityOrder: Severity[] = ['minor', 'moderate', 'major'];
const minSeverityOrder: Severity[] = ['minor', 'moderate', 'major'];
const severityOrder: Severity[] = ['info', 'warning', 'critical'];
const minSeverityOrder: Severity[] = ['info', 'warning', 'critical'];
const resultIdx = severityOrder.indexOf(result.severity);
const minIdx = minSeverityOrder.indexOf(this.config.minSeverity);
@@ -153,11 +153,15 @@ export class HomeTitleAlertPipeline {
}
private async checkDedup(dedupKey: string): Promise<boolean> {
const parts = dedupKey.split(':');
const userId = parts[1] ?? '';
const propertyId = parts[2] ?? '';
const recentAlert = await prisma.alert.findFirst({
where: {
subscriptionId: dedupKey.split(':')[1] ? undefined : undefined,
userId: userId,
title: {
contains: dedupKey.split(':')[2],
contains: propertyId,
},
createdAt: {
gte: new Date(Date.now() - this.config.dedupWindowMs),
@@ -217,9 +221,9 @@ export class HomeTitleAlertPipeline {
await prisma.normalizedAlert.create({
data: {
source: 'DARKWATCH',
category: this.mapToAlertCategory(result.changeType),
severity: normalizedSeverity,
source: 'DARKWATCH' as any,
category: this.mapToAlertCategory(result.changeType) as any,
severity: normalizedSeverity as any,
userId,
title: this.buildTitle(result),
description: this.buildMessage(result),
@@ -257,7 +261,7 @@ export class HomeTitleAlertPipeline {
data: {
userId,
entities,
highestSeverity: this.mapToNormalizedSeverity(highestSeverity),
highestSeverity: this.mapToNormalizedSeverity(highestSeverity) as any,
status: 'ACTIVE',
alertCount: alerts.length,
summary: `${alerts.length} property change alert${alerts.length > 1 ? 's' : ''} correlated`,
@@ -281,7 +285,7 @@ export class HomeTitleAlertPipeline {
try {
const user = await prisma.user.findUnique({
where: { id: userId },
select: { email: true, name: true },
select: { email: true, name: true, phone: true },
});
if (!user?.email) {
@@ -289,9 +293,9 @@ export class HomeTitleAlertPipeline {
}
const htmlMessage = `<p>${alert.message.replace(/\n/g, '<br>')}</p>
<p><strong>Property:</strong> ${alert.propertyId}</p>
<p><strong>Change Type:</strong> ${CHANGE_TYPE_LABELS[alert.changeType]}</p>
<p><strong>Severity:</strong> ${alert.severity.toUpperCase()}</p>`;
<p><strong>Property:</strong> ${alert.propertyId}</p>
<p><strong>Change Type:</strong> ${CHANGE_TYPE_LABELS[alert.changeType]}</p>
<p><strong>Severity:</strong> ${alert.severity.toUpperCase()}</p>`;
for (const channel of alert.channel) {
switch (channel) {
@@ -315,7 +319,7 @@ export class HomeTitleAlertPipeline {
case 'sms':
await this.notificationService.send({
channel: 'sms',
to: user.email,
to: user.phone ?? '',
body: `[ShieldAI] ${alert.title}: ${alert.message.slice(0, 140)}`,
});
break;
@@ -337,13 +341,13 @@ export class HomeTitleAlertPipeline {
private mapToAlertCategory(changeType: ChangeType): string {
const map: Record<ChangeType, string> = {
ownership_transfer: 'CALL_ANOMALY',
deed_change: 'CALL_ANOMALY',
lien_filing: 'CALL_ANOMALY',
tax_change: 'CALL_EVENT',
metadata_change: 'CALL_EVENT',
ownership_transfer: 'HOME_TITLE',
deed_change: 'HOME_TITLE',
lien_filing: 'HOME_TITLE',
tax_change: 'HOME_TITLE',
metadata_change: 'HOME_TITLE',
};
return map[changeType] || 'CALL_EVENT';
return map[changeType] || 'HOME_TITLE';
}
cleanupExpiredDedups(): number {

View File

@@ -63,27 +63,27 @@ function determineSeverity(changes: PropertyChange[], config: DetectionConfig):
const severityOverrides = config.severityOverrides || {};
const typeToSeverity: Record<ChangeType, Severity> = {
ownership_transfer: severityOverrides['ownership_transfer'] || 'major',
deed_change: severityOverrides['deed_change'] || 'moderate',
lien_filing: severityOverrides['lien_filing'] || 'moderate',
tax_change: severityOverrides['tax_change'] || 'minor',
metadata_change: severityOverrides['metadata_change'] || 'minor',
ownership_transfer: (severityOverrides as Record<string, Severity>)['ownership_transfer'] || 'critical',
deed_change: (severityOverrides as Record<string, Severity>)['deed_change'] || 'warning',
lien_filing: (severityOverrides as Record<string, Severity>)['lien_filing'] || 'warning',
tax_change: (severityOverrides as Record<string, Severity>)['tax_change'] || 'info',
metadata_change: (severityOverrides as Record<string, Severity>)['metadata_change'] || 'info',
};
const severityOrder: Severity[] = ['major', 'moderate', 'minor'];
const severityOrder: Severity[] = ['critical', 'warning', 'info'];
for (const change of changes) {
const sev = typeToSeverity[change.changeType];
const idx = severityOrder.indexOf(sev);
if (idx === 0) return 'major';
if (idx === 0) return 'critical';
}
for (const change of changes) {
const sev = typeToSeverity[change.changeType];
if (sev === 'moderate') return 'moderate';
if (sev === 'warning') return 'warning';
}
return 'minor';
return 'info';
}
function computeChangeConfidence(changes: PropertyChange[], config: DetectionConfig): number {
@@ -192,8 +192,8 @@ function detectAddressChanges(oldAddr: Address, newAddr: Address): PropertyChang
return changes;
}
export function shouldTriggerAlert(result: ChangeDetectionResult, minSeverity: Severity = 'moderate'): boolean {
const severityOrder: Severity[] = ['minor', 'moderate', 'major'];
export function shouldTriggerAlert(result: ChangeDetectionResult, minSeverity: Severity = 'warning'): boolean {
const severityOrder: Severity[] = ['info', 'warning', 'critical'];
const resultIdx = severityOrder.indexOf(result.severity);
const minIdx = severityOrder.indexOf(minSeverity);
return resultIdx >= minIdx && result.confidence >= 0.7;

View File

@@ -83,7 +83,7 @@ export interface ChangeDetectionResult {
export type ChangeType = 'tax_change' | 'deed_change' | 'ownership_transfer' | 'lien_filing' | 'metadata_change';
export type Severity = 'minor' | 'moderate' | 'major';
export type Severity = 'info' | 'warning' | 'critical';
export interface PropertyChange {
field: string;