integratedhub/services/propertyMergeService.ts
2026-01-20 09:02:26 -06:00

201 lines
6.0 KiB
TypeScript

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";
import {
getE2EPropertyById,
isE2EEnabled,
mergeE2EProperty,
} from "./e2eFixtures.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<PropertyMergeResult> {
if (!canonicalPropertyId || !duplicatePropertyId) {
throw new Error("Missing property selections.");
}
if (canonicalPropertyId === duplicatePropertyId) {
throw new Error("Select two different properties to merge.");
}
if (isE2EEnabled()) {
const canonical = getE2EPropertyById(canonicalPropertyId);
const duplicate = getE2EPropertyById(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);
return mergeE2EProperty(
canonicalPropertyId,
duplicatePropertyId,
canonicalAddress,
);
}
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<string, unknown> = {
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,
};
}