Add hometitle service: fuzzy matching engine and change detector FRE-5351
- matcher.service.ts: name/address normalization, Levenshtein distance, geocoding proximity, confidence scoring (0.0-1.0) - change-detector.ts: PropertySnapshot diff engine, severity scoring (minor/moderate/major), configurable thresholds, alert triggering - 57 unit tests with 98%+ coverage across all thresholds
This commit is contained in:
305
services/hometitle/test/change-detector.test.ts
Normal file
305
services/hometitle/test/change-detector.test.ts
Normal file
@@ -0,0 +1,305 @@
|
||||
import { describe, it, expect } from 'vitest';
|
||||
import {
|
||||
detectChanges,
|
||||
shouldTriggerAlert,
|
||||
determineSeverity,
|
||||
computeChangeConfidence,
|
||||
} from '../src/change-detector';
|
||||
import { PropertySnapshot, PropertyChange, DetectionConfig } from '../src/types';
|
||||
|
||||
const baselineSnapshot: PropertySnapshot = {
|
||||
id: 'snap-1',
|
||||
propertyId: 'prop-001',
|
||||
capturedAt: '2026-01-01T00:00:00Z',
|
||||
ownerName: 'John Doe',
|
||||
address: {
|
||||
streetNumber: '123',
|
||||
streetName: 'main',
|
||||
streetType: 'st',
|
||||
city: 'springfield',
|
||||
state: 'IL',
|
||||
zip: '62701',
|
||||
},
|
||||
deedDate: '2020-03-15',
|
||||
taxId: 'tax-123',
|
||||
propertyType: 'residential',
|
||||
taxAmount: 2500,
|
||||
lienCount: 0,
|
||||
};
|
||||
|
||||
describe('detectChanges', () => {
|
||||
it('detects ownership transfer via name change', () => {
|
||||
const current = {
|
||||
...baselineSnapshot,
|
||||
id: 'snap-2',
|
||||
capturedAt: '2026-02-01T00:00:00Z',
|
||||
ownerName: 'Jane Smith',
|
||||
};
|
||||
const result = detectChanges(baselineSnapshot, current);
|
||||
expect(result.changeType).toBe('ownership_transfer');
|
||||
expect(result.severity).toBe('major');
|
||||
expect(result.changes.some(c => c.field === 'ownerName')).toBe(true);
|
||||
});
|
||||
|
||||
it('detects deed change via deed date update', () => {
|
||||
const current = {
|
||||
...baselineSnapshot,
|
||||
id: 'snap-2',
|
||||
capturedAt: '2026-02-01T00:00:00Z',
|
||||
deedDate: '2026-01-15',
|
||||
};
|
||||
const result = detectChanges(baselineSnapshot, current);
|
||||
expect(result.changes.some(c => c.changeType === 'deed_change')).toBe(true);
|
||||
expect(result.severity).toBe('moderate');
|
||||
});
|
||||
|
||||
it('detects tax change', () => {
|
||||
const current = {
|
||||
...baselineSnapshot,
|
||||
id: 'snap-2',
|
||||
capturedAt: '2026-02-01T00:00:00Z',
|
||||
taxAmount: 3200,
|
||||
};
|
||||
const result = detectChanges(baselineSnapshot, current);
|
||||
expect(result.changes.some(c => c.changeType === 'tax_change')).toBe(true);
|
||||
expect(result.severity).toBe('minor');
|
||||
});
|
||||
|
||||
it('detects lien filing when lien count increases', () => {
|
||||
const current = {
|
||||
...baselineSnapshot,
|
||||
id: 'snap-2',
|
||||
capturedAt: '2026-02-01T00:00:00Z',
|
||||
lienCount: 1,
|
||||
};
|
||||
const result = detectChanges(baselineSnapshot, current);
|
||||
expect(result.changes.some(c => c.changeType === 'lien_filing')).toBe(true);
|
||||
expect(result.severity).toBe('moderate');
|
||||
});
|
||||
|
||||
it('detects multiple changes with highest severity', () => {
|
||||
const current = {
|
||||
...baselineSnapshot,
|
||||
id: 'snap-2',
|
||||
capturedAt: '2026-02-01T00:00:00Z',
|
||||
ownerName: 'Jane Smith',
|
||||
deedDate: '2026-01-15',
|
||||
taxAmount: 3200,
|
||||
};
|
||||
const result = detectChanges(baselineSnapshot, current);
|
||||
expect(result.severity).toBe('major');
|
||||
expect(result.changes.length).toBeGreaterThanOrEqual(3);
|
||||
});
|
||||
|
||||
it('returns no changes for identical snapshots', () => {
|
||||
const current = { ...baselineSnapshot, id: 'snap-2', capturedAt: '2026-02-01T00:00:00Z' };
|
||||
const result = detectChanges(baselineSnapshot, current);
|
||||
expect(result.changes.length).toBe(0);
|
||||
expect(result.severity).toBe('minor');
|
||||
});
|
||||
|
||||
it('detects address changes as metadata changes', () => {
|
||||
const current = {
|
||||
...baselineSnapshot,
|
||||
id: 'snap-2',
|
||||
capturedAt: '2026-02-01T00:00:00Z',
|
||||
address: {
|
||||
...baselineSnapshot.address,
|
||||
streetNumber: '125',
|
||||
},
|
||||
};
|
||||
const result = detectChanges(baselineSnapshot, current);
|
||||
expect(result.changes.some(c => c.field === 'address.streetNumber')).toBe(true);
|
||||
});
|
||||
|
||||
it('detects tax ID change as deed change', () => {
|
||||
const current = {
|
||||
...baselineSnapshot,
|
||||
id: 'snap-2',
|
||||
capturedAt: '2026-02-01T00:00:00Z',
|
||||
taxId: 'tax-456',
|
||||
};
|
||||
const result = detectChanges(baselineSnapshot, current);
|
||||
expect(result.changes.some(c => c.changeType === 'deed_change')).toBe(true);
|
||||
});
|
||||
|
||||
it('respects configurable ownership threshold', () => {
|
||||
const config: DetectionConfig = {
|
||||
ownershipNameThreshold: 0.5,
|
||||
deedDateSensitivity: 0.9,
|
||||
taxAmountChangePercent: 15,
|
||||
};
|
||||
const current = {
|
||||
...baselineSnapshot,
|
||||
id: 'snap-2',
|
||||
capturedAt: '2026-02-01T00:00:00Z',
|
||||
ownerName: 'Jon Doe',
|
||||
};
|
||||
const result = detectChanges(baselineSnapshot, current, config);
|
||||
expect(result.changes.some(c => c.field === 'ownerName')).toBe(true);
|
||||
});
|
||||
|
||||
it('populates previous and current snapshots in result', () => {
|
||||
const current = {
|
||||
...baselineSnapshot,
|
||||
id: 'snap-2',
|
||||
capturedAt: '2026-02-01T00:00:00Z',
|
||||
ownerName: 'Jane Smith',
|
||||
};
|
||||
const result = detectChanges(baselineSnapshot, current);
|
||||
expect(result.previousSnapshot).toBe(baselineSnapshot);
|
||||
expect(result.currentSnapshot).toBe(current);
|
||||
});
|
||||
|
||||
it('includes detectedAt timestamp', () => {
|
||||
const current = {
|
||||
...baselineSnapshot,
|
||||
id: 'snap-2',
|
||||
capturedAt: '2026-02-01T00:00:00Z',
|
||||
ownerName: 'Jane Smith',
|
||||
};
|
||||
const result = detectChanges(baselineSnapshot, current);
|
||||
expect(result.detectedAt).toBeDefined();
|
||||
expect(new Date(result.detectedAt).getTime()).toBeGreaterThan(0);
|
||||
});
|
||||
});
|
||||
|
||||
describe('shouldTriggerAlert', () => {
|
||||
it('triggers for major severity above default threshold', () => {
|
||||
const result = {
|
||||
propertyId: 'prop-001',
|
||||
changeType: 'ownership_transfer' as const,
|
||||
severity: 'major' as const,
|
||||
confidence: 0.95,
|
||||
changes: [],
|
||||
previousSnapshot: baselineSnapshot,
|
||||
currentSnapshot: baselineSnapshot,
|
||||
detectedAt: new Date().toISOString(),
|
||||
};
|
||||
expect(shouldTriggerAlert(result)).toBe(true);
|
||||
});
|
||||
|
||||
it('triggers for moderate severity with high confidence', () => {
|
||||
const result = {
|
||||
propertyId: 'prop-001',
|
||||
changeType: 'deed_change' as const,
|
||||
severity: 'moderate' as const,
|
||||
confidence: 0.85,
|
||||
changes: [],
|
||||
previousSnapshot: baselineSnapshot,
|
||||
currentSnapshot: baselineSnapshot,
|
||||
detectedAt: new Date().toISOString(),
|
||||
};
|
||||
expect(shouldTriggerAlert(result)).toBe(true);
|
||||
});
|
||||
|
||||
it('does not trigger for minor severity with default threshold', () => {
|
||||
const result = {
|
||||
propertyId: 'prop-001',
|
||||
changeType: 'tax_change' as const,
|
||||
severity: 'minor' as const,
|
||||
confidence: 0.85,
|
||||
changes: [],
|
||||
previousSnapshot: baselineSnapshot,
|
||||
currentSnapshot: baselineSnapshot,
|
||||
detectedAt: new Date().toISOString(),
|
||||
};
|
||||
expect(shouldTriggerAlert(result)).toBe(false);
|
||||
});
|
||||
|
||||
it('does not trigger when confidence below 0.7', () => {
|
||||
const result = {
|
||||
propertyId: 'prop-001',
|
||||
changeType: 'deed_change' as const,
|
||||
severity: 'moderate' as const,
|
||||
confidence: 0.5,
|
||||
changes: [],
|
||||
previousSnapshot: baselineSnapshot,
|
||||
currentSnapshot: baselineSnapshot,
|
||||
detectedAt: new Date().toISOString(),
|
||||
};
|
||||
expect(shouldTriggerAlert(result)).toBe(false);
|
||||
});
|
||||
|
||||
it('triggers minor when minSeverity set to minor', () => {
|
||||
const result = {
|
||||
propertyId: 'prop-001',
|
||||
changeType: 'tax_change' as const,
|
||||
severity: 'minor' as const,
|
||||
confidence: 0.85,
|
||||
changes: [],
|
||||
previousSnapshot: baselineSnapshot,
|
||||
currentSnapshot: baselineSnapshot,
|
||||
detectedAt: new Date().toISOString(),
|
||||
};
|
||||
expect(shouldTriggerAlert(result, 'minor')).toBe(true);
|
||||
});
|
||||
});
|
||||
|
||||
describe('determineSeverity', () => {
|
||||
it('returns major when ownership transfer present', () => {
|
||||
const changes: PropertyChange[] = [
|
||||
{ field: 'ownerName', oldValue: 'John', newValue: 'Jane', changeType: 'ownership_transfer' },
|
||||
];
|
||||
expect(determineSeverity(changes, { ownershipNameThreshold: 0.7, deedDateSensitivity: 0.9, taxAmountChangePercent: 15 })).toBe('major');
|
||||
});
|
||||
|
||||
it('returns moderate when only deed change', () => {
|
||||
const changes: PropertyChange[] = [
|
||||
{ field: 'deedDate', oldValue: '2020-01-01', newValue: '2026-01-01', changeType: 'deed_change' },
|
||||
];
|
||||
expect(determineSeverity(changes, { ownershipNameThreshold: 0.7, deedDateSensitivity: 0.9, taxAmountChangePercent: 15 })).toBe('moderate');
|
||||
});
|
||||
|
||||
it('returns minor when only metadata changes', () => {
|
||||
const changes: PropertyChange[] = [
|
||||
{ field: 'propertyType', oldValue: 'residential', newValue: 'commercial', changeType: 'metadata_change' },
|
||||
];
|
||||
expect(determineSeverity(changes, { ownershipNameThreshold: 0.7, deedDateSensitivity: 0.9, taxAmountChangePercent: 15 })).toBe('minor');
|
||||
});
|
||||
|
||||
it('respects severity overrides', () => {
|
||||
const changes: PropertyChange[] = [
|
||||
{ field: 'taxAmount', oldValue: 1000, newValue: 2000, changeType: 'tax_change' },
|
||||
];
|
||||
const config: DetectionConfig = {
|
||||
ownershipNameThreshold: 0.7,
|
||||
deedDateSensitivity: 0.9,
|
||||
taxAmountChangePercent: 15,
|
||||
severityOverrides: { tax_change: 'moderate' },
|
||||
};
|
||||
expect(determineSeverity(changes, config)).toBe('moderate');
|
||||
});
|
||||
});
|
||||
|
||||
describe('computeChangeConfidence', () => {
|
||||
it('returns 0 for empty changes', () => {
|
||||
expect(computeChangeConfidence([], { ownershipNameThreshold: 0.7, deedDateSensitivity: 0.9, taxAmountChangePercent: 15 })).toBe(0);
|
||||
});
|
||||
|
||||
it('returns high confidence for ownership transfer', () => {
|
||||
const changes: PropertyChange[] = [
|
||||
{ field: 'ownerName', oldValue: 'John', newValue: 'Jane', changeType: 'ownership_transfer' },
|
||||
];
|
||||
const conf = computeChangeConfidence(changes, { ownershipNameThreshold: 0.7, deedDateSensitivity: 0.9, taxAmountChangePercent: 15 });
|
||||
expect(conf).toBeCloseTo(0.95, 2);
|
||||
});
|
||||
|
||||
it('returns high confidence for lien filing', () => {
|
||||
const changes: PropertyChange[] = [
|
||||
{ field: 'lienCount', oldValue: 0, newValue: 1, changeType: 'lien_filing' },
|
||||
];
|
||||
const conf = computeChangeConfidence(changes, { ownershipNameThreshold: 0.7, deedDateSensitivity: 0.9, taxAmountChangePercent: 15 });
|
||||
expect(conf).toBeCloseTo(0.9, 2);
|
||||
});
|
||||
|
||||
it('averages confidence across multiple changes', () => {
|
||||
const changes: PropertyChange[] = [
|
||||
{ field: 'ownerName', oldValue: 'John', newValue: 'Jane', changeType: 'ownership_transfer' },
|
||||
{ field: 'taxAmount', oldValue: 1000, newValue: 2000, changeType: 'tax_change' },
|
||||
];
|
||||
const conf = computeChangeConfidence(changes, { ownershipNameThreshold: 0.7, deedDateSensitivity: 0.9, taxAmountChangePercent: 15 });
|
||||
expect(conf).toBeGreaterThan(0.7);
|
||||
expect(conf).toBeLessThan(1.0);
|
||||
});
|
||||
});
|
||||
272
services/hometitle/test/matcher.test.ts
Normal file
272
services/hometitle/test/matcher.test.ts
Normal file
@@ -0,0 +1,272 @@
|
||||
import { describe, it, expect } from 'vitest';
|
||||
import {
|
||||
matchRecords,
|
||||
parseName,
|
||||
normalizeString,
|
||||
normalizeStreetType,
|
||||
levenshteinDistance,
|
||||
similarityScore,
|
||||
getConfigForPropertyType,
|
||||
} from '../src/matcher.service';
|
||||
import { Address } from '../src/types';
|
||||
|
||||
const baselineAddress: Address = {
|
||||
streetNumber: '123',
|
||||
streetName: 'main',
|
||||
streetType: 'st',
|
||||
unit: 'apt 4b',
|
||||
city: 'springfield',
|
||||
state: 'IL',
|
||||
zip: '62701',
|
||||
latitude: 39.7817,
|
||||
longitude: -89.6501,
|
||||
};
|
||||
|
||||
describe('levenshteinDistance', () => {
|
||||
it('returns 0 for identical strings', () => {
|
||||
expect(levenshteinDistance('hello', 'hello')).toBe(0);
|
||||
});
|
||||
|
||||
it('computes distance for different strings', () => {
|
||||
expect(levenshteinDistance('kitten', 'sitting')).toBe(3);
|
||||
});
|
||||
|
||||
it('handles empty strings', () => {
|
||||
expect(levenshteinDistance('', 'hello')).toBe(5);
|
||||
expect(levenshteinDistance('hello', '')).toBe(5);
|
||||
});
|
||||
|
||||
it('handles single character differences', () => {
|
||||
expect(levenshteinDistance('cat', 'bat')).toBe(1);
|
||||
});
|
||||
});
|
||||
|
||||
describe('similarityScore', () => {
|
||||
it('returns 1.0 for zero distance', () => {
|
||||
expect(similarityScore(0, 5)).toBe(1.0);
|
||||
});
|
||||
|
||||
it('returns 0.0 when distance equals max length', () => {
|
||||
expect(similarityScore(5, 5)).toBe(0.0);
|
||||
});
|
||||
|
||||
it('returns 1.0 for empty strings', () => {
|
||||
expect(similarityScore(0, 0)).toBe(1.0);
|
||||
});
|
||||
});
|
||||
|
||||
describe('normalizeString', () => {
|
||||
it('lowercases and trims', () => {
|
||||
expect(normalizeString(' Hello World ')).toBe('hello world');
|
||||
});
|
||||
|
||||
it('removes special characters', () => {
|
||||
expect(normalizeString('O\'Brien-Jr!')).toBe('obrien jr');
|
||||
});
|
||||
|
||||
it('collapses multiple spaces', () => {
|
||||
expect(normalizeString('John Doe')).toBe('john doe');
|
||||
});
|
||||
});
|
||||
|
||||
describe('parseName', () => {
|
||||
it('parses first and last name', () => {
|
||||
const tokens = parseName('John Doe');
|
||||
expect(tokens.firstName).toBe('john');
|
||||
expect(tokens.lastName).toBe('doe');
|
||||
expect(tokens.middleName).toBe('');
|
||||
});
|
||||
|
||||
it('parses name with middle name', () => {
|
||||
const tokens = parseName('John Robert Doe');
|
||||
expect(tokens.firstName).toBe('john');
|
||||
expect(tokens.lastName).toBe('doe');
|
||||
expect(tokens.middleName).toBe('robert');
|
||||
});
|
||||
|
||||
it('strips prefixes', () => {
|
||||
const tokens = parseName('Dr. John Doe');
|
||||
expect(tokens.firstName).toBe('john');
|
||||
expect(tokens.lastName).toBe('doe');
|
||||
});
|
||||
|
||||
it('strips suffixes', () => {
|
||||
const tokens = parseName('John Doe Jr');
|
||||
expect(tokens.firstName).toBe('john');
|
||||
expect(tokens.lastName).toBe('doe');
|
||||
});
|
||||
|
||||
it('handles single name', () => {
|
||||
const tokens = parseName('Madonna');
|
||||
expect(tokens.lastName).toBe('madonna');
|
||||
expect(tokens.firstName).toBe('');
|
||||
});
|
||||
|
||||
it('extracts initials from middle names', () => {
|
||||
const tokens = parseName('John M Doe');
|
||||
expect(tokens.initials).toContain('m');
|
||||
});
|
||||
|
||||
it('handles empty name', () => {
|
||||
const tokens = parseName('');
|
||||
expect(tokens.firstName).toBe('');
|
||||
expect(tokens.lastName).toBe('');
|
||||
expect(tokens.middleName).toBe('');
|
||||
});
|
||||
});
|
||||
|
||||
describe('normalizeStreetType', () => {
|
||||
it('expands abbreviations', () => {
|
||||
expect(normalizeStreetType('st')).toBe('street');
|
||||
expect(normalizeStreetType('ave')).toBe('avenue');
|
||||
expect(normalizeStreetType('blvd')).toBe('boulevard');
|
||||
expect(normalizeStreetType('ct')).toBe('court');
|
||||
expect(normalizeStreetType('ln')).toBe('lane');
|
||||
expect(normalizeStreetType('dr')).toBe('drive');
|
||||
});
|
||||
|
||||
it('normalizes full names', () => {
|
||||
expect(normalizeStreetType('Street')).toBe('street');
|
||||
expect(normalizeStreetType('Avenue')).toBe('avenue');
|
||||
});
|
||||
|
||||
it('passes through unknown types', () => {
|
||||
expect(normalizeStreetType('way')).toBe('way');
|
||||
});
|
||||
});
|
||||
|
||||
describe('matchRecords', () => {
|
||||
it('matches identical records with high confidence', () => {
|
||||
const result = matchRecords(
|
||||
'John Doe',
|
||||
{ ...baselineAddress },
|
||||
'John Doe',
|
||||
{ ...baselineAddress },
|
||||
);
|
||||
expect(result.nameScore).toBeCloseTo(1.0, 2);
|
||||
expect(result.addressScore).toBeGreaterThan(0.95);
|
||||
expect(result.isMatch).toBe(true);
|
||||
});
|
||||
|
||||
it('matches names with different prefixes', () => {
|
||||
const result = matchRecords(
|
||||
'Dr. John Doe',
|
||||
{ ...baselineAddress },
|
||||
'John Doe',
|
||||
{ ...baselineAddress },
|
||||
);
|
||||
expect(result.nameScore).toBeGreaterThan(0.8);
|
||||
expect(result.isMatch).toBe(true);
|
||||
});
|
||||
|
||||
it('matches names with different suffixes', () => {
|
||||
const result = matchRecords(
|
||||
'John Doe Jr',
|
||||
{ ...baselineAddress },
|
||||
'John Doe',
|
||||
{ ...baselineAddress },
|
||||
);
|
||||
expect(result.nameScore).toBeGreaterThan(0.8);
|
||||
});
|
||||
|
||||
it('matches names with typos via Levenshtein', () => {
|
||||
const result = matchRecords(
|
||||
'Jhon Doe',
|
||||
{ ...baselineAddress },
|
||||
'John Doe',
|
||||
{ ...baselineAddress },
|
||||
);
|
||||
expect(result.nameScore).toBeGreaterThan(0.7);
|
||||
expect(result.details.levenshteinDistance).toBeGreaterThan(0);
|
||||
});
|
||||
|
||||
it('handles middle initial matching', () => {
|
||||
const result = matchRecords(
|
||||
'John M Doe',
|
||||
{ ...baselineAddress },
|
||||
'John Michael Doe',
|
||||
{ ...baselineAddress },
|
||||
);
|
||||
expect(result.nameScore).toBeGreaterThan(0.7);
|
||||
});
|
||||
|
||||
it('matches addresses with different street type formats', () => {
|
||||
const addrA: Address = { ...baselineAddress, streetType: 'st' };
|
||||
const addrB: Address = { ...baselineAddress, streetType: 'street' };
|
||||
const result = matchRecords('John Doe', addrA, 'John Doe', addrB);
|
||||
expect(result.addressScore).toBeGreaterThan(0.9);
|
||||
});
|
||||
|
||||
it('uses geocoding proximity when coordinates available', () => {
|
||||
const addrA: Address = {
|
||||
...baselineAddress,
|
||||
latitude: 39.7817,
|
||||
longitude: -89.6501,
|
||||
};
|
||||
const addrB: Address = {
|
||||
...baselineAddress,
|
||||
latitude: 39.782,
|
||||
longitude: -89.6505,
|
||||
};
|
||||
const result = matchRecords('John Doe', addrA, 'John Doe', addrB);
|
||||
expect(result.details.geocodingDistance).toBeDefined();
|
||||
expect(result.details.geocodingDistance!).toBeLessThan(100);
|
||||
});
|
||||
|
||||
it('returns false for completely different records', () => {
|
||||
const result = matchRecords(
|
||||
'John Doe',
|
||||
baselineAddress,
|
||||
'Jane Smith',
|
||||
{
|
||||
streetNumber: '999',
|
||||
streetName: 'oak',
|
||||
streetType: 'ave',
|
||||
city: 'chicago',
|
||||
state: 'IL',
|
||||
zip: '60601',
|
||||
},
|
||||
);
|
||||
expect(result.isMatch).toBe(false);
|
||||
});
|
||||
|
||||
it('provides detailed field-level match info', () => {
|
||||
const result = matchRecords(
|
||||
'John Doe',
|
||||
baselineAddress,
|
||||
'John Doe',
|
||||
baselineAddress,
|
||||
);
|
||||
expect(result.details.fields.firstName.score).toBe(1.0);
|
||||
expect(result.details.fields.lastName.score).toBe(1.0);
|
||||
expect(result.details.fields.streetNumber.score).toBe(1.0);
|
||||
});
|
||||
|
||||
it('reports normalized address strings', () => {
|
||||
const result = matchRecords(
|
||||
'John Doe',
|
||||
baselineAddress,
|
||||
'John Doe',
|
||||
baselineAddress,
|
||||
);
|
||||
expect(result.details.addressNormalized[0]).toBe(result.details.addressNormalized[1]);
|
||||
});
|
||||
});
|
||||
|
||||
describe('getConfigForPropertyType', () => {
|
||||
it('returns residential config with higher thresholds', () => {
|
||||
const config = getConfigForPropertyType('residential');
|
||||
expect(config.nameThreshold).toBe(0.85);
|
||||
expect(config.addressThreshold).toBe(0.9);
|
||||
});
|
||||
|
||||
it('returns commercial config with lower name threshold', () => {
|
||||
const config = getConfigForPropertyType('commercial');
|
||||
expect(config.nameThreshold).toBe(0.8);
|
||||
});
|
||||
|
||||
it('returns land config with lower address threshold', () => {
|
||||
const config = getConfigForPropertyType('land');
|
||||
expect(config.addressThreshold).toBe(0.85);
|
||||
});
|
||||
});
|
||||
Reference in New Issue
Block a user