Add alphanumeric sender ID to SMS workflow actions (#5471)

* add sender id

* add sender to twilio from

* added missing sender

* add migration

* fix design of add action dialog

* add cal as sender when creating new workflow

* fix type errors

Co-authored-by: CarinaWolli <wollencarina@gmail.com>
Co-authored-by: Alex van Andel <me@alexvanandel.com>
This commit is contained in:
Carina Wollendorfer 2022-11-11 16:01:17 +01:00 committed by GitHub
parent ef3e7fae20
commit 54f4e665a3
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
13 changed files with 179 additions and 70 deletions

View File

@ -1361,6 +1361,8 @@
"invalid_credential": "Oh no! Looks like permission expired or was revoked. Please reinstall again.", "invalid_credential": "Oh no! Looks like permission expired or was revoked. Please reinstall again.",
"choose_common_schedule_team_event": "Choose a common schedule", "choose_common_schedule_team_event": "Choose a common schedule",
"choose_common_schedule_team_event_description": "Enable this if you want to use a common schedule between hosts. When disabled, each host will be booked based on their default schedule.", "choose_common_schedule_team_event_description": "Enable this if you want to use a common schedule between hosts. When disabled, each host will be booked based on their default schedule.",
"sender_id": "Sender ID",
"sender_id_error_message":"Only letters, numbers and spaces allowed (max. 11 characters)",
"test_routing_form": "Test Routing Form", "test_routing_form": "Test Routing Form",
"test_preview": "Test Preview", "test_preview": "Test Preview",
"route_to": "Route to", "route_to": "Route to",

View File

@ -105,7 +105,12 @@ async function handler(req: NextApiRequest, res: NextApiResponse) {
break; break;
} }
if (message?.length && message?.length > 0 && sendTo) { if (message?.length && message?.length > 0 && sendTo) {
const scheduledSMS = await twilio.scheduleSMS(sendTo, message, reminder.scheduledDate); const scheduledSMS = await twilio.scheduleSMS(
sendTo,
message,
reminder.scheduledDate,
reminder.workflowStep.sender || "Cal"
);
await prisma.workflowReminder.update({ await prisma.workflowReminder.update({
where: { where: {

View File

@ -6,17 +6,18 @@ import { Controller, useForm } from "react-hook-form";
import { z } from "zod"; import { z } from "zod";
import { useLocale } from "@calcom/lib/hooks/useLocale"; import { useLocale } from "@calcom/lib/hooks/useLocale";
import { Button, Checkbox, EmailField, Form, Label } from "@calcom/ui/components"; import { Button, Checkbox, EmailField, Form, Label, TextField } from "@calcom/ui/components";
import PhoneInput from "@calcom/ui/form/PhoneInputLazy"; import PhoneInput from "@calcom/ui/form/PhoneInputLazy";
import { Dialog, DialogClose, DialogContent, DialogFooter, Select } from "@calcom/ui/v2"; import { Dialog, DialogClose, DialogContent, DialogFooter, Select } from "@calcom/ui/v2";
import { WORKFLOW_ACTIONS } from "../../lib/constants"; import { WORKFLOW_ACTIONS } from "../../lib/constants";
import { getWorkflowActionOptions } from "../../lib/getOptions"; import { getWorkflowActionOptions } from "../../lib/getOptions";
import { onlyLettersNumbersSpaces } from "../../pages/v2/workflow";
interface IAddActionDialog { interface IAddActionDialog {
isOpenDialog: boolean; isOpenDialog: boolean;
setIsOpenDialog: Dispatch<SetStateAction<boolean>>; setIsOpenDialog: Dispatch<SetStateAction<boolean>>;
addAction: (action: WorkflowActions, sendTo?: string, numberRequired?: boolean) => void; addAction: (action: WorkflowActions, sendTo?: string, numberRequired?: boolean, sender?: string) => void;
isFreeUser: boolean; isFreeUser: boolean;
} }
@ -29,6 +30,7 @@ type AddActionFormValues = {
action: WorkflowActions; action: WorkflowActions;
sendTo?: string; sendTo?: string;
numberRequired?: boolean; numberRequired?: boolean;
sender?: string;
}; };
const cleanUpActionsForFreeUser = (actions: ISelectActionOption[]) => { const cleanUpActionsForFreeUser = (actions: ISelectActionOption[]) => {
@ -41,6 +43,7 @@ export const AddActionDialog = (props: IAddActionDialog) => {
const { t } = useLocale(); const { t } = useLocale();
const { isOpenDialog, setIsOpenDialog, addAction, isFreeUser } = props; const { isOpenDialog, setIsOpenDialog, addAction, isFreeUser } = props;
const [isPhoneNumberNeeded, setIsPhoneNumberNeeded] = useState(false); const [isPhoneNumberNeeded, setIsPhoneNumberNeeded] = useState(false);
const [isSenderIdNeeded, setIsSenderIdNeeded] = useState(false);
const [isEmailAddressNeeded, setIsEmailAddressNeeded] = useState(false); const [isEmailAddressNeeded, setIsEmailAddressNeeded] = useState(false);
const workflowActions = getWorkflowActionOptions(t); const workflowActions = getWorkflowActionOptions(t);
const actionOptions = isFreeUser ? cleanUpActionsForFreeUser(workflowActions) : workflowActions; const actionOptions = isFreeUser ? cleanUpActionsForFreeUser(workflowActions) : workflowActions;
@ -52,12 +55,17 @@ export const AddActionDialog = (props: IAddActionDialog) => {
.refine((val) => isValidPhoneNumber(val) || val.includes("@")) .refine((val) => isValidPhoneNumber(val) || val.includes("@"))
.optional(), .optional(),
numberRequired: z.boolean().optional(), numberRequired: z.boolean().optional(),
sender: z
.string()
.refine((val) => onlyLettersNumbersSpaces(val))
.nullable(),
}); });
const form = useForm<AddActionFormValues>({ const form = useForm<AddActionFormValues>({
mode: "onSubmit", mode: "onSubmit",
defaultValues: { defaultValues: {
action: WorkflowActions.EMAIL_HOST, action: WorkflowActions.EMAIL_HOST,
sender: "Cal",
}, },
resolver: zodResolver(formSchema), resolver: zodResolver(formSchema),
}); });
@ -67,11 +75,18 @@ export const AddActionDialog = (props: IAddActionDialog) => {
form.setValue("action", newValue.value); form.setValue("action", newValue.value);
if (newValue.value === WorkflowActions.SMS_NUMBER) { if (newValue.value === WorkflowActions.SMS_NUMBER) {
setIsPhoneNumberNeeded(true); setIsPhoneNumberNeeded(true);
setIsSenderIdNeeded(true);
setIsEmailAddressNeeded(false); setIsEmailAddressNeeded(false);
} else if (newValue.value === WorkflowActions.EMAIL_ADDRESS) { } else if (newValue.value === WorkflowActions.EMAIL_ADDRESS) {
setIsEmailAddressNeeded(true); setIsEmailAddressNeeded(true);
setIsSenderIdNeeded(false);
setIsPhoneNumberNeeded(false);
} else if (newValue.value === WorkflowActions.SMS_ATTENDEE) {
setIsSenderIdNeeded(true);
setIsEmailAddressNeeded(false);
setIsPhoneNumberNeeded(false); setIsPhoneNumberNeeded(false);
} else { } else {
setIsSenderIdNeeded(false);
setIsEmailAddressNeeded(false); setIsEmailAddressNeeded(false);
setIsPhoneNumberNeeded(false); setIsPhoneNumberNeeded(false);
} }
@ -90,13 +105,14 @@ export const AddActionDialog = (props: IAddActionDialog) => {
<Form <Form
form={form} form={form}
handleSubmit={(values) => { handleSubmit={(values) => {
addAction(values.action, values.sendTo, values.numberRequired); addAction(values.action, values.sendTo, values.numberRequired, values.sender);
form.unregister("sendTo"); form.unregister("sendTo");
form.unregister("action"); form.unregister("action");
form.unregister("numberRequired"); form.unregister("numberRequired");
setIsOpenDialog(false); setIsOpenDialog(false);
setIsPhoneNumberNeeded(false); setIsPhoneNumberNeeded(false);
setIsEmailAddressNeeded(false); setIsEmailAddressNeeded(false);
setIsSenderIdNeeded(false);
}}> }}>
<div className="mt-5 space-y-1"> <div className="mt-5 space-y-1">
<Label htmlFor="label">{t("action")}:</Label> <Label htmlFor="label">{t("action")}:</Label>
@ -119,25 +135,10 @@ export const AddActionDialog = (props: IAddActionDialog) => {
<p className="mt-1 text-sm text-red-500">{form.formState.errors.action.message}</p> <p className="mt-1 text-sm text-red-500">{form.formState.errors.action.message}</p>
)} )}
</div> </div>
{form.getValues("action") === WorkflowActions.SMS_ATTENDEE && (
<div className="mt-5">
<Controller
name="numberRequired"
control={form.control}
render={() => (
<Checkbox
defaultChecked={form.getValues("numberRequired") || false}
description={t("make_phone_number_required")}
onChange={(e) => form.setValue("numberRequired", e.target.checked)}
/>
)}
/>
</div>
)}
{isPhoneNumberNeeded && ( {isPhoneNumberNeeded && (
<div className="mt-5 space-y-1"> <div className="mt-5 space-y-1">
<Label htmlFor="sendTo">{t("phone_number")}</Label> <Label htmlFor="sendTo">{t("phone_number")}</Label>
<div className="mt-1"> <div className="mt-1 mb-5">
<PhoneInput<AddActionFormValues> <PhoneInput<AddActionFormValues>
control={form.control} control={form.control}
name="sendTo" name="sendTo"
@ -157,6 +158,32 @@ export const AddActionDialog = (props: IAddActionDialog) => {
<EmailField required label={t("email_address")} {...form.register("sendTo")} /> <EmailField required label={t("email_address")} {...form.register("sendTo")} />
</div> </div>
)} )}
{isSenderIdNeeded && (
<div className="mt-5">
<TextField
label={t("sender_id")}
type="text"
placeholder="Cal"
maxLength={11}
{...form.register(`sender`)}
/>
</div>
)}
{form.getValues("action") === WorkflowActions.SMS_ATTENDEE && (
<div className="mt-5">
<Controller
name="numberRequired"
control={form.control}
render={() => (
<Checkbox
defaultChecked={form.getValues("numberRequired") || false}
description={t("make_phone_number_required")}
onChange={(e) => form.setValue("numberRequired", e.target.checked)}
/>
)}
/>
</div>
)}
<DialogFooter> <DialogFooter>
<DialogClose asChild> <DialogClose asChild>
<Button <Button
@ -168,6 +195,7 @@ export const AddActionDialog = (props: IAddActionDialog) => {
form.unregister("numberRequired"); form.unregister("numberRequired");
setIsPhoneNumberNeeded(false); setIsPhoneNumberNeeded(false);
setIsEmailAddressNeeded(false); setIsEmailAddressNeeded(false);
setIsSenderIdNeeded(false);
}}> }}>
{t("cancel")} {t("cancel")}
</Button> </Button>

View File

@ -51,7 +51,7 @@ export default function WorkflowDetailsPage(props: Props) {
[data] [data]
); );
const addAction = (action: WorkflowActions, sendTo?: string, numberRequired?: boolean) => { const addAction = (action: WorkflowActions, sendTo?: string, numberRequired?: boolean, sender?: string) => {
const steps = form.getValues("steps"); const steps = form.getValues("steps");
const id = const id =
steps?.length > 0 steps?.length > 0
@ -75,6 +75,7 @@ export default function WorkflowDetailsPage(props: Props) {
emailSubject: null, emailSubject: null,
template: WorkflowTemplates.CUSTOM, template: WorkflowTemplates.CUSTOM,
numberRequired: numberRequired || false, numberRequired: numberRequired || false,
sender: sender || "Cal",
}; };
steps?.push(step); steps?.push(step);
form.setValue("steps", steps); form.setValue("steps", steps);

View File

@ -17,7 +17,7 @@ import Dropdown, { DropdownMenuContent, DropdownMenuItem, DropdownMenuTrigger }
import { Icon } from "@calcom/ui/Icon"; import { Icon } from "@calcom/ui/Icon";
import { Button } from "@calcom/ui/components"; import { Button } from "@calcom/ui/components";
import { Checkbox } from "@calcom/ui/components"; import { Checkbox } from "@calcom/ui/components";
import { EmailField, Label, TextArea } from "@calcom/ui/components/form"; import { EmailField, Label, TextArea, TextField } from "@calcom/ui/components/form";
import PhoneInput from "@calcom/ui/form/PhoneInputLazy"; import PhoneInput from "@calcom/ui/form/PhoneInputLazy";
import { DialogClose, DialogContent } from "@calcom/ui/v2"; import { DialogClose, DialogContent } from "@calcom/ui/v2";
import ConfirmationDialogContent from "@calcom/ui/v2/core/ConfirmationDialogContent"; import ConfirmationDialogContent from "@calcom/ui/v2/core/ConfirmationDialogContent";
@ -52,6 +52,12 @@ export default function WorkflowStepContainer(props: WorkflowStepProps) {
step?.action === WorkflowActions.SMS_NUMBER ? true : false step?.action === WorkflowActions.SMS_NUMBER ? true : false
); );
const [isSenderIdNeeded, setIsSenderIdNeeded] = useState(
step?.action === WorkflowActions.SMS_NUMBER || step?.action === WorkflowActions.SMS_ATTENDEE
? true
: false
);
const [isEmailAddressNeeded, setIsEmailAddressNeeded] = useState( const [isEmailAddressNeeded, setIsEmailAddressNeeded] = useState(
step?.action === WorkflowActions.EMAIL_ADDRESS ? true : false step?.action === WorkflowActions.EMAIL_ADDRESS ? true : false
); );
@ -279,13 +285,20 @@ export default function WorkflowStepContainer(props: WorkflowStepProps) {
if (val) { if (val) {
if (val.value === WorkflowActions.SMS_NUMBER) { if (val.value === WorkflowActions.SMS_NUMBER) {
setIsPhoneNumberNeeded(true); setIsPhoneNumberNeeded(true);
setIsSenderIdNeeded(true);
setIsEmailAddressNeeded(false); setIsEmailAddressNeeded(false);
} else if (val.value === WorkflowActions.EMAIL_ADDRESS) { } else if (val.value === WorkflowActions.EMAIL_ADDRESS) {
setIsEmailAddressNeeded(true); setIsEmailAddressNeeded(true);
setIsPhoneNumberNeeded(false); setIsPhoneNumberNeeded(false);
setIsSenderIdNeeded(false);
} else if (val.value === WorkflowActions.SMS_ATTENDEE) {
setIsSenderIdNeeded(true);
setIsEmailAddressNeeded(false);
setIsPhoneNumberNeeded(false);
} else { } else {
setIsEmailAddressNeeded(false); setIsEmailAddressNeeded(false);
setIsPhoneNumberNeeded(false); setIsPhoneNumberNeeded(false);
setIsSenderIdNeeded(false);
} }
form.unregister(`steps.${step.stepNumber - 1}.sendTo`); form.unregister(`steps.${step.stepNumber - 1}.sendTo`);
form.clearErrors(`steps.${step.stepNumber - 1}.sendTo`); form.clearErrors(`steps.${step.stepNumber - 1}.sendTo`);
@ -315,43 +328,64 @@ export default function WorkflowStepContainer(props: WorkflowStepProps) {
); );
}} }}
/> />
{form.getValues(`steps.${step.stepNumber - 1}.action`) === WorkflowActions.SMS_ATTENDEE && (
<div className="mt-5">
<Controller
name={`steps.${step.stepNumber - 1}.numberRequired`}
control={form.control}
render={() => (
<Checkbox
defaultChecked={
form.getValues(`steps.${step.stepNumber - 1}.numberRequired`) || false
}
description={t("make_phone_number_required")}
onChange={(e) =>
form.setValue(`steps.${step.stepNumber - 1}.numberRequired`, e.target.checked)
}
/>
)}
/>
</div>
)}
</div> </div>
{isPhoneNumberNeeded && ( {(isPhoneNumberNeeded || isSenderIdNeeded) && (
<div className="mt-5 rounded-md bg-gray-50 p-4"> <div className="mt-2 rounded-md bg-gray-50 p-4 pt-0">
<Label>{t("custom_phone_number")}</Label> {isPhoneNumberNeeded && (
<PhoneInput<FormValues> <>
<Label className="pt-4">{t("custom_phone_number")}</Label>
<PhoneInput<FormValues>
control={form.control}
name={`steps.${step.stepNumber - 1}.sendTo`}
placeholder={t("phone_number")}
id={`steps.${step.stepNumber - 1}.sendTo`}
className="w-full rounded-md"
required
/>
{form.formState.errors.steps &&
form.formState?.errors?.steps[step.stepNumber - 1]?.sendTo && (
<p className="mt-1 text-xs text-red-500">
{form.formState?.errors?.steps[step.stepNumber - 1]?.sendTo?.message || ""}
</p>
)}
</>
)}
{isSenderIdNeeded && (
<>
<div className="pt-4">
<TextField
label={t("sender_id")}
type="text"
placeholder="Cal"
maxLength={11}
{...form.register(`steps.${step.stepNumber - 1}.sender`)}
/>
</div>
{form.formState.errors.steps &&
form.formState?.errors?.steps[step.stepNumber - 1]?.sender && (
<p className="mt-1 text-xs text-red-500">{t("sender_id_error_message")}</p>
)}
</>
)}
</div>
)}
{form.getValues(`steps.${step.stepNumber - 1}.action`) === WorkflowActions.SMS_ATTENDEE && (
<div className="mt-2">
<Controller
name={`steps.${step.stepNumber - 1}.numberRequired`}
control={form.control} control={form.control}
name={`steps.${step.stepNumber - 1}.sendTo`} render={() => (
placeholder={t("phone_number")} <Checkbox
id={`steps.${step.stepNumber - 1}.sendTo`} defaultChecked={
className="w-full rounded-md" form.getValues(`steps.${step.stepNumber - 1}.numberRequired`) || false
required }
/> description={t("make_phone_number_required")}
{form.formState.errors.steps && onChange={(e) =>
form.formState?.errors?.steps[step.stepNumber - 1]?.sendTo && ( form.setValue(`steps.${step.stepNumber - 1}.numberRequired`, e.target.checked)
<p className="mt-1 text-sm text-red-500"> }
{form.formState?.errors?.steps[step.stepNumber - 1]?.sendTo?.message || ""} />
</p>
)} )}
/>
</div> </div>
)} )}
{isEmailAddressNeeded && ( {isEmailAddressNeeded && (
@ -408,7 +442,7 @@ export default function WorkflowStepContainer(props: WorkflowStepProps) {
/> />
{form.formState.errors.steps && {form.formState.errors.steps &&
form.formState?.errors?.steps[step.stepNumber - 1]?.emailSubject && ( form.formState?.errors?.steps[step.stepNumber - 1]?.emailSubject && (
<p className="mt-1 text-sm text-red-500"> <p className="mt-1 text-xs text-red-500">
{form.formState?.errors?.steps[step.stepNumber - 1]?.emailSubject?.message || ""} {form.formState?.errors?.steps[step.stepNumber - 1]?.emailSubject?.message || ""}
</p> </p>
)} )}
@ -433,7 +467,7 @@ export default function WorkflowStepContainer(props: WorkflowStepProps) {
/> />
{form.formState.errors.steps && {form.formState.errors.steps &&
form.formState?.errors?.steps[step.stepNumber - 1]?.reminderBody && ( form.formState?.errors?.steps[step.stepNumber - 1]?.reminderBody && (
<p className="mt-1 text-sm text-red-500"> <p className="mt-1 text-xs text-red-500">
{form.formState?.errors?.steps[step.stepNumber - 1]?.reminderBody?.message || ""} {form.formState?.errors?.steps[step.stepNumber - 1]?.reminderBody?.message || ""}
</p> </p>
)} )}
@ -497,6 +531,7 @@ export default function WorkflowStepContainer(props: WorkflowStepProps) {
emailSubject, emailSubject,
reminderBody, reminderBody,
template: step.template, template: step.template,
sender: step.sender || "Cal",
}); });
} else { } else {
const isNumberValid = const isNumberValid =

View File

@ -47,7 +47,8 @@ export const scheduleWorkflowReminders = async (
}, },
step.reminderBody || "", step.reminderBody || "",
step.id, step.id,
step.template step.template,
step.sender || "Cal"
); );
} else if ( } else if (
step.action === WorkflowActions.EMAIL_ATTENDEE || step.action === WorkflowActions.EMAIL_ATTENDEE ||
@ -115,7 +116,8 @@ export const sendCancelledReminders = async (
}, },
step.reminderBody || "", step.reminderBody || "",
step.id, step.id,
step.template step.template,
step.sender || "Cal"
); );
} else if ( } else if (
step.action === WorkflowActions.EMAIL_ATTENDEE || step.action === WorkflowActions.EMAIL_ATTENDEE ||

View File

@ -19,18 +19,19 @@ function assertTwilio(twilio: TwilioClient.Twilio | undefined): asserts twilio i
if (!twilio) throw new Error("Twilio credentials are missing from the .env file"); if (!twilio) throw new Error("Twilio credentials are missing from the .env file");
} }
export const sendSMS = async (phoneNumber: string, body: string) => { export const sendSMS = async (phoneNumber: string, body: string, sender: string) => {
assertTwilio(twilio); assertTwilio(twilio);
const response = await twilio.messages.create({ const response = await twilio.messages.create({
body: body, body: body,
messagingServiceSid: process.env.TWILIO_MESSAGING_SID, messagingServiceSid: process.env.TWILIO_MESSAGING_SID,
to: phoneNumber, to: phoneNumber,
from: sender,
}); });
return response; return response;
}; };
export const scheduleSMS = async (phoneNumber: string, body: string, scheduledDate: Date) => { export const scheduleSMS = async (phoneNumber: string, body: string, scheduledDate: Date, sender: string) => {
assertTwilio(twilio); assertTwilio(twilio);
const response = await twilio.messages.create({ const response = await twilio.messages.create({
body: body, body: body,
@ -38,6 +39,7 @@ export const scheduleSMS = async (phoneNumber: string, body: string, scheduledDa
to: phoneNumber, to: phoneNumber,
scheduleType: "fixed", scheduleType: "fixed",
sendAt: scheduledDate, sendAt: scheduledDate,
from: sender,
}); });
return response; return response;

View File

@ -48,7 +48,8 @@ export const scheduleSMSReminder = async (
}, },
message: string, message: string,
workflowStepId: number, workflowStepId: number,
template: WorkflowTemplates template: WorkflowTemplates,
sender: string
) => { ) => {
const { startTime, endTime } = evt; const { startTime, endTime } = evt;
const uid = evt.uid as string; const uid = evt.uid as string;
@ -97,7 +98,7 @@ export const scheduleSMSReminder = async (
triggerEvent === WorkflowTriggerEvents.RESCHEDULE_EVENT triggerEvent === WorkflowTriggerEvents.RESCHEDULE_EVENT
) { ) {
try { try {
await twilio.sendSMS(reminderPhone, message); await twilio.sendSMS(reminderPhone, message, sender);
} catch (error) { } catch (error) {
console.log(`Error sending SMS with error ${error}`); console.log(`Error sending SMS with error ${error}`);
} }
@ -112,7 +113,12 @@ export const scheduleSMSReminder = async (
!scheduledDate.isAfter(currentDate.add(7, "day")) !scheduledDate.isAfter(currentDate.add(7, "day"))
) { ) {
try { try {
const scheduledSMS = await twilio.scheduleSMS(reminderPhone, message, scheduledDate.toDate()); const scheduledSMS = await twilio.scheduleSMS(
reminderPhone,
message,
scheduledDate.toDate(),
sender
);
await prisma.workflowReminder.create({ await prisma.workflowReminder.create({
data: { data: {

View File

@ -38,6 +38,13 @@ export type FormValues = {
timeUnit?: TimeUnit; timeUnit?: TimeUnit;
}; };
export function onlyLettersNumbersSpaces(str: string) {
if (str.length <= 11 && /^[A-Za-z0-9\s]*$/.test(str)) {
return true;
}
return false;
}
const formSchema = z.object({ const formSchema = z.object({
name: z.string(), name: z.string(),
activeOn: z.object({ value: z.string(), label: z.string() }).array(), activeOn: z.object({ value: z.string(), label: z.string() }).array(),
@ -58,6 +65,11 @@ const formSchema = z.object({
.string() .string()
.refine((val) => isValidPhoneNumber(val) || val.includes("@")) .refine((val) => isValidPhoneNumber(val) || val.includes("@"))
.nullable(), .nullable(),
sender: z
.string()
.refine((val) => onlyLettersNumbersSpaces(val))
.optional()
.nullable(),
}) })
.array(), .array(),
}); });

View File

@ -0,0 +1,2 @@
-- AlterTable
ALTER TABLE "WorkflowStep" ADD COLUMN "sender" TEXT;

View File

@ -573,6 +573,7 @@ model WorkflowStep {
template WorkflowTemplates @default(REMINDER) template WorkflowTemplates @default(REMINDER)
workflowReminders WorkflowReminder[] workflowReminders WorkflowReminder[]
numberRequired Boolean? numberRequired Boolean?
sender String?
} }
model Workflow { model Workflow {

View File

@ -160,6 +160,7 @@ export const workflowsRouter = router({
action: WorkflowActions.EMAIL_HOST, action: WorkflowActions.EMAIL_HOST,
template: WorkflowTemplates.REMINDER, template: WorkflowTemplates.REMINDER,
workflowId: workflow.id, workflowId: workflow.id,
sender: "Cal",
}, },
}); });
return { workflow }; return { workflow };
@ -235,6 +236,7 @@ export const workflowsRouter = router({
emailSubject: z.string().optional().nullable(), emailSubject: z.string().optional().nullable(),
template: z.enum(WORKFLOW_TEMPLATES), template: z.enum(WORKFLOW_TEMPLATES),
numberRequired: z.boolean().nullable(), numberRequired: z.boolean().nullable(),
sender: z.string().optional().nullable(),
}) })
.array(), .array(),
trigger: z.enum(WORKFLOW_TRIGGER_EVENTS), trigger: z.enum(WORKFLOW_TRIGGER_EVENTS),
@ -472,7 +474,8 @@ export const workflowsRouter = router({
}, },
step.reminderBody || "", step.reminderBody || "",
step.id, step.id,
step.template step.template,
step.sender || "Cal"
); );
} }
}); });
@ -541,6 +544,7 @@ export const workflowsRouter = router({
emailSubject: newStep.template === WorkflowTemplates.CUSTOM ? newStep.emailSubject : null, emailSubject: newStep.template === WorkflowTemplates.CUSTOM ? newStep.emailSubject : null,
template: newStep.template, template: newStep.template,
numberRequired: newStep.numberRequired, numberRequired: newStep.numberRequired,
sender: newStep.sender || "Cal",
}, },
}); });
//cancel all reminders of step and create new ones (not for newEventTypes) //cancel all reminders of step and create new ones (not for newEventTypes)
@ -651,7 +655,8 @@ export const workflowsRouter = router({
}, },
newStep.reminderBody || "", newStep.reminderBody || "",
newStep.id || 0, newStep.id || 0,
newStep.template newStep.template,
newStep.sender || "Cal"
); );
} }
}); });
@ -677,6 +682,8 @@ export const workflowsRouter = router({
}); });
addedSteps.forEach(async (step) => { addedSteps.forEach(async (step) => {
if (step) { if (step) {
const newStep = step;
newStep.sender = step.sender || "Cal";
const createdStep = await ctx.prisma.workflowStep.create({ const createdStep = await ctx.prisma.workflowStep.create({
data: step, data: step,
}); });
@ -764,7 +771,8 @@ export const workflowsRouter = router({
}, },
step.reminderBody || "", step.reminderBody || "",
createdStep.id, createdStep.id,
step.template step.template,
step.sender || "Cal"
); );
} }
}); });
@ -812,10 +820,11 @@ export const workflowsRouter = router({
reminderBody: z.string(), reminderBody: z.string(),
template: z.enum(WORKFLOW_TEMPLATES), template: z.enum(WORKFLOW_TEMPLATES),
sendTo: z.string().optional(), sendTo: z.string().optional(),
sender: z.string().optional(),
}) })
) )
.mutation(async ({ ctx, input }) => { .mutation(async ({ ctx, input }) => {
const { action, emailSubject, reminderBody, template, sendTo } = input; const { action, emailSubject, reminderBody, template, sendTo, sender } = input;
try { try {
const booking = await ctx.prisma.booking.findFirst({ const booking = await ctx.prisma.booking.findFirst({
orderBy: { orderBy: {
@ -899,7 +908,8 @@ export const workflowsRouter = router({
{ time: null, timeUnit: null }, { time: null, timeUnit: null },
reminderBody, reminderBody,
0, 0,
template template,
sender || "Cal"
); );
return { message: "Notification sent" }; return { message: "Notification sent" };
} }

View File

@ -183,6 +183,9 @@
"$CLOSECOM_API_KEY", "$CLOSECOM_API_KEY",
"$SENDGRID_API_KEY", "$SENDGRID_API_KEY",
"$SENDGRID_EMAIL", "$SENDGRID_EMAIL",
"$TWILIO_TOKEN",
"$TWILIO_SID",
"$TWILIO_MESSAGING_SID",
"$CRON_API_KEY", "$CRON_API_KEY",
"$DAILY_API_KEY", "$DAILY_API_KEY",
"$DAILY_SCALE_PLAN", "$DAILY_SCALE_PLAN",