diff --git a/packages/server/src/services/Sales/Receipts/SaleReceiptApplication.ts b/packages/server/src/services/Sales/Receipts/SaleReceiptApplication.ts index 8dfdd4c75..9fdfb548a 100644 --- a/packages/server/src/services/Sales/Receipts/SaleReceiptApplication.ts +++ b/packages/server/src/services/Sales/Receipts/SaleReceiptApplication.ts @@ -5,6 +5,7 @@ import { IPaginationMeta, ISaleReceipt, ISalesReceiptsFilter, + SaleReceiptMailOpts, } from '@/interfaces'; import { EditSaleReceipt } from './EditSaleReceipt'; import { GetSaleReceipt } from './GetSaleReceipt'; @@ -13,6 +14,7 @@ import { GetSaleReceipts } from './GetSaleReceipts'; import { CloseSaleReceipt } from './CloseSaleReceipt'; import { SaleReceiptsPdf } from './SaleReceiptsPdfService'; import { SaleReceiptNotifyBySms } from './SaleReceiptNotifyBySms'; +import { SaleReceiptMailNotification } from './SaleReceiptMailNotification'; @Service() export class SaleReceiptApplication { @@ -40,6 +42,9 @@ export class SaleReceiptApplication { @Inject() private saleReceiptNotifyBySmsService: SaleReceiptNotifyBySms; + @Inject() + private saleReceiptNotifyByMailService: SaleReceiptMailNotification; + /** * Creates a new sale receipt with associated entries. * @param {number} tenantId @@ -166,4 +171,21 @@ export class SaleReceiptApplication { saleReceiptId ); } + + /** + * Sends the receipt mail of the given sale receipt. + * @param {number} tenantId + * @param {number} saleReceiptId + */ + public sendSaleReceiptMail( + tenantId: number, + saleReceiptId: number, + messageOpts: SaleReceiptMailOpts + ) { + return this.saleReceiptNotifyByMailService.triggerMail( + tenantId, + saleReceiptId, + messageOpts + ); + } } diff --git a/packages/server/src/services/Sales/Receipts/SaleReceiptMailNotification.ts b/packages/server/src/services/Sales/Receipts/SaleReceiptMailNotification.ts new file mode 100644 index 000000000..bf6c4c893 --- /dev/null +++ b/packages/server/src/services/Sales/Receipts/SaleReceiptMailNotification.ts @@ -0,0 +1,147 @@ +import * as R from 'ramda'; +import HasTenancyService from '@/services/Tenancy/TenancyService'; +import { Inject, Service } from 'typedi'; +import { Tenant } from '@/system/models'; +import { formatSmsMessage } from '@/utils'; +import { ServiceError } from '@/exceptions'; +import Mail from '@/lib/Mail'; +import { GetSaleReceipt } from './GetSaleReceipt'; +import { SaleReceiptsPdf } from './SaleReceiptsPdfService'; +import { + DEFAULT_ESTIMATE_REMINDER_MAIL_CONTENT, + DEFAULT_ESTIMATE_REMINDER_MAIL_SUBJECT, +} from '../Estimates/constants'; +import { ERRORS } from './constants'; +import { SaleReceiptMailOpts } from '@/interfaces'; + +@Service() +export class SaleReceiptMailNotification { + @Inject() + private tenancy: HasTenancyService; + + @Inject() + private getSaleReceiptService: GetSaleReceipt; + + @Inject() + private receiptPdfService: SaleReceiptsPdf; + + @Inject('agenda') + private agenda: any; + + /** + * Sends the receipt mail of the given sale receipt. + * @param {number} tenantId + * @param {number} saleInvoiceId + * @param {SendInvoiceMailDTO} messageDTO + */ + public async triggerMail( + tenantId: number, + saleReceiptId: number, + messageOpts: SaleReceiptMailOpts + ) { + const payload = { + tenantId, + saleReceiptId, + messageOpts, + }; + await this.agenda.now('sale-receipt-mail-send', payload); + } + + /** + * Retrieves the default receipt mail options. + * @param {number} tenantId + * @param {number} invoiceId + * @returns {Promise} + */ + public getDefaultMailOpts = async (tenantId: number, invoiceId: number) => { + const { SaleReceipt } = this.tenancy.models(tenantId); + const saleReceipt = await SaleReceipt.query() + .findById(invoiceId) + .withGraphFetched('customer') + .throwIfNotFound(); + + return { + attachInvoice: true, + subject: DEFAULT_ESTIMATE_REMINDER_MAIL_SUBJECT, + body: DEFAULT_ESTIMATE_REMINDER_MAIL_CONTENT, + to: saleReceipt.customer.email, + }; + }; + + /** + * Retrieves the formatted text of the given sale invoice. + * @param {number} tenantId - Tenant id. + * @param {number} receiptId - Sale receipt id. + * @param {string} text - The given text. + * @returns {Promise} + */ + public textFormatter = async ( + tenantId: number, + receiptId: number, + text: string + ): Promise => { + const invoice = await this.getSaleReceiptService.getSaleReceipt( + tenantId, + receiptId + ); + const organization = await Tenant.query() + .findById(tenantId) + .withGraphFetched('metadata'); + + return formatSmsMessage(text, { + CompanyName: organization.metadata.name, + CustomerName: invoice.customer.displayName, + InvoiceNumber: invoice.invoiceNo, + InvoiceDueAmount: invoice.dueAmountFormatted, + InvoiceDueDate: invoice.dueDateFormatted, + InvoiceDate: invoice.invoiceDateFormatted, + InvoiceAmount: invoice.totalFormatted, + }); + }; + + /** + * Triggers the mail invoice. + * @param {number} tenantId + * @param {number} saleInvoiceId + * @param {SendInvoiceMailDTO} messageDTO + * @returns {Promise} + */ + public async sendMail( + tenantId: number, + saleReceiptId: number, + messageOpts: SaleReceiptMailOpts + ) { + const defaultMessageOpts = await this.getDefaultMailOpts( + tenantId, + saleReceiptId + ); + // Parsed message opts with default options. + const parsedMessageOpts = { + ...defaultMessageOpts, + ...messageOpts, + }; + // In case there is no email address from the customer or from options, throw an error. + if (!parsedMessageOpts.to) { + throw new ServiceError(ERRORS.NO_INVOICE_CUSTOMER_EMAIL_ADDR); + } + const formatter = R.curry(this.textFormatter)(tenantId, saleReceiptId); + const body = await formatter(parsedMessageOpts.body); + const subject = await formatter(parsedMessageOpts.subject); + const attachments = []; + + if (parsedMessageOpts.attachInvoice) { + // Retrieves document buffer of the invoice pdf document. + const receiptPdfBuffer = await this.receiptPdfService.saleReceiptPdf( + tenantId, + saleReceiptId + ); + attachments.push({ filename: 'invoice.pdf', content: receiptPdfBuffer }); + } + await new Mail() + .setSubject(subject) + .setTo(parsedMessageOpts.to) + .setContent(body) + .setAttachments(attachments) + .send(); + } +} diff --git a/packages/server/src/services/Sales/Receipts/SaleReceiptMailNotificationJob.ts b/packages/server/src/services/Sales/Receipts/SaleReceiptMailNotificationJob.ts new file mode 100644 index 000000000..f32325114 --- /dev/null +++ b/packages/server/src/services/Sales/Receipts/SaleReceiptMailNotificationJob.ts @@ -0,0 +1,36 @@ +import Container, { Service } from 'typedi'; +import { SaleReceiptMailNotification } from './SaleReceiptMailNotification'; + +@Service() +export class SaleReceiptMailNotificationJob { + /** + * Constructor method. + */ + constructor(agenda) { + agenda.define( + 'sale-receipt-mail-send', + { priority: 'high', concurrency: 2 }, + this.handler + ); + } + + /** + * Triggers sending invoice mail. + */ + private handler = async (job, done: Function) => { + const { tenantId, saleReceiptId, messageOpts } = job.attrs.data; + const receiveMailNotification = Container.get(SaleReceiptMailNotification); + + try { + await receiveMailNotification.sendMail( + tenantId, + saleReceiptId, + messageOpts + ); + done(); + } catch (error) { + console.log(error); + done(error); + } + }; +} diff --git a/packages/server/src/services/Sales/Receipts/SaleReceiptsPdfService.ts b/packages/server/src/services/Sales/Receipts/SaleReceiptsPdfService.ts index c06263212..cad2b5f93 100644 --- a/packages/server/src/services/Sales/Receipts/SaleReceiptsPdfService.ts +++ b/packages/server/src/services/Sales/Receipts/SaleReceiptsPdfService.ts @@ -1,6 +1,7 @@ import { Inject, Service } from 'typedi'; import { TemplateInjectable } from '@/services/TemplateInjectable/TemplateInjectable'; import { ChromiumlyTenancy } from '@/services/ChromiumlyTenancy/ChromiumlyTenancy'; +import { GetSaleReceipt } from './GetSaleReceipt'; @Service() export class SaleReceiptsPdf { @@ -10,11 +11,20 @@ export class SaleReceiptsPdf { @Inject() private templateInjectable: TemplateInjectable; + @Inject() + private getSaleReceiptService: GetSaleReceipt; + /** - * Retrieve sale invoice pdf content. - * @param {} saleInvoice - + * Retrieves sale invoice pdf content. + * @param {number} tenantId - + * @param {number} saleInvoiceId - + * @returns {Promise} */ - public async saleReceiptPdf(tenantId: number, saleReceipt) { + public async saleReceiptPdf(tenantId: number, saleReceiptId: number) { + const saleReceipt = await this.getSaleReceiptService.getSaleReceipt( + tenantId, + saleReceiptId + ); const htmlContent = await this.templateInjectable.render( tenantId, 'modules/receipt-regular', diff --git a/packages/server/src/services/Sales/Receipts/constants.ts b/packages/server/src/services/Sales/Receipts/constants.ts index bf0cdef18..977a5d9b1 100644 --- a/packages/server/src/services/Sales/Receipts/constants.ts +++ b/packages/server/src/services/Sales/Receipts/constants.ts @@ -1,3 +1,15 @@ +export const DEFAULT_RECEIPT_MAIL_SUBJECT = + 'Invoice {InvoiceNumber} from {CompanyName}'; +export const DEFAULT_RECEIPT_MAIL_CONTENT = ` +

Dear {CustomerName}

+

Thank you for your business, You can view or print your invoice from attachements.

+

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

+`; + export const ERRORS = { SALE_RECEIPT_NOT_FOUND: 'SALE_RECEIPT_NOT_FOUND', DEPOSIT_ACCOUNT_NOT_FOUND: 'DEPOSIT_ACCOUNT_NOT_FOUND', @@ -6,6 +18,7 @@ export const ERRORS = { SALE_RECEIPT_IS_ALREADY_CLOSED: 'SALE_RECEIPT_IS_ALREADY_CLOSED', SALE_RECEIPT_NO_IS_REQUIRED: 'SALE_RECEIPT_NO_IS_REQUIRED', CUSTOMER_HAS_SALES_INVOICES: 'CUSTOMER_HAS_SALES_INVOICES', + NO_INVOICE_CUSTOMER_EMAIL_ADDR: 'NO_INVOICE_CUSTOMER_EMAIL_ADDR' }; export const DEFAULT_VIEW_COLUMNS = [];