paint-brush
Envoi d'une facture et ajout d'un rappel de paiement dans Next.js avec l'API Courierpar@courier
1,495 lectures
1,495 lectures

Envoi d'une facture et ajout d'un rappel de paiement dans Next.js avec l'API Courier

par Courier14m2023/02/14
Read on Terminal Reader

Trop long; Pour lire

De nombreuses applications de gestion de factures open source sont construites avec Laravel. Je voulais créer la "solution React" pour les développeurs qui connaissent React et Javascript. Un problème que j'ai trouvé lors de la construction avec des services dans Node.js est qu'il n'y a pas de courrier intégré. J'ai donc dû trouver un service tiers pour le faire pour moi. Dans cet article, je vais intégrer [Courrier] pour envoyer des e-mails pour ce projet.
featured image - Envoi d'une facture et ajout d'un rappel de paiement dans Next.js avec l'API Courier
Courier HackerNoon profile picture
0-item

De nombreuses applications de gestion de factures open source sont construites avec Laravel. En tant que développeur Javascript, je voulais créer la "Solution React" pour les développeurs qui connaissent React et Javascript.


Un problème que j'ai trouvé lors de la construction avec des services dans Node.js est qu'il n'y a pas de courrier intégré. J'ai donc dû trouver un service tiers pour le faire pour moi. Dans cet article, j'intégrerai Courier pour envoyer des e-mails pour ce projet https://github.com/fazzaamiarso/invoys .

Conditions préalables

Étant donné que cet article n'est pas votre suivi habituel (plutôt du genre « veuillez patienter et voyez comment je le fais »), il n'est pas obligatoire de se familiariser avec toutes les technologies utilisées. Cependant, la familiarité avec Typescript et Next.js sera bénéfique pour une compréhension plus rapide.


Technologies dans ce blog :


  • Typescript : la sécurité de type et l'auto-complétion sont les meilleures, n'est-ce pas ?

  • Next.js : un framework prêt pour la production pour créer une application full-stack, même pour les débutants.

  • Prisma : un excellent ORM pour travailler avec des bases de données. Nous utilisons Prisma en raison de sa sécurité de type et de sa saisie semi-automatique, offrant une excellente expérience de développement avec l'ajout de texte dactylographié.

  • Trpc : nous permet de construire facilement une sécurité de type de bout en bout entre notre client et notre serveur Next.js.

  • Courier API : un excellent service/plate-forme pour gérer nos notifications, telles que les e-mails, les SMS et bien plus encore.


Vous pouvez trouver le code source complet ici pour référence.

Buts

Avant de construire les fonctionnalités, définissons nos objectifs.


  1. Envoyer le lien de la facture à l'e-mail du client.
  2. Envoyez un rappel un jour avant la date d'échéance d'une facture.
  3. Annuler un rappel de date d'échéance de facture lorsque la facture est déjà payée.
  4. Gestion des erreurs réseau.

Partie 1 : Configurer la plate-forme de messagerie

Passons au tableau de bord Courier. Par défaut, c'est dans un environnement de production. Puisque je veux tester des choses, je vais passer à l'environnement de test en cliquant sur le menu déroulant dans le coin supérieur droit.


Nous pouvons copier tous les modèles plus tard en production ou vice-versa.


Maintenant, je vais créer une marque pour mes notifications par e-mail.



Je vais juste ajouter un logo (attention la largeur du logo est fixée à 140px) sur le header et des liens sociaux sur le footer. L'interface utilisateur du concepteur est assez simple, voici donc le résultat final.



N'oubliez pas de publier les modifications.

Partie 2 : Envoyer la facture par e-mail

Actuellement, le bouton d'envoi d'e-mail sur l'interface utilisateur ne fait rien.


Je vais créer un fichier courier.ts dans src/lib/ pour conserver tout le code lié à Courier. De plus, j'utiliserai la bibliothèque client courier node.js qui a déjà résumé tous les points de terminaison de l'API Courier en fonctions.


Avant de créer la fonctionnalité, créons la conception de la notification par e-mail dans Courier's Designer et configurons le fournisseur Gmail.


Sur la page du concepteur d'email, nous verrons que la marque créée est déjà intégrée. Après cela, concevons le modèle en conséquence avec les données nécessaires. Voici le résultat final.




Remarquez la valeur avec {} qui devient verte, cela signifie que c'est une variable qui peut être insérée dynamiquement. J'ai également défini le bouton (ou l'action) "Voir la facture" avec une variable.


Avant de pouvoir utiliser le modèle, je dois créer un événement test en cliquant sur l'onglet Aperçu. Ensuite, il affichera une invite pour nommer l'événement et définir data au format JSON. Ce champ de données est ce qui remplira la valeur des variables vertes {} (les données peuvent également être définies à partir du code). Puisqu'il s'agit d'un événement de test, je vais le remplir avec des valeurs arbitraires.


Ensuite, je publierai le modèle afin que je puisse l'utiliser. Ensuite, allez dans l'onglet envoyer. Il affichera le code nécessaire pour envoyer l'e-mail par programmation et les data seront renseignées avec l' événement de test précédent que j'ai créé.



Backend

Je vais copier le test AUTH_TOKEN dans le fichier .env et copier l'extrait dans 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, });

Créez une fonction sendInvoice qui sera responsable de l'envoi d'un e-mail. Pour envoyer un email depuis le code, j'utilise la fonction 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 };

Définissez des types pour la fonction sendInvoice .

 // src/lib/courier.ts interface SendInvoice { productName: string; dueDate: string; customerName: string; invoiceNumber: string; invoiceViewUrl: string; emailTo: string; }

Maintenant que je peux envoyer l'e-mail, je vais l'appeler dans le point de terminaison sendEmail trpc qui réside dans src/server/trpc/router/invoice.ts .


N'oubliez pas que le point de terminaison trpc est une route d'API Next.js. Dans ce cas, sendEmail sera identique à l'appel de la route /api/trpc/sendEmail avec fetch sous le capot. Pour plus d'explications 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); }),


Pour ceux qui ne connaissent pas trpc, ce que j'ai fait est identique à la gestion d'une requête POST . Décomposons-le.


  1. Manière Trpc de définir l'entrée de requête du client en validant avec Zod. Ici, je définis toutes les données nécessaires à la fonction 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. Définissez un gestionnaire de requête POST (mutation).
 // 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); }),

L'extrémité avant

Maintenant, je peux commencer à ajouter la fonctionnalité au bouton d'envoi d'e-mail. Je vais utiliser la fonction trpc.useMutation() qui est un fin wrapper de useMutation` tanstack-query's .


Ajoutons la fonction de mutation. En cas de réponse positive, je souhaite envoyer un toast de réussite sur l'interface utilisateur.

 //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!'); } }); }

Je peux simplement utiliser la fonction comme gestionnaire en ligne, mais je souhaite créer un nouveau gestionnaire pour le bouton.

 //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, }); };

Maintenant, je peux attacher le gestionnaire au bouton d'envoi d'e-mail.

 //src/pages/invoices/[invoiceId]/index.tsx <Button variant="primary" onClick={sendInvoiceEmail} isLoading={sendEmailMutation.isLoading}> Send to Email </Button>

Voici l'interface utilisateur de travail.

Partie 3 : Envoyer un rappel de paiement

Pour programmer un rappel qui sera envoyé un jour avant la date d'échéance d'une facture, je vais utiliser l'API d'automatisation de Courier .


Tout d'abord, concevons le modèle d'e-mail dans Courier Designer. Comme je suis déjà passé par le processus avant, voici le résultat final.


Avant d'ajouter la fonction, définissez les types du paramètre et refactorisez les types.

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

Maintenant, j'ajoute la fonction 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; };

Pour envoyer le rappel, j'appellerai scheduleReminder après une tentative réussie sendInvoice . Modifions le point de terminaison 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, }); }

Maintenant, si j'essaie d'envoyer une facture par e-mail, je devrais recevoir un rappel 20 secondes plus tard puisque je suis dans l'environnement de développement.

Partie 4 : Annuler un rappel

Enfin, toutes les fonctionnalités sont prêtes. Cependant, j'ai un problème, que se passe-t-il si un client a payé avant la date prévue pour le rappel de paiement ? Actuellement, l'e-mail de rappel sera toujours envoyé. Ce n'est pas une excellente expérience utilisateur et potentiellement un client confus. Heureusement, Courier dispose d'une fonction d'annulation d'automatisation.


Ajoutons la fonction cancelAutomationWorkflow qui peut annuler tout workflow d'automatisation dans 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; };

Qu'est-ce qu'un jeton d'annulation ? Il s'agit d'un jeton unique qui peut être défini sur un flux de travail d'automatisation. Il peut donc être annulé en envoyant une action cancel avec un cancelation_token correspondant.


Ajoutez cancelation_token à scheduleReminder , j'utilise l'identifiant de la facture comme jeton.

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

J'appellera cancelAutomationWorkflow lorsque le statut d'une facture est mis à jour sur PAID dans le point de terminaison trpc updateStatus .

 // 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; }),

Voici l'interface utilisateur de travail.

Partie 5 : Gestion des erreurs

Une remarque importante lors des requêtes réseau est qu'il existe des possibilités d'échec des requêtes/erreurs. Je veux gérer l'erreur en la lançant au client, afin qu'elle puisse être reflétée dans l'interface utilisateur.


En cas d'erreur, l'API Courier génère une erreur avec le type CourierHttpClientError par défaut. J'aurai également la valeur de retour de toutes les fonctions dans src/lib/courier.ts conformément au format ci-dessous.

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

Maintenant, je peux gérer les erreurs en ajoutant un bloc try-catch à toutes les fonctions dans 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!" }; } }

Voyons un exemple de manipulation sur le point de terminaison 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);

Partie 6 : Passer à la production

Maintenant que tous les modèles sont prêts, je vais copier tous les actifs de l'environnement de test vers la production. Voici un exemple.

Conclusion

Enfin, toutes les fonctionnalités sont intégrées à Courier. Nous avons suivi un workflow d'intégration de l'API Courier à une application Next.js. Bien que ce soit dans Next.js et trpc, le flux de travail sera à peu près le même avec n'importe quelle autre technologie. J'espère maintenant que vous pourrez intégrer Courier dans votre application par vous-même.


Commencez dès maintenant : https://app.courier.com/signup

A propos de l'auteur

Je suis Fazza Razaq Amiarso, un développeur web full-stack d'Indonésie. Je suis aussi un passionné de l'Open Source. J'aime partager mes connaissances et mon apprentissage sur mon blog . J'aide occasionnellement d'autres développeurs sur FrontendMentor pendant mon temps libre.


Connectez-vous avec moi sur LinkedIn .

Liens rapides

🔗 Documents de messagerie

🔗 Contribuer aux Invoys

🔗 Invoys Motivation


Également publié ici .