// services/leaseService.ts // Handle lease management, creation, updates, and tenant relationships import { and, eq } from "drizzle-orm"; import { db } from "../db/db.ts"; import { leases, tenant_invites } from "../db/schema.ts"; import { Lease } from "../db/interfaces.ts"; import { getE2ELeaseById, getE2ELeasesByOwner, getE2ELeasesByTenant, isE2EEnabled, getE2EPendingApprovals, addE2ELeaseWithApproval, } from "./e2eFixtures.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) { if (isE2EEnabled()) { return getE2ELeasesByOwner(owner_id); } 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) { if (isE2EEnabled()) { return getE2ELeasesByTenant(tenant_id); } 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; } // Get lease by ID export async function getLeaseById(id: string) { if (isE2EEnabled()) { return getE2ELeaseById(id); } 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>, ) { 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, ) { if (isE2EEnabled()) { return [addE2ELeaseWithApproval(tenant_id, property_id, invite_id)]; } const now = Date.now(); return await db.insert(leases) .values({ id: crypto.randomUUID(), title: `Lease for Property ${property_id}`, property_id, tenant_id, owner_id: null, start_date: now, end_date: now + 365 * 24 * 60 * 60 * 1000, rent: null, 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, ) { if (isE2EEnabled()) { const lease = getE2ELeaseById(lease_id); if (lease && lease.owner_id === landlord_id) { const now = Date.now(); return [{ ...lease, approval_status: "approved", approved_at: now, approved_by: landlord_id, lease_status: "active", updated_on: now, }]; } return []; } const now = Date.now(); return await db.update(leases) .set({ approval_status: "approved", approved_at: now, approved_by: landlord_id, lease_status: "active", updated_on: now, }) .where( and( eq(leases.id, lease_id), eq(leases.owner_id, landlord_id), ), ) .returning(); } // Reject a lease request export async function rejectLease( lease_id: string, landlord_id: string, rejection_reason?: string, ) { if (isE2EEnabled()) { const lease = getE2ELeaseById(lease_id); if (lease && lease.owner_id === landlord_id) { const now = Date.now(); return [{ ...lease, approval_status: "rejected", rejection_reason: rejection_reason || null, updated_on: now, }]; } return []; } const now = Date.now(); return await db.update(leases) .set({ approval_status: "rejected", rejection_reason: rejection_reason || null, updated_on: now, }) .where( and( eq(leases.id, lease_id), eq(leases.owner_id, landlord_id), ), ) .returning(); } // Get pending lease approvals for a landlord export async function getPendingApprovals(landlord_id: string) { if (isE2EEnabled()) { return getE2EPendingApprovals(landlord_id); } return await db.select() .from(leases) .where( and( eq(leases.owner_id, landlord_id), eq(leases.approval_status, "pending_approval"), ), ) .orderBy(leases.approval_requested_at); }