paint-brush
使用 Courier API 在 Next.js 中发送发票并添加付款提醒经过@courier
1,476 讀數
1,476 讀數

使用 Courier API 在 Next.js 中发送发票并添加付款提醒

经过 Courier14m2023/02/14
Read on Terminal Reader

太長; 讀書

许多开源发票管理应用程序都是使用 Laravel 构建的。我想为熟悉 React 和 Javascript 的开发人员构建“React 解决方案”。我在 Node.js 中构建服务时发现的一个问题是没有内置邮件程序。所以,我必须找到第 3 方服务来为我做这件事。在这篇文章中,我将集成 [Courier] 来为这个项目发送电子邮件。
featured image - 使用 Courier API 在 Next.js 中发送发票并添加付款提醒
Courier HackerNoon profile picture
0-item

许多开源发票管理应用程序都是使用 Laravel 构建的。作为一名 Javascript 开发人员,我想为熟悉 React 和 Javascript 的开发人员构建“React 解决方案”。


我在 Node.js 中构建服务时发现的一个问题是没有内置邮件程序。所以,我必须找到第 3 方服务来为我做这件事。在这篇文章中,我将集成Courier来为这个项目https://github.com/fazzaamiarso/invoys发送电子邮件。

先决条件

由于本文不是您的典型跟进文章(更像是“请坐好,看看我是怎么做的”),因此不必一定要熟悉所有使用的技术。但是,熟悉 Typescript 和 Next.js 将有助于更快地理解。


本博客中的技术:


  • Typescript :类型安全和自动完成是最好的,对吧?

  • Next.js :用于构建全栈应用程序的生产就绪框架,即使对于初学者也是如此。

  • Prisma :一个很棒的 ORM 来处理数据库。我们使用 Prisma 是因为它的类型安全和自动完成功能,为添加了 typescript 的开发人员提供了很好的体验。

  • Trpc :使我们能够轻松地在 Next.js 客户端和服务器之间构建端到端类型安全。

  • Courier API:一个很棒的服务/平台来处理我们的通知,例如电子邮件、短信等等。


您可以在此处找到完整的源代码以供参考。

目标

在构建功能之前,让我们定义我们的目标。


  1. 将发票链接发送到客户的电子邮件。
  2. 在发票到期日的前一天发送提醒。
  3. 当发票已支付时取消发票到期日提醒。
  4. 处理网络错误。

第 1 部分:设置快递平台

让我们转到 Courier Dashboard。默认情况下,它在生产环境中。因为我想测试一下,所以我将通过单击右上角的下拉菜单更改为测试环境。


我们可以稍后将所有模板复制到生产环境中,反之亦然。


现在,我将为我的电子邮件通知创建一个品牌



我只是要在页眉上添加一个徽标(注意徽标宽度固定为 140 像素),在页脚上添加社交链接。设计器 UI 非常简单,所以这是最终结果。



不要忘记发布更改。

第 2 部分:将发票发送到电子邮件

目前,UI 上的发送电子邮件按钮没有任何作用。


我将在src/lib/中创建一个courier.ts文件以保留所有与 Courier 相关的代码。此外,我将使用courier node.js 客户端库,它已经将所有 Courier API 端点抽象为函数。


在构建功能之前,让我们在 Courier 的设计器中创建电子邮件通知设计并设置 Gmail 提供商。


在电子邮件设计器页面上,我们将看到创建的品牌已经集成。之后,让我们根据需要的数据设计模板。这是最终结果。




注意带{}的值变为绿色,这意味着它是一个可以动态插入的变量。我还使用变量设置“查看发票”按钮(或操作)。


在我可以使用该模板之前,我需要通过单击预览选项卡来创建一个测试事件。然后,它会显示一个提示,以 JSON 格式命名事件和设置data 。该数据字段将填充绿色{}变量的值(也可以从代码中设置数据)。由于这是一个测试事件,我将用任意值填充它。


接下来,我将发布模板以便我可以使用它。然后,转到发送选项卡。它将显示以编程方式发送电子邮件所需的代码,并且data将填充我创建的先前测试事件



后端

我会将测试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; }

现在我可以发送电子邮件了,我将在位于src/server/trpc/router/invoice.ts sendEmail trpc 端点中调用它。


请记住,trpc 端点是一个 Next.js API 路由。在这种情况下, sendEmail将与调用/api/trpc/sendEmail路由相同,并在后台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请求相同。让我们分解一下。


  1. 通过使用 Zod 验证来定义来自客户端的请求输入的 Trpc 方式。我在这里定义了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(), }) )
  1. 定义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's useMutation 的精简包装器。


让我们添加突变函数。成功响应后,我想在 UI 上发送成功祝酒词。

 //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 的 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; };

要发送提醒,我将在成功sendInvoice尝试后调用scheduleReminder 。让我们修改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_tokencancel操作来取消它。


将 cancelation_token 添加到scheduleReminder ,我使用发票的 Id 作为标记。

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

当发票的状态在updateStatus trpc 端点中更新为PAID时,我将调用cancelAutomationWorkflow

 // 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 部分:错误处理

进行网络请求时的一个重要注意事项是请求失败/错误的可能性。我想通过将错误抛给客户端来处理错误,以便在 UI 中反映出来。


出错时,Courier API 默认会抛出CourierHttpClientError类型的错误。我还将在src/lib/courier.ts中将所有函数的返回值与以下格式一致。

 // On Success type SuccessResponse = { data: any, error: null } // On Error type ErrorResponse = { data: any, error: string }

现在,我可以通过向src/lib/courier.ts中的所有函数添加一个try-catch块来处理错误。

 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

关于作者

我是来自印度尼西亚的全栈 Web 开发人员 Fazza Razaq Amiarso。我也是一个开源爱好者。我喜欢在我的博客上分享我的知识和学习。我偶尔会在空闲时间帮助FrontendMentor上的其他开发人员。


LinkedIn上与我联系。

快速链接

🔗快递文件

🔗贡献发票

🔗发票动机


也发布在这里