178 lines
5.4 KiB
TypeScript
178 lines
5.4 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";
|
|
|
|
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.");
|
|
}
|
|
|
|
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,
|
|
};
|
|
}
|