// services/landlordService.ts // Handle landlord-specific operations, onboarding workflow, and dashboard data import { and, desc, eq, or } from "drizzle-orm"; import { db } from "../db/db.ts"; import { leases, messages, payment_methods, properties, tenant_applications, users, } from "../db/schema.ts"; import { DashboardTenantApplication } from "./tenantApplicationService.ts"; import { DashboardMaintenanceRequest, getMaintenanceDataByLandlord, } from "./maintenanceRequestService.ts"; import { DashboardMessage } from "./messageService.ts"; import { Lease, Message, PaymentMethod, Property, TenantApplication, User, } from "../db/interfaces.ts"; import { getUser, updateUser } from "./userService.ts"; import { getE2ELandlordDashboardData, getE2EPaymentMethodsByUser, getE2EPropertiesByOwner, getE2EUserById, isE2EEnabled, } from "./e2eFixtures.ts"; export interface LandlordOnboardingData { user_id: string; current_step: number; steps_completed: { profile_setup: boolean; email_verification: boolean; property_added: boolean; payment_setup: boolean; first_property_published: boolean; }; total_steps: number; completion_percentage: number; } export interface LandlordDashboardData { propertyData: { total: number; available: number; occupied: number; draft: number; }; properties?: Property[]; leaseData: { active: number; draft: number; expired: number; }; applicationData: { pending: number; total_this_month: number; }; paymentData: { expected_this_month: number; received_this_month: number; overdue: number; }; tenantApplications?: DashboardTenantApplication[]; maintenanceRequests?: DashboardMaintenanceRequest[]; recentMessages?: DashboardMessage[]; monthlyRevenue?: number; } // Extended dashboard interface with additional UI data arrays export interface ExtendedLandlordDashboardData extends LandlordDashboardData { tenantApplications: DashboardTenantApplication[]; maintenanceRequests: DashboardMaintenanceRequest[]; recentMessages: DashboardMessage[]; monthlyRevenue?: number; } // Initialize landlord user type and onboarding export async function initializeLandlordOnboarding(user_id: string) { const now = Date.now(); return await updateUser(user_id, { user_type: "landlord", onboarding_step: 1, email_verified: false, banking_setup_complete: false, updated_on: now, }); } // Update onboarding step export async function updateOnboardingStep(user_id: string, step: number) { const now = Date.now(); return await updateUser(user_id, { onboarding_step: step, updated_on: now, }); } // Update landlord onboarding step (alias for compatibility with routes) export async function updateLandlordOnboardingStep( user_id: string, step: number, ) { return await updateOnboardingStep(user_id, step); } // Mark email as verified export async function markEmailVerified(user_id: string) { const now = Date.now(); return await updateUser(user_id, { email_verified: true, updated_on: now, }); } // Mark banking setup as complete export async function markBankingSetupComplete(user_id: string) { const now = Date.now(); return await updateUser(user_id, { banking_setup_complete: true, updated_on: now, }); } // Get landlord onboarding status export async function getLandlordOnboardingStatus( user_id: string, ): Promise { if (isE2EEnabled()) { const user = getE2EUserById(user_id); if (!user) { throw new Error("User not found"); } const propertiesResult = getE2EPropertiesByOwner(user_id); const paymentMethodsResult = getE2EPaymentMethodsByUser(user_id); const steps_completed = { profile_setup: !!(user.name && user.email && user.phone), email_verification: user.email_verified || false, property_added: propertiesResult.length > 0, payment_setup: paymentMethodsResult.length > 0, first_property_published: propertiesResult.some((p) => p.rental_status === "available" ), }; const completed_count = Object.values(steps_completed).filter(Boolean) .length; const total_steps = 5; const completion_percentage = Math.round( (completed_count / total_steps) * 100, ); return { user_id, current_step: user.onboarding_step || 1, steps_completed, total_steps, completion_percentage, }; } // Get user data const user = await getUser(user_id); if (!user) { throw new Error("User not found"); } // Get properties count const propertiesResult = await db.select() .from(properties) .where(eq(properties.owner_id, user_id)); // Get payment methods count const paymentMethodsResult = await db.select() .from(payment_methods) .where(eq(payment_methods.user_id, user_id)); // Determine completion status const steps_completed = { profile_setup: !!(user.name && user.email && user.phone), email_verification: user.email_verified || false, property_added: propertiesResult.length > 0, payment_setup: paymentMethodsResult.length > 0, first_property_published: propertiesResult.some((p) => p.rental_status === "available" ), }; const completed_count = Object.values(steps_completed).filter(Boolean).length; const total_steps = 5; const completion_percentage = Math.round( (completed_count / total_steps) * 100, ); return { user_id, current_step: user.onboarding_step || 1, steps_completed, total_steps, completion_percentage, }; } // Get landlord dashboard data export async function getLandlordDashboardData( user_id: string, ): Promise { if (isE2EEnabled()) { return getE2ELandlordDashboardData(user_id); } // Get properties data const propertiesResult = await db.select() .from(properties) .where(eq(properties.owner_id, user_id)); // Get leases data const leasesResult = await db.select() .from(leases) .where(eq(leases.owner_id, user_id)); // Get applications data const property_ids = propertiesResult.map((p) => p.id); let applicationsResult: TenantApplication[] = []; if (property_ids.length > 0) { // In a real implementation, you'd use an IN clause // For now, we'll get all applications and filter applicationsResult = await db.select() .from(tenant_applications); applicationsResult = applicationsResult.filter((app) => app.property_id && property_ids.includes(app.property_id) ); } const tenantApplications = 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, applicant_name: users.name, applicant_email: users.email, property_address: properties.address_1, }) .from(tenant_applications) .innerJoin(properties, eq(tenant_applications.property_id, properties.id)) .leftJoin(users, eq(tenant_applications.applicant_id, users.id)) .where(eq(properties.owner_id, user_id)) .orderBy(tenant_applications.submitted_on); const maintenanceRequests = await getMaintenanceDataByLandlord(user_id); const recentMessages = await db.select({ id: messages.id, lease_id: messages.lease_id, sender_id: messages.sender_id, recipient_id: messages.recipient_id, message_type: messages.message_type, subject: messages.subject, content: messages.content, content_encrypted: messages.content_encrypted, is_read: messages.is_read, created_on: messages.created_on, sender_name: users.name, }) .from(messages) .leftJoin(users, eq(messages.sender_id, users.id)) .where(or( eq(messages.sender_id, user_id), eq(messages.recipient_id, user_id), )) .orderBy(desc(messages.created_on)) .limit(10) as unknown as DashboardMessage[]; // Calculate current month timestamp range const now = new Date(); const startOfMonth = new Date(now.getFullYear(), now.getMonth(), 1).getTime(); const endOfMonth = new Date( now.getFullYear(), now.getMonth() + 1, 0, 23, 59, 59, ).getTime(); // Calculate dashboard metrics const dashboardData: LandlordDashboardData = { propertyData: { total: propertiesResult.length, available: propertiesResult.filter((p) => p.rental_status === "available").length, occupied: propertiesResult.filter((p) => p.rental_status === "occupied").length, draft: propertiesResult.filter((p) => p.rental_status === "draft").length, }, properties: propertiesResult, leaseData: { active: leasesResult.filter((l) => l.lease_status === "active").length, draft: leasesResult.filter((l) => l.lease_status === "draft").length, expired: leasesResult.filter((l) => l.lease_status === "expired").length, }, applicationData: { pending: applicationsResult.filter((app) => app.application_status === "pending") .length, total_this_month: applicationsResult.filter((app) => app.submitted_on && app.submitted_on >= startOfMonth && app.submitted_on <= endOfMonth ).length, }, paymentData: { expected_this_month: leasesResult .filter((l) => l.lease_status === "active") .reduce((sum, lease) => sum + (lease.rent || 0), 0), received_this_month: 0, // Would need payment_transactions data overdue: 0, // Would need payment_transactions data with due date logic }, tenantApplications: tenantApplications.map((app) => ({ ...app, applicant_name: app.applicant_name ?? undefined, applicant_email: app.applicant_email ?? undefined, property_address: app.property_address ?? undefined, status: app.application_status || "pending", created_at: app.submitted_on ?? null, })), maintenanceRequests, recentMessages: recentMessages.map((message) => ({ ...message, sender_name: message.sender_name || "Tenant", created_at: message.created_on ?? null, })), monthlyRevenue: propertiesResult.reduce( (sum, property) => sum + (property.monthly_rent || 0), 0, ), }; return dashboardData; } // Check if user is a landlord export async function isUserLandlord(user_id: string): Promise { if (isE2EEnabled()) { const user = getE2EUserById(user_id); return Boolean( user && (user.user_type === "landlord" || user.onboarded_landlord), ); } const result = await db.select() .from(users) .where(and( eq(users.id, user_id), or(eq(users.user_type, "landlord"), eq(users.onboarded_landlord, true)), )); return result.length > 0; } // Get landlord profile export async function getLandlordProfile(user_id: string) { const result = await db.select() .from(users) .where(and( eq(users.id, user_id), or(eq(users.user_type, "landlord"), eq(users.onboarded_landlord, true)), )); return result[0] || null; } // Complete onboarding export async function completeLandlordOnboarding(user_id: string) { const now = Date.now(); return await db.update(users) .set({ onboarding_step: 6, // Completed updated_on: now, }) .where(eq(users.id, user_id)) .returning(); } // Get onboarding progress for multiple landlords (admin function) export async function getAllLandlordsOnboardingProgress() { const result = await db.select() .from(users) .where( or(eq(users.user_type, "landlord"), eq(users.onboarded_landlord, true)), ); return result.map((user) => ({ user_id: user.id, name: user.name, email: user.email, onboarding_step: user.onboarding_step || 1, email_verified: user.email_verified || false, banking_setup_complete: user.banking_setup_complete || false, started_on: user.started_on, })); }