Files
Kordant/services/hometitle/test/scanner.test.ts
Michael Freno d6f574ff8e FRE-5350: Add property scanner service with ATTOM, USPS, county data integration
- ATTOM Property API integration for structured property data
- USPS address standardization via API
- County clerk/recorder feed scraping for deed changes and liens
- Rate limiting, caching, and retry logic
- Unit tests for each data source adapter
- PropertyRecord, CountyDeedRecord, DataSourceType types in types.ts
- Consolidated type exports in index.ts

Co-Authored-By: Paperclip <noreply@paperclip.ing>
2026-05-16 09:50:21 -04:00

649 lines
19 KiB
TypeScript

import { describe, it, expect, beforeEach, vi, afterEach } from 'vitest';
import { PropertyScannerService } from '../src/scanner.service';
import { prisma } from '@shieldai/db';
vi.mock('@shieldai/db', () => ({
prisma: {
countyDeedRecord: {
findMany: vi.fn(),
},
},
}));
describe('PropertyScannerService', () => {
let scanner: PropertyScannerService;
beforeEach(() => {
vi.clearAllMocks();
scanner = new PropertyScannerService({
attomApiKey: 'test-api-key',
uspsApiKey: 'usps-test-key',
countyScraperEnabled: true,
cacheTTL: 60,
rateLimitPoints: 100,
rateLimitDuration: 60,
retryAttempts: 2,
retryDelayMs: 100,
});
});
afterEach(() => {
vi.restoreAllMocks();
});
describe('fetchByAddress', () => {
it('should fetch property data from ATTOM API by address', async () => {
global.fetch = vi.fn().mockResolvedValueOnce({
ok: true,
status: 200,
json: async () => ({
data: {
propertyId: 'prop-123',
parcelNumber: '123-456-789',
address: {
streetAddress: '123 Main St',
city: 'San Francisco',
state: 'CA',
zipCode: '94102',
latitude: 37.7749,
longitude: -122.4194,
},
owner: {
ownerName1: 'John Doe',
ownerName2: 'Jane Doe',
},
assessment: {
totalValue: 1000000,
landValue: 400000,
improvementValue: 600000,
taxYear: 2024,
taxAmount: 12000,
},
propertyDetails: {
propertyType: 'residential',
yearBuilt: 1990,
squareFeet: 2500,
bedrooms: 4,
bathrooms: 3,
},
salesHistory: [],
},
}),
});
vi.mocked(prisma.countyDeedRecord.findMany).mockResolvedValueOnce([]);
const result = await scanner.fetchByAddress('123 Main St, San Francisco, CA 94102');
expect(result).not.toBeNull();
expect(result?.parcelNumber).toBe('123-456-789');
expect(result?.ownerName).toBe('John Doe');
expect(result?.address.city).toBe('San Francisco');
expect(result?.dataSource).toBe('attom');
expect(result?.assessment?.totalValue).toBe(1000000);
});
it('should return null when property not found', async () => {
global.fetch = vi.fn().mockResolvedValueOnce({
ok: false,
status: 404,
statusText: 'Not Found',
});
const result = await scanner.fetchByAddress('999 Nonexistent St');
expect(result).toBeNull();
});
it('should combine ATTOM data with county deed records', async () => {
global.fetch = vi.fn().mockResolvedValueOnce({
ok: true,
status: 200,
json: async () => ({
data: {
propertyId: 'prop-123',
parcelNumber: '123-456-789',
address: {
streetAddress: '123 Main St',
city: 'San Francisco',
state: 'CA',
zipCode: '94102',
latitude: 37.7749,
longitude: -122.4194,
},
owner: {
ownerName1: 'John Doe',
},
assessment: {
totalValue: 1000000,
landValue: 400000,
improvementValue: 600000,
taxYear: 2024,
taxAmount: 12000,
},
propertyDetails: {
propertyType: 'residential',
yearBuilt: 1990,
squareFeet: 2500,
},
salesHistory: [],
},
}),
});
vi.mocked(prisma.countyDeedRecord.findMany).mockResolvedValueOnce([
{
id: 'deed-1',
documentId: 'DOC-2024-001',
recordingDate: new Date('2024-01-15'),
documentType: 'WARRANTY DEED',
grantorName: 'Previous Owner LLC',
granteeName: 'John Doe',
propertyAddress: '123 Main St',
parcelNumber: '123-456-789',
considerationAmount: 950000,
},
]);
const result = await scanner.fetchByAddress('123 Main St, San Francisco, CA 94102', 'San Francisco');
expect(result).not.toBeNull();
expect(result?.dataSource).toBe('combined');
expect(result?.deedHistory?.length).toBe(1);
expect(result?.deedHistory?.[0].documentType).toBe('WARRANTY DEED');
});
it('should handle ATTOM API errors with retry', async () => {
global.fetch = vi.fn()
.mockRejectedValueOnce(new Error('Network error'))
.mockRejectedValueOnce(new Error('Network error'))
.mockResolvedValueOnce({
ok: true,
status: 200,
json: async () => ({
data: {
propertyId: 'prop-123',
parcelNumber: '123-456-789',
address: {
streetAddress: '123 Main St',
city: 'San Francisco',
state: 'CA',
zipCode: '94102',
latitude: 37.7749,
longitude: -122.4194,
},
owner: {
ownerName1: 'John Doe',
},
assessment: {
totalValue: 1000000,
landValue: 400000,
improvementValue: 600000,
taxYear: 2024,
taxAmount: 12000,
},
propertyDetails: {
propertyType: 'residential',
yearBuilt: 1990,
squareFeet: 2500,
},
salesHistory: [],
},
}),
});
vi.mocked(prisma.countyDeedRecord.findMany).mockResolvedValueOnce([]);
const result = await scanner.fetchByAddress('123 Main St, San Francisco, CA 94102');
expect(result).not.toBeNull();
expect(result?.ownerName).toBe('John Doe');
expect(global.fetch).toHaveBeenCalledTimes(3);
});
});
describe('fetchByParcel', () => {
it('should fetch property data by parcel number', async () => {
global.fetch = vi.fn().mockResolvedValueOnce({
ok: true,
status: 200,
json: async () => ({
data: {
propertyId: 'prop-456',
parcelNumber: '987-654-321',
address: {
streetAddress: '456 Oak Ave',
city: 'Los Angeles',
state: 'CA',
zipCode: '90001',
latitude: 34.0522,
longitude: -118.2437,
},
owner: {
ownerName1: 'Alice Smith',
},
assessment: {
totalValue: 750000,
landValue: 300000,
improvementValue: 450000,
taxYear: 2024,
taxAmount: 9000,
},
propertyDetails: {
propertyType: 'residential',
yearBuilt: 2000,
squareFeet: 1800,
},
salesHistory: [],
},
}),
});
vi.mocked(prisma.countyDeedRecord.findMany).mockResolvedValueOnce([]);
const result = await scanner.fetchByParcel('987-654-321', 'Los Angeles');
expect(result).not.toBeNull();
expect(result?.parcelNumber).toBe('987-654-321');
expect(result?.ownerName).toBe('Alice Smith');
expect(result?.address.city).toBe('Los Angeles');
});
it('should fetch county liens for parcel', async () => {
global.fetch = vi.fn().mockResolvedValueOnce({
ok: true,
status: 200,
json: async () => ({
data: {
propertyId: 'prop-456',
parcelNumber: '987-654-321',
address: {
streetAddress: '456 Oak Ave',
city: 'Los Angeles',
state: 'CA',
zipCode: '90001',
latitude: 34.0522,
longitude: -118.2437,
},
owner: {
ownerName1: 'Alice Smith',
},
assessment: {
totalValue: 750000,
landValue: 300000,
improvementValue: 450000,
taxYear: 2024,
taxAmount: 9000,
},
propertyDetails: {
propertyType: 'residential',
yearBuilt: 2000,
squareFeet: 1800,
},
salesHistory: [],
},
}),
});
vi.mocked(prisma.countyDeedRecord.findMany)
.mockResolvedValueOnce([])
.mockResolvedValueOnce([
{
id: 'lien-1',
documentId: 'LIEN-2024-001',
recordingDate: new Date('2024-02-01'),
documentType: 'MECHANIC LIEN',
grantorName: 'ABC Construction',
granteeName: 'Alice Smith',
propertyAddress: '456 Oak Ave',
parcelNumber: '987-654-321',
considerationAmount: 15000,
},
]);
const result = await scanner.fetchByParcel('987-654-321', 'Los Angeles');
expect(result).not.toBeNull();
expect(result?.liens?.length).toBe(1);
expect(result?.liens?.[0].lienType).toBe('MECHANIC LIEN');
expect(result?.liens?.[0].lienAmount).toBe(15000);
});
});
describe('standardizeAddress', () => {
it('should standardize address using USPS API', async () => {
global.fetch = vi.fn().mockResolvedValueOnce({
ok: true,
status: 200,
json: async () => ({
streetNumber: '123',
streetName: 'Main',
streetSuffix: 'St',
unitType: 'Apt',
unitValue: '4B',
city: 'San Francisco',
state: 'CA',
zip5: '94102',
zip4: '1234',
deliveryPointBarCode: '941021234123',
isDeliverable: true,
}),
});
const result = await scanner.standardizeAddress('123 main street apt 4b san francisco ca 94102');
expect(result).not.toBeNull();
expect(result?.streetNumber).toBe('123');
expect(result?.streetName).toBe('Main');
expect(result?.city).toBe('San Francisco');
expect(result?.isDeliverable).toBe(true);
});
it('should return null for undeliverable address', async () => {
global.fetch = vi.fn().mockResolvedValueOnce({
ok: true,
status: 200,
json: async () => ({
streetNumber: '999',
streetName: 'Nonexistent',
streetSuffix: 'St',
city: 'Nowhere',
state: 'XX',
zip5: '00000',
isDeliverable: false,
}),
});
const result = await scanner.standardizeAddress('999 nonexistent st nowhere xx 00000');
expect(result).not.toBeNull();
expect(result?.isDeliverable).toBe(false);
});
});
describe('batchFetchProperties', () => {
it('should fetch multiple properties in batch', async () => {
global.fetch = vi.fn().mockResolvedValueOnce({
ok: true,
status: 200,
json: async () => ({
data: {
propertyId: 'prop-1',
parcelNumber: '111-222-333',
address: {
streetAddress: '111 First St',
city: 'San Francisco',
state: 'CA',
zipCode: '94102',
latitude: 37.7749,
longitude: -122.4194,
},
owner: {
ownerName1: 'Owner One',
},
assessment: {
totalValue: 500000,
landValue: 200000,
improvementValue: 300000,
taxYear: 2024,
taxAmount: 6000,
},
propertyDetails: {
propertyType: 'residential',
yearBuilt: 1985,
squareFeet: 1500,
},
salesHistory: [],
},
}),
}).mockResolvedValueOnce({
ok: true,
status: 200,
json: async () => ({
data: {
propertyId: 'prop-2',
parcelNumber: '444-555-666',
address: {
streetAddress: '222 Second Ave',
city: 'Los Angeles',
state: 'CA',
zipCode: '90001',
latitude: 34.0522,
longitude: -118.2437,
},
owner: {
ownerName1: 'Owner Two',
},
assessment: {
totalValue: 800000,
landValue: 350000,
improvementValue: 450000,
taxYear: 2024,
taxAmount: 9600,
},
propertyDetails: {
propertyType: 'residential',
yearBuilt: 1995,
squareFeet: 2000,
},
salesHistory: [],
},
}),
});
vi.mocked(prisma.countyDeedRecord.findMany).mockResolvedValue([]);
const requests = [
{ address: '111 First St, San Francisco, CA 94102' },
{ address: '222 Second Ave, Los Angeles, CA 90001' },
];
const results = await scanner.batchFetchProperties(requests);
expect(results.length).toBe(2);
expect(results[0]).not.toBeNull();
expect(results[1]).not.toBeNull();
expect(results[0]?.ownerName).toBe('Owner One');
expect(results[1]?.ownerName).toBe('Owner Two');
});
it('should handle batch with mixed valid and invalid requests', async () => {
global.fetch = vi.fn().mockResolvedValueOnce({
ok: true,
status: 200,
json: async () => ({
data: {
propertyId: 'prop-1',
parcelNumber: '111-222-333',
address: {
streetAddress: '111 First St',
city: 'San Francisco',
state: 'CA',
zipCode: '94102',
latitude: 37.7749,
longitude: -122.4194,
},
owner: {
ownerName1: 'Owner One',
},
assessment: {
totalValue: 500000,
landValue: 200000,
improvementValue: 300000,
taxYear: 2024,
taxAmount: 6000,
},
propertyDetails: {
propertyType: 'residential',
yearBuilt: 1985,
squareFeet: 1500,
},
salesHistory: [],
},
}),
}).mockResolvedValueOnce({
ok: false,
status: 404,
});
vi.mocked(prisma.countyDeedRecord.findMany).mockResolvedValue([]);
const requests = [
{ address: '111 First St, San Francisco, CA 94102' },
{ address: '999 Invalid Address' },
];
const results = await scanner.batchFetchProperties(requests);
expect(results.length).toBe(2);
expect(results[0]).not.toBeNull();
expect(results[1]).toBeNull();
});
});
describe('cache behavior', () => {
it('should use cached results for repeated requests', async () => {
global.fetch = vi.fn().mockResolvedValueOnce({
ok: true,
status: 200,
json: async () => ({
data: {
propertyId: 'prop-123',
parcelNumber: '123-456-789',
address: {
streetAddress: '123 Main St',
city: 'San Francisco',
state: 'CA',
zipCode: '94102',
latitude: 37.7749,
longitude: -122.4194,
},
owner: {
ownerName1: 'John Doe',
},
assessment: {
totalValue: 1000000,
landValue: 400000,
improvementValue: 600000,
taxYear: 2024,
taxAmount: 12000,
},
propertyDetails: {
propertyType: 'residential',
yearBuilt: 1990,
squareFeet: 2500,
},
salesHistory: [],
},
}),
});
vi.mocked(prisma.countyDeedRecord.findMany).mockResolvedValueOnce([]);
await scanner.fetchByAddress('123 Main St, San Francisco, CA 94102');
await scanner.fetchByAddress('123 Main St, San Francisco, CA 94102');
expect(global.fetch).toHaveBeenCalledTimes(1);
});
it('should invalidate cache when requested', async () => {
global.fetch = vi.fn().mockResolvedValue({
ok: true,
status: 200,
json: async () => ({
data: {
propertyId: 'prop-123',
parcelNumber: '123-456-789',
address: {
streetAddress: '123 Main St',
city: 'San Francisco',
state: 'CA',
zipCode: '94102',
latitude: 37.7749,
longitude: -122.4194,
},
owner: {
ownerName1: 'John Doe',
},
assessment: {
totalValue: 1000000,
landValue: 400000,
improvementValue: 600000,
taxYear: 2024,
taxAmount: 12000,
},
propertyDetails: {
propertyType: 'residential',
yearBuilt: 1990,
squareFeet: 2500,
},
salesHistory: [],
},
}),
});
vi.mocked(prisma.countyDeedRecord.findMany).mockResolvedValue([]);
await scanner.fetchByAddress('123 Main St, San Francisco, CA 94102');
await scanner.invalidateCache('123 Main St, San Francisco, CA 94102');
await scanner.fetchByAddress('123 Main St, San Francisco, CA 94102');
expect(global.fetch).toHaveBeenCalledTimes(2);
});
});
describe('rate limiting', () => {
it('should handle rate limit errors gracefully', async () => {
global.fetch = vi.fn().mockResolvedValue({
ok: true,
status: 200,
json: async () => ({
data: {
propertyId: 'prop-123',
parcelNumber: '123-456-789',
address: {
streetAddress: '123 Main St',
city: 'San Francisco',
state: 'CA',
zipCode: '94102',
latitude: 37.7749,
longitude: -122.4194,
},
owner: {
ownerName1: 'John Doe',
},
assessment: {
totalValue: 1000000,
landValue: 400000,
improvementValue: 600000,
taxYear: 2024,
taxAmount: 12000,
},
propertyDetails: {
propertyType: 'residential',
yearBuilt: 1990,
squareFeet: 2500,
},
salesHistory: [],
},
}),
});
vi.mocked(prisma.countyDeedRecord.findMany).mockResolvedValue([]);
const scannerWithStrictLimit = new PropertyScannerService({
attomApiKey: 'test-api-key',
countyScraperEnabled: false,
rateLimitPoints: 1,
rateLimitDuration: 60,
});
await scannerWithStrictLimit.fetchByAddress('123 Main St, San Francisco, CA 94102');
await expect(
scannerWithStrictLimit.fetchByAddress('456 Oak Ave, San Francisco, CA 94102')
).rejects.toThrow('Rate limit exceeded');
});
});
});