From f0e15d43d300df7a4849e439d35fd9b0aead65f3 Mon Sep 17 00:00:00 2001 From: Ahmed Bouhuolia Date: Fri, 22 Dec 2023 23:56:37 +0200 Subject: [PATCH] feat: send sale estimate mail notification --- .../Estimates/SaleEstimatesApplication.ts | 41 ++++-- .../Sales/Estimates/SaleEstimatesPdf.ts | 13 +- .../Sales/Estimates/SendSaleEstimateMail.ts | 137 +++++++++++++++++- .../Estimates/SendSaleEstimateMailJob.ts | 36 +++++ .../src/services/Sales/Estimates/constants.ts | 11 +- 5 files changed, 217 insertions(+), 21 deletions(-) create mode 100644 packages/server/src/services/Sales/Estimates/SendSaleEstimateMailJob.ts diff --git a/packages/server/src/services/Sales/Estimates/SaleEstimatesApplication.ts b/packages/server/src/services/Sales/Estimates/SaleEstimatesApplication.ts index 907693ea0..6a208746e 100644 --- a/packages/server/src/services/Sales/Estimates/SaleEstimatesApplication.ts +++ b/packages/server/src/services/Sales/Estimates/SaleEstimatesApplication.ts @@ -7,6 +7,7 @@ import { ISaleEstimate, ISaleEstimateDTO, ISalesEstimatesFilter, + SaleEstimateMailOptions, } from '@/interfaces'; import { EditSaleEstimate } from './EditSaleEstimate'; import { DeleteSaleEstimate } from './DeleteSaleEstimate'; @@ -202,25 +203,33 @@ export class SaleEstimatesApplication { }; /** - * - * @param {number} tenantId - * @param {} saleEstimate - * @returns - */ - public getSaleEstimatePdf(tenantId: number, saleEstimate) { - return this.saleEstimatesPdfService.getSaleEstimatePdf( - tenantId, - saleEstimate - ); - } - - /** - * + * Retrieve the PDF content of the given sale estimate. * @param {number} tenantId * @param {number} saleEstimateId * @returns */ - public sendSaleEstimateMail(tenantId: number, saleEstimateId: number) { - return this.sendEstimateMailService.sendMail(tenantId, saleEstimateId); + public getSaleEstimatePdf(tenantId: number, saleEstimateId: number) { + return this.saleEstimatesPdfService.getSaleEstimatePdf( + tenantId, + saleEstimateId + ); + } + + /** + * Send the reminder mail of the given sale estimate. + * @param {number} tenantId + * @param {number} saleEstimateId + * @returns {Promise} + */ + public sendSaleEstimateMail( + tenantId: number, + saleEstimateId: number, + saleEstimateMailOpts: SaleEstimateMailOptions + ) { + return this.sendEstimateMailService.triggerMail( + tenantId, + saleEstimateId, + saleEstimateMailOpts + ); } } diff --git a/packages/server/src/services/Sales/Estimates/SaleEstimatesPdf.ts b/packages/server/src/services/Sales/Estimates/SaleEstimatesPdf.ts index db19743f7..af1d2098c 100644 --- a/packages/server/src/services/Sales/Estimates/SaleEstimatesPdf.ts +++ b/packages/server/src/services/Sales/Estimates/SaleEstimatesPdf.ts @@ -1,6 +1,7 @@ import { Inject, Service } from 'typedi'; import { ChromiumlyTenancy } from '@/services/ChromiumlyTenancy/ChromiumlyTenancy'; import { TemplateInjectable } from '@/services/TemplateInjectable/TemplateInjectable'; +import { GetSaleEstimate } from './GetSaleEstimate'; @Service() export class SaleEstimatesPdf { @@ -10,11 +11,19 @@ export class SaleEstimatesPdf { @Inject() private templateInjectable: TemplateInjectable; + @Inject() + private getSaleEstimate: GetSaleEstimate; + /** * Retrieve sale invoice pdf content. - * @param {} saleInvoice - + * @param {number} tenantId - + * @param {ISaleInvoice} saleInvoice - */ - async getSaleEstimatePdf(tenantId: number, saleEstimate) { + public async getSaleEstimatePdf(tenantId: number, saleEstimateId: number) { + const saleEstimate = await this.getSaleEstimate.getEstimate( + tenantId, + saleEstimateId + ); const htmlContent = await this.templateInjectable.render( tenantId, 'modules/estimate-regular', diff --git a/packages/server/src/services/Sales/Estimates/SendSaleEstimateMail.ts b/packages/server/src/services/Sales/Estimates/SendSaleEstimateMail.ts index 8dca133bb..bc558d424 100644 --- a/packages/server/src/services/Sales/Estimates/SendSaleEstimateMail.ts +++ b/packages/server/src/services/Sales/Estimates/SendSaleEstimateMail.ts @@ -1,6 +1,139 @@ -import { Service } from "typedi"; +import { Inject, Service } from 'typedi'; +import * as R from 'ramda'; +import Mail from '@/lib/Mail'; +import HasTenancyService from '@/services/Tenancy/TenancyService'; +import { + DEFAULT_ESTIMATE_REMINDER_MAIL_CONTENT, + DEFAULT_ESTIMATE_REMINDER_MAIL_SUBJECT, +} from './constants'; +import { SaleEstimatesPdf } from './SaleEstimatesPdf'; +import { GetSaleEstimate } from './GetSaleEstimate'; +import { formatSmsMessage } from '@/utils'; +import { SaleEstimateMailOptions } from '@/interfaces'; @Service() export class SendSaleEstimateMail { - sendMail(tenantId: number, saleEstimateId: number) {} + @Inject() + private tenancy: HasTenancyService; + + @Inject('agenda') + private agenda: any; + + @Inject() + private estimatePdf: SaleEstimatesPdf; + + @Inject() + private getSaleEstimateService: GetSaleEstimate; + + /** + * Triggers the reminder mail of the given sale estimate. + * @param {number} tenantId + * @param {number} saleEstimateId + * @param messageOptions + */ + public async triggerMail( + tenantId: number, + saleEstimateId: number, + messageOptions: any + ) { + const payload = { + tenantId, + saleEstimateId, + messageOptions, + }; + await this.agenda.now('sale-estimate-mail-send', payload); + } + + /** + * Formates the text of the mail. + * @param {number} tenantId + * @param {number} estimateId + * @param {string} text + */ + public formatText = async ( + tenantId: number, + estimateId: number, + text: string + ) => { + const estimate = await this.getSaleEstimateService.getEstimate( + tenantId, + estimateId + ); + return formatSmsMessage(text, { + CustomerName: estimate.customer.displayName, + EstimateNumber: estimate.estimateNo, + EstimateDate: estimate.estimateDateFormatted, + EstimateAmount: estimate.totalFormatted, + EstimateDueDate: estimate.dueDateFormatted, + EstimateDueAmount: estimate.dueAmountFormatted, + }); + }; + + /** + * Retrieves the default mail options. + * @param {number} tenantId + * @param {number} saleEstimateId + * @returns {Promise} + */ + public getDefaultMailOpts = async ( + tenantId: number, + saleEstimateId: number + ) => { + const { SaleEstimate } = this.tenancy.models(tenantId); + + const saleEstimate = await SaleEstimate.query() + .findById(saleEstimateId) + .withGraphFetched('customer') + .throwIfNotFound(); + + return { + attachPdf: true, + subject: DEFAULT_ESTIMATE_REMINDER_MAIL_SUBJECT, + body: DEFAULT_ESTIMATE_REMINDER_MAIL_CONTENT, + to: saleEstimate.customer.email, + }; + }; + + /** + * Sends the mail. + * @param {number} tenantId + * @param {number} saleEstimateId + * @param {SaleEstimateMailOptions} messageOptions + */ + public async sendMail( + tenantId: number, + saleEstimateId: number, + messageOptions: SaleEstimateMailOptions + ) { + const defaultMessageOpts = await this.getDefaultMailOpts( + tenantId, + saleEstimateId + ); + const parsedMessageOpts = { + ...defaultMessageOpts, + ...messageOptions, + }; + const formatter = R.curry(this.formatText)(tenantId, saleEstimateId); + const toEmail = parsedMessageOpts.to; + const subject = await formatter(parsedMessageOpts.subject); + const body = await formatter(parsedMessageOpts.body); + const attachments = []; + + if (parsedMessageOpts.to) { + const estimatePdfBuffer = await this.estimatePdf.getSaleEstimatePdf( + tenantId, + saleEstimateId + ); + attachments.push({ + filename: 'estimate.pdf', + content: estimatePdfBuffer, + }); + } + await new Mail() + .setSubject(subject) + .setTo(toEmail) + .setContent(body) + .setAttachments(attachments) + .send(); + } } diff --git a/packages/server/src/services/Sales/Estimates/SendSaleEstimateMailJob.ts b/packages/server/src/services/Sales/Estimates/SendSaleEstimateMailJob.ts new file mode 100644 index 000000000..b5e8eda39 --- /dev/null +++ b/packages/server/src/services/Sales/Estimates/SendSaleEstimateMailJob.ts @@ -0,0 +1,36 @@ +import Container, { Service } from 'typedi'; +import { SendSaleEstimateMail } from './SendSaleEstimateMail'; + +@Service() +export class SendSaleEstimateMailJob { + /** + * Constructor method. + */ + constructor(agenda) { + agenda.define( + 'sale-estimate-mail-send', + { priority: 'high', concurrency: 2 }, + this.handler + ); + } + + /** + * Triggers sending invoice mail. + */ + private handler = async (job, done: Function) => { + const { tenantId, saleEstimateId, messageOptions } = job.attrs.data; + const sendSaleEstimateMail = Container.get(SendSaleEstimateMail); + + try { + await sendSaleEstimateMail.sendMail( + tenantId, + saleEstimateId, + messageOptions + ); + done(); + } catch (error) { + console.log(error); + done(error); + } + }; +} diff --git a/packages/server/src/services/Sales/Estimates/constants.ts b/packages/server/src/services/Sales/Estimates/constants.ts index 2b58c74a8..b59350430 100644 --- a/packages/server/src/services/Sales/Estimates/constants.ts +++ b/packages/server/src/services/Sales/Estimates/constants.ts @@ -1,3 +1,12 @@ +export const DEFAULT_ESTIMATE_REMINDER_MAIL_SUBJECT = + 'Invoice {InvoiceNumber} reminder from {CompanyName}'; +export const DEFAULT_ESTIMATE_REMINDER_MAIL_CONTENT = ` +

Dear {CustomerName}

+

You might have missed the payment date and the invoice is now overdue by {OverdueDays} days.

+

Invoice #{InvoiceNumber}
+Due Date : {InvoiceDueDate}
+Amount : {InvoiceAmount}

+`; export const ERRORS = { SALE_ESTIMATE_NOT_FOUND: 'SALE_ESTIMATE_NOT_FOUND', @@ -8,7 +17,7 @@ export const ERRORS = { CUSTOMER_HAS_SALES_ESTIMATES: 'CUSTOMER_HAS_SALES_ESTIMATES', SALE_ESTIMATE_NO_IS_REQUIRED: 'SALE_ESTIMATE_NO_IS_REQUIRED', SALE_ESTIMATE_ALREADY_DELIVERED: 'SALE_ESTIMATE_ALREADY_DELIVERED', - SALE_ESTIMATE_ALREADY_APPROVED: 'SALE_ESTIMATE_ALREADY_APPROVED' + SALE_ESTIMATE_ALREADY_APPROVED: 'SALE_ESTIMATE_ALREADY_APPROVED', }; export const DEFAULT_VIEW_COLUMNS = [];