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

149 lines
4.2 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";
import {
addE2ETenantInvite,
getE2ETwitterInviteById,
getE2ETwitterInviteByProperty,
getE2ETwitterInvitesByEmail,
isE2EEnabled,
updateE2ETwitterInvite,
} from "./e2eFixtures.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 } = {},
) {
if (isE2EEnabled()) {
return [addE2ETenantInvite(landlordId, propertyId, options)];
}
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> {
if (isE2EEnabled()) {
return getE2ETwitterInviteById(inviteId);
}
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> {
if (isE2EEnabled()) {
return getE2ETwitterInviteByProperty(landlordId, propertyId);
}
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>,
) {
if (isE2EEnabled()) {
const updated = updateE2ETwitterInvite(inviteId, updates);
return updated ? [updated] : [];
}
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 [];
if (isE2EEnabled()) {
return getE2ETwitterInvitesByEmail(normalizedEmail);
}
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 };
}