attach phone numbers to twilio account
This commit is contained in:
		@@ -5,6 +5,7 @@ import { type Message, Prisma } from "@prisma/client";
 | 
				
			|||||||
 | 
					
 | 
				
			||||||
import db from "~/utils/db.server";
 | 
					import db from "~/utils/db.server";
 | 
				
			||||||
import { requireLoggedIn } from "~/utils/auth.server";
 | 
					import { requireLoggedIn } from "~/utils/auth.server";
 | 
				
			||||||
 | 
					import { redirect } from "@remix-run/node";
 | 
				
			||||||
 | 
					
 | 
				
			||||||
type ConversationType = {
 | 
					type ConversationType = {
 | 
				
			||||||
	recipient: string;
 | 
						recipient: string;
 | 
				
			||||||
@@ -17,16 +18,20 @@ export type ConversationLoaderData = {
 | 
				
			|||||||
};
 | 
					};
 | 
				
			||||||
 | 
					
 | 
				
			||||||
const loader: LoaderFunction = async ({ request, params }) => {
 | 
					const loader: LoaderFunction = async ({ request, params }) => {
 | 
				
			||||||
	const { organization } = await requireLoggedIn(request);
 | 
						const { twilio } = await requireLoggedIn(request);
 | 
				
			||||||
 | 
						if (!twilio) {
 | 
				
			||||||
 | 
							return redirect("/messages");
 | 
				
			||||||
 | 
						}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
						const twilioAccountSid = twilio.accountSid;
 | 
				
			||||||
	const recipient = decodeURIComponent(params.recipient ?? "");
 | 
						const recipient = decodeURIComponent(params.recipient ?? "");
 | 
				
			||||||
	const conversation = await getConversation(recipient);
 | 
						const conversation = await getConversation(recipient);
 | 
				
			||||||
 | 
					
 | 
				
			||||||
	return json<ConversationLoaderData>({ conversation });
 | 
						return json<ConversationLoaderData>({ conversation });
 | 
				
			||||||
 | 
					
 | 
				
			||||||
	async function getConversation(recipient: string): Promise<ConversationType> {
 | 
						async function getConversation(recipient: string): Promise<ConversationType> {
 | 
				
			||||||
		const organizationId = organization.id;
 | 
					 | 
				
			||||||
		const phoneNumber = await db.phoneNumber.findUnique({
 | 
							const phoneNumber = await db.phoneNumber.findUnique({
 | 
				
			||||||
			where: { organizationId_isCurrent: { organizationId, isCurrent: true } },
 | 
								where: { twilioAccountSid_isCurrent: { twilioAccountSid, isCurrent: true } },
 | 
				
			||||||
		});
 | 
							});
 | 
				
			||||||
		if (!phoneNumber || phoneNumber.isFetchingMessages) {
 | 
							if (!phoneNumber || phoneNumber.isFetchingMessages) {
 | 
				
			||||||
			throw new Error("unreachable");
 | 
								throw new Error("unreachable");
 | 
				
			||||||
 
 | 
				
			|||||||
@@ -41,7 +41,15 @@ const action: ActionFunction = async ({ request }) => {
 | 
				
			|||||||
export type SetPhoneNumberActionData = FormActionData<typeof validations, "setPhoneNumber">;
 | 
					export type SetPhoneNumberActionData = FormActionData<typeof validations, "setPhoneNumber">;
 | 
				
			||||||
 | 
					
 | 
				
			||||||
async function setPhoneNumber(request: Request, formData: unknown) {
 | 
					async function setPhoneNumber(request: Request, formData: unknown) {
 | 
				
			||||||
	const { organization } = await requireLoggedIn(request);
 | 
						const { organization, twilio } = await requireLoggedIn(request);
 | 
				
			||||||
 | 
						if (!twilio) {
 | 
				
			||||||
 | 
							return badRequest<SetPhoneNumberActionData>({
 | 
				
			||||||
 | 
								setPhoneNumber: {
 | 
				
			||||||
 | 
									errors: { general: "Connect your Twilio account first" },
 | 
				
			||||||
 | 
								},
 | 
				
			||||||
 | 
							});
 | 
				
			||||||
 | 
						}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
	const validation = validate(validations.setPhoneNumber, formData);
 | 
						const validation = validate(validations.setPhoneNumber, formData);
 | 
				
			||||||
	if (validation.errors) {
 | 
						if (validation.errors) {
 | 
				
			||||||
		return badRequest<SetPhoneNumberActionData>({ setPhoneNumber: { errors: validation.errors } });
 | 
							return badRequest<SetPhoneNumberActionData>({ setPhoneNumber: { errors: validation.errors } });
 | 
				
			||||||
@@ -49,7 +57,7 @@ async function setPhoneNumber(request: Request, formData: unknown) {
 | 
				
			|||||||
 | 
					
 | 
				
			||||||
	try {
 | 
						try {
 | 
				
			||||||
		await db.phoneNumber.update({
 | 
							await db.phoneNumber.update({
 | 
				
			||||||
			where: { organizationId_isCurrent: { organizationId: organization.id, isCurrent: true } },
 | 
								where: { twilioAccountSid_isCurrent: { twilioAccountSid: twilio.accountSid, isCurrent: true } },
 | 
				
			||||||
			data: { isCurrent: false },
 | 
								data: { isCurrent: false },
 | 
				
			||||||
		});
 | 
							});
 | 
				
			||||||
	} catch (error: any) {
 | 
						} catch (error: any) {
 | 
				
			||||||
@@ -150,7 +158,7 @@ async function setTwilioCredentials(request: Request, formData: unknown) {
 | 
				
			|||||||
				await db.phoneNumber.create({
 | 
									await db.phoneNumber.create({
 | 
				
			||||||
					data: {
 | 
										data: {
 | 
				
			||||||
						id: phoneNumberId,
 | 
											id: phoneNumberId,
 | 
				
			||||||
						organizationId: organization.id,
 | 
											twilioAccountSid,
 | 
				
			||||||
						number: phoneNumber.phoneNumber,
 | 
											number: phoneNumber.phoneNumber,
 | 
				
			||||||
						isCurrent: false,
 | 
											isCurrent: false,
 | 
				
			||||||
						isFetchingCalls: true,
 | 
											isFetchingCalls: true,
 | 
				
			||||||
@@ -187,7 +195,7 @@ async function setTwilioCredentials(request: Request, formData: unknown) {
 | 
				
			|||||||
}
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
async function refreshPhoneNumbers(request: Request) {
 | 
					async function refreshPhoneNumbers(request: Request) {
 | 
				
			||||||
	const { organization, twilio } = await requireLoggedIn(request);
 | 
						const { twilio } = await requireLoggedIn(request);
 | 
				
			||||||
	if (!twilio) {
 | 
						if (!twilio) {
 | 
				
			||||||
		throw new Error("unreachable");
 | 
							throw new Error("unreachable");
 | 
				
			||||||
	}
 | 
						}
 | 
				
			||||||
@@ -205,13 +213,19 @@ async function refreshPhoneNumbers(request: Request) {
 | 
				
			|||||||
				await db.phoneNumber.create({
 | 
									await db.phoneNumber.create({
 | 
				
			||||||
					data: {
 | 
										data: {
 | 
				
			||||||
						id: phoneNumberId,
 | 
											id: phoneNumberId,
 | 
				
			||||||
						organizationId: organization.id,
 | 
											twilioAccountSid: twilioAccount.accountSid,
 | 
				
			||||||
						number: phoneNumber.phoneNumber,
 | 
											number: phoneNumber.phoneNumber,
 | 
				
			||||||
						isCurrent: false,
 | 
											isCurrent: false,
 | 
				
			||||||
						isFetchingCalls: true,
 | 
											isFetchingCalls: true,
 | 
				
			||||||
						isFetchingMessages: true,
 | 
											isFetchingMessages: true,
 | 
				
			||||||
					},
 | 
										},
 | 
				
			||||||
				});
 | 
									});
 | 
				
			||||||
 | 
								} catch (error: any) {
 | 
				
			||||||
 | 
									if (error.code !== "P2002") {
 | 
				
			||||||
 | 
										// if it's not a duplicate, it's a real error we need to handle
 | 
				
			||||||
 | 
										throw error;
 | 
				
			||||||
 | 
									}
 | 
				
			||||||
 | 
								}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
			await Promise.all([
 | 
								await Promise.all([
 | 
				
			||||||
				fetchPhoneCallsQueue.add(`fetch calls of id=${phoneNumberId}`, {
 | 
									fetchPhoneCallsQueue.add(`fetch calls of id=${phoneNumberId}`, {
 | 
				
			||||||
@@ -221,12 +235,6 @@ async function refreshPhoneNumbers(request: Request) {
 | 
				
			|||||||
					phoneNumberId,
 | 
										phoneNumberId,
 | 
				
			||||||
				}),
 | 
									}),
 | 
				
			||||||
			]);
 | 
								]);
 | 
				
			||||||
			} catch (error: any) {
 | 
					 | 
				
			||||||
				if (error.code !== "P2002") {
 | 
					 | 
				
			||||||
					// if it's not a duplicate, it's a real error we need to handle
 | 
					 | 
				
			||||||
					throw error;
 | 
					 | 
				
			||||||
				}
 | 
					 | 
				
			||||||
			}
 | 
					 | 
				
			||||||
		}),
 | 
							}),
 | 
				
			||||||
	);
 | 
						);
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 
 | 
				
			|||||||
@@ -10,13 +10,16 @@ import type { SetPhoneNumberActionData } from "~/features/settings/actions/phone
 | 
				
			|||||||
import clsx from "clsx";
 | 
					import clsx from "clsx";
 | 
				
			||||||
 | 
					
 | 
				
			||||||
export default function PhoneNumberForm() {
 | 
					export default function PhoneNumberForm() {
 | 
				
			||||||
	const { twilio } = useSession();
 | 
						const { twilio, phoneNumber } = useSession();
 | 
				
			||||||
	const fetcher = useFetcher();
 | 
						const fetcher = useFetcher();
 | 
				
			||||||
	const transition = useTransition();
 | 
						const transition = useTransition();
 | 
				
			||||||
	const actionData = useActionData<SetPhoneNumberActionData>()?.setPhoneNumber;
 | 
						const actionData = useActionData<SetPhoneNumberActionData>()?.setPhoneNumber;
 | 
				
			||||||
	const availablePhoneNumbers = useLoaderData<PhoneSettingsLoaderData>().phoneNumbers;
 | 
						const availablePhoneNumbers = useLoaderData<PhoneSettingsLoaderData>().phoneNumbers;
 | 
				
			||||||
 | 
					
 | 
				
			||||||
	const isSubmitting = transition.state === "submitting";
 | 
						const actionSubmitted = transition.submission?.formData.get("_action");
 | 
				
			||||||
 | 
						const isCurrentFormTransition =
 | 
				
			||||||
 | 
							!!actionSubmitted && ["setPhoneNumber", "refreshPhoneNumbers"].includes(actionSubmitted.toString());
 | 
				
			||||||
 | 
						const isSubmitting = isCurrentFormTransition && transition.state === "submitting";
 | 
				
			||||||
	const isSuccess = actionData?.submitted === true;
 | 
						const isSuccess = actionData?.submitted === true;
 | 
				
			||||||
	const errors = actionData?.errors;
 | 
						const errors = actionData?.errors;
 | 
				
			||||||
	const topErrorMessage = errors?.general ?? errors?.phoneNumberSid;
 | 
						const topErrorMessage = errors?.general ?? errors?.phoneNumberSid;
 | 
				
			||||||
 
 | 
				
			|||||||
@@ -20,7 +20,7 @@ export default function TwilioConnect() {
 | 
				
			|||||||
 | 
					
 | 
				
			||||||
	const topErrorMessage = actionData?.errors?.general;
 | 
						const topErrorMessage = actionData?.errors?.general;
 | 
				
			||||||
	const isError = typeof topErrorMessage !== "undefined";
 | 
						const isError = typeof topErrorMessage !== "undefined";
 | 
				
			||||||
	const isCurrentFormTransition = transition.submission?.formData.get("_action") === "changePassword";
 | 
						const isCurrentFormTransition = transition.submission?.formData.get("_action") === "setTwilioCredentials";
 | 
				
			||||||
	const isSubmitting = isCurrentFormTransition && transition.state === "submitting";
 | 
						const isSubmitting = isCurrentFormTransition && transition.state === "submitting";
 | 
				
			||||||
 | 
					
 | 
				
			||||||
	return (
 | 
						return (
 | 
				
			||||||
 
 | 
				
			|||||||
@@ -20,7 +20,7 @@ const loader: LoaderFunction = async ({ request }) => {
 | 
				
			|||||||
	}
 | 
						}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
	const phoneNumbers = await db.phoneNumber.findMany({
 | 
						const phoneNumbers = await db.phoneNumber.findMany({
 | 
				
			||||||
		where: { organizationId: organization.id },
 | 
							where: { twilioAccount: { organizationId: organization.id } },
 | 
				
			||||||
		select: { id: true, number: true, isCurrent: true },
 | 
							select: { id: true, number: true, isCurrent: true },
 | 
				
			||||||
		orderBy: { id: Prisma.SortOrder.desc },
 | 
							orderBy: { id: Prisma.SortOrder.desc },
 | 
				
			||||||
	});
 | 
						});
 | 
				
			||||||
 
 | 
				
			|||||||
@@ -12,24 +12,14 @@ export default Queue<Payload>("fetch messages", async ({ data }) => {
 | 
				
			|||||||
	const { phoneNumberId } = data;
 | 
						const { phoneNumberId } = data;
 | 
				
			||||||
	const phoneNumber = await db.phoneNumber.findUnique({
 | 
						const phoneNumber = await db.phoneNumber.findUnique({
 | 
				
			||||||
		where: { id: phoneNumberId },
 | 
							where: { id: phoneNumberId },
 | 
				
			||||||
		include: {
 | 
							include: { twilioAccount: true },
 | 
				
			||||||
			organization: {
 | 
					 | 
				
			||||||
				select: { twilioAccount: true },
 | 
					 | 
				
			||||||
			},
 | 
					 | 
				
			||||||
		},
 | 
					 | 
				
			||||||
	});
 | 
						});
 | 
				
			||||||
	if (!phoneNumber) {
 | 
						if (!phoneNumber) {
 | 
				
			||||||
		logger.warn(`No phone number found with id=${phoneNumberId}`);
 | 
							logger.warn(`No phone number found with id=${phoneNumberId}`);
 | 
				
			||||||
		return;
 | 
							return;
 | 
				
			||||||
	}
 | 
						}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
	const twilioAccount = phoneNumber.organization.twilioAccount;
 | 
						const twilioClient = getTwilioClient(phoneNumber.twilioAccount);
 | 
				
			||||||
	if (!twilioAccount) {
 | 
					 | 
				
			||||||
		logger.warn(`Phone number with id=${phoneNumberId} doesn't have a connected twilio account`);
 | 
					 | 
				
			||||||
		return;
 | 
					 | 
				
			||||||
	}
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
	const twilioClient = getTwilioClient(twilioAccount);
 | 
					 | 
				
			||||||
	const [sent, received] = await Promise.all([
 | 
						const [sent, received] = await Promise.all([
 | 
				
			||||||
		twilioClient.messages.list({ from: phoneNumber.number }),
 | 
							twilioClient.messages.list({ from: phoneNumber.number }),
 | 
				
			||||||
		twilioClient.messages.list({ to: phoneNumber.number }),
 | 
							twilioClient.messages.list({ to: phoneNumber.number }),
 | 
				
			||||||
 
 | 
				
			|||||||
@@ -12,24 +12,14 @@ export default Queue<Payload>("fetch phone calls", async ({ data }) => {
 | 
				
			|||||||
	const { phoneNumberId } = data;
 | 
						const { phoneNumberId } = data;
 | 
				
			||||||
	const phoneNumber = await db.phoneNumber.findUnique({
 | 
						const phoneNumber = await db.phoneNumber.findUnique({
 | 
				
			||||||
		where: { id: phoneNumberId },
 | 
							where: { id: phoneNumberId },
 | 
				
			||||||
		include: {
 | 
							include: { twilioAccount: true },
 | 
				
			||||||
			organization: {
 | 
					 | 
				
			||||||
				select: { twilioAccount: true },
 | 
					 | 
				
			||||||
			},
 | 
					 | 
				
			||||||
		},
 | 
					 | 
				
			||||||
	});
 | 
						});
 | 
				
			||||||
	if (!phoneNumber) {
 | 
						if (!phoneNumber) {
 | 
				
			||||||
		logger.warn(`No phone number found with id=${phoneNumberId}`);
 | 
							logger.warn(`No phone number found with id=${phoneNumberId}`);
 | 
				
			||||||
		return;
 | 
							return;
 | 
				
			||||||
	}
 | 
						}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
	const twilioAccount = phoneNumber.organization.twilioAccount;
 | 
						const twilioClient = getTwilioClient(phoneNumber.twilioAccount);
 | 
				
			||||||
	if (!twilioAccount) {
 | 
					 | 
				
			||||||
		logger.warn(`Phone number with id=${phoneNumberId} doesn't have a connected twilio account`);
 | 
					 | 
				
			||||||
		return;
 | 
					 | 
				
			||||||
	}
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
	const twilioClient = getTwilioClient(twilioAccount);
 | 
					 | 
				
			||||||
	const [callsSent, callsReceived] = await Promise.all([
 | 
						const [callsSent, callsReceived] = await Promise.all([
 | 
				
			||||||
		twilioClient.calls.list({ from: phoneNumber.number }),
 | 
							twilioClient.calls.list({ from: phoneNumber.number }),
 | 
				
			||||||
		twilioClient.calls.list({ to: phoneNumber.number }),
 | 
							twilioClient.calls.list({ to: phoneNumber.number }),
 | 
				
			||||||
 
 | 
				
			|||||||
@@ -16,24 +16,14 @@ export default Queue<Payload>("insert incoming message", async ({ data }) => {
 | 
				
			|||||||
	logger.info(`received message ${messageSid} for ${phoneNumberId}`);
 | 
						logger.info(`received message ${messageSid} for ${phoneNumberId}`);
 | 
				
			||||||
	const phoneNumber = await db.phoneNumber.findUnique({
 | 
						const phoneNumber = await db.phoneNumber.findUnique({
 | 
				
			||||||
		where: { id: phoneNumberId },
 | 
							where: { id: phoneNumberId },
 | 
				
			||||||
		include: {
 | 
							include: { twilioAccount: true },
 | 
				
			||||||
			organization: {
 | 
					 | 
				
			||||||
				select: { twilioAccount: true },
 | 
					 | 
				
			||||||
			},
 | 
					 | 
				
			||||||
		},
 | 
					 | 
				
			||||||
	});
 | 
						});
 | 
				
			||||||
	if (!phoneNumber) {
 | 
						if (!phoneNumber) {
 | 
				
			||||||
		logger.warn(`No phone number found with id=${phoneNumberId}`);
 | 
							logger.warn(`No phone number found with id=${phoneNumberId}`);
 | 
				
			||||||
		return;
 | 
							return;
 | 
				
			||||||
	}
 | 
						}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
	const twilioAccount = phoneNumber.organization.twilioAccount;
 | 
						const twilioClient = getTwilioClient(phoneNumber.twilioAccount);
 | 
				
			||||||
	if (!twilioAccount) {
 | 
					 | 
				
			||||||
		logger.warn(`Phone number with id=${phoneNumberId} doesn't have a connected twilio account`);
 | 
					 | 
				
			||||||
		return;
 | 
					 | 
				
			||||||
	}
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
	const twilioClient = getTwilioClient(twilioAccount);
 | 
					 | 
				
			||||||
	const message = await twilioClient.messages.get(messageSid).fetch();
 | 
						const message = await twilioClient.messages.get(messageSid).fetch();
 | 
				
			||||||
	const status = translateMessageStatus(message.status);
 | 
						const status = translateMessageStatus(message.status);
 | 
				
			||||||
	const direction = translateMessageDirection(message.direction);
 | 
						const direction = translateMessageDirection(message.direction);
 | 
				
			||||||
 
 | 
				
			|||||||
@@ -13,10 +13,7 @@ type Payload = {
 | 
				
			|||||||
 | 
					
 | 
				
			||||||
export default Queue<Payload>("insert messages", async ({ data }) => {
 | 
					export default Queue<Payload>("insert messages", async ({ data }) => {
 | 
				
			||||||
	const { messages, phoneNumberId } = data;
 | 
						const { messages, phoneNumberId } = data;
 | 
				
			||||||
	const phoneNumber = await db.phoneNumber.findUnique({
 | 
						const phoneNumber = await db.phoneNumber.findUnique({ where: { id: phoneNumberId } });
 | 
				
			||||||
		where: { id: phoneNumberId },
 | 
					 | 
				
			||||||
		include: { organization: true },
 | 
					 | 
				
			||||||
	});
 | 
					 | 
				
			||||||
	if (!phoneNumber) {
 | 
						if (!phoneNumber) {
 | 
				
			||||||
		return;
 | 
							return;
 | 
				
			||||||
	}
 | 
						}
 | 
				
			||||||
 
 | 
				
			|||||||
@@ -13,10 +13,7 @@ type Payload = {
 | 
				
			|||||||
 | 
					
 | 
				
			||||||
export default Queue<Payload>("insert phone calls", async ({ data }) => {
 | 
					export default Queue<Payload>("insert phone calls", async ({ data }) => {
 | 
				
			||||||
	const { calls, phoneNumberId } = data;
 | 
						const { calls, phoneNumberId } = data;
 | 
				
			||||||
	const phoneNumber = await db.phoneNumber.findUnique({
 | 
						const phoneNumber = await db.phoneNumber.findUnique({ where: { id: phoneNumberId } });
 | 
				
			||||||
		where: { id: phoneNumberId },
 | 
					 | 
				
			||||||
		include: { organization: true },
 | 
					 | 
				
			||||||
	});
 | 
					 | 
				
			||||||
	if (!phoneNumber) {
 | 
						if (!phoneNumber) {
 | 
				
			||||||
		return;
 | 
							return;
 | 
				
			||||||
	}
 | 
						}
 | 
				
			||||||
@@ -39,8 +36,8 @@ export default Queue<Payload>("insert phone calls", async ({ data }) => {
 | 
				
			|||||||
		})
 | 
							})
 | 
				
			||||||
		.sort((a, b) => a.createdAt.getTime() - b.createdAt.getTime());
 | 
							.sort((a, b) => a.createdAt.getTime() - b.createdAt.getTime());
 | 
				
			||||||
 | 
					
 | 
				
			||||||
	const ddd = await db.phoneCall.createMany({ data: phoneCalls, skipDuplicates: true });
 | 
						const { count } = await db.phoneCall.createMany({ data: phoneCalls, skipDuplicates: true });
 | 
				
			||||||
	logger.info(`inserted ${ddd.count || "no"} new phone calls for phoneNumberId=${phoneNumberId}`);
 | 
						logger.info(`inserted ${count} new phone calls for phoneNumberId=${phoneNumberId}`);
 | 
				
			||||||
 | 
					
 | 
				
			||||||
	if (!phoneNumber.isFetchingCalls) {
 | 
						if (!phoneNumber.isFetchingCalls) {
 | 
				
			||||||
		return;
 | 
							return;
 | 
				
			||||||
 
 | 
				
			|||||||
@@ -14,22 +14,18 @@ type Payload = {
 | 
				
			|||||||
export default Queue<Payload>("set twilio webhooks", async ({ data }) => {
 | 
					export default Queue<Payload>("set twilio webhooks", async ({ data }) => {
 | 
				
			||||||
	const { phoneNumberId, organizationId } = data;
 | 
						const { phoneNumberId, organizationId } = data;
 | 
				
			||||||
	const phoneNumber = await db.phoneNumber.findFirst({
 | 
						const phoneNumber = await db.phoneNumber.findFirst({
 | 
				
			||||||
		where: { id: phoneNumberId, organizationId },
 | 
							where: { id: phoneNumberId, twilioAccount: { organizationId } },
 | 
				
			||||||
		include: {
 | 
							include: {
 | 
				
			||||||
			organization: {
 | 
					 | 
				
			||||||
				select: {
 | 
					 | 
				
			||||||
			twilioAccount: {
 | 
								twilioAccount: {
 | 
				
			||||||
				select: { accountSid: true, twimlAppSid: true, authToken: true },
 | 
									select: { accountSid: true, twimlAppSid: true, authToken: true },
 | 
				
			||||||
			},
 | 
								},
 | 
				
			||||||
		},
 | 
							},
 | 
				
			||||||
			},
 | 
					 | 
				
			||||||
		},
 | 
					 | 
				
			||||||
	});
 | 
						});
 | 
				
			||||||
	if (!phoneNumber || !phoneNumber.organization.twilioAccount) {
 | 
						if (!phoneNumber) {
 | 
				
			||||||
		return;
 | 
							return;
 | 
				
			||||||
	}
 | 
						}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
	const twilioAccount = phoneNumber.organization.twilioAccount;
 | 
						const twilioAccount = phoneNumber.twilioAccount;
 | 
				
			||||||
	const authToken = decrypt(twilioAccount.authToken);
 | 
						const authToken = decrypt(twilioAccount.authToken);
 | 
				
			||||||
	const twilioClient = twilio(twilioAccount.accountSid, authToken);
 | 
						const twilioClient = twilio(twilioAccount.accountSid, authToken);
 | 
				
			||||||
	const twimlApp = await getTwimlApplication(twilioClient, twilioAccount.twimlAppSid);
 | 
						const twimlApp = await getTwimlApplication(twilioClient, twilioAccount.twimlAppSid);
 | 
				
			||||||
 
 | 
				
			|||||||
@@ -13,6 +13,8 @@ export const action: ActionFunction = async () => {
 | 
				
			|||||||
	const phoneNumber = await db.phoneNumber.findUnique({
 | 
						const phoneNumber = await db.phoneNumber.findUnique({
 | 
				
			||||||
		where: { id: "PN4f11f0c4155dfb5d5ac8bbab2cc23cbc" },
 | 
							where: { id: "PN4f11f0c4155dfb5d5ac8bbab2cc23cbc" },
 | 
				
			||||||
		select: {
 | 
							select: {
 | 
				
			||||||
 | 
								twilioAccount: {
 | 
				
			||||||
 | 
									include: {
 | 
				
			||||||
					organization: {
 | 
										organization: {
 | 
				
			||||||
						select: {
 | 
											select: {
 | 
				
			||||||
							memberships: {
 | 
												memberships: {
 | 
				
			||||||
@@ -21,8 +23,10 @@ export const action: ActionFunction = async () => {
 | 
				
			|||||||
						},
 | 
											},
 | 
				
			||||||
					},
 | 
										},
 | 
				
			||||||
				},
 | 
									},
 | 
				
			||||||
 | 
								},
 | 
				
			||||||
 | 
							},
 | 
				
			||||||
	});
 | 
						});
 | 
				
			||||||
	const subscriptions = phoneNumber!.organization.memberships.flatMap(
 | 
						const subscriptions = phoneNumber!.twilioAccount.organization.memberships.flatMap(
 | 
				
			||||||
		(membership) => membership.notificationSubscription,
 | 
							(membership) => membership.notificationSubscription,
 | 
				
			||||||
	);
 | 
						);
 | 
				
			||||||
	await notify(subscriptions, {
 | 
						await notify(subscriptions, {
 | 
				
			||||||
 
 | 
				
			|||||||
@@ -22,11 +22,19 @@ export const action: ActionFunction = async ({ request }) => {
 | 
				
			|||||||
		const organizationId = body.From.slice("client:".length).split("__")[0];
 | 
							const organizationId = body.From.slice("client:".length).split("__")[0];
 | 
				
			||||||
 | 
					
 | 
				
			||||||
		try {
 | 
							try {
 | 
				
			||||||
 | 
								const twilioAccount = await db.twilioAccount.findUnique({ where: { organizationId } });
 | 
				
			||||||
 | 
								if (!twilioAccount) {
 | 
				
			||||||
 | 
									// this shouldn't be happening
 | 
				
			||||||
 | 
									return new Response(null, { status: 402 });
 | 
				
			||||||
 | 
								}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
			const phoneNumber = await db.phoneNumber.findUnique({
 | 
								const phoneNumber = await db.phoneNumber.findUnique({
 | 
				
			||||||
				where: { organizationId_isCurrent: { organizationId, isCurrent: true } },
 | 
									where: { twilioAccountSid_isCurrent: { twilioAccountSid: twilioAccount.accountSid, isCurrent: true } },
 | 
				
			||||||
 | 
									include: {
 | 
				
			||||||
 | 
										twilioAccount: {
 | 
				
			||||||
						include: {
 | 
											include: {
 | 
				
			||||||
							organization: {
 | 
												organization: {
 | 
				
			||||||
						include: {
 | 
													select: {
 | 
				
			||||||
									subscriptions: {
 | 
														subscriptions: {
 | 
				
			||||||
										where: {
 | 
															where: {
 | 
				
			||||||
											OR: [
 | 
																OR: [
 | 
				
			||||||
@@ -39,19 +47,20 @@ export const action: ActionFunction = async ({ request }) => {
 | 
				
			|||||||
										},
 | 
															},
 | 
				
			||||||
										orderBy: { lastEventTime: Prisma.SortOrder.desc },
 | 
															orderBy: { lastEventTime: Prisma.SortOrder.desc },
 | 
				
			||||||
									},
 | 
														},
 | 
				
			||||||
							twilioAccount: true,
 | 
													},
 | 
				
			||||||
 | 
												},
 | 
				
			||||||
						},
 | 
											},
 | 
				
			||||||
					},
 | 
										},
 | 
				
			||||||
				},
 | 
									},
 | 
				
			||||||
			});
 | 
								});
 | 
				
			||||||
 | 
					
 | 
				
			||||||
			if (phoneNumber?.organization.subscriptions.length === 0) {
 | 
								if (phoneNumber?.twilioAccount.organization.subscriptions.length === 0) {
 | 
				
			||||||
				// decline the outgoing call because
 | 
									// decline the outgoing call because
 | 
				
			||||||
				// the organization is on the free plan
 | 
									// the organization is on the free plan
 | 
				
			||||||
				return new Response(null, { status: 402 });
 | 
									return new Response(null, { status: 402 });
 | 
				
			||||||
			}
 | 
								}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
			const encryptedAuthToken = phoneNumber?.organization.twilioAccount?.authToken;
 | 
								const encryptedAuthToken = phoneNumber?.twilioAccount.authToken;
 | 
				
			||||||
			const authToken = encryptedAuthToken ? decrypt(encryptedAuthToken) : "";
 | 
								const authToken = encryptedAuthToken ? decrypt(encryptedAuthToken) : "";
 | 
				
			||||||
			if (
 | 
								if (
 | 
				
			||||||
				!phoneNumber ||
 | 
									!phoneNumber ||
 | 
				
			||||||
 
 | 
				
			|||||||
@@ -20,8 +20,10 @@ export const action: ActionFunction = async ({ request }) => {
 | 
				
			|||||||
		const phoneNumbers = await db.phoneNumber.findMany({
 | 
							const phoneNumbers = await db.phoneNumber.findMany({
 | 
				
			||||||
			where: { number: body.To },
 | 
								where: { number: body.To },
 | 
				
			||||||
			include: {
 | 
								include: {
 | 
				
			||||||
				organization: {
 | 
									twilioAccount: {
 | 
				
			||||||
					include: {
 | 
										include: {
 | 
				
			||||||
 | 
											organization: {
 | 
				
			||||||
 | 
												select: {
 | 
				
			||||||
								subscriptions: {
 | 
													subscriptions: {
 | 
				
			||||||
									where: {
 | 
														where: {
 | 
				
			||||||
										OR: [
 | 
															OR: [
 | 
				
			||||||
@@ -34,7 +36,8 @@ export const action: ActionFunction = async ({ request }) => {
 | 
				
			|||||||
									},
 | 
														},
 | 
				
			||||||
									orderBy: { lastEventTime: Prisma.SortOrder.desc },
 | 
														orderBy: { lastEventTime: Prisma.SortOrder.desc },
 | 
				
			||||||
								},
 | 
													},
 | 
				
			||||||
						twilioAccount: true,
 | 
												},
 | 
				
			||||||
 | 
											},
 | 
				
			||||||
					},
 | 
										},
 | 
				
			||||||
				},
 | 
									},
 | 
				
			||||||
			},
 | 
								},
 | 
				
			||||||
@@ -45,7 +48,7 @@ export const action: ActionFunction = async ({ request }) => {
 | 
				
			|||||||
		}
 | 
							}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
		const phoneNumbersWithActiveSub = phoneNumbers.filter(
 | 
							const phoneNumbersWithActiveSub = phoneNumbers.filter(
 | 
				
			||||||
			(phoneNumber) => phoneNumber.organization.subscriptions.length > 0,
 | 
								(phoneNumber) => phoneNumber.twilioAccount.organization.subscriptions.length > 0,
 | 
				
			||||||
		);
 | 
							);
 | 
				
			||||||
		if (phoneNumbersWithActiveSub.length === 0) {
 | 
							if (phoneNumbersWithActiveSub.length === 0) {
 | 
				
			||||||
			// accept the webhook but don't store incoming message
 | 
								// accept the webhook but don't store incoming message
 | 
				
			||||||
@@ -57,7 +60,7 @@ export const action: ActionFunction = async ({ request }) => {
 | 
				
			|||||||
			// if multiple organizations have the same number
 | 
								// if multiple organizations have the same number
 | 
				
			||||||
			// find the organization currently using that phone number
 | 
								// find the organization currently using that phone number
 | 
				
			||||||
			// maybe we shouldn't let that happen by restricting a phone number to one org?
 | 
								// maybe we shouldn't let that happen by restricting a phone number to one org?
 | 
				
			||||||
			const encryptedAuthToken = phoneNumber.organization.twilioAccount?.authToken;
 | 
								const encryptedAuthToken = phoneNumber.twilioAccount.authToken;
 | 
				
			||||||
			const authToken = encryptedAuthToken ? decrypt(encryptedAuthToken) : "";
 | 
								const authToken = encryptedAuthToken ? decrypt(encryptedAuthToken) : "";
 | 
				
			||||||
			return twilio.validateRequest(authToken, twilioSignature, smsUrl, body);
 | 
								return twilio.validateRequest(authToken, twilioSignature, smsUrl, body);
 | 
				
			||||||
		});
 | 
							});
 | 
				
			||||||
 
 | 
				
			|||||||
@@ -199,7 +199,7 @@ async function buildSessionData(id: string): Promise<SessionData> {
 | 
				
			|||||||
	}));
 | 
						}));
 | 
				
			||||||
	const { twilioAccount, ...organization } = organizations[0];
 | 
						const { twilioAccount, ...organization } = organizations[0];
 | 
				
			||||||
	const phoneNumber = await db.phoneNumber.findUnique({
 | 
						const phoneNumber = await db.phoneNumber.findUnique({
 | 
				
			||||||
		where: { organizationId_isCurrent: { organizationId: organization.id, isCurrent: true } },
 | 
							where: { twilioAccountSid_isCurrent: { twilioAccountSid: twilioAccount?.accountSid ?? "", isCurrent: true } },
 | 
				
			||||||
	});
 | 
						});
 | 
				
			||||||
	return {
 | 
						return {
 | 
				
			||||||
		user: rest,
 | 
							user: rest,
 | 
				
			||||||
 
 | 
				
			|||||||
@@ -151,7 +151,7 @@ CREATE TABLE "PhoneNumber" (
 | 
				
			|||||||
    "isCurrent" BOOLEAN NOT NULL,
 | 
					    "isCurrent" BOOLEAN NOT NULL,
 | 
				
			||||||
    "isFetchingMessages" BOOLEAN,
 | 
					    "isFetchingMessages" BOOLEAN,
 | 
				
			||||||
    "isFetchingCalls" BOOLEAN,
 | 
					    "isFetchingCalls" BOOLEAN,
 | 
				
			||||||
    "organizationId" TEXT NOT NULL,
 | 
					    "twilioAccountSid" TEXT NOT NULL,
 | 
				
			||||||
 | 
					
 | 
				
			||||||
    CONSTRAINT "PhoneNumber_pkey" PRIMARY KEY ("id")
 | 
					    CONSTRAINT "PhoneNumber_pkey" PRIMARY KEY ("id")
 | 
				
			||||||
);
 | 
					);
 | 
				
			||||||
@@ -195,7 +195,7 @@ CREATE INDEX "Message_phoneNumberId_recipient_idx" ON "Message"("phoneNumberId",
 | 
				
			|||||||
CREATE INDEX "PhoneCall_phoneNumberId_recipient_idx" ON "PhoneCall"("phoneNumberId", "recipient");
 | 
					CREATE INDEX "PhoneCall_phoneNumberId_recipient_idx" ON "PhoneCall"("phoneNumberId", "recipient");
 | 
				
			||||||
 | 
					
 | 
				
			||||||
-- CreateIndex
 | 
					-- CreateIndex
 | 
				
			||||||
CREATE UNIQUE INDEX "PhoneNumber_organizationId_isCurrent_key" ON "PhoneNumber"("organizationId", "isCurrent") WHERE ("isCurrent" = true);
 | 
					CREATE UNIQUE INDEX "PhoneNumber_twilioAccountSid_isCurrent_key" ON "PhoneNumber"("twilioAccountSid", "isCurrent") WHERE ("isCurrent" = true);
 | 
				
			||||||
 | 
					
 | 
				
			||||||
-- CreateIndex
 | 
					-- CreateIndex
 | 
				
			||||||
CREATE UNIQUE INDEX "NotificationSubscription_endpoint_key" ON "NotificationSubscription"("endpoint");
 | 
					CREATE UNIQUE INDEX "NotificationSubscription_endpoint_key" ON "NotificationSubscription"("endpoint");
 | 
				
			||||||
@@ -228,7 +228,7 @@ ALTER TABLE "Message" ADD CONSTRAINT "Message_phoneNumberId_fkey" FOREIGN KEY ("
 | 
				
			|||||||
ALTER TABLE "PhoneCall" ADD CONSTRAINT "PhoneCall_phoneNumberId_fkey" FOREIGN KEY ("phoneNumberId") REFERENCES "PhoneNumber"("id") ON DELETE CASCADE ON UPDATE CASCADE;
 | 
					ALTER TABLE "PhoneCall" ADD CONSTRAINT "PhoneCall_phoneNumberId_fkey" FOREIGN KEY ("phoneNumberId") REFERENCES "PhoneNumber"("id") ON DELETE CASCADE ON UPDATE CASCADE;
 | 
				
			||||||
 | 
					
 | 
				
			||||||
-- AddForeignKey
 | 
					-- AddForeignKey
 | 
				
			||||||
ALTER TABLE "PhoneNumber" ADD CONSTRAINT "PhoneNumber_organizationId_fkey" FOREIGN KEY ("organizationId") REFERENCES "Organization"("id") ON DELETE CASCADE ON UPDATE CASCADE;
 | 
					ALTER TABLE "PhoneNumber" ADD CONSTRAINT "PhoneNumber_twilioAccountSid_fkey" FOREIGN KEY ("twilioAccountSid") REFERENCES "TwilioAccount"("accountSid") ON DELETE CASCADE ON UPDATE CASCADE;
 | 
				
			||||||
 | 
					
 | 
				
			||||||
-- AddForeignKey
 | 
					-- AddForeignKey
 | 
				
			||||||
ALTER TABLE "NotificationSubscription" ADD CONSTRAINT "NotificationSubscription_membershipId_fkey" FOREIGN KEY ("membershipId") REFERENCES "Membership"("id") ON DELETE CASCADE ON UPDATE CASCADE;
 | 
					ALTER TABLE "NotificationSubscription" ADD CONSTRAINT "NotificationSubscription_membershipId_fkey" FOREIGN KEY ("membershipId") REFERENCES "Membership"("id") ON DELETE CASCADE ON UPDATE CASCADE;
 | 
				
			||||||
 
 | 
				
			|||||||
@@ -15,9 +15,9 @@ model TwilioAccount {
 | 
				
			|||||||
  twimlAppSid    String?
 | 
					  twimlAppSid    String?
 | 
				
			||||||
  apiKeySid      String?
 | 
					  apiKeySid      String?
 | 
				
			||||||
  apiKeySecret   String?
 | 
					  apiKeySecret   String?
 | 
				
			||||||
 | 
					 | 
				
			||||||
  organizationId String        @unique
 | 
					  organizationId String        @unique
 | 
				
			||||||
  organization   Organization  @relation(fields: [organizationId], references: [id], onDelete: Cascade)
 | 
					  organization   Organization  @relation(fields: [organizationId], references: [id], onDelete: Cascade)
 | 
				
			||||||
 | 
					  phoneNumbers   PhoneNumber[]
 | 
				
			||||||
}
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
model Organization {
 | 
					model Organization {
 | 
				
			||||||
@@ -27,7 +27,6 @@ model Organization {
 | 
				
			|||||||
 | 
					
 | 
				
			||||||
  twilioAccount TwilioAccount?
 | 
					  twilioAccount TwilioAccount?
 | 
				
			||||||
  memberships   Membership[]
 | 
					  memberships   Membership[]
 | 
				
			||||||
  phoneNumbers  PhoneNumber[]
 | 
					 | 
				
			||||||
  subscriptions Subscription[] // many subscriptions to keep a history
 | 
					  subscriptions Subscription[] // many subscriptions to keep a history
 | 
				
			||||||
}
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
@@ -139,12 +138,12 @@ model PhoneNumber {
 | 
				
			|||||||
  isCurrent          Boolean
 | 
					  isCurrent          Boolean
 | 
				
			||||||
  isFetchingMessages Boolean?
 | 
					  isFetchingMessages Boolean?
 | 
				
			||||||
  isFetchingCalls    Boolean?
 | 
					  isFetchingCalls    Boolean?
 | 
				
			||||||
  organizationId     String
 | 
					  twilioAccountSid   String
 | 
				
			||||||
  organization       Organization @relation(fields: [organizationId], references: [id], onDelete: Cascade)
 | 
					  twilioAccount      TwilioAccount @relation(fields: [twilioAccountSid], references: [accountSid], onDelete: Cascade)
 | 
				
			||||||
  messages           Message[]
 | 
					  messages           Message[]
 | 
				
			||||||
  phoneCalls         PhoneCall[]
 | 
					  phoneCalls         PhoneCall[]
 | 
				
			||||||
 | 
					
 | 
				
			||||||
  @@unique([organizationId, isCurrent])
 | 
					  @@unique([twilioAccountSid, isCurrent])
 | 
				
			||||||
}
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
model NotificationSubscription {
 | 
					model NotificationSubscription {
 | 
				
			||||||
 
 | 
				
			|||||||
		Reference in New Issue
	
	Block a user