176 lines
4.9 KiB
TypeScript
176 lines
4.9 KiB
TypeScript
// services/tenantInviteService.ts
|
|
// Handle tenant invite creation and updates
|
|
|
|
import { and, eq, ilike, desc } 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,
|
|
deleteE2ETenantInvite,
|
|
getE2ETwitterInviteById,
|
|
getE2ETwitterInviteByProperty,
|
|
getE2ETwitterInvitesByEmail,
|
|
getE2ETenantInvitesByLandlord,
|
|
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 };
|
|
}
|
|
|
|
export async function getTenantInvitesByLandlord(landlordId: string): Promise<TenantInvite[]> {
|
|
if (isE2EEnabled()) {
|
|
return getE2ETenantInvitesByLandlord(landlordId);
|
|
}
|
|
|
|
const result = await db
|
|
.select()
|
|
.from(tenant_invites)
|
|
.where(eq(tenant_invites.landlord_id, landlordId))
|
|
.orderBy(desc(tenant_invites.created_on));
|
|
|
|
return result;
|
|
}
|
|
|
|
export async function deleteTenantInvite(inviteId: string): Promise<void> {
|
|
if (isE2EEnabled()) {
|
|
deleteE2ETenantInvite(inviteId);
|
|
return;
|
|
}
|
|
|
|
await db
|
|
.delete(tenant_invites)
|
|
.where(eq(tenant_invites.id, inviteId));
|
|
}
|