integratedhub/services/tenantApplicationService.ts
2026-01-20 09:02:26 -06:00

218 lines
6.4 KiB
TypeScript

// services/tenantApplicationService.ts
// Handle tenant applications, reviews, and approval workflow
import { and, eq } from "drizzle-orm";
import { db } from "../db/db.ts";
import { properties, tenant_applications } from "../db/schema.ts";
import {
createE2ETenantApplication,
getE2ETenantApplicationsByLandlord,
hasE2EApplication,
isE2EEnabled,
updateE2ETenantApplicationStatus,
} from "./e2eFixtures.ts";
export interface TenantApplication {
id: string;
property_id?: string | null;
applicant_id?: string | null;
application_status?: string | null; // 'pending', 'approved', 'rejected'
application_data_encrypted?: string | null; // Encrypted application details
document_urls_json?: string | null;
submitted_on?: number | null;
reviewed_on?: number | null;
}
export interface ApplicationData {
employment_info?: {
employer: string;
position: string;
annual_income: number;
employment_length: string;
};
references?: {
previous_landlord?: string;
previous_landlord_phone?: string;
personal_reference?: string;
personal_reference_phone?: string;
};
background_info?: {
credit_score?: number;
criminal_background?: boolean;
eviction_history?: boolean;
pets?: {
has_pets: boolean;
pet_details?: string;
};
};
additional_info?: string;
}
// Extended interface for dashboard tenant applications with UI properties
export interface DashboardTenantApplication extends TenantApplication {
applicant_name?: string;
applicant_email?: string;
property_address?: string;
monthly_income?: number;
application_notes?: string;
status?: string; // UI-friendly status (maps to application_status)
created_at?: number | null; // UI-friendly timestamp (maps to submitted_on)
}
// Create new tenant application
export async function createTenantApplication(
property_id: string,
applicant_id: string,
application_data_encrypted: string,
documentUrls: { url: string; name: string; type: string }[] | null = null,
) {
const documentUrlsJson = documentUrls && documentUrls.length > 0
? JSON.stringify(documentUrls)
: null;
if (isE2EEnabled()) {
return [createE2ETenantApplication(
property_id,
applicant_id,
application_data_encrypted,
documentUrlsJson,
)];
}
const now = Date.now();
return await db.insert(tenant_applications)
.values({
id: crypto.randomUUID(),
property_id,
applicant_id,
application_status: "submitted",
application_data_encrypted,
document_urls_json: documentUrlsJson,
submitted_on: now,
})
.returning();
}
// Get applications by property
export async function getApplicationsByProperty(property_id: string) {
return await db.select()
.from(tenant_applications)
.where(eq(tenant_applications.property_id, property_id));
}
// Get applications by applicant
export async function getApplicationsByApplicant(applicant_id: string) {
return await db.select()
.from(tenant_applications)
.where(eq(tenant_applications.applicant_id, applicant_id));
}
// Get specific application
export async function getApplicationById(id: string) {
const result = await db.select()
.from(tenant_applications)
.where(eq(tenant_applications.id, id));
return result[0] || null;
}
// Check if user has already applied for property
export async function hasUserAppliedForProperty(
property_id: string,
applicant_id: string,
) {
if (isE2EEnabled()) {
return hasE2EApplication(property_id, applicant_id);
}
const result = await db.select()
.from(tenant_applications)
.where(and(
eq(tenant_applications.property_id, property_id),
eq(tenant_applications.applicant_id, applicant_id),
));
return result.length > 0;
}
// Update application status (approve/reject)
export async function updateApplicationStatus(
id: string,
status: "approved" | "rejected",
reviewed_on?: number,
) {
const now = reviewed_on || Date.now();
return await db.update(tenant_applications)
.set({
application_status: status,
reviewed_on: now,
})
.where(eq(tenant_applications.id, id))
.returning();
}
// Get pending applications for landlord review
export async function getPendingApplications(property_ids: string[]) {
if (property_ids.length === 0) return [];
return await db.select()
.from(tenant_applications)
.where(and(
eq(tenant_applications.application_status, "pending"),
// Note: In a real implementation, you'd use an IN clause here
// For now, we'll filter by the first property or implement proper IN logic
));
}
// Delete application (if needed)
export async function deleteApplication(id: string) {
return await db.delete(tenant_applications)
.where(eq(tenant_applications.id, id))
.returning();
}
// Get application statistics for a property
export async function getApplicationStats(property_id: string) {
const applications = await getApplicationsByProperty(property_id);
return {
total: applications.length,
pending:
applications.filter((app) => app.application_status === "pending").length,
approved:
applications.filter((app) => app.application_status === "approved")
.length,
rejected:
applications.filter((app) => app.application_status === "rejected")
.length,
};
}
// Get tenant applications by landlord (via properties they own)
export async function getTenantApplicationsByLandlord(landlord_id: string) {
if (isE2EEnabled()) {
return getE2ETenantApplicationsByLandlord(landlord_id);
}
return await db.select({
id: tenant_applications.id,
property_id: tenant_applications.property_id,
applicant_id: tenant_applications.applicant_id,
application_status: tenant_applications.application_status,
application_data_encrypted: tenant_applications.application_data_encrypted,
submitted_on: tenant_applications.submitted_on,
reviewed_on: tenant_applications.reviewed_on,
})
.from(tenant_applications)
.innerJoin(properties, eq(tenant_applications.property_id, properties.id))
.where(eq(properties.owner_id, landlord_id))
.orderBy(tenant_applications.submitted_on);
}
// Update tenant application status (alias for compatibility)
export async function updateTenantApplicationStatus(
id: string,
status: "approved" | "rejected",
reviewed_on?: number,
) {
if (isE2EEnabled()) {
updateE2ETenantApplicationStatus(id, status);
return [];
}
return await updateApplicationStatus(id, status, reviewed_on);
}