import { eq } from "drizzle-orm"; import { db } from "../db/db.ts"; import { leases, maintenance_requests, properties, property_media, rent_schedules, tenant_applications, } from "../db/schema.ts"; import { Property } from "../db/interfaces.ts"; export interface PropertyMergeResult { canonicalPropertyId: string; duplicatePropertyId: string; updated: { leases: number; rentSchedules: number; tenantApplications: number; maintenanceRequests: number; propertyMedia: number; }; warnings: string[]; } function formatPropertyAddress(property: Property | null) { if (!property) return ""; const street = [property.address_1, property.address_2] .filter(Boolean) .join(" ") .trim(); const cityState = [property.city, property.state] .filter(Boolean) .join(", ") .trim(); const zip = property.zip?.trim() || ""; const region = [cityState, zip].filter(Boolean).join(" ").trim(); return [street, region].filter(Boolean).join(", ").trim(); } export async function mergeDuplicateProperty( canonicalPropertyId: string, duplicatePropertyId: string, ownerId: string, ): Promise { if (!canonicalPropertyId || !duplicatePropertyId) { throw new Error("Missing property selections."); } if (canonicalPropertyId === duplicatePropertyId) { throw new Error("Select two different properties to merge."); } const [canonical] = await db.select() .from(properties) .where(eq(properties.id, canonicalPropertyId)); const [duplicate] = await db.select() .from(properties) .where(eq(properties.id, duplicatePropertyId)); if (!canonical || !duplicate) { throw new Error("Property not found."); } if (canonical.owner_id !== ownerId || duplicate.owner_id !== ownerId) { throw new Error("Property owner mismatch."); } const canonicalAddress = formatPropertyAddress(canonical) || formatPropertyAddress(duplicate); const warnings: string[] = []; const canonicalLeases = await db.select({ id: leases.id, lease_status: leases.lease_status, }) .from(leases) .where(eq(leases.property_id, canonicalPropertyId)); const duplicateLeases = await db.select({ id: leases.id, lease_status: leases.lease_status, }) .from(leases) .where(eq(leases.property_id, duplicatePropertyId)); const hasCanonicalActive = canonicalLeases.some((lease) => lease.lease_status === "active" ); const hasDuplicateActive = duplicateLeases.some((lease) => lease.lease_status === "active" ); if (hasCanonicalActive && hasDuplicateActive) { warnings.push( "Both properties have active leases. Confirm lease expectations after merge.", ); } const canonicalSchedules = await db.select({ id: rent_schedules.id }) .from(rent_schedules) .where(eq(rent_schedules.property_id, canonicalPropertyId)); const duplicateSchedules = await db.select({ id: rent_schedules.id }) .from(rent_schedules) .where(eq(rent_schedules.property_id, duplicatePropertyId)); if (canonicalSchedules.length && duplicateSchedules.length) { warnings.push( "Both properties have rent schedules. Review schedules after merge.", ); } const now = Date.now(); const leaseUpdatePayload: Record = { property_id: canonicalPropertyId, updated_on: now, }; if (canonicalAddress) { leaseUpdatePayload.property_address = canonicalAddress; } let leaseUpdates = 0; let scheduleUpdates = 0; let applicationUpdates = 0; let maintenanceUpdates = 0; let mediaUpdates = 0; await db.transaction(async (tx) => { const leasesResult = await tx.update(leases) .set(leaseUpdatePayload) .where(eq(leases.property_id, duplicatePropertyId)) .returning({ id: leases.id }); leaseUpdates = leasesResult.length; const schedulesResult = await tx.update(rent_schedules) .set({ property_id: canonicalPropertyId, updated_on: now }) .where(eq(rent_schedules.property_id, duplicatePropertyId)) .returning({ id: rent_schedules.id }); scheduleUpdates = schedulesResult.length; const applicationsResult = await tx.update(tenant_applications) .set({ property_id: canonicalPropertyId }) .where(eq(tenant_applications.property_id, duplicatePropertyId)) .returning({ id: tenant_applications.id }); applicationUpdates = applicationsResult.length; const maintenanceResult = await tx.update(maintenance_requests) .set({ property_id: canonicalPropertyId }) .where(eq(maintenance_requests.property_id, duplicatePropertyId)) .returning({ id: maintenance_requests.id }); maintenanceUpdates = maintenanceResult.length; const mediaResult = await tx.update(property_media) .set({ property_id: canonicalPropertyId }) .where(eq(property_media.property_id, duplicatePropertyId)) .returning({ id: property_media.id }); mediaUpdates = mediaResult.length; await tx.update(properties) .set({ updated_on: now }) .where(eq(properties.id, canonicalPropertyId)); await tx.delete(properties) .where(eq(properties.id, duplicatePropertyId)); }); return { canonicalPropertyId, duplicatePropertyId, updated: { leases: leaseUpdates, rentSchedules: scheduleUpdates, tenantApplications: applicationUpdates, maintenanceRequests: maintenanceUpdates, propertyMedia: mediaUpdates, }, warnings, }; }