integratedhub/services/landlordService.ts
open-swe-dev-andrinoid[bot] f88da558b1
[WIP]: Implement Landlord Approval Mechanism for Tenant Leases (#127)
* Apply patch [skip ci]

* Apply patch [skip ci]

* Apply patch [skip ci]

* Apply patch [skip ci]

* Apply patch [skip ci]

* Apply patch [skip ci]

* Apply patch [skip ci]

* Apply patch [skip ci]

* Apply patch [skip ci]

* Apply patch [skip ci]

* Apply patch [skip ci]

* Apply patch [skip ci]

* Apply patch [skip ci]

* Apply patch [skip ci]

* Apply patch [skip ci]

* Apply patch [skip ci]

* Apply patch [skip ci]

* Apply patch [skip ci]

* Apply patch [skip ci]

---------

Co-authored-by: open-swe-dev-andrinoid[bot] <open-swe-dev-andrinoid@users.noreply.github.com>
2026-01-28 16:48:47 -06:00

490 lines
14 KiB
TypeScript

// 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,
getE2EPendingApprovals,
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<LandlordOnboardingData> {
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 pending lease approvals for a landlord
export async function getPendingLeaseApprovals(
landlord_id: string,
) {
if (isE2EEnabled()) {
return getE2EPendingApprovals(landlord_id);
}
const pendingLeases = await db.select()
.from(leases)
.where(
and(
eq(leases.owner_id, landlord_id),
eq(leases.approval_status, "pending_approval"),
),
)
.orderBy(leases.approval_requested_at);
if (pendingLeases.length === 0) {
return [];
}
const propertyIds = pendingLeases
.map((lease) => lease.property_id)
.filter((id): id is string => Boolean(id));
const tenantIds = pendingLeases
.map((lease) => lease.tenant_id)
.filter((id): id is string => Boolean(id));
const propertiesData = propertyIds.length > 0
? await db
.select({
id: properties.id,
title: properties.title,
address_1: properties.address_1,
address_2: properties.address_2,
city: properties.city,
state: properties.state,
zip: properties.zip,
image_url: properties.image_url,
monthly_rent: properties.monthly_rent,
bedrooms: properties.bedrooms,
bathrooms: properties.bathrooms,
})
.from(properties)
.where(eq(properties.id, propertyIds[0]))
: [];
const tenantsData = tenantIds.length > 0
? await db
.select({
id: users.id,
name: users.name,
email: users.email,
phone: users.phone,
})
.from(users)
.where(eq(users.id, tenantIds[0]))
: [];
const propertyMap = new Map(
propertiesData.map((p) => [p.id, p]),
);
const tenantMap = new Map(
tenantsData.map((t) => [t.id, t]),
);
return pendingLeases.map((lease) => ({
...lease,
property: lease.property_id ? propertyMap.get(lease.property_id) : null,
tenant: lease.tenant_id ? tenantMap.get(lease.tenant_id) : null,
}));
}
// Get landlord dashboard data
export async function getLandlordDashboardData(
user_id: string,
): Promise<LandlordDashboardData> {
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<boolean> {
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,
}));
}