// 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; invitedEmails?: string[]; } = {}, ) { 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(); const invitedEmails = options.invitedEmails ?? []; 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( invitedEmails.map((e) => String(e).trim()).filter(Boolean), ), created_on: now, updated_on: now, }).returning(); } export async function getTenantInviteById( inviteId: string, ): Promise { 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 { 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, ) { const now = Date.now(); return await db.update(tenant_invites) .set({ ...updates, updated_on: now }) .where(eq(tenant_invites.id, inviteId)) .returning(); } /** Extract plain email from "Display Name " or return trimmed input. */ function toPlainEmail(email: string): string { const trimmed = email.trim(); const match = trimmed.match(/<([^>]+)>/); return (match ? match[1].trim() : trimmed).toLowerCase(); } export async function getTenantInvitesByEmail(email: string) { const normalizedEmail = toPlainEmail(email); if (!normalizedEmail) { return []; } // ILIKE substring match then exact filter (handles JSON array and comma-separated stored format). const pattern = `%${normalizedEmail}%`; const result = await db.select().from(tenant_invites).where( ilike(tenant_invites.invited_emails, pattern), ); return result.filter((invite) => { return 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 }; }