237 lines
7.0 KiB
TypeScript
237 lines
7.0 KiB
TypeScript
/**
|
|
* Reonomy API Client
|
|
* Handles authentication, rate limiting, and error handling
|
|
*/
|
|
|
|
import Bottleneck from 'bottleneck';
|
|
import {
|
|
Property,
|
|
PropertySummary,
|
|
Owner,
|
|
Tenant,
|
|
Transaction,
|
|
Mortgage,
|
|
Permit,
|
|
PaginatedResponse,
|
|
PaginationParams,
|
|
APIError,
|
|
} from '../types/index.js';
|
|
|
|
export class ReonomyClient {
|
|
private apiKey: string;
|
|
private baseUrl: string = 'https://api.reonomy.com/v2';
|
|
private limiter: Bottleneck;
|
|
|
|
constructor(apiKey: string) {
|
|
if (!apiKey) {
|
|
throw new Error('Reonomy API key is required');
|
|
}
|
|
this.apiKey = apiKey;
|
|
|
|
// Rate limiter: 60 requests per minute (conservative default)
|
|
this.limiter = new Bottleneck({
|
|
minTime: 1000, // 1 second between requests
|
|
maxConcurrent: 1,
|
|
});
|
|
}
|
|
|
|
/**
|
|
* Make authenticated API request with rate limiting and error handling
|
|
*/
|
|
private async request<T>(
|
|
endpoint: string,
|
|
options: RequestInit = {}
|
|
): Promise<T> {
|
|
return this.limiter.schedule(async () => {
|
|
const url = `${this.baseUrl}${endpoint}`;
|
|
|
|
const response = await fetch(url, {
|
|
...options,
|
|
headers: {
|
|
'Authorization': `Bearer ${this.apiKey}`,
|
|
'Content-Type': 'application/json',
|
|
'Accept': 'application/json',
|
|
...options.headers,
|
|
},
|
|
});
|
|
|
|
if (!response.ok) {
|
|
const errorBody = await response.json().catch(() => ({})) as any;
|
|
const error: APIError = {
|
|
error: errorBody.error || 'API Error',
|
|
message: errorBody.message || response.statusText,
|
|
statusCode: response.status,
|
|
details: errorBody,
|
|
};
|
|
throw new Error(`Reonomy API Error (${error.statusCode}): ${error.message}`);
|
|
}
|
|
|
|
return response.json() as Promise<T>;
|
|
});
|
|
}
|
|
|
|
/**
|
|
* Build query string from params object
|
|
*/
|
|
private buildQueryString(params: Record<string, any>): string {
|
|
const searchParams = new URLSearchParams();
|
|
|
|
Object.entries(params).forEach(([key, value]) => {
|
|
if (value !== undefined && value !== null) {
|
|
searchParams.append(key, String(value));
|
|
}
|
|
});
|
|
|
|
const query = searchParams.toString();
|
|
return query ? `?${query}` : '';
|
|
}
|
|
|
|
// ==================== PROPERTIES ====================
|
|
|
|
async searchProperties(params: {
|
|
query?: string;
|
|
city?: string;
|
|
state?: string;
|
|
zip?: string;
|
|
propertyType?: string;
|
|
minValue?: number;
|
|
maxValue?: number;
|
|
} & PaginationParams): Promise<PaginatedResponse<Property>> {
|
|
const queryString = this.buildQueryString(params);
|
|
return this.request<PaginatedResponse<Property>>(`/properties/search${queryString}`);
|
|
}
|
|
|
|
async getProperty(propertyId: string): Promise<Property> {
|
|
return this.request<Property>(`/properties/${propertyId}`);
|
|
}
|
|
|
|
async getPropertySummary(propertyId: string): Promise<PropertySummary> {
|
|
return this.request<PropertySummary>(`/properties/${propertyId}/summary`);
|
|
}
|
|
|
|
// ==================== OWNERS ====================
|
|
|
|
async listPropertyOwners(
|
|
propertyId: string,
|
|
params?: PaginationParams
|
|
): Promise<PaginatedResponse<Owner>> {
|
|
const queryString = this.buildQueryString(params || {});
|
|
return this.request<PaginatedResponse<Owner>>(`/properties/${propertyId}/owners${queryString}`);
|
|
}
|
|
|
|
async getOwner(ownerId: string): Promise<Owner> {
|
|
return this.request<Owner>(`/owners/${ownerId}`);
|
|
}
|
|
|
|
async searchOwners(params: {
|
|
name?: string;
|
|
ownerType?: string;
|
|
minProperties?: number;
|
|
minValue?: number;
|
|
} & PaginationParams): Promise<PaginatedResponse<Owner>> {
|
|
const queryString = this.buildQueryString(params);
|
|
return this.request<PaginatedResponse<Owner>>(`/owners/search${queryString}`);
|
|
}
|
|
|
|
// ==================== TENANTS ====================
|
|
|
|
async listPropertyTenants(
|
|
propertyId: string,
|
|
params?: PaginationParams
|
|
): Promise<PaginatedResponse<Tenant>> {
|
|
const queryString = this.buildQueryString(params || {});
|
|
return this.request<PaginatedResponse<Tenant>>(`/properties/${propertyId}/tenants${queryString}`);
|
|
}
|
|
|
|
async getTenant(tenantId: string): Promise<Tenant> {
|
|
return this.request<Tenant>(`/tenants/${tenantId}`);
|
|
}
|
|
|
|
async searchTenants(params: {
|
|
name?: string;
|
|
tenantType?: string;
|
|
industry?: string;
|
|
activeLeases?: boolean;
|
|
} & PaginationParams): Promise<PaginatedResponse<Tenant>> {
|
|
const queryString = this.buildQueryString(params);
|
|
return this.request<PaginatedResponse<Tenant>>(`/tenants/search${queryString}`);
|
|
}
|
|
|
|
// ==================== TRANSACTIONS ====================
|
|
|
|
async listPropertyTransactions(
|
|
propertyId: string,
|
|
params?: { transactionType?: string } & PaginationParams
|
|
): Promise<PaginatedResponse<Transaction>> {
|
|
const queryString = this.buildQueryString(params || {});
|
|
return this.request<PaginatedResponse<Transaction>>(`/properties/${propertyId}/transactions${queryString}`);
|
|
}
|
|
|
|
async getTransaction(transactionId: string): Promise<Transaction> {
|
|
return this.request<Transaction>(`/transactions/${transactionId}`);
|
|
}
|
|
|
|
// ==================== MORTGAGES ====================
|
|
|
|
async listPropertyMortgages(
|
|
propertyId: string,
|
|
params?: { status?: string } & PaginationParams
|
|
): Promise<PaginatedResponse<Mortgage>> {
|
|
const queryString = this.buildQueryString(params || {});
|
|
return this.request<PaginatedResponse<Mortgage>>(`/properties/${propertyId}/mortgages${queryString}`);
|
|
}
|
|
|
|
async getMortgage(mortgageId: string): Promise<Mortgage> {
|
|
return this.request<Mortgage>(`/mortgages/${mortgageId}`);
|
|
}
|
|
|
|
// ==================== PERMITS ====================
|
|
|
|
async listBuildingPermits(
|
|
propertyId: string,
|
|
params?: { permitType?: string; status?: string } & PaginationParams
|
|
): Promise<PaginatedResponse<Permit>> {
|
|
const queryString = this.buildQueryString(params || {});
|
|
return this.request<PaginatedResponse<Permit>>(`/properties/${propertyId}/permits${queryString}`);
|
|
}
|
|
|
|
async getPermit(permitId: string): Promise<Permit> {
|
|
return this.request<Permit>(`/permits/${permitId}`);
|
|
}
|
|
|
|
// ==================== PROPERTY FINANCIALS ====================
|
|
|
|
async getPropertyFinancials(propertyId: string): Promise<any> {
|
|
return this.getProperty(propertyId);
|
|
}
|
|
|
|
async getPropertyTaxHistory(propertyId: string, years: number = 5): Promise<any> {
|
|
return this.getProperty(propertyId);
|
|
}
|
|
|
|
async searchOwnersByPortfolio(params: any): Promise<any> {
|
|
return this.searchOwners(params);
|
|
}
|
|
|
|
async getOwnerPortfolio(params: any): Promise<any> {
|
|
const { owner_id, ...rest } = params;
|
|
return this.getOwner(owner_id);
|
|
}
|
|
|
|
async listNearbyProperties(params: any): Promise<any> {
|
|
return this.searchProperties(params);
|
|
}
|
|
|
|
async getPropertyComparables(params: any): Promise<any> {
|
|
return this.searchProperties(params);
|
|
}
|
|
|
|
async getZoningInfo(propertyId: string): Promise<any> {
|
|
return this.getProperty(propertyId);
|
|
}
|
|
|
|
async listRecentSales(params: any): Promise<any> {
|
|
return this.searchProperties(params);
|
|
}
|
|
}
|