attach phone numbers to twilio account
This commit is contained in:
parent
c47b57e4bf
commit
3ddd0d73ea
@ -5,6 +5,7 @@ import { type Message, Prisma } from "@prisma/client";
|
||||
|
||||
import db from "~/utils/db.server";
|
||||
import { requireLoggedIn } from "~/utils/auth.server";
|
||||
import { redirect } from "@remix-run/node";
|
||||
|
||||
type ConversationType = {
|
||||
recipient: string;
|
||||
@ -17,16 +18,20 @@ export type ConversationLoaderData = {
|
||||
};
|
||||
|
||||
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 conversation = await getConversation(recipient);
|
||||
|
||||
return json<ConversationLoaderData>({ conversation });
|
||||
|
||||
async function getConversation(recipient: string): Promise<ConversationType> {
|
||||
const organizationId = organization.id;
|
||||
const phoneNumber = await db.phoneNumber.findUnique({
|
||||
where: { organizationId_isCurrent: { organizationId, isCurrent: true } },
|
||||
where: { twilioAccountSid_isCurrent: { twilioAccountSid, isCurrent: true } },
|
||||
});
|
||||
if (!phoneNumber || phoneNumber.isFetchingMessages) {
|
||||
throw new Error("unreachable");
|
||||
|
@ -41,7 +41,15 @@ const action: ActionFunction = async ({ request }) => {
|
||||
export type SetPhoneNumberActionData = FormActionData<typeof validations, "setPhoneNumber">;
|
||||
|
||||
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);
|
||||
if (validation.errors) {
|
||||
return badRequest<SetPhoneNumberActionData>({ setPhoneNumber: { errors: validation.errors } });
|
||||
@ -49,7 +57,7 @@ async function setPhoneNumber(request: Request, formData: unknown) {
|
||||
|
||||
try {
|
||||
await db.phoneNumber.update({
|
||||
where: { organizationId_isCurrent: { organizationId: organization.id, isCurrent: true } },
|
||||
where: { twilioAccountSid_isCurrent: { twilioAccountSid: twilio.accountSid, isCurrent: true } },
|
||||
data: { isCurrent: false },
|
||||
});
|
||||
} catch (error: any) {
|
||||
@ -150,7 +158,7 @@ async function setTwilioCredentials(request: Request, formData: unknown) {
|
||||
await db.phoneNumber.create({
|
||||
data: {
|
||||
id: phoneNumberId,
|
||||
organizationId: organization.id,
|
||||
twilioAccountSid,
|
||||
number: phoneNumber.phoneNumber,
|
||||
isCurrent: false,
|
||||
isFetchingCalls: true,
|
||||
@ -187,7 +195,7 @@ async function setTwilioCredentials(request: Request, formData: unknown) {
|
||||
}
|
||||
|
||||
async function refreshPhoneNumbers(request: Request) {
|
||||
const { organization, twilio } = await requireLoggedIn(request);
|
||||
const { twilio } = await requireLoggedIn(request);
|
||||
if (!twilio) {
|
||||
throw new Error("unreachable");
|
||||
}
|
||||
@ -205,28 +213,28 @@ async function refreshPhoneNumbers(request: Request) {
|
||||
await db.phoneNumber.create({
|
||||
data: {
|
||||
id: phoneNumberId,
|
||||
organizationId: organization.id,
|
||||
twilioAccountSid: twilioAccount.accountSid,
|
||||
number: phoneNumber.phoneNumber,
|
||||
isCurrent: false,
|
||||
isFetchingCalls: true,
|
||||
isFetchingMessages: true,
|
||||
},
|
||||
});
|
||||
|
||||
await Promise.all([
|
||||
fetchPhoneCallsQueue.add(`fetch calls of id=${phoneNumberId}`, {
|
||||
phoneNumberId,
|
||||
}),
|
||||
fetchMessagesQueue.add(`fetch messages of id=${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;
|
||||
}
|
||||
}
|
||||
|
||||
await Promise.all([
|
||||
fetchPhoneCallsQueue.add(`fetch calls of id=${phoneNumberId}`, {
|
||||
phoneNumberId,
|
||||
}),
|
||||
fetchMessagesQueue.add(`fetch messages of id=${phoneNumberId}`, {
|
||||
phoneNumberId,
|
||||
}),
|
||||
]);
|
||||
}),
|
||||
);
|
||||
|
||||
|
@ -10,13 +10,16 @@ import type { SetPhoneNumberActionData } from "~/features/settings/actions/phone
|
||||
import clsx from "clsx";
|
||||
|
||||
export default function PhoneNumberForm() {
|
||||
const { twilio } = useSession();
|
||||
const { twilio, phoneNumber } = useSession();
|
||||
const fetcher = useFetcher();
|
||||
const transition = useTransition();
|
||||
const actionData = useActionData<SetPhoneNumberActionData>()?.setPhoneNumber;
|
||||
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 errors = actionData?.errors;
|
||||
const topErrorMessage = errors?.general ?? errors?.phoneNumberSid;
|
||||
|
@ -20,7 +20,7 @@ export default function TwilioConnect() {
|
||||
|
||||
const topErrorMessage = actionData?.errors?.general;
|
||||
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";
|
||||
|
||||
return (
|
||||
|
@ -20,7 +20,7 @@ const loader: LoaderFunction = async ({ request }) => {
|
||||
}
|
||||
|
||||
const phoneNumbers = await db.phoneNumber.findMany({
|
||||
where: { organizationId: organization.id },
|
||||
where: { twilioAccount: { organizationId: organization.id } },
|
||||
select: { id: true, number: true, isCurrent: true },
|
||||
orderBy: { id: Prisma.SortOrder.desc },
|
||||
});
|
||||
|
@ -12,24 +12,14 @@ export default Queue<Payload>("fetch messages", async ({ data }) => {
|
||||
const { phoneNumberId } = data;
|
||||
const phoneNumber = await db.phoneNumber.findUnique({
|
||||
where: { id: phoneNumberId },
|
||||
include: {
|
||||
organization: {
|
||||
select: { twilioAccount: true },
|
||||
},
|
||||
},
|
||||
include: { twilioAccount: true },
|
||||
});
|
||||
if (!phoneNumber) {
|
||||
logger.warn(`No phone number found with id=${phoneNumberId}`);
|
||||
return;
|
||||
}
|
||||
|
||||
const twilioAccount = phoneNumber.organization.twilioAccount;
|
||||
if (!twilioAccount) {
|
||||
logger.warn(`Phone number with id=${phoneNumberId} doesn't have a connected twilio account`);
|
||||
return;
|
||||
}
|
||||
|
||||
const twilioClient = getTwilioClient(twilioAccount);
|
||||
const twilioClient = getTwilioClient(phoneNumber.twilioAccount);
|
||||
const [sent, received] = await Promise.all([
|
||||
twilioClient.messages.list({ from: 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 phoneNumber = await db.phoneNumber.findUnique({
|
||||
where: { id: phoneNumberId },
|
||||
include: {
|
||||
organization: {
|
||||
select: { twilioAccount: true },
|
||||
},
|
||||
},
|
||||
include: { twilioAccount: true },
|
||||
});
|
||||
if (!phoneNumber) {
|
||||
logger.warn(`No phone number found with id=${phoneNumberId}`);
|
||||
return;
|
||||
}
|
||||
|
||||
const twilioAccount = phoneNumber.organization.twilioAccount;
|
||||
if (!twilioAccount) {
|
||||
logger.warn(`Phone number with id=${phoneNumberId} doesn't have a connected twilio account`);
|
||||
return;
|
||||
}
|
||||
|
||||
const twilioClient = getTwilioClient(twilioAccount);
|
||||
const twilioClient = getTwilioClient(phoneNumber.twilioAccount);
|
||||
const [callsSent, callsReceived] = await Promise.all([
|
||||
twilioClient.calls.list({ from: 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}`);
|
||||
const phoneNumber = await db.phoneNumber.findUnique({
|
||||
where: { id: phoneNumberId },
|
||||
include: {
|
||||
organization: {
|
||||
select: { twilioAccount: true },
|
||||
},
|
||||
},
|
||||
include: { twilioAccount: true },
|
||||
});
|
||||
if (!phoneNumber) {
|
||||
logger.warn(`No phone number found with id=${phoneNumberId}`);
|
||||
return;
|
||||
}
|
||||
|
||||
const twilioAccount = phoneNumber.organization.twilioAccount;
|
||||
if (!twilioAccount) {
|
||||
logger.warn(`Phone number with id=${phoneNumberId} doesn't have a connected twilio account`);
|
||||
return;
|
||||
}
|
||||
|
||||
const twilioClient = getTwilioClient(twilioAccount);
|
||||
const twilioClient = getTwilioClient(phoneNumber.twilioAccount);
|
||||
const message = await twilioClient.messages.get(messageSid).fetch();
|
||||
const status = translateMessageStatus(message.status);
|
||||
const direction = translateMessageDirection(message.direction);
|
||||
|
@ -13,10 +13,7 @@ type Payload = {
|
||||
|
||||
export default Queue<Payload>("insert messages", async ({ data }) => {
|
||||
const { messages, phoneNumberId } = data;
|
||||
const phoneNumber = await db.phoneNumber.findUnique({
|
||||
where: { id: phoneNumberId },
|
||||
include: { organization: true },
|
||||
});
|
||||
const phoneNumber = await db.phoneNumber.findUnique({ where: { id: phoneNumberId } });
|
||||
if (!phoneNumber) {
|
||||
return;
|
||||
}
|
||||
|
@ -13,10 +13,7 @@ type Payload = {
|
||||
|
||||
export default Queue<Payload>("insert phone calls", async ({ data }) => {
|
||||
const { calls, phoneNumberId } = data;
|
||||
const phoneNumber = await db.phoneNumber.findUnique({
|
||||
where: { id: phoneNumberId },
|
||||
include: { organization: true },
|
||||
});
|
||||
const phoneNumber = await db.phoneNumber.findUnique({ where: { id: phoneNumberId } });
|
||||
if (!phoneNumber) {
|
||||
return;
|
||||
}
|
||||
@ -39,8 +36,8 @@ export default Queue<Payload>("insert phone calls", async ({ data }) => {
|
||||
})
|
||||
.sort((a, b) => a.createdAt.getTime() - b.createdAt.getTime());
|
||||
|
||||
const ddd = await db.phoneCall.createMany({ data: phoneCalls, skipDuplicates: true });
|
||||
logger.info(`inserted ${ddd.count || "no"} new phone calls for phoneNumberId=${phoneNumberId}`);
|
||||
const { count } = await db.phoneCall.createMany({ data: phoneCalls, skipDuplicates: true });
|
||||
logger.info(`inserted ${count} new phone calls for phoneNumberId=${phoneNumberId}`);
|
||||
|
||||
if (!phoneNumber.isFetchingCalls) {
|
||||
return;
|
||||
|
@ -14,22 +14,18 @@ type Payload = {
|
||||
export default Queue<Payload>("set twilio webhooks", async ({ data }) => {
|
||||
const { phoneNumberId, organizationId } = data;
|
||||
const phoneNumber = await db.phoneNumber.findFirst({
|
||||
where: { id: phoneNumberId, organizationId },
|
||||
where: { id: phoneNumberId, twilioAccount: { organizationId } },
|
||||
include: {
|
||||
organization: {
|
||||
select: {
|
||||
twilioAccount: {
|
||||
select: { accountSid: true, twimlAppSid: true, authToken: true },
|
||||
},
|
||||
},
|
||||
twilioAccount: {
|
||||
select: { accountSid: true, twimlAppSid: true, authToken: true },
|
||||
},
|
||||
},
|
||||
});
|
||||
if (!phoneNumber || !phoneNumber.organization.twilioAccount) {
|
||||
if (!phoneNumber) {
|
||||
return;
|
||||
}
|
||||
|
||||
const twilioAccount = phoneNumber.organization.twilioAccount;
|
||||
const twilioAccount = phoneNumber.twilioAccount;
|
||||
const authToken = decrypt(twilioAccount.authToken);
|
||||
const twilioClient = twilio(twilioAccount.accountSid, authToken);
|
||||
const twimlApp = await getTwimlApplication(twilioClient, twilioAccount.twimlAppSid);
|
||||
|
@ -13,16 +13,20 @@ export const action: ActionFunction = async () => {
|
||||
const phoneNumber = await db.phoneNumber.findUnique({
|
||||
where: { id: "PN4f11f0c4155dfb5d5ac8bbab2cc23cbc" },
|
||||
select: {
|
||||
organization: {
|
||||
select: {
|
||||
memberships: {
|
||||
select: { notificationSubscription: true },
|
||||
twilioAccount: {
|
||||
include: {
|
||||
organization: {
|
||||
select: {
|
||||
memberships: {
|
||||
select: { notificationSubscription: true },
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
});
|
||||
const subscriptions = phoneNumber!.organization.memberships.flatMap(
|
||||
const subscriptions = phoneNumber!.twilioAccount.organization.memberships.flatMap(
|
||||
(membership) => membership.notificationSubscription,
|
||||
);
|
||||
await notify(subscriptions, {
|
||||
|
@ -22,36 +22,45 @@ export const action: ActionFunction = async ({ request }) => {
|
||||
const organizationId = body.From.slice("client:".length).split("__")[0];
|
||||
|
||||
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({
|
||||
where: { organizationId_isCurrent: { organizationId, isCurrent: true } },
|
||||
where: { twilioAccountSid_isCurrent: { twilioAccountSid: twilioAccount.accountSid, isCurrent: true } },
|
||||
include: {
|
||||
organization: {
|
||||
twilioAccount: {
|
||||
include: {
|
||||
subscriptions: {
|
||||
where: {
|
||||
OR: [
|
||||
{ status: { not: SubscriptionStatus.deleted } },
|
||||
{
|
||||
status: SubscriptionStatus.deleted,
|
||||
cancellationEffectiveDate: { gt: new Date() },
|
||||
organization: {
|
||||
select: {
|
||||
subscriptions: {
|
||||
where: {
|
||||
OR: [
|
||||
{ status: { not: SubscriptionStatus.deleted } },
|
||||
{
|
||||
status: SubscriptionStatus.deleted,
|
||||
cancellationEffectiveDate: { gt: new Date() },
|
||||
},
|
||||
],
|
||||
},
|
||||
],
|
||||
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
|
||||
// the organization is on the free plan
|
||||
return new Response(null, { status: 402 });
|
||||
}
|
||||
|
||||
const encryptedAuthToken = phoneNumber?.organization.twilioAccount?.authToken;
|
||||
const encryptedAuthToken = phoneNumber?.twilioAccount.authToken;
|
||||
const authToken = encryptedAuthToken ? decrypt(encryptedAuthToken) : "";
|
||||
if (
|
||||
!phoneNumber ||
|
||||
|
@ -20,21 +20,24 @@ export const action: ActionFunction = async ({ request }) => {
|
||||
const phoneNumbers = await db.phoneNumber.findMany({
|
||||
where: { number: body.To },
|
||||
include: {
|
||||
organization: {
|
||||
twilioAccount: {
|
||||
include: {
|
||||
subscriptions: {
|
||||
where: {
|
||||
OR: [
|
||||
{ status: { not: SubscriptionStatus.deleted } },
|
||||
{
|
||||
status: SubscriptionStatus.deleted,
|
||||
cancellationEffectiveDate: { gt: new Date() },
|
||||
organization: {
|
||||
select: {
|
||||
subscriptions: {
|
||||
where: {
|
||||
OR: [
|
||||
{ status: { not: SubscriptionStatus.deleted } },
|
||||
{
|
||||
status: SubscriptionStatus.deleted,
|
||||
cancellationEffectiveDate: { gt: new Date() },
|
||||
},
|
||||
],
|
||||
},
|
||||
],
|
||||
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(
|
||||
(phoneNumber) => phoneNumber.organization.subscriptions.length > 0,
|
||||
(phoneNumber) => phoneNumber.twilioAccount.organization.subscriptions.length > 0,
|
||||
);
|
||||
if (phoneNumbersWithActiveSub.length === 0) {
|
||||
// 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
|
||||
// find the organization currently using that phone number
|
||||
// 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) : "";
|
||||
return twilio.validateRequest(authToken, twilioSignature, smsUrl, body);
|
||||
});
|
||||
|
@ -199,7 +199,7 @@ async function buildSessionData(id: string): Promise<SessionData> {
|
||||
}));
|
||||
const { twilioAccount, ...organization } = organizations[0];
|
||||
const phoneNumber = await db.phoneNumber.findUnique({
|
||||
where: { organizationId_isCurrent: { organizationId: organization.id, isCurrent: true } },
|
||||
where: { twilioAccountSid_isCurrent: { twilioAccountSid: twilioAccount?.accountSid ?? "", isCurrent: true } },
|
||||
});
|
||||
return {
|
||||
user: rest,
|
||||
|
@ -151,7 +151,7 @@ CREATE TABLE "PhoneNumber" (
|
||||
"isCurrent" BOOLEAN NOT NULL,
|
||||
"isFetchingMessages" BOOLEAN,
|
||||
"isFetchingCalls" BOOLEAN,
|
||||
"organizationId" TEXT NOT NULL,
|
||||
"twilioAccountSid" TEXT NOT NULL,
|
||||
|
||||
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");
|
||||
|
||||
-- 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
|
||||
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;
|
||||
|
||||
-- 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
|
||||
ALTER TABLE "NotificationSubscription" ADD CONSTRAINT "NotificationSubscription_membershipId_fkey" FOREIGN KEY ("membershipId") REFERENCES "Membership"("id") ON DELETE CASCADE ON UPDATE CASCADE;
|
||||
|
@ -8,16 +8,16 @@ datasource db {
|
||||
}
|
||||
|
||||
model TwilioAccount {
|
||||
accountSid String @id
|
||||
createdAt DateTime @default(now()) @db.Timestamptz(6)
|
||||
updatedAt DateTime @updatedAt @db.Timestamptz(6)
|
||||
authToken String
|
||||
twimlAppSid String?
|
||||
apiKeySid String?
|
||||
apiKeySecret String?
|
||||
|
||||
organizationId String @unique
|
||||
organization Organization @relation(fields: [organizationId], references: [id], onDelete: Cascade)
|
||||
accountSid String @id
|
||||
createdAt DateTime @default(now()) @db.Timestamptz(6)
|
||||
updatedAt DateTime @updatedAt @db.Timestamptz(6)
|
||||
authToken String
|
||||
twimlAppSid String?
|
||||
apiKeySid String?
|
||||
apiKeySecret String?
|
||||
organizationId String @unique
|
||||
organization Organization @relation(fields: [organizationId], references: [id], onDelete: Cascade)
|
||||
phoneNumbers PhoneNumber[]
|
||||
}
|
||||
|
||||
model Organization {
|
||||
@ -27,7 +27,6 @@ model Organization {
|
||||
|
||||
twilioAccount TwilioAccount?
|
||||
memberships Membership[]
|
||||
phoneNumbers PhoneNumber[]
|
||||
subscriptions Subscription[] // many subscriptions to keep a history
|
||||
}
|
||||
|
||||
@ -133,18 +132,18 @@ model PhoneCall {
|
||||
}
|
||||
|
||||
model PhoneNumber {
|
||||
id String @id
|
||||
createdAt DateTime @default(now()) @db.Timestamptz(6)
|
||||
id String @id
|
||||
createdAt DateTime @default(now()) @db.Timestamptz(6)
|
||||
number String
|
||||
isCurrent Boolean
|
||||
isFetchingMessages Boolean?
|
||||
isFetchingCalls Boolean?
|
||||
organizationId String
|
||||
organization Organization @relation(fields: [organizationId], references: [id], onDelete: Cascade)
|
||||
twilioAccountSid String
|
||||
twilioAccount TwilioAccount @relation(fields: [twilioAccountSid], references: [accountSid], onDelete: Cascade)
|
||||
messages Message[]
|
||||
phoneCalls PhoneCall[]
|
||||
|
||||
@@unique([organizationId, isCurrent])
|
||||
@@unique([twilioAccountSid, isCurrent])
|
||||
}
|
||||
|
||||
model NotificationSubscription {
|
||||
|
Loading…
Reference in New Issue
Block a user