import { CampaignStatus } from "@prisma/client"; import { DEFAULT_DOUBLE_OPT_IN_CONTENT, DEFAULT_DOUBLE_OPT_IN_SUBJECT, hasDoubleOptInUrlPlaceholder, } from "~/lib/constants/double-opt-in"; import { db } from "../db"; import { UnsendApiError } from "../public-api/api-error"; import { validateDomainFromEmail } from "./domain-service"; import { LimitService } from "./limit-service"; import { normalizeContactBookVariables, validateContactBookVariables, } from "./contact-variable-service"; type ContactBookDbClient = Pick; export async function getContactBooks(teamId: number, search?: string) { return db.contactBook.findMany({ where: { teamId, ...(search ? { name: { contains: search, mode: "insensitive" } } : {}), }, select: { id: true, name: true, teamId: true, properties: true, variables: true, emoji: true, createdAt: true, updatedAt: true, doubleOptInEnabled: true, doubleOptInFrom: true, doubleOptInSubject: true, doubleOptInContent: true, _count: { select: { contacts: true }, }, }, }); } export async function createContactBook( teamId: number, name: string, variables?: string[], client: ContactBookDbClient = db, ) { const { isLimitReached, reason } = await LimitService.checkContactBookLimit(teamId); if (isLimitReached) { throw new UnsendApiError({ code: "FORBIDDEN", message: reason ?? "Contact book limit reached", }); } const normalizedVariables = normalizeContactBookVariables(variables); try { validateContactBookVariables(normalizedVariables); } catch (error) { throw new UnsendApiError({ code: "BAD_REQUEST", message: error instanceof Error ? error.message : "Invalid variables", }); } const created = await client.contactBook.create({ data: { name, teamId, properties: {}, variables: normalizedVariables, doubleOptInEnabled: true, doubleOptInSubject: DEFAULT_DOUBLE_OPT_IN_SUBJECT, doubleOptInContent: DEFAULT_DOUBLE_OPT_IN_CONTENT, }, }); return created; } export async function getContactBookDetails(contactBookId: string) { const [totalContacts, unsubscribedContacts, campaigns] = await Promise.all([ db.contact.count({ where: { contactBookId }, }), db.contact.count({ where: { contactBookId, subscribed: false }, }), db.campaign.findMany({ where: { contactBookId, status: CampaignStatus.SENT, }, orderBy: { createdAt: "desc", }, take: 2, }), ]); return { totalContacts, unsubscribedContacts, campaigns, }; } export async function updateContactBook( contactBookId: string, data: { name?: string; properties?: Record; emoji?: string; variables?: string[]; doubleOptInEnabled?: boolean; doubleOptInFrom?: string | null; doubleOptInSubject?: string; doubleOptInContent?: string; }, client: ContactBookDbClient = db, ) { const restData = { ...data }; delete restData.variables; const normalizedVariables = data.variables === undefined ? undefined : normalizeContactBookVariables(data.variables); if (normalizedVariables !== undefined) { try { validateContactBookVariables(normalizedVariables); } catch (error) { throw new UnsendApiError({ code: "BAD_REQUEST", message: error instanceof Error ? error.message : "Invalid variables", }); } } const updateData: { name?: string; properties?: Record; emoji?: string; variables?: string[]; doubleOptInEnabled?: boolean; doubleOptInSubject?: string; doubleOptInContent?: string; } = { ...restData, ...(normalizedVariables !== undefined ? { variables: normalizedVariables } : {}), }; if (data.doubleOptInFrom !== undefined) { const normalizedFrom = data.doubleOptInFrom?.trim() ?? ""; if (!normalizedFrom) { updateData.doubleOptInFrom = null; } else { const contactBook = await client.contactBook.findUnique({ where: { id: contactBookId }, select: { teamId: true }, }); if (!contactBook) { throw new UnsendApiError({ code: "BAD_REQUEST", message: "Contact book not found", }); } await validateDomainFromEmail(normalizedFrom, contactBook.teamId); updateData.doubleOptInFrom = normalizedFrom; } } if ( data.doubleOptInContent !== undefined && !data.doubleOptInContent.trim() ) { updateData.doubleOptInContent = DEFAULT_DOUBLE_OPT_IN_CONTENT; } else if ( data.doubleOptInContent !== undefined && !hasDoubleOptInUrlPlaceholder(data.doubleOptInContent) ) { throw new UnsendApiError({ code: "BAD_REQUEST", message: "Double opt-in email content must include the {{doubleOptInUrl}} placeholder", }); } if ( data.doubleOptInSubject !== undefined && !data.doubleOptInSubject.trim() ) { updateData.doubleOptInSubject = DEFAULT_DOUBLE_OPT_IN_SUBJECT; } if (data.doubleOptInEnabled === true) { const contactBook = await client.contactBook.findUnique({ where: { id: contactBookId }, select: { doubleOptInSubject: true, doubleOptInContent: true, }, }); if (!updateData.doubleOptInSubject && !contactBook?.doubleOptInSubject) { updateData.doubleOptInSubject = DEFAULT_DOUBLE_OPT_IN_SUBJECT; } if (!updateData.doubleOptInContent && !contactBook?.doubleOptInContent) { updateData.doubleOptInContent = DEFAULT_DOUBLE_OPT_IN_CONTENT; } } return client.contactBook.update({ where: { id: contactBookId }, data: updateData, }); } export async function deleteContactBook(contactBookId: string) { const deleted = await db.contactBook.delete({ where: { id: contactBookId } }); return deleted; }