integratedhub/services/tenantInviteService.ts

145 lines
4.1 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;
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<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();
}
/** Extract plain email from "Display Name <email@example.com>" 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 };
}