125 lines
3.6 KiB
TypeScript
125 lines
3.6 KiB
TypeScript
// services/tenantInviteService.ts
|
|
// Handle tenant invite creation and updates
|
|
|
|
import { and, eq, ilike } from "drizzle-orm";
|
|
import { db } from "../db/db.ts";
|
|
import { tenant_invites } from "../db/schema.ts";
|
|
import type { TenantInvite } from "../db/interfaces.ts";
|
|
|
|
function parseInviteEmails(rawEmails?: string | null) {
|
|
if (!rawEmails) return [];
|
|
try {
|
|
const parsed = JSON.parse(rawEmails);
|
|
if (!Array.isArray(parsed)) return [];
|
|
return parsed.filter((entry) => typeof entry === "string");
|
|
} catch {
|
|
return rawEmails.split(/[\n,]+/)
|
|
.map((entry) => entry.trim())
|
|
.filter(Boolean);
|
|
}
|
|
}
|
|
|
|
function normalizeInviteEmail(email: string) {
|
|
return email.trim().toLowerCase();
|
|
}
|
|
|
|
function hasMatchingInviteEmail(
|
|
invite: TenantInvite,
|
|
normalizedEmail: string,
|
|
) {
|
|
const emails = parseInviteEmails(invite.invited_emails);
|
|
return emails.some((email) => email.toLowerCase() === normalizedEmail);
|
|
}
|
|
|
|
export async function createTenantInvite(
|
|
landlordId: string,
|
|
propertyId: string,
|
|
options: { availableOn?: number | null } = {},
|
|
) {
|
|
const existing = await db.select().from(tenant_invites).where(
|
|
and(
|
|
eq(tenant_invites.landlord_id, landlordId),
|
|
eq(tenant_invites.property_id, propertyId),
|
|
),
|
|
);
|
|
if (existing.length > 0) {
|
|
return existing;
|
|
}
|
|
const now = Date.now();
|
|
return await db.insert(tenant_invites).values({
|
|
id: crypto.randomUUID(),
|
|
landlord_id: landlordId,
|
|
property_id: propertyId,
|
|
available_on: options.availableOn ?? null,
|
|
invited_emails: JSON.stringify([]),
|
|
created_on: now,
|
|
updated_on: now,
|
|
}).returning();
|
|
}
|
|
|
|
export async function getTenantInviteById(
|
|
inviteId: string,
|
|
): Promise<TenantInvite | null> {
|
|
const result = await db.select().from(tenant_invites).where(
|
|
eq(tenant_invites.id, inviteId),
|
|
);
|
|
return result[0] || null;
|
|
}
|
|
|
|
export async function getTenantInviteByProperty(
|
|
landlordId: string,
|
|
propertyId: string,
|
|
): Promise<TenantInvite | null> {
|
|
const result = await db.select().from(tenant_invites).where(
|
|
and(
|
|
eq(tenant_invites.property_id, propertyId),
|
|
eq(tenant_invites.landlord_id, landlordId),
|
|
),
|
|
);
|
|
return result[0] || null;
|
|
}
|
|
|
|
export async function updateTenantInvite(
|
|
inviteId: string,
|
|
updates: Partial<TenantInvite>,
|
|
) {
|
|
const now = Date.now();
|
|
return await db.update(tenant_invites)
|
|
.set({ ...updates, updated_on: now })
|
|
.where(eq(tenant_invites.id, inviteId))
|
|
.returning();
|
|
}
|
|
|
|
export async function getTenantInvitesByEmail(email: string) {
|
|
const normalizedEmail = email.trim().toLowerCase();
|
|
if (!normalizedEmail) return [];
|
|
const pattern = `%${normalizedEmail}%`;
|
|
const result = await db.select().from(tenant_invites).where(
|
|
ilike(tenant_invites.invited_emails, pattern),
|
|
);
|
|
return result.filter((invite) =>
|
|
hasMatchingInviteEmail(invite, normalizedEmail)
|
|
);
|
|
}
|
|
|
|
export async function removeTenantInviteEmail(inviteId: string, email: string) {
|
|
const normalizedEmail = normalizeInviteEmail(email);
|
|
if (!normalizedEmail) {
|
|
return { removed: false, invite: null };
|
|
}
|
|
const invite = await getTenantInviteById(inviteId);
|
|
if (!invite) return { removed: false, invite: null };
|
|
|
|
const existingEmails = parseInviteEmails(invite.invited_emails);
|
|
const filteredEmails = existingEmails.filter((entry) =>
|
|
entry.toLowerCase() !== normalizedEmail
|
|
);
|
|
if (filteredEmails.length === existingEmails.length) {
|
|
return { removed: false, invite };
|
|
}
|
|
const updated = await updateTenantInvite(inviteId, {
|
|
invited_emails: JSON.stringify(filteredEmails),
|
|
});
|
|
return { removed: true, invite: updated[0] ?? invite };
|
|
}
|