attach phone numbers to twilio account

This commit is contained in:
m5r 2022-06-11 02:09:37 +02:00
parent c47b57e4bf
commit 3ddd0d73ea
17 changed files with 119 additions and 128 deletions

View File

@ -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");

View File

@ -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;
}
}
}), }),
); );

View File

@ -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;

View File

@ -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 (

View File

@ -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 },
}); });

View File

@ -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 }),

View File

@ -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 }),

View File

@ -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);

View File

@ -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;
} }

View File

@ -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;

View File

@ -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);

View File

@ -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, {

View File

@ -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 ||

View File

@ -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);
}); });

View File

@ -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,

View File

@ -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;

View File

@ -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 {