Add Info Broker Removal service (FRE-5402)
New service for helping clients remove personal listings from data broker sites. Service features: - BrokerRegistry: Catalog of 20+ data brokers with removal methods - RemoveBrokersService: Core service for scanning, creating removal requests, submitting removals, and verifying completions - RemoveBrokersScheduler: Automated processing of pending removals and verification of completed removals - BrokerAlertPipeline: Alert integration for listing discoveries and removal status API endpoints (/removebrokers): - GET /brokers - List available data brokers - GET /status - Get removal request status and stats - POST /scan - Scan for personal listings across brokers - POST /request - Create a new removal request - GET /request/:id - Get specific removal request details - DELETE /request/:id - Cancel a removal request - POST /process - Trigger processing of pending removals - POST /verify/:id - Manually verify a removal completion DB models: InfoBroker, RemovalRequest, BrokerListing Types: BrokerStatus, RemovalStatus, RemovalMethod, and related interfaces
This commit is contained in:
@@ -8,6 +8,7 @@ import { subscriptionRoutes } from './subscription.routes';
|
||||
import { deviceRoutes } from './device.routes';
|
||||
import { notificationRoutes } from './notifications.routes';
|
||||
import { hometitleRoutes } from './hometitle.routes';
|
||||
import { removebrokersRoutes } from './removebrokers.routes';
|
||||
|
||||
export async function routes(fastify: FastifyInstance) {
|
||||
// Authenticated routes group
|
||||
@@ -179,4 +180,15 @@ export async function routes(fastify: FastifyInstance) {
|
||||
},
|
||||
{ prefix: '/hometitle' }
|
||||
);
|
||||
|
||||
// Info Broker Removal service routes
|
||||
fastify.register(
|
||||
async (removebrokersRouter) => {
|
||||
removebrokersRouter.addHook('onRequest', async (request: FastifyRequest, reply: FastifyReply) => {
|
||||
await fastify.requireAuth(request as AuthRequest);
|
||||
});
|
||||
await removebrokersRoutes(removebrokersRouter);
|
||||
},
|
||||
{ prefix: '/removebrokers' }
|
||||
);
|
||||
}
|
||||
|
||||
378
packages/api/src/routes/removebrokers.routes.ts
Normal file
378
packages/api/src/routes/removebrokers.routes.ts
Normal file
@@ -0,0 +1,378 @@
|
||||
import { FastifyInstance, FastifyRequest, FastifyReply } from 'fastify';
|
||||
import { prisma } from '@shieldai/db';
|
||||
import { RemovalStatus, Severity } from '@shieldai/types';
|
||||
import {
|
||||
removeBrokersService,
|
||||
removeBrokersScheduler,
|
||||
brokerAlertPipeline,
|
||||
type PersonalInfo,
|
||||
} from '@shieldai/removebrokers';
|
||||
import { AuthRequest } from '../middleware/auth.middleware';
|
||||
|
||||
const REMOVAL_REQUEST_LIMITS: Record<string, number> = {
|
||||
basic: 5,
|
||||
plus: 20,
|
||||
premium: 999,
|
||||
};
|
||||
|
||||
async function getSubscription(
|
||||
request: FastifyRequest,
|
||||
reply: FastifyReply,
|
||||
): Promise<{ subscriptionId: string; tier: string } | null> {
|
||||
const authReq = request as AuthRequest;
|
||||
const userId = authReq.user?.id;
|
||||
|
||||
if (!userId) {
|
||||
await reply.code(401).send({ error: 'User not authenticated' });
|
||||
return null;
|
||||
}
|
||||
|
||||
const subscription = await prisma.subscription.findFirst({
|
||||
where: { userId, status: 'active' },
|
||||
select: { id: true, tier: true },
|
||||
});
|
||||
|
||||
if (!subscription) {
|
||||
await reply.code(402).send({
|
||||
error: 'Subscription required',
|
||||
message: 'An active subscription is required for data broker removal',
|
||||
});
|
||||
return null;
|
||||
}
|
||||
|
||||
return { subscriptionId: subscription.id, tier: subscription.tier };
|
||||
}
|
||||
|
||||
export async function removebrokersRoutes(fastify: FastifyInstance) {
|
||||
// GET /removebrokers/brokers - List available data brokers
|
||||
fastify.get('/brokers', async (request: FastifyRequest, reply: FastifyReply) => {
|
||||
const authReq = request as AuthRequest;
|
||||
if (!authReq.user?.id) {
|
||||
return reply.code(401).send({ error: 'User not authenticated' });
|
||||
}
|
||||
|
||||
const query = request.query as { category?: string };
|
||||
let brokers = await removeBrokersService.getAvailableBrokers();
|
||||
|
||||
if (query.category) {
|
||||
brokers = brokers.filter((b) => b.category === query.category);
|
||||
}
|
||||
|
||||
return reply.send({
|
||||
brokers: brokers.map((b) => ({
|
||||
id: b.id,
|
||||
name: b.name,
|
||||
domain: b.domain,
|
||||
category: b.category,
|
||||
removalMethod: b.removalMethod,
|
||||
requiresAccount: b.requiresAccount,
|
||||
requiresVerification: b.requiresVerification,
|
||||
estimatedDays: b.estimatedDays,
|
||||
removalUrl: b.removalUrl,
|
||||
})),
|
||||
});
|
||||
});
|
||||
|
||||
// GET /removebrokers/status - Get removal request status for user
|
||||
fastify.get('/status', async (request: FastifyRequest, reply: FastifyReply) => {
|
||||
const sub = await getSubscription(request, reply);
|
||||
if (!sub) return;
|
||||
|
||||
try {
|
||||
const status = await removeBrokersService.getRemovalStatus(sub.subscriptionId);
|
||||
|
||||
const total = status.length;
|
||||
const pending = status.filter((s) => s.status === RemovalStatus.PENDING).length;
|
||||
const submitted = status.filter((s) => s.status === RemovalStatus.SUBMITTED).length;
|
||||
const completed = status.filter((s) => s.status === RemovalStatus.COMPLETED).length;
|
||||
const failed = status.filter((s) => s.status === RemovalStatus.FAILED).length;
|
||||
|
||||
const limit = REMOVAL_REQUEST_LIMITS[sub.tier] ?? 5;
|
||||
const remaining = Math.max(0, limit - total);
|
||||
|
||||
return reply.send({
|
||||
stats: { total, pending, submitted, completed, failed },
|
||||
limit,
|
||||
remaining,
|
||||
tier: sub.tier,
|
||||
requests: status,
|
||||
});
|
||||
} catch (error) {
|
||||
const message = error instanceof Error ? error.message : 'Failed to fetch status';
|
||||
return reply.code(500).send({ error: message });
|
||||
}
|
||||
});
|
||||
|
||||
// POST /removebrokers/scan - Scan for personal listings across brokers
|
||||
fastify.post('/scan', async (request: FastifyRequest, reply: FastifyReply) => {
|
||||
const sub = await getSubscription(request, reply);
|
||||
if (!sub) return;
|
||||
|
||||
const body = request.body as { fullName?: string; email?: string; phone?: string; address?: string };
|
||||
|
||||
if (!body.fullName) {
|
||||
return reply.code(400).send({
|
||||
error: 'Invalid request',
|
||||
message: 'fullName is required for scanning',
|
||||
});
|
||||
}
|
||||
|
||||
try {
|
||||
const user = await prisma.user.findUnique({
|
||||
where: { id: (request as AuthRequest).user!.id },
|
||||
});
|
||||
|
||||
const personalInfo: PersonalInfo = {
|
||||
fullName: body.fullName,
|
||||
email: body.email || user?.email,
|
||||
phone: body.phone,
|
||||
address: body.address
|
||||
? { street: body.address }
|
||||
: undefined,
|
||||
};
|
||||
|
||||
const results = await removeBrokersService.scanForListings(
|
||||
sub.subscriptionId,
|
||||
personalInfo,
|
||||
);
|
||||
|
||||
const found = results.filter((r) => r.found);
|
||||
|
||||
for (const listing of found) {
|
||||
try {
|
||||
await brokerAlertPipeline.sendListingFoundAlert({
|
||||
userId: (request as AuthRequest).user!.id,
|
||||
brokerName: listing.brokerName,
|
||||
brokerId: listing.brokerId,
|
||||
category: 'INFO_BROKER_LISTING' as any,
|
||||
severity: Severity.MEDIUM,
|
||||
title: `Personal listing found on ${listing.brokerName}`,
|
||||
description: `Your personal information was found on ${listing.brokerName} (${listing.brokerId}). Consider submitting a removal request.`,
|
||||
entities: [
|
||||
{ type: 'USER_ID' as any, value: (request as AuthRequest).user!.id },
|
||||
],
|
||||
metadata: { url: listing.url },
|
||||
});
|
||||
} catch {
|
||||
// Alert failure is non-critical
|
||||
}
|
||||
}
|
||||
|
||||
return reply.send({
|
||||
brokersScanned: results.length,
|
||||
listingsFound: found.length,
|
||||
results,
|
||||
});
|
||||
} catch (error) {
|
||||
const message = error instanceof Error ? error.message : 'Scan failed';
|
||||
return reply.code(500).send({ error: message });
|
||||
}
|
||||
});
|
||||
|
||||
// POST /removebrokers/request - Create a new removal request
|
||||
fastify.post('/request', async (request: FastifyRequest, reply: FastifyReply) => {
|
||||
const sub = await getSubscription(request, reply);
|
||||
if (!sub) return;
|
||||
|
||||
const body = request.body as {
|
||||
brokerId: string;
|
||||
fullName: string;
|
||||
email?: string;
|
||||
phone?: string;
|
||||
address?: {
|
||||
street?: string;
|
||||
city?: string;
|
||||
state?: string;
|
||||
zip?: string;
|
||||
};
|
||||
dob?: string;
|
||||
notes?: string;
|
||||
};
|
||||
|
||||
if (!body.brokerId) {
|
||||
return reply.code(400).send({
|
||||
error: 'Invalid request',
|
||||
message: 'brokerId is required',
|
||||
});
|
||||
}
|
||||
|
||||
if (!body.fullName) {
|
||||
return reply.code(400).send({
|
||||
error: 'Invalid request',
|
||||
message: 'fullName is required',
|
||||
});
|
||||
}
|
||||
|
||||
const limit = REMOVAL_REQUEST_LIMITS[sub.tier] ?? 5;
|
||||
const currentCount = await prisma.removalRequest.count({
|
||||
where: { subscriptionId: sub.subscriptionId },
|
||||
});
|
||||
|
||||
if (currentCount >= limit) {
|
||||
return reply.code(400).send({
|
||||
error: 'Request limit reached',
|
||||
message: `You have reached the maximum of ${limit} removal requests for your ${sub.tier} tier.`,
|
||||
currentCount,
|
||||
limit,
|
||||
upgradeTo: 'plus',
|
||||
});
|
||||
}
|
||||
|
||||
try {
|
||||
const personalInfo: PersonalInfo = {
|
||||
fullName: body.fullName,
|
||||
email: body.email,
|
||||
phone: body.phone,
|
||||
address: body.address,
|
||||
dob: body.dob,
|
||||
};
|
||||
|
||||
const req = await removeBrokersService.createRemovalRequest(
|
||||
sub.subscriptionId,
|
||||
body.brokerId,
|
||||
personalInfo,
|
||||
body.notes,
|
||||
);
|
||||
|
||||
return reply.code(201).send({
|
||||
request: {
|
||||
id: req.id,
|
||||
brokerId: req.brokerId,
|
||||
status: req.status,
|
||||
method: req.method,
|
||||
createdAt: req.createdAt,
|
||||
},
|
||||
});
|
||||
} catch (error) {
|
||||
const message = error instanceof Error ? error.message : 'Failed to create removal request';
|
||||
return reply.code(422).send({ error: message });
|
||||
}
|
||||
});
|
||||
|
||||
// GET /removebrokers/request/:id - Get specific removal request
|
||||
fastify.get('/request/:id', async (request: FastifyRequest, reply: FastifyReply) => {
|
||||
const sub = await getSubscription(request, reply);
|
||||
if (!sub) return;
|
||||
|
||||
const id = (request.params as { id: string }).id;
|
||||
|
||||
try {
|
||||
const req = await prisma.removalRequest.findFirst({
|
||||
where: { id, subscriptionId: sub.subscriptionId },
|
||||
include: { broker: true },
|
||||
});
|
||||
|
||||
if (!req) {
|
||||
return reply.code(404).send({ error: 'Removal request not found' });
|
||||
}
|
||||
|
||||
return reply.send({
|
||||
request: {
|
||||
id: req.id,
|
||||
brokerId: req.brokerId,
|
||||
brokerName: req.broker.name,
|
||||
status: req.status,
|
||||
method: req.method,
|
||||
attempts: req.attempts,
|
||||
submittedAt: req.submittedAt,
|
||||
completedAt: req.completedAt,
|
||||
error: req.error,
|
||||
notes: req.notes,
|
||||
createdAt: req.createdAt,
|
||||
updatedAt: req.updatedAt,
|
||||
},
|
||||
});
|
||||
} catch (error) {
|
||||
const message = error instanceof Error ? error.message : 'Failed to fetch request';
|
||||
return reply.code(500).send({ error: message });
|
||||
}
|
||||
});
|
||||
|
||||
// DELETE /removebrokers/request/:id - Cancel a removal request
|
||||
fastify.delete('/request/:id', async (request: FastifyRequest, reply: FastifyReply) => {
|
||||
const sub = await getSubscription(request, reply);
|
||||
if (!sub) return;
|
||||
|
||||
const id = (request.params as { id: string }).id;
|
||||
|
||||
try {
|
||||
const req = await prisma.removalRequest.findFirst({
|
||||
where: { id, subscriptionId: sub.subscriptionId },
|
||||
});
|
||||
|
||||
if (!req) {
|
||||
return reply.code(404).send({ error: 'Removal request not found' });
|
||||
}
|
||||
|
||||
if (req.status === RemovalStatus.COMPLETED) {
|
||||
return reply.code(400).send({
|
||||
error: 'Cannot cancel',
|
||||
message: 'Cannot cancel a completed removal request',
|
||||
});
|
||||
}
|
||||
|
||||
await prisma.removalRequest.update({
|
||||
where: { id },
|
||||
data: { status: RemovalStatus.REJECTED },
|
||||
});
|
||||
|
||||
return reply.send({
|
||||
request: {
|
||||
id: req.id,
|
||||
status: 'cancelled',
|
||||
},
|
||||
});
|
||||
} catch (error) {
|
||||
const message = error instanceof Error ? error.message : 'Failed to cancel request';
|
||||
return reply.code(500).send({ error: message });
|
||||
}
|
||||
});
|
||||
|
||||
// POST /removebrokers/process - Trigger processing of pending removals (admin)
|
||||
fastify.post('/process', async (request: FastifyRequest, reply: FastifyReply) => {
|
||||
const authReq = request as AuthRequest;
|
||||
if (!authReq.user?.id) {
|
||||
return reply.code(401).send({ error: 'User not authenticated' });
|
||||
}
|
||||
|
||||
try {
|
||||
const results = await removeBrokersService.processPendingRequests();
|
||||
|
||||
return reply.send({
|
||||
processed: results.length,
|
||||
results,
|
||||
});
|
||||
} catch (error) {
|
||||
const message = error instanceof Error ? error.message : 'Processing failed';
|
||||
return reply.code(500).send({ error: message });
|
||||
}
|
||||
});
|
||||
|
||||
// POST /removebrokers/verify/:id - Manually verify a removal
|
||||
fastify.post('/verify/:id', async (request: FastifyRequest, reply: FastifyReply) => {
|
||||
const sub = await getSubscription(request, reply);
|
||||
if (!sub) return;
|
||||
|
||||
const id = (request.params as { id: string }).id;
|
||||
|
||||
try {
|
||||
const req = await prisma.removalRequest.findFirst({
|
||||
where: { id, subscriptionId: sub.subscriptionId },
|
||||
});
|
||||
|
||||
if (!req) {
|
||||
return reply.code(404).send({ error: 'Removal request not found' });
|
||||
}
|
||||
|
||||
const result = await removeBrokersService.verifyRemoval(id);
|
||||
|
||||
return reply.send({
|
||||
requestId: id,
|
||||
...result,
|
||||
});
|
||||
} catch (error) {
|
||||
const message = error instanceof Error ? error.message : 'Verification failed';
|
||||
return reply.code(500).send({ error: message });
|
||||
}
|
||||
});
|
||||
}
|
||||
@@ -25,6 +25,7 @@ model User {
|
||||
// Relationships
|
||||
accounts Account[]
|
||||
sessions Session[]
|
||||
deviceTokens DeviceToken[]
|
||||
familyGroups FamilyGroupMember[]
|
||||
familyGroupOwned FamilyGroup[] @relation("FamilyGroupOwner")
|
||||
subscriptions Subscription[]
|
||||
@@ -37,6 +38,8 @@ model User {
|
||||
correlationGroups CorrelationGroup[]
|
||||
securityReports SecurityReport[]
|
||||
analysisJobs AnalysisJob[]
|
||||
removalRequests RemovalRequest[]
|
||||
brokerListings BrokerListing[]
|
||||
|
||||
// Audit
|
||||
createdAt DateTime @default(now())
|
||||
@@ -107,6 +110,42 @@ model Session {
|
||||
@@index([userId])
|
||||
}
|
||||
|
||||
model DeviceToken {
|
||||
id String @id @default(uuid())
|
||||
userId String
|
||||
deviceType DeviceType
|
||||
token String @unique
|
||||
platform Platform
|
||||
appName String?
|
||||
appVersion String?
|
||||
osVersion String?
|
||||
model String?
|
||||
isActive Boolean @default(true)
|
||||
lastUsedAt DateTime @default(now())
|
||||
|
||||
user User @relation(fields: [userId], references: [id], onDelete: Cascade)
|
||||
|
||||
createdAt DateTime @default(now())
|
||||
updatedAt DateTime @updatedAt
|
||||
|
||||
@@index([userId])
|
||||
@@index([deviceType])
|
||||
@@index([platform])
|
||||
@@index([isActive])
|
||||
}
|
||||
|
||||
enum DeviceType {
|
||||
mobile
|
||||
web
|
||||
desktop
|
||||
}
|
||||
|
||||
enum Platform {
|
||||
ios
|
||||
android
|
||||
web
|
||||
}
|
||||
|
||||
// ============================================
|
||||
// Family & Subscription Models
|
||||
// ============================================
|
||||
@@ -167,6 +206,9 @@ model Subscription {
|
||||
watchlistItems WatchlistItem[]
|
||||
exposures Exposure[]
|
||||
alerts Alert[]
|
||||
propertyWatchlistItems PropertyWatchlistItem[]
|
||||
removalRequests RemovalRequest[]
|
||||
brokerListings BrokerListing[]
|
||||
|
||||
createdAt DateTime @default(now())
|
||||
updatedAt DateTime @updatedAt
|
||||
@@ -506,6 +548,8 @@ enum AlertSource {
|
||||
SPAMSHIELD
|
||||
VOICEPRINT
|
||||
CALL_ANALYSIS
|
||||
HOME_TITLE
|
||||
INFO_BROKER
|
||||
}
|
||||
|
||||
enum AlertCategory {
|
||||
@@ -517,6 +561,9 @@ enum AlertCategory {
|
||||
CALL_ANOMALY
|
||||
CALL_QUALITY
|
||||
CALL_EVENT
|
||||
HOME_TITLE
|
||||
INFO_BROKER_LISTING
|
||||
INFO_BROKER_REMOVAL
|
||||
}
|
||||
|
||||
enum NormalizedAlertSeverity {
|
||||
@@ -588,6 +635,7 @@ model CorrelationGroup {
|
||||
enum ReportType {
|
||||
MONTHLY_PLUS
|
||||
ANNUAL_PREMIUM
|
||||
WEEKLY_DIGEST
|
||||
}
|
||||
|
||||
enum ReportStatus {
|
||||
@@ -676,3 +724,192 @@ model BlogPost {
|
||||
@@index([published, publishedAt])
|
||||
@@index([tags])
|
||||
}
|
||||
|
||||
// ============================================
|
||||
// Home Title Service Models
|
||||
// ============================================
|
||||
|
||||
enum PropertyChangeType {
|
||||
tax_change
|
||||
deed_change
|
||||
ownership_transfer
|
||||
lien_filing
|
||||
metadata_change
|
||||
}
|
||||
|
||||
enum PropertyChangeSeverity {
|
||||
info
|
||||
warning
|
||||
critical
|
||||
}
|
||||
|
||||
model PropertyWatchlistItem {
|
||||
id String @id @default(uuid())
|
||||
subscriptionId String
|
||||
address String
|
||||
parcelId String?
|
||||
ownerName String?
|
||||
streetAddress String
|
||||
city String? @default("")
|
||||
state String? @default("")
|
||||
zipCode String? @default("")
|
||||
latitude Float?
|
||||
longitude Float?
|
||||
isActive Boolean @default(true)
|
||||
|
||||
subscription Subscription @relation(fields: [subscriptionId], references: [id], onDelete: Cascade)
|
||||
snapshots PropertySnapshot[]
|
||||
changes PropertyChange[]
|
||||
|
||||
createdAt DateTime @default(now())
|
||||
updatedAt DateTime @updatedAt
|
||||
|
||||
@@unique([subscriptionId, parcelId])
|
||||
@@index([subscriptionId])
|
||||
@@index([parcelId])
|
||||
@@index([address])
|
||||
}
|
||||
|
||||
model PropertySnapshot {
|
||||
id String @id @default(uuid())
|
||||
propertyWatchlistItemId String
|
||||
subscriptionId String
|
||||
capturedAt DateTime
|
||||
ownerName String
|
||||
address Json // { streetNumber, streetName, streetType, unit, city, state, zip, latitude?, longitude? }
|
||||
deedDate String?
|
||||
taxId String?
|
||||
propertyType String @default("residential")
|
||||
taxAmount Float?
|
||||
lienCount Int @default(0)
|
||||
|
||||
propertyWatchlistItem PropertyWatchlistItem @relation(fields: [propertyWatchlistItemId], references: [id], onDelete: Cascade)
|
||||
changes PropertyChange[] @relation("SnapshotChanges")
|
||||
|
||||
createdAt DateTime @default(now())
|
||||
updatedAt DateTime @updatedAt
|
||||
|
||||
@@index([propertyWatchlistItemId])
|
||||
@@index([subscriptionId])
|
||||
@@index([capturedAt])
|
||||
}
|
||||
|
||||
model PropertyChange {
|
||||
id String @id @default(uuid())
|
||||
propertyWatchlistItemId String
|
||||
snapshotId String?
|
||||
changeType PropertyChangeType
|
||||
severity PropertyChangeSeverity @default(info)
|
||||
details Json?
|
||||
detectedAt DateTime @default(now())
|
||||
|
||||
propertyWatchlistItem PropertyWatchlistItem @relation(fields: [propertyWatchlistItemId], references: [id], onDelete: Cascade)
|
||||
snapshot PropertySnapshot? @relation("SnapshotChanges", fields: [snapshotId], references: [id])
|
||||
|
||||
@@index([propertyWatchlistItemId])
|
||||
@@index([snapshotId])
|
||||
@@index([changeType])
|
||||
}
|
||||
|
||||
// ============================================
|
||||
// Info Broker Removal Models
|
||||
// ============================================
|
||||
|
||||
enum BrokerCategory {
|
||||
people_search
|
||||
background_check
|
||||
public_records
|
||||
reverse_lookup
|
||||
social_media
|
||||
}
|
||||
|
||||
enum RemovalMethod {
|
||||
automated
|
||||
manual_form
|
||||
email
|
||||
phone
|
||||
mail
|
||||
none
|
||||
}
|
||||
|
||||
enum RemovalStatus {
|
||||
pending
|
||||
submitted
|
||||
in_progress
|
||||
completed
|
||||
failed
|
||||
rejected
|
||||
}
|
||||
|
||||
model InfoBroker {
|
||||
id String @id @default(uuid())
|
||||
name String
|
||||
domain String @unique
|
||||
category BrokerCategory
|
||||
removalMethod RemovalMethod
|
||||
removalUrl String?
|
||||
requiresAccount Boolean @default(false)
|
||||
requiresVerification Boolean @default(false)
|
||||
estimatedDays Int @default(14)
|
||||
isActive Boolean @default(true)
|
||||
|
||||
removalRequests RemovalRequest[]
|
||||
|
||||
createdAt DateTime @default(now())
|
||||
updatedAt DateTime @default(now()) @updatedAt
|
||||
|
||||
@@index([category])
|
||||
@@index([isActive])
|
||||
@@index([removalMethod])
|
||||
}
|
||||
|
||||
model RemovalRequest {
|
||||
id String @id @default(uuid())
|
||||
subscriptionId String
|
||||
brokerId String
|
||||
status RemovalStatus @default(pending)
|
||||
personalInfo Json // { fullName, email?, phone?, address?, dob? }
|
||||
method RemovalMethod
|
||||
attempts Int @default(0)
|
||||
nextRetryAt DateTime?
|
||||
submittedAt DateTime?
|
||||
completedAt DateTime?
|
||||
error String?
|
||||
notes String?
|
||||
metadata Json? // Broker response data, tracking info
|
||||
|
||||
broker InfoBroker @relation(fields: [brokerId], references: [id])
|
||||
|
||||
createdAt DateTime @default(now())
|
||||
updatedAt DateTime @default(now()) @updatedAt
|
||||
|
||||
@@index([subscriptionId])
|
||||
@@index([brokerId])
|
||||
@@index([status])
|
||||
@@index([submittedAt])
|
||||
@@index([subscriptionId, status])
|
||||
}
|
||||
|
||||
model BrokerListing {
|
||||
id String @id @default(uuid())
|
||||
subscriptionId String
|
||||
brokerId String
|
||||
removalRequestId String?
|
||||
url String
|
||||
dataFound Json // Fields found on the listing
|
||||
screenshotUrl String?
|
||||
isRemoved Boolean @default(false)
|
||||
removedAt DateTime?
|
||||
|
||||
removalRequest RemovalRequest? @relation(fields: [removalRequestId], references: [id])
|
||||
|
||||
scannedAt DateTime @default(now())
|
||||
createdAt DateTime @default(now())
|
||||
updatedAt DateTime @default(now()) @updatedAt
|
||||
|
||||
@@index([subscriptionId])
|
||||
@@index([brokerId])
|
||||
@@index([removalRequestId])
|
||||
@@index([isRemoved])
|
||||
@@index([subscriptionId, isRemoved])
|
||||
}
|
||||
|
||||
@@ -27,6 +27,7 @@ export const AlertSource = {
|
||||
SPAMSHIELD: "SPAMSHIELD",
|
||||
VOICEPRINT: "VOICEPRINT",
|
||||
CALL_ANALYSIS: "CALL_ANALYSIS",
|
||||
INFO_BROKER: "INFO_BROKER",
|
||||
} as const;
|
||||
export type AlertSource = (typeof AlertSource)[keyof typeof AlertSource];
|
||||
|
||||
@@ -39,6 +40,9 @@ export const AlertCategory = {
|
||||
CALL_QUALITY: "CALL_QUALITY",
|
||||
CALL_ANOMALY: "CALL_ANOMALY",
|
||||
CALL_EVENT: "CALL_EVENT",
|
||||
HOME_TITLE: "HOME_TITLE",
|
||||
INFO_BROKER_LISTING: "INFO_BROKER_LISTING",
|
||||
INFO_BROKER_REMOVAL: "INFO_BROKER_REMOVAL",
|
||||
} as const;
|
||||
export type AlertCategory = (typeof AlertCategory)[keyof typeof AlertCategory];
|
||||
|
||||
@@ -280,6 +284,7 @@ export { generateRequestId, extractOrGenerateRequestId } from "./requestId";
|
||||
export const ReportType = {
|
||||
MONTHLY_PLUS: "MONTHLY_PLUS",
|
||||
ANNUAL_PREMIUM: "ANNUAL_PREMIUM",
|
||||
WEEKLY_DIGEST: "WEEKLY_DIGEST",
|
||||
} as const;
|
||||
export type ReportType = (typeof ReportType)[keyof typeof ReportType];
|
||||
|
||||
@@ -365,3 +370,93 @@ export interface SecurityReportOutput {
|
||||
createdAt: Date;
|
||||
deliveredAt?: Date;
|
||||
}
|
||||
|
||||
// ============================================
|
||||
// Info Broker Removal Types
|
||||
// ============================================
|
||||
|
||||
export const BrokerStatus = {
|
||||
ACTIVE: "ACTIVE",
|
||||
INACTIVE: "INACTIVE",
|
||||
UNKNOWN: "UNKNOWN",
|
||||
} as const;
|
||||
export type BrokerStatus = (typeof BrokerStatus)[keyof typeof BrokerStatus];
|
||||
|
||||
export const RemovalStatus = {
|
||||
PENDING: "PENDING",
|
||||
SUBMITTED: "SUBMITTED",
|
||||
IN_PROGRESS: "IN_PROGRESS",
|
||||
COMPLETED: "COMPLETED",
|
||||
FAILED: "FAILED",
|
||||
REJECTED: "REJECTED",
|
||||
} as const;
|
||||
export type RemovalStatus = (typeof RemovalStatus)[keyof typeof RemovalStatus];
|
||||
|
||||
export const RemovalMethod = {
|
||||
AUTOMATED: "AUTOMATED",
|
||||
MANUAL_FORM: "MANUAL_FORM",
|
||||
EMAIL: "EMAIL",
|
||||
PHONE: "PHONE",
|
||||
MAIL: "MAIL",
|
||||
NONE: "NONE",
|
||||
} as const;
|
||||
export type RemovalMethod = (typeof RemovalMethod)[keyof typeof RemovalMethod];
|
||||
|
||||
export interface BrokerDefinition {
|
||||
id: string;
|
||||
name: string;
|
||||
domain: string;
|
||||
category: string;
|
||||
removalMethod: RemovalMethod;
|
||||
removalUrl?: string;
|
||||
requiresAccount: boolean;
|
||||
requiresVerification: boolean;
|
||||
estimatedDays: number;
|
||||
isActive: boolean;
|
||||
}
|
||||
|
||||
export interface RemovalRequestInput {
|
||||
brokerId: string;
|
||||
personalInfo: {
|
||||
fullName: string;
|
||||
email?: string;
|
||||
phone?: string;
|
||||
address?: {
|
||||
street?: string;
|
||||
city?: string;
|
||||
state?: string;
|
||||
zip?: string;
|
||||
};
|
||||
dob?: string;
|
||||
};
|
||||
notes?: string;
|
||||
}
|
||||
|
||||
export interface RemovalRequestOutput {
|
||||
id: string;
|
||||
brokerId: string;
|
||||
brokerName: string;
|
||||
status: RemovalStatus;
|
||||
submittedAt?: Date;
|
||||
completedAt?: Date;
|
||||
method: RemovalMethod;
|
||||
attempts: number;
|
||||
nextRetryAt?: Date;
|
||||
notes?: string;
|
||||
error?: string;
|
||||
createdAt: Date;
|
||||
updatedAt: Date;
|
||||
}
|
||||
|
||||
export interface BrokerScanResult {
|
||||
brokerId: string;
|
||||
brokerName: string;
|
||||
listingsFound: number;
|
||||
listings: Array<{
|
||||
id: string;
|
||||
url: string;
|
||||
dataFound: Record<string, string>;
|
||||
screenshotUrl?: string;
|
||||
}>;
|
||||
scannedAt: Date;
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user