Отправка счета и добавление напоминания об оплате в Next.js с помощью Courier API
15 февраля 2023 г.Многие приложения для управления счетами с открытым исходным кодом созданы с помощью Laravel. Как разработчик Javascript, я хотел создать «Решение React» для разработчиков, знакомых с React и Javascript.
Проблема, которую я обнаружил при сборке с помощью сервисов в Node.js, заключается в отсутствии встроенного почтового модуля. Итак, мне пришлось найти сторонний сервис, чтобы сделать это за меня. В этой статье я буду интегрировать Courier для отправки электронных писем для этого проекта https://github.com/fazzaamiarso/invoys.
Предварительные условия
Поскольку эта статья не является вашим типичным продолжением (больше похоже на "пожалуйста, присядьте и посмотрите, как я это делаю"), не обязательно быть знакомым со всеми используемыми технологиями. Однако знакомство с Typescript и Next.js будет полезно для более быстрого понимания.
Технологии в этом блоге:
* Typescript: безопасность типов и автодополнение лучше, верно? * Next.js: готовая к работе среда для создания полнофункционального приложения даже для новичков. * Prisma: отличная ORM для работы с базами данных. Мы используем Prisma из-за ее безопасности типов и автоматического завершения, что обеспечивает отличный опыт разработчика с добавлением машинописного текста. * Trpc: позволяет нам легко создавать сквозную безопасность типов между нашим клиентом Next.js и сервером. * Courier API: отличный сервис/платформа для обработки наших уведомлений, таких как электронная почта, SMS и многое другое.
Вы можете найти полный исходный код здесь для справки.
Цели
Прежде чем создавать функции, давайте определим наши цели.
- Отправить ссылку на счет на электронную почту клиента.
- Отправить напоминание за день до даты оплаты счета.
- Отменить напоминание о сроке оплаты счета, когда счет уже оплачен.
- Обработка сетевых ошибок.
Часть 1. Настройка Courier Platform
Перейдем к панели управления Courier. По умолчанию это производственная среда. Поскольку я хочу все проверить, я перейду к тестовой среде, щелкнув раскрывающийся список в правом верхнем углу.
<цитата>Мы можем позже скопировать все шаблоны в рабочую среду или наоборот.
Теперь я создам бренд для своей электронной почты. уведомления.
Я просто добавлю логотип (учтите, что ширина логотипа фиксирована и составляет 140 пикселей) в шапке и ссылки на социальные сети в футере. Пользовательский интерфейс конструктора довольно прост, поэтому вот окончательный результат.
Не забудьте опубликовать изменения.
Часть 2. Отправка счета на электронную почту
В настоящее время кнопка отправки электронной почты в пользовательском интерфейсе ничего не делает.
Я собираюсь создать файл courier.ts
в src/lib/
, чтобы сохранить весь код, связанный с Courier. Кроме того, я буду использовать клиентскую библиотеку courier node.js, которая уже абстрагировала все конечные точки Courier API в функции.
Прежде чем я создам функциональность, давайте создадим дизайн уведомлений по электронной почте в Courier’s Designer и настроим провайдера Gmail.
На странице конструктора писем мы увидим, что созданный бренд уже интегрирован. После этого давайте разработаем шаблон в соответствии с необходимыми данными. Вот окончательный результат.
Обратите внимание на значение с {}
, которое становится зеленым, это означает, что это переменная, которую можно вставлять динамически. Я также установил кнопку "Просмотреть счет" (или действие) с помощью переменной.
Прежде чем я смогу использовать шаблон, мне нужно создать тестовое событие, щелкнув вкладку предварительного просмотра. Затем появится приглашение назвать событие и установить data
в формате JSON. Это поле данных — это то, что будет заполнять значение зеленых переменных {}
(данные также можно установить из кода). Поскольку это тестовое событие, я заполню его произвольными значениями.
Далее я опубликую шаблон, чтобы я мог его использовать. Затем перейдите на вкладку «Отправить». Он покажет необходимый код для отправки электронной почты программным способом, а данные
будут заполнены предыдущим тестовым событием, которое я создал.
Бэкенд
Я скопирую тестовый AUTH_TOKEN
в файл .env
, а фрагмент скопирую в src/lib/courier.ts
. р>
const authToken = process.env.COURIER_AUTH_TOKEN;
// email to receive all sent notifications in DEVELOPMENT mode
const testEmail = process.env.COURIER_TEST_EMAIL;
const INVOICE_TEMPLATE_ID = <TEMPLATE_ID>;
const courierClient = CourierClient({
authorizationToken: authToken,
});
Создайте функцию sendInvoice
, которая будет отвечать за отправку электронной почты. Чтобы отправить электронное письмо из кода, я использую функцию courierClient.send()
.
// src/lib/courier.ts
export const sendInvoice = async ({
customerName,
invoiceNumber,
invoiceViewUrl,
emailTo,
productName,
dueDate,
}: SendInvoice) => {
const recipientEmail = process.env.NODE_ENV === "production" ? emailTo : testEmail;
const { requestId } = await courierClient.send({
message: {
to: {
email: recipientEmail,
},
template: INVOICE_TEMPLATE_ID,
// Data for courier template designer
data: {
customerName,
invoiceNumber,
invoiceViewUrl,
productName,
dueDate,
},
},
});
return requestId
};
Определите типы для функции sendInvoice
.
// src/lib/courier.ts
interface SendInvoice {
productName: string;
dueDate: string;
customerName: string;
invoiceNumber: string;
invoiceViewUrl: string;
emailTo: string;
}
Теперь, когда я могу отправить электронное письмо, я вызову его в конечной точке trpc sendEmail
, которая находится в src/server/trpc/router/invoice.ts
.< /p>
Помните, что конечная точка trpc — это маршрут API Next.js. В этом случае sendEmail
будет то же самое, что и вызов /api/trpc/sendEmail
маршрута с em> fetch
под капотом. Подробнее см. https://trpc.io/docs/quickstart.
// src/server/trpc/router/invoice.ts
import { sendInvoice } from '@lib/courier';
import { dayjs } from '@lib/dayjs';
// .....SOMEWHERE BELOW
sendEmail: protectedProcedure
.input(
z.object({
customerName: z.string(),
invoiceNumber: z.string(),
invoiceViewUrl: z.string(),
emailTo: z.string(),
invoiceId: z.string(),
productName: z.string(),
dueDate: z.date(),
})
)
.mutation(async ({ input }) => {
const invoiceData = {
...input,
dueDate: dayjs(input.dueDate).format('D MMMM YYYY'),
};
await sendInvoice(invoiceData);
}),
Для тех, кто не знаком с trpc, то, что я сделал, это то же самое, что и обработка запроса POST
. Давайте разберемся.
- Trpc способ определения запроса входных данных от клиента путем проверки с помощью Zod. Здесь я определяю все данные, необходимые для функции
sendInvoice
.
.input(
z.object({
customerName: z.string(),
invoiceNumber: z.string(),
invoiceViewUrl: z.string(),
emailTo: z.string(),
invoiceId: z.string(),
productName: z.string(),
dueDate: z.date(),
})
)
- Определить обработчик запроса
POST
(мутацию).
// input from before
.mutation(async ({ input }) => {
const invoiceData = {
...input,
// format a date to string with a defined format.
dueDate: dayjs(input.dueDate).format('D MMMM YYYY'), // ex.'2 January 2023'
};
// send the email
await sendInvoice(invoiceData);
}),
Внешний интерфейс
Теперь я могу начать добавлять функциональность к кнопке отправки электронной почты. Я собираюсь использовать функцию trpc.useMutation()
, которая является тонкой оболочкой функции tanstack-query
useMutation`.
Добавим функцию мутации. При успешном ответе я хочу отправить всплывающее уведомление об успехе в пользовательском интерфейсе.
//src/pages/invoices/[invoiceId]/index.tsx
import toast from 'react-hot-toast';
const InvoiceDetail: NextPage = () => {
// calling the `sendEmail` trpc endpoint with tanstack-query.
const sendEmailMutation = trpc.invoice.sendEmail.useMutation({
onSuccess() {
toast.success('Email sent!');
}
});
}
Я могу просто использовать функцию как встроенный обработчик, но я хочу создать новый обработчик для кнопки.
//src/pages/invoices/[invoiceId]/index.tsx
// still inside the InvoiceDetail component
const sendInvoiceEmail = () => {
const hostUrl = window.location.origin;
// prevent a user from spamming when the API call is not done.
if (sendEmailMutation.isLoading) return;
// send input data to `sendEmail` trpc endpoint
sendEmailMutation.mutate({
customerName: invoiceDetail.customer.name,
invoiceNumber: `#${invoiceDetail.invoiceNumber}`,
invoiceViewUrl: `${hostUrl}/invoices/${invoiceDetail.id}/preview`,
emailTo: invoiceDetail.customer.email,
invoiceId: invoiceDetail.id,
dueDate: invoiceDetail.dueDate,
productName: invoiceDetail.name,
});
};
Теперь я могу прикрепить обработчик к кнопке отправки электронной почты.
//src/pages/invoices/[invoiceId]/index.tsx
<Button
variant="primary"
onClick={sendInvoiceEmail}
isLoading={sendEmailMutation.isLoading}>
Send to Email
</Button>
Вот рабочий интерфейс.
Часть 3. Отправка напоминания об оплате
Чтобы запланировать напоминание, которое будет отправлено за день до даты оплаты счета, я буду использовать Courier's Automation API< /а>.
Во-первых, давайте создадим шаблон электронной почты в конструкторе Courier. Поскольку я уже проходил этот процесс ранее, вот окончательный результат.
Перед добавлением функции определите типы для параметра и реорганизуйте типы.
// src/lib/courier
interface CourierBaseData {
customerName: string;
invoiceNumber: string;
invoiceViewUrl: string;
emailTo: string;
}
interface SendInvoice extends CourierBaseData {
productName: string;
dueDate: string;
}
interface ScheduleReminder extends CourierBaseData {
scheduledDate: Date;
invoiceId: string;
}
Теперь я добавляю функцию scheduleReminder
в src/lib/courier
//src/pages/invoices/[invoiceId]/index.tsx
// check if the development environment is production
const __IS_PROD__ = process.env.NODE_ENV === 'production';
const PAYMENT_REMINDER_TEMPLATE_ID = '<TEMPLATE_ID>';
export const scheduleReminder = async ({
scheduledDate,
emailTo,
invoiceViewUrl,
invoiceId,
customerName,
invoiceNumber,
}: ScheduleReminder) => {
// delay until a day before due date in production, else 20 seconds after sent for development
const delayUntilDate = __IS_PROD__
? scheduledDate
: new Date(Date.now() + SECOND_TO_MS * 20);
const recipientEmail = __IS_PROD__ ? emailTo : testEmail;
// define the automation steps programmatically
const { runId } = await courierClient.automations.invokeAdHocAutomation({
automation: {
steps: [
// 1. Set delay for the next steps until given date in ISO string
{ action: 'delay', until: delayUntilDate.toISOString() },
// 2. Send the email notification. Equivalent to `courierClient.send()`
{
action: 'send',
message: {
to: { email: recipientEmail },
template: PAYMENT_REMINDER_TEMPLATE_ID,
data: {
invoiceViewUrl,
customerName,
invoiceNumber,
},
},
},
],
},
});
return runId;
};
Чтобы отправить напоминание, я вызову scheduleReminder
после успешной попытки sendInvoice
. Давайте изменим конечную точку sendEmail
trpc.
// src/server/trpc/router/invoice.ts
sendEmail: protectedProcedure
.input(..) // omitted for brevity
.mutation(async ({ input }) => {
// multiplier for converting day to milliseconds.
const DAY_TO_MS = 1000 * 60 * 60 * 24;
// get a day before the due date
const scheduledDate = new Date(input.dueDate.getTime() - DAY_TO_MS * 1);
const invoiceData = {..}; //omitted for brevity
await sendInvoice(invoiceData);
//after the invoice is sent, schedule the reminder
await scheduleReminder({
...invoiceData,
scheduledDate,
});
}
Теперь, если я попытаюсь отправить счет по электронной почте, я должен получить напоминание через 20 секунд, так как я нахожусь в среде разработки.
Часть 4. Отмена напоминания
Наконец все функции готовы. Однако у меня возникла проблема: что, если клиент оплатил до запланированной даты для напоминания об оплате? В настоящее время напоминание по электронной почте все еще будет отправлено. Это не лучший пользовательский опыт и потенциально запутанный клиент. К счастью, в Courier есть функция автоматической отмены.
Давайте добавим функцию cancelAutomationWorkflow
, которая может отменить любой рабочий процесс автоматизации в src/lib/courier.ts
.
export const cancelAutomationWorkflow = async ({
cancelation_token,
}: {
cancelation_token: string;
}) => {
const { runId } = await courierClient.automations.invokeAdHocAutomation({
automation: {
// define a cancel action, that sends a cancelation_token
steps: [{ action: 'cancel', cancelation_token }],
},
});
return runId;
};
Что такое Cancelation_token? Это уникальный токен, который можно настроить для рабочего процесса автоматизации, поэтому его можно отменить, отправив действие cancel
с соответствующим cancelation_token
.
Добавьте cancelation_token в scheduleReminder
, я использую идентификатор счета в качестве токена.
// src/lib/courier.ts
export const scheduleReminder = async (..) => {
// ...omitted for brevity
const { runId } = await courierClient.automations.invokeAdHocAutomation({
automation: {
// add cancelation token here
cancelation_token: `${invoiceId}-reminder`,
steps: [
{ action: 'delay', until: delayUntilDate.toISOString() },
// ... omitted for brevity
Я вызову cancelAutomationWorkflow
, когда статус счета изменится на PAID
в конечной точке updateStatus
trpc.
// src/server/trpc/router/invoice.ts
updateStatus: protectedProcedure
.input(..) // omitted for brevity
.mutation(async ({ ctx, input }) => {
const { invoiceId, status } = input;
// update an invoice's status in database
const updatedInvoice = await ctx.prisma.invoice.update({
where: { id: invoiceId },
data: { status },
});
// cancel payment reminder automation workflow if the status is paid.
if (updatedInvoice.status === 'PAID') {
//call the cancel workflow to cancel the payment reminder for matching cancelation_token.
await cancelAutomationWorkflow({
cancelation_token: `${invoiceId}-reminder`,
});
}
return updatedStatus;
}),
Вот рабочий интерфейс.
Часть 5. Обработка ошибок
Важным примечанием при выполнении сетевых запросов является вероятность неудачных запросов/ошибок. Я хочу обработать ошибку, передав ее клиенту, чтобы ее можно было отразить в пользовательском интерфейсе.
При ошибке Courier API по умолчанию выдает ошибку типа CourierHttpClientError
. У меня также будет возвращаемое значение всех функций в src/lib/courier.ts
в соответствии с приведенным ниже форматом.
// On Success
type SuccessResponse = { data: any, error: null }
// On Error
type ErrorResponse = { data: any, error: string }
Теперь я могу обрабатывать ошибки, добавляя блок try-catch
ко всем функциям в src/lib/courier.ts
.
try {
// ..function code
// modified return example
return { data: runId, error: null };
} catch (error) {
// make sure it's an error from Courier
if (error instanceof CourierHttpClientError) {
return { data: error.data, error: error.message };
} else {
return { data: null, error: "Something went wrong!" };
}
}
Давайте рассмотрим пример обработки на конечной точке sendEmail
trpc.
// src/server/trpc/router/invoice.ts
const { error: sendError } = await sendInvoice(..);
if (sendError) throw new TRPCClientError(sendError);
const { error: scheduleError } = await scheduleReminder(..);
if (scheduleError) throw new TRPCClientError(scheduleError);
Часть 6. Переход к производству
Теперь, когда все шаблоны готовы, я скопирую все ресурсы из тестовой среды в рабочую среду. Вот пример.
Заключение
Наконец, все функции интегрированы с Courier. Мы прошли рабочий процесс интеграции Courier API в приложение Next.js. Хотя это и есть в Next.js и trpc, рабочий процесс будет практически таким же, как и с любой другой технологией. Надеюсь, теперь вы сможете самостоятельно интегрировать Courier в свое приложение.
Начните прямо сейчас: https://app.courier.com/signup
Об авторе
Меня зовут Фазза Разак Амиарсо, я веб-разработчик полного цикла из Индонезии. Я также являюсь энтузиастом Open Source. Я люблю делиться своими знаниями и знаниями в своем блоге. Время от времени я помогаю другим разработчикам в FrontendMentor в свободное время.
Свяжитесь со мной на LinkedIn.
Быстрые ссылки
:::информация Также опубликовано здесь.
:::
Оригинал