多くのオープンソースの請求書管理アプリが Laravel で構築されています。 Javascript 開発者として、React と Javascript に精通している開発者向けの「React ソリューション」を構築したいと考えていました。
Node.js でサービスを構築するときに私が見つけた問題は、メーラーが組み込まれていないことです。そのため、それを行うサードパーティのサービスを見つける必要がありました。この記事では、 Courierを統合して、このプロジェクトhttps://github.com/fazzaamiarso/invoysのメールを送信します。
この記事は一般的なフォロー アロングではないため (「じっと座って、私のやり方を見てください」というようなものです)、使用されているすべてのテクノロジに精通している必要はありません。ただし、Typescript と Next.js に精通していると、より迅速に理解できるようになります。
このブログのテクノロジー:
Typescript : 型安全性とオートコンプリートが最適ですよね?
Next.js : 初心者でもフルスタック アプリを構築するための本番環境対応フレームワーク。
Prisma : データベースを操作するための優れた ORM。型安全性とオートコンプリートのために Prisma を使用しており、typescript が追加された優れた開発者エクスペリエンスを提供します。
Trpc : Next.js クライアントとサーバーの間でエンドツーエンドの型安全性を簡単に構築できるようにします。
Courier API: メール、SMS などの通知を処理するための優れたサービス/プラットフォームです。
完全なソース コードは、参照用にここにあります。
機能を構築する前に、目標を定義しましょう。
Courier ダッシュボードに行きましょう。デフォルトでは、本番環境にあります。テストしたいので、右上隅のドロップダウンをクリックしてテスト環境に変更します。
後ですべてのテンプレートを本番環境にコピーしたり、その逆にコピーしたりできます。
次に、メール通知用のブランドを作成します。
ヘッダーにロゴを追加し (ロゴの幅は 140px に固定されていることに注意してください)、フッターにソーシャル リンクを追加します。デザイナーの UI は非常に単純なので、最終的な結果は次のとおりです。
変更を公開することを忘れないでください。
現在、UI のメール送信ボタンは何もしていません。
Courier 関連のすべてのコードを保持するためにsrc/lib/
にcourier.ts
ファイルを作成します。また、すべての Courier API エンドポイントを関数に抽象化したcourier node.js クライアント ライブラリを使用します。
機能を構築する前に、Courier's Designer 内で電子メール通知デザインを作成し、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
内部でfetch
を使用して/api/trpc/sendEmail
ルートを呼び出すのと同じです。詳細については、 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
リクエストの処理と同じです。分解してみましょう。
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(), }) )
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); }),
これで、メール送信ボタンに機能を追加できるようになりました。 tanstack-query's
useMutation` の薄いラッパーであるtrpc.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>
これが作業UIです。
請求書の期日の前日に送信されるリマインダーをスケジュールするには、 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; }
ここで、 src/lib/courier
にscheduleReminder
関数を追加します。
//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 秒後にリマインダーを受け取るはずです。
最後に、すべての機能の準備が整いました。しかし、問題が発生しました。クライアントが支払督促の予定日よりも前に支払った場合はどうなりますか?現在、リマインダー メールは引き続き送信されます。これは優れたユーザー エクスペリエンスではなく、クライアントが混乱する可能性があります。ありがたいことに、Courier には自動キャンセル機能があります。
src/lib/courier.ts
に任意の自動化ワークフローをキャンセルできるcancelAutomationWorkflow
関数を追加しましょう。
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 とは何ですか?自動化ワークフローに設定できる一意のトークンであるため、対応するcancelation_token
でcancel
アクションを送信することでキャンセルできます。
scheduleReminder
に cancelation_token を追加し、請求書の 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; }),
これが作業UIです。
ネットワーク リクエストを行う際の重要な注意点は、失敗したリクエストやエラーが発生する可能性があることです。 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);
すべてのテンプレートの準備が整ったので、テスト環境のすべてのアセットを本番環境にコピーします。ここに例があります。
最後に、すべての機能が Courier に統合されています。 Courier API を Next.js アプリケーションに統合するワークフローを確認しました。 Next.js と trpc にありますが、ワークフローは他のテクノロジとほとんど同じです。 Courier を自分でアプリケーションに統合できるようになったことを願っています。
今すぐ始めましょう: https://app.courier.com/signup
私は、インドネシアのフルスタック Web 開発者である Fazza Razaq Amiarso です。私はオープンソースの愛好家でもあります。ブログで自分の知識と学習を共有するのが大好きです。私は時々、空き時間にFrontendMentorで他の開発者を支援しています。
LinkedInで私とつながりましょう。
ここにも掲載されています。