Muchas aplicaciones de gestión de facturas de código abierto se crean con Laravel. Como desarrollador de Javascript, quería crear la "Solución React" para desarrolladores que están familiarizados con React y Javascript.
Un problema que encontré al compilar con servicios en Node.js es que no hay una aplicación de correo integrada. Entonces, tuve que encontrar un servicio de terceros para que hiciera eso por mí. En este artículo, integraré Courier para enviar correos electrónicos para este proyecto https://github.com/fazzaamiarso/invoys .
Dado que este artículo no es el típico seguimiento (más bien "siéntate y mira cómo lo hago"), no es obligatorio estar familiarizado con todas las tecnologías utilizadas. Sin embargo, la familiaridad con Typescript y Next.js será beneficiosa para una comprensión más rápida.
Tecnologías en este blog:
Texto mecanografiado : la seguridad de tipo y la finalización automática son las mejores, ¿verdad?
Next.js : un marco listo para producción para crear una aplicación de pila completa, incluso para principiantes.
Prisma : un gran ORM para trabajar con bases de datos. Usamos Prisma debido a su seguridad de tipo y autocompletado, lo que brinda una excelente experiencia de desarrollador con texto mecanografiado agregado.
Trpc : nos permite crear fácilmente seguridad de tipo de extremo a extremo entre nuestro cliente y servidor Next.js.
Courier API: un excelente servicio/plataforma para manejar nuestras notificaciones, como correo electrónico, SMS y mucho más.
Puede encontrar el código fuente completo aquí como referencia.
Antes de construir las funciones, definamos nuestros objetivos.
Vayamos al panel de Courier. Por defecto, está en un entorno de producción. Como quiero probar cosas, voy a cambiar al entorno de prueba haciendo clic en el menú desplegable en la esquina superior derecha.
Podemos copiar todas las plantillas más tarde a producción o viceversa.
Ahora, crearé una marca para mis notificaciones por correo electrónico.
Solo voy a agregar un logotipo (tenga en cuenta que el ancho del logotipo está fijado en 140 px) en el encabezado y los enlaces sociales en el pie de página. La interfaz de usuario del diseñador es bastante sencilla, así que aquí está el resultado final.
No olvides publicar los cambios.
Actualmente, el botón de enviar correo electrónico en la interfaz de usuario no hace nada.
Voy a crear un archivo courier.ts
en src/lib/
para mantener todo el código relacionado con Courier. Además, usaré la biblioteca de cliente Courier Node.js que ya abstrajo todos los puntos finales de Courier API para funciones.
Antes de crear la funcionalidad, creemos el diseño de notificación por correo electrónico dentro de Courier's Designer y configuremos el proveedor de Gmail.
En la página del diseñador de correo electrónico, veremos que la marca creada ya está integrada. Después de eso, diseñemos la plantilla de acuerdo con los datos necesarios. Aquí esta el resultado final.
Observe el valor con {}
que se vuelve verde, significa que es una variable que se puede insertar dinámicamente. También configuré el botón (o acción) 'Ver factura' con una variable.
Antes de poder usar la plantilla, necesito crear un evento de prueba haciendo clic en la pestaña de vista previa. Luego, mostrará un aviso para nombrar el evento y configurar data
en formato JSON. Ese campo de datos es lo que llenará el valor de las variables verdes {}
(los datos también se pueden configurar desde el código). Dado que es un evento de prueba, lo llenaré con valores arbitrarios.
A continuación, publicaré la plantilla para poder usarla. Luego, ve a la pestaña de envío. Mostrará el código necesario para enviar el correo electrónico mediante programación y los data
se completarán con el evento de prueba anterior que creé.
Copiaré la prueba AUTH_TOKEN
en el archivo .env
y copiaré el fragmento en 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, });
Cree una función sendInvoice
que se encargará de enviar un correo electrónico. Para enviar un correo electrónico desde el código, uso la función 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 };
Defina tipos para la función sendInvoice
.
// src/lib/courier.ts interface SendInvoice { productName: string; dueDate: string; customerName: string; invoiceNumber: string; invoiceViewUrl: string; emailTo: string; }
Ahora que puedo enviar el correo electrónico, lo llamaré en el punto final de trpc sendEmail
que reside en src/server/trpc/router/invoice.ts
.
Solo recuerde que el punto final de trpc es una ruta API de Next.js. En este caso,
sendEmail
será lo mismo que llamar a la ruta/api/trpc/sendEmail
confetch
oculta. Para obtener más explicaciones , 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); }),
Para aquellos que no están familiarizados con trpc, lo que hice fue lo mismo que manejar una solicitud POST
. Vamos a desglosarlo.
Trpc forma de definir la entrada de solicitud del cliente mediante la validación con Zod. Aquí defino todos los datos que se necesitan para la función 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
(mutación). // 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); }),
Ahora, puedo comenzar a agregar la funcionalidad al botón de enviar correo electrónico. Voy a utilizar la función trpc.useMutation()
, que es un envoltorio delgado de useMutation` tanstack-query's
.
Agreguemos la función de mutación. En respuesta exitosa, quiero enviar un brindis de éxito en la interfaz de usuario.
//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!'); } }); }
Solo puedo usar la función como un controlador en línea, pero quiero crear un nuevo controlador para el botón.
//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, }); };
Ahora puedo adjuntar el controlador al botón de enviar correo electrónico.
//src/pages/invoices/[invoiceId]/index.tsx <Button variant="primary" onClick={sendInvoiceEmail} isLoading={sendEmailMutation.isLoading}> Send to Email </Button>
Aquí está la interfaz de usuario de trabajo.
Para programar un recordatorio que se enviará un día antes de la fecha de vencimiento de una factura, usaré la API de automatización de Courier .
Primero, diseñemos la plantilla de correo electrónico en Courier Designer. Como ya pasé por el proceso antes, aquí está el resultado final.
Antes de agregar la función, defina los tipos para el parámetro y refactorice los tipos.
// 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; }
Ahora, agrego la función scheduleReminder
a 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; };
Para enviar el recordatorio, llamaré a scheduleReminder
después de un intento exitoso sendInvoice
. Modifiquemos el punto final de trpc sendEmail
.
// 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, }); }
Ahora, si trato de enviar una factura por correo electrónico, debería recibir un recordatorio 20 segundos después, ya que estoy en el entorno de desarrollo.
Finalmente, todas las características están listas. Sin embargo, tengo un problema, ¿y si un cliente hubiera pagado antes de la fecha programada para el recordatorio de pago? Actualmente, el correo electrónico de recordatorio aún se enviará. Esa no es una gran experiencia de usuario y potencialmente un cliente confuso. Afortunadamente, Courier tiene una función de cancelación de automatización.
Agreguemos la función cancelAutomationWorkflow
que puede cancelar cualquier flujo de trabajo de automatización en 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é es un token de cancelación? Es un token único que se puede configurar en un flujo de trabajo de automatización, por lo que se puede cancelar enviando una acción cancel
con un cancelation_token
coincidente.
Agregue cancelation_token a scheduleReminder
, uso la identificación de la factura como token.
// 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
Llamaré a cancelAutomationWorkflow
cuando el estado de una factura se actualice a PAID
en el punto final 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; }),
Aquí está la interfaz de usuario de trabajo.
Una nota importante al realizar solicitudes de red es que existen posibilidades de solicitudes fallidas/errores. Quiero manejar el error arrojándolo al cliente, para que pueda reflejarse en la interfaz de usuario.
En caso de error, Courier API genera un error con el tipo CourierHttpClientError
de forma predeterminada. También tendré el valor de retorno de todas las funciones en src/lib/courier.ts
de acuerdo con el siguiente formato.
// On Success type SuccessResponse = { data: any, error: null } // On Error type ErrorResponse = { data: any, error: string }
Ahora, puedo manejar los errores agregando un bloque try-catch
a todas las funciones en 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!" }; } }
Veamos un ejemplo de manejo en el punto final trpc sendEmail
.
// 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);
Ahora que todas las plantillas están listas, copiaré todos los recursos del entorno de prueba a producción. Aquí hay un ejemplo.
Finalmente, todas las funciones están integradas con Courier. Pasamos por un flujo de trabajo de integración de Courier API a una aplicación Next.js. Aunque está en Next.js y trpc, el flujo de trabajo será prácticamente el mismo con cualquier otra tecnología. Espero que ahora pueda integrar Courier en su aplicación usted mismo.
Comience ahora: https://app.courier.com/signup
Soy Fazza Razaq Amiarso, un desarrollador web full-stack de Indonesia. También soy un entusiasta del código abierto. Me encanta compartir mis conocimientos y aprendizajes en mi blog . De vez en cuando ayudo a otros desarrolladores en FrontendMentor en mi tiempo libre.
Conéctate conmigo en LinkedIn .
También publicado aquí .