import { CampaignStatus, Prisma } from "@prisma/client"; import { TRPCError } from "@trpc/server"; import { z } from "zod"; import { contactBookProcedure, createTRPCRouter, teamProcedure, } from "~/server/api/trpc"; import * as contactService from "~/server/service/contact-service"; import * as contactBookService from "~/server/service/contact-book-service"; export const contactsRouter = createTRPCRouter({ getContactBooks: teamProcedure .input(z.object({ search: z.string().optional() })) .query(async ({ ctx: { team }, input }) => { return contactBookService.getContactBooks(team.id, input.search); }), createContactBook: teamProcedure .input( z.object({ name: z.string(), variables: z.array(z.string()).optional(), }), ) .mutation(async ({ ctx: { team }, input }) => { const { name, variables } = input; return contactBookService.createContactBook(team.id, name, variables); }), getContactBookDetails: contactBookProcedure.query( async ({ ctx: { contactBook } }) => { const { totalContacts, unsubscribedContacts, campaigns } = await contactBookService.getContactBookDetails(contactBook.id); return { ...contactBook, totalContacts, unsubscribedContacts, campaigns, }; }, ), updateContactBook: contactBookProcedure .input( z.object({ contactBookId: z.string(), name: z.string().optional(), properties: z.record(z.string()).optional(), emoji: z.string().optional(), doubleOptInEnabled: z.boolean().optional(), doubleOptInFrom: z.string().nullable().optional(), doubleOptInSubject: z.string().optional(), doubleOptInContent: z.string().optional(), variables: z.array(z.string()).optional(), }), ) .mutation(async ({ ctx: { contactBook }, input }) => { const { contactBookId, ...data } = input; return contactBookService.updateContactBook(contactBook.id, data); }), deleteContactBook: contactBookProcedure .input(z.object({ contactBookId: z.string() })) .mutation(async ({ ctx: { contactBook }, input }) => { return contactBookService.deleteContactBook(contactBook.id); }), contacts: contactBookProcedure .input( z.object({ page: z.number().optional(), subscribed: z.boolean().optional(), search: z.string().optional(), }), ) .query(async ({ ctx: { db }, input }) => { const page = input.page || 1; const limit = 30; const offset = (page - 1) * limit; const whereConditions: Prisma.ContactFindManyArgs["where"] = { contactBookId: input.contactBookId, ...(input.subscribed !== undefined ? { subscribed: input.subscribed } : {}), ...(input.search ? { OR: [ { email: { contains: input.search, mode: "insensitive" } }, { firstName: { contains: input.search, mode: "insensitive" } }, { lastName: { contains: input.search, mode: "insensitive" } }, ], } : {}), }; const countP = db.contact.count({ where: whereConditions }); const contactsP = db.contact.findMany({ where: whereConditions, select: { id: true, email: true, firstName: true, lastName: true, properties: true, subscribed: true, createdAt: true, contactBookId: true, unsubscribeReason: true, }, orderBy: { createdAt: "desc", }, skip: offset, take: limit, }); const [contacts, count] = await Promise.all([contactsP, countP]); return { contacts, totalPage: Math.ceil(count / limit) }; }), addContacts: contactBookProcedure .input( z.object({ contacts: z .array( z.object({ email: z.string(), firstName: z.string().optional(), lastName: z.string().optional(), properties: z.record(z.string()).optional(), subscribed: z.boolean().optional(), }), ) .max(50000), }), ) .mutation(async ({ ctx: { contactBook, team }, input }) => { return contactService.bulkAddContacts( contactBook.id, input.contacts, team.id, ); }), updateContact: contactBookProcedure .input( z.object({ contactId: z.string(), email: z.string().optional(), firstName: z.string().optional(), lastName: z.string().optional(), properties: z.record(z.string()).optional(), subscribed: z.boolean().optional(), }), ) .mutation(async ({ ctx: { contactBook, team }, input }) => { const { contactId, ...contact } = input; const updatedContact = await contactService.updateContactInContactBook( contactId, contactBook.id, contact, team.id, ); if (!updatedContact) { throw new TRPCError({ code: "NOT_FOUND", message: "Contact not found", }); } return updatedContact; }), deleteContact: contactBookProcedure .input(z.object({ contactId: z.string() })) .mutation(async ({ ctx: { contactBook, team }, input }) => { const deletedContact = await contactService.deleteContactInContactBook( input.contactId, contactBook.id, team.id, ); if (!deletedContact) { throw new TRPCError({ code: "NOT_FOUND", message: "Contact not found", }); } return deletedContact; }), bulkDeleteContacts: contactBookProcedure .input(z.object({ contactIds: z.array(z.string()).min(1).max(1000) })) .mutation(async ({ ctx: { contactBook, team }, input }) => { const deletedContacts = await contactService.bulkDeleteContactsInContactBook( input.contactIds, contactBook.id, team.id, ); return { count: deletedContacts.length }; }), resendDoubleOptInConfirmation: contactBookProcedure .input(z.object({ contactId: z.string() })) .mutation(async ({ ctx: { contactBook, team }, input }) => { try { const contact = await contactService.resendDoubleOptInConfirmationInContactBook( input.contactId, contactBook.id, team.id, ); if (!contact) { throw new TRPCError({ code: "NOT_FOUND", message: "Contact not found", }); } return { success: true }; } catch (error) { if (error instanceof TRPCError) { throw error; } if ( error instanceof Error && error.message === "Double opt-in confirmation can only be resent to pending contacts" ) { throw new TRPCError({ code: "BAD_REQUEST", message: error.message, }); } throw error; } }), exportContacts: contactBookProcedure .input( z.object({ subscribed: z.boolean().optional(), search: z.string().optional(), }), ) .query(async ({ ctx: { db }, input }) => { const whereConditions: Prisma.ContactFindManyArgs["where"] = { contactBookId: input.contactBookId, ...(input.subscribed !== undefined ? { subscribed: input.subscribed } : {}), ...(input.search ? { OR: [ { email: { contains: input.search, mode: "insensitive" } }, { firstName: { contains: input.search, mode: "insensitive" } }, { lastName: { contains: input.search, mode: "insensitive" } }, ], } : {}), }; const contacts = await db.contact.findMany({ where: whereConditions, select: { email: true, firstName: true, lastName: true, properties: true, subscribed: true, unsubscribeReason: true, createdAt: true, }, orderBy: { createdAt: "desc", }, take: 100000, // Limit to 100k contacts to prevent memory issues }); return contacts; }), });