216 lines
6.6 KiB
TypeScript
216 lines
6.6 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";
|
|
|
|
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?: Record<string, unknown> | 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 {
|
|
id: string;
|
|
property_id?: string | null;
|
|
applicant_id?: string | null;
|
|
application_status?: string | null;
|
|
application_data_encrypted?: string | null;
|
|
submitted_on?: number | null;
|
|
reviewed_on?: number | null;
|
|
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)
|
|
document_urls_json?: string | null; // UI-friendly string representation
|
|
}
|
|
|
|
// 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;
|
|
const now = Date.now();
|
|
return await db.insert(tenant_applications)
|
|
.values({
|
|
id: crypto.randomUUID(),
|
|
property_id,
|
|
applicant_id,
|
|
application_status: "pending",
|
|
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,
|
|
) {
|
|
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) {
|
|
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);
|
|
}
|
|
|
|
// Get tenant applications based on user role (tenant or landlord)
|
|
// This is the main function for data isolation - filters applications by user context
|
|
export async function getTenantApplicationsByUser(
|
|
user_id: string,
|
|
user_type: string,
|
|
) {
|
|
if (user_type === "landlord") {
|
|
// Landlords see applications for their properties
|
|
return await getTenantApplicationsByLandlord(user_id);
|
|
} else {
|
|
// Tenants see only their own applications
|
|
return await getApplicationsByApplicant(user_id);
|
|
}
|
|
}
|
|
|
|
// Update tenant application status (alias for compatibility)
|
|
export async function updateTenantApplicationStatus(
|
|
id: string,
|
|
status: "approved" | "rejected",
|
|
reviewed_on?: number,
|
|
) {
|
|
return await updateApplicationStatus(id, status, reviewed_on);
|
|
}
|