integratedhub/services/leaseService.ts

467 lines
12 KiB
TypeScript

// services/leaseService.ts
// Handle lease management, creation, updates, and tenant relationships
import { and, eq, inArray } from "drizzle-orm";
import { db } from "../db/db.ts";
import { leases, properties, tenant_invites } from "../db/schema.ts";
import { Lease } from "../db/interfaces.ts";
import { getPropertyById } from "./propertyService.ts";
// export interface Lease {
// id: string;
// title: string | null;
// property_id: string;
// property_address: string;
// tenant_id: string | null;
// owner_id: string;
// start_date: number;
// end_date: number;
// rent: number | null;
// security_deposit: number | null;
// lease_status: string | null; // 'draft', 'active', 'expired', 'terminated'
// lease_document_url: string | null;
// payment_schedule_id: string | null;
// late_fee_rules: string | null; // JSON string of late fee rules
// auto_renewal: boolean | null;
// encrypted_data: string | null;
// created_on: number;
// updated_on: number;
// }
// Create new lease
export async function createLease(
title: string,
property_id: string,
owner_id: string,
start_date: number,
end_date: number,
rent: number,
security_deposit: number = 0,
tenant_id?: string,
payment_schedule_id?: string,
) {
const now = Date.now();
return await db.insert(leases)
.values({
id: crypto.randomUUID(),
title,
property_id,
tenant_id: tenant_id || null,
owner_id,
start_date,
end_date,
rent,
security_deposit,
lease_status: "new",
lease_document_url: null,
payment_schedule_id: payment_schedule_id || null,
late_fee_rules: null,
auto_renewal: false,
encrypted_data: null,
created_on: now,
updated_on: now,
})
.returning();
}
// Get leases by property
export async function getLeasesByProperty(property_id: string) {
return await db.select()
.from(leases)
.where(eq(leases.property_id, property_id))
.orderBy(leases.created_on);
}
// Get leases by owner (landlord)
export async function getLeasesByOwner(owner_id: string) {
return await db.select()
.from(leases)
.where(eq(leases.owner_id, owner_id))
.orderBy(leases.created_on);
}
// Get leases by tenant
export async function getLeasesByTenant(tenant_id: string) {
return await db.select()
.from(leases)
.where(eq(leases.tenant_id, tenant_id))
.orderBy(leases.created_on);
}
// Get active leases by owner
export async function getActiveLeasesByOwner(owner_id: string) {
return await db.select()
.from(leases)
.where(and(
eq(leases.owner_id, owner_id),
eq(leases.lease_status, "active"),
))
.orderBy(leases.created_on);
}
// Get current lease for a property
export async function getCurrentLeaseForProperty(property_id: string) {
const result = await db.select()
.from(leases)
.where(and(
eq(leases.property_id, property_id),
eq(leases.lease_status, "active"),
));
return result[0] || null;
}
// Verify if tenant has an active lease for a property
export async function verifyTenantForProperty(
tenant_id: string,
property_id: string,
): Promise<boolean> {
const result = await db.select()
.from(leases)
.where(and(
eq(leases.property_id, property_id),
eq(leases.tenant_id, tenant_id),
eq(leases.lease_status, "active"),
));
return result.length > 0;
}
// Get lease by ID
export async function getLeaseById(id: string) {
const result = await db.select()
.from(leases)
.where(eq(leases.id, id));
return result[0] || null;
}
// Update lease
export async function updateLease(
id: string,
updates: Partial<Omit<Lease, "id" | "created_on">>,
) {
const now = Date.now();
return await db.update(leases)
.set({ ...updates, updated_on: now })
.where(eq(leases.id, id))
.returning();
}
// Assign tenant to lease
export async function assignTenantToLease(lease_id: string, tenant_id: string) {
const now = Date.now();
return await db.update(leases)
.set({
tenant_id,
lease_status: "active",
updated_on: now,
})
.where(eq(leases.id, lease_id))
.returning();
}
// Update lease status
export async function updateLeaseStatus(
id: string,
status: "draft" | "active" | "expired" | "terminated",
) {
const now = Date.now();
return await db.update(leases)
.set({
lease_status: status,
updated_on: now,
})
.where(eq(leases.id, id))
.returning();
}
// Set lease document URL
export async function setLeaseDocument(lease_id: string, document_url: string) {
const now = Date.now();
return await db.update(leases)
.set({
lease_document_url: document_url,
updated_on: now,
})
.where(eq(leases.id, lease_id))
.returning();
}
// Set payment schedule
export async function setPaymentSchedule(
lease_id: string,
payment_schedule_id: string,
) {
const now = Date.now();
return await db.update(leases)
.set({
payment_schedule_id,
updated_on: now,
})
.where(eq(leases.id, lease_id))
.returning();
}
// Set late fee rules
export async function setLateFeeRules(
lease_id: string,
late_fee_rules: object,
) {
const now = Date.now();
return await db.update(leases)
.set({
late_fee_rules: JSON.stringify(late_fee_rules),
updated_on: now,
})
.where(eq(leases.id, lease_id))
.returning();
}
// Enable/disable auto renewal
export async function setAutoRenewal(lease_id: string, auto_renewal: boolean) {
const now = Date.now();
return await db.update(leases)
.set({
auto_renewal,
updated_on: now,
})
.where(eq(leases.id, lease_id))
.returning();
}
// Delete lease
export async function deleteLease(id: string) {
return await db.delete(leases)
.where(eq(leases.id, id))
.returning();
}
// Sign a lease (mark it as signed by a user)
export async function signLease(lease_id: string, _user_id: string) {
const now = Date.now();
return await db.update(leases)
.set({
lease_document_url: `signed_${lease_id}_${now}`,
updated_on: now,
})
.where(eq(leases.id, lease_id))
.returning();
}
// Get lease statistics for an owner
export async function getLeaseStatsByOwner(owner_id: string) {
const allLeases = await getLeasesByOwner(owner_id);
return {
total: allLeases.length,
draft: allLeases.filter((l) => l.lease_status === "draft").length,
active: allLeases.filter((l) => l.lease_status === "active").length,
expired: allLeases.filter((l) => l.lease_status === "expired").length,
terminated: allLeases.filter((l) => l.lease_status === "terminated").length,
with_tenant: allLeases.filter((l) => l.tenant_id !== null).length,
without_tenant: allLeases.filter((l) => l.tenant_id === null).length,
};
}
/**
* Link a lease to a tenant invite. If an inviteId is provided, update that
* tenant_invites row to reference this lease. Otherwise attempt to find an
* existing invite by: lease's property and owner and attach lease_id
* there; if none exists, create a new tenant_invites row linking to: lease.
*/
export async function inviteLease(lease_id: string, inviteId?: string) {
// If an explicit inviteId was provided, update that invite record
if (inviteId) {
const now = Date.now();
return await db.update(tenant_invites)
.set({ lease_id, updated_on: now })
.where(eq(tenant_invites.id, inviteId))
.returning();
}
// Otherwise derive of property and landlord from lease
const lease = await getLeaseById(lease_id);
if (!lease) return null;
const property_id = lease.property_id;
const landlord_id = lease.owner_id;
if (!property_id || !landlord_id) {
return null;
}
const existing = await db.select().from(tenant_invites).where(
and(
eq(tenant_invites.property_id, property_id),
eq(tenant_invites.landlord_id, landlord_id),
),
);
const now = Date.now();
if (existing.length > 0) {
return await db.update(tenant_invites)
.set({ lease_id, updated_on: now })
.where(eq(tenant_invites.id, existing[0].id))
.returning();
}
// Create a new invite linking to this lease
return await db.insert(tenant_invites).values({
id: crypto.randomUUID(),
landlord_id,
property_id,
lease_id,
available_on: null,
invited_emails: JSON.stringify([]),
created_on: now,
updated_on: now,
}).returning();
}
// Request lease approval from tenant
export async function requestLeaseApproval(
tenant_id: string,
property_id: string,
_invite_id: string,
) {
const now = Date.now();
// Fetch property to populate lease details
const property = await getPropertyById(property_id);
// Build property address from components
const propertyAddress = property
? [
property.address_1,
property.address_2,
property.city,
property.state,
property.zip,
]
.filter(Boolean)
.join(", ")
: "";
return await db.insert(leases)
.values({
id: crypto.randomUUID(),
title: property?.title || `Lease for Property ${property_id}`,
property_id,
property_address: propertyAddress,
tenant_id,
owner_id: null,
start_date: now,
end_date: now + 365 * 24 * 60 * 60 * 1000,
rent: property?.monthly_rent || null,
security_deposit: property?.security_deposit || null,
lease_status: "draft",
approval_status: "pending_approval",
approval_requested_at: now,
approved_at: null,
approved_by: null,
rejection_reason: null,
lease_document_url: null,
payment_schedule_id: null,
late_fee_rules: null,
auto_renewal: false,
encrypted_data: null,
created_on: now,
updated_on: now,
})
.returning();
}
// Approve a lease request
export async function approveLease(
lease_id: string,
landlord_id: string,
property_id?: string,
) {
const now = Date.now();
// If property_id provided, verify landlord owns the property
// (pending leases have owner_id=null, so we check property ownership)
if (property_id) {
const property = await db.select({ owner_id: properties.owner_id })
.from(properties)
.where(eq(properties.id, property_id))
.limit(1);
if (!property || property[0]?.owner_id !== landlord_id) {
return [];
}
}
return await db.update(leases)
.set({
approval_status: "approved",
approved_at: now,
approved_by: landlord_id,
owner_id: landlord_id, // Set owner_id on approval
lease_status: "active",
updated_on: now,
})
.where(eq(leases.id, lease_id))
.returning();
}
// Reject a lease request
export async function rejectLease(
lease_id: string,
landlord_id: string,
rejection_reason?: string,
property_id?: string,
) {
const now = Date.now();
// If property_id provided, verify landlord owns the property
// (pending leases have owner_id=null, so we check property ownership)
if (property_id) {
const property = await db.select({ owner_id: properties.owner_id })
.from(properties)
.where(eq(properties.id, property_id))
.limit(1);
if (!property || property[0]?.owner_id !== landlord_id) {
return [];
}
}
return await db.update(leases)
.set({
approval_status: "rejected",
rejection_reason: rejection_reason || null,
updated_on: now,
})
.where(eq(leases.id, lease_id))
.returning();
}
// Get pending lease approvals for a landlord
// Queries by property ownership since pending leases have owner_id=null
export async function getPendingApprovals(landlord_id: string) {
// First get property IDs owned by this landlord
const landlordProperties = await db.select({ id: properties.id })
.from(properties)
.where(eq(properties.owner_id, landlord_id));
const propertyIds = landlordProperties.map((p) => p.id);
if (propertyIds.length === 0) {
return [];
}
// Then get pending leases for those properties
const pendingLeases = await db.select()
.from(leases)
.where(
and(
eq(leases.approval_status, "pending_approval"),
// Only return leases for properties owned by this landlord
inArray(leases.property_id, propertyIds),
),
)
.orderBy(leases.approval_requested_at);
return pendingLeases;
}