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:
@@ -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 {
|
||||
|
||||
@@ -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;
|
||||
|
||||
@@ -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;
|
||||
|
||||
Reference in New Issue
Block a user