diff --git a/packages/server/src/api/controllers/Sales/SalesInvoices.ts b/packages/server/src/api/controllers/Sales/SalesInvoices.ts index cd7f5bfec..9e5ac8d25 100644 --- a/packages/server/src/api/controllers/Sales/SalesInvoices.ts +++ b/packages/server/src/api/controllers/Sales/SalesInvoices.ts @@ -696,7 +696,10 @@ export default class SaleInvoicesController extends BaseController { invoiceId, invoiceMailDTO ); - return res.status(200).send({}); + return res.status(200).send({ + code: 200, + message: 'The sale invoice mail has been sent successfully.', + }); } catch (error) { next(error); } @@ -717,18 +720,18 @@ export default class SaleInvoicesController extends BaseController { const { id: invoiceId } = req.params; try { - await this.saleInvoiceApplication.getSaleInvoiceMailReminder( + const data = await this.saleInvoiceApplication.getSaleInvoiceMailReminder( tenantId, invoiceId ); - return res.status(200).send({}); + return res.status(200).send(data); } catch (error) { next(error); } } /** - * + * Sends mail invoice of the given sale invoice. * @param {Request} req * @param {Response} res * @param {NextFunction} next @@ -749,7 +752,10 @@ export default class SaleInvoicesController extends BaseController { invoiceId, invoiceMailDTO ); - return res.status(200).send({}); + return res.status(200).send({ + code: 200, + message: 'The sale invoice mail reminder has been sent successfully.', + }); } catch (error) { next(error); } diff --git a/packages/server/src/config/index.ts b/packages/server/src/config/index.ts index bc6833130..0dc9d9676 100644 --- a/packages/server/src/config/index.ts +++ b/packages/server/src/config/index.ts @@ -58,6 +58,7 @@ module.exports = { secure: !!parseInt(process.env.MAIL_SECURE, 10), username: process.env.MAIL_USERNAME, password: process.env.MAIL_PASSWORD, + from: process.env.MAIL_FROM_ADDRESS, }, /** diff --git a/packages/server/src/interfaces/SaleInvoice.ts b/packages/server/src/interfaces/SaleInvoice.ts index 7d7633b87..d660b17ca 100644 --- a/packages/server/src/interfaces/SaleInvoice.ts +++ b/packages/server/src/interfaces/SaleInvoice.ts @@ -1,5 +1,5 @@ import { Knex } from 'knex'; -import { ISystemUser, IAccount, ITaxTransaction } from '@/interfaces'; +import { ISystemUser, IAccount, ITaxTransaction, AddressItem } from '@/interfaces'; import { IDynamicListFilter } from '@/interfaces/DynamicFilter'; import { IItemEntry, IItemEntryDTO } from './ItemEntry'; @@ -187,8 +187,18 @@ export enum SaleInvoiceAction { NotifyBySms = 'NotifyBySms', } +export interface SaleInvoiceMailOptions { + toAddresses: AddressItem[]; + fromAddresses: AddressItem[]; + from: string; + to: string | string[]; + subject: string; + body: string; + attachInvoice: boolean; +} + export interface SendInvoiceMailDTO { - to: string; + to: string | string[]; from: string; subject: string; body: string; diff --git a/packages/server/src/models/Customer.ts b/packages/server/src/models/Customer.ts index 690b77d55..631763b71 100644 --- a/packages/server/src/models/Customer.ts +++ b/packages/server/src/models/Customer.ts @@ -24,6 +24,9 @@ export default class Customer extends mixin(TenantModel, [ CustomViewBaseModel, ModelSearchable, ]) { + email: string; + displayName: string; + /** * Query builder. */ @@ -76,6 +79,19 @@ export default class Customer extends mixin(TenantModel, [ return 'debit'; } + /** + * + */ + get contactAddresses() { + return [ + { + mail: this.email, + label: this.displayName, + primary: true + }, + ].filter((c) => c.mail); + } + /** * Model modifiers. */ diff --git a/packages/server/src/services/MailNotification/ContactMailNotification.ts b/packages/server/src/services/MailNotification/ContactMailNotification.ts new file mode 100644 index 000000000..21c745a96 --- /dev/null +++ b/packages/server/src/services/MailNotification/ContactMailNotification.ts @@ -0,0 +1,78 @@ +import { Inject, Service } from 'typedi'; +import * as R from 'ramda'; +import { SaleInvoiceMailOptions } from '@/interfaces'; +import HasTenancyService from '@/services/Tenancy/TenancyService'; +import { MailTenancy } from '@/services/MailTenancy/MailTenancy'; +import { formatSmsMessage } from '@/utils'; + +@Service() +export class ContactMailNotification { + @Inject() + private mailTenancy: MailTenancy; + + @Inject() + private tenancy: HasTenancyService; + + /** + * Parses the default message options. + * @param {number} tenantId + * @param {number} invoiceId + * @returns {Promise} + */ + public async getDefaultMailOptions( + tenantId: number, + contactId: number, + subject: string = '', + body: string = '' + ): Promise { + const { Contact, Customer } = this.tenancy.models(tenantId); + const contact = await Customer.query().findById(contactId).throwIfNotFound(); + + const toAddresses = contact.contactAddresses; + const fromAddresses = await this.mailTenancy.senders(tenantId); + + const toAddress = toAddresses.find((a) => a.primary); + const fromAddress = fromAddresses.find((a) => a.primary); + + const to = toAddress?.mail || ''; + const from = fromAddress?.mail || ''; + + return { + subject, + body, + to, + from, + fromAddresses, + toAddresses, + }; + } + + /** + * Retrieves the mail options. + * @param {number} + * @param {number} invoiceId + * @returns {} + */ + public async getMailOptions( + tenantId: number, + contactId: number, + defaultSubject?: string, + defaultBody?: string, + formatterData?: Record + ): Promise { + const mailOpts = await this.getDefaultMailOptions( + tenantId, + contactId, + defaultSubject, + defaultBody + ); + const subject = formatSmsMessage(mailOpts.subject, formatterData); + const body = formatSmsMessage(mailOpts.body, formatterData); + + return { + ...mailOpts, + subject, + body, + }; + } +} diff --git a/packages/server/src/services/MailTenancy/MailTenancy.ts b/packages/server/src/services/MailTenancy/MailTenancy.ts new file mode 100644 index 000000000..6f8e82e11 --- /dev/null +++ b/packages/server/src/services/MailTenancy/MailTenancy.ts @@ -0,0 +1,25 @@ +import config from '@/config'; +import { Tenant } from "@/system/models"; +import { Service } from 'typedi'; + + +@Service() +export class MailTenancy { + /** + * Retrieves the senders mails of the given tenant. + * @param {number} tenantId + */ + public async senders(tenantId: number) { + const tenant = await Tenant.query() + .findById(tenantId) + .withGraphFetched('metadata'); + + return [ + { + mail: config.mail.from, + label: tenant.metadata.name, + primary: true, + } + ].filter((item) => item.mail) + } +} \ No newline at end of file diff --git a/packages/server/src/services/Sales/Estimates/SaleEstimatesApplication.ts b/packages/server/src/services/Sales/Estimates/SaleEstimatesApplication.ts index 68cd8e601..1ceb2bbcc 100644 --- a/packages/server/src/services/Sales/Estimates/SaleEstimatesApplication.ts +++ b/packages/server/src/services/Sales/Estimates/SaleEstimatesApplication.ts @@ -240,7 +240,7 @@ export class SaleEstimatesApplication { * @returns {} */ public getSaleEstimateMail(tenantId: number, saleEstimateId: number) { - return this.sendEstimateMailService.getDefaultMailOpts( + return this.sendEstimateMailService.getMailOptions( tenantId, saleEstimateId ); diff --git a/packages/server/src/services/Sales/Estimates/SendSaleEstimateMail.ts b/packages/server/src/services/Sales/Estimates/SendSaleEstimateMail.ts index 9ae6fa2f8..5777ab55a 100644 --- a/packages/server/src/services/Sales/Estimates/SendSaleEstimateMail.ts +++ b/packages/server/src/services/Sales/Estimates/SendSaleEstimateMail.ts @@ -1,5 +1,4 @@ import { Inject, Service } from 'typedi'; -import * as R from 'ramda'; import Mail from '@/lib/Mail'; import HasTenancyService from '@/services/Tenancy/TenancyService'; import { @@ -8,28 +7,31 @@ import { } from './constants'; import { SaleEstimatesPdf } from './SaleEstimatesPdf'; import { GetSaleEstimate } from './GetSaleEstimate'; -import { formatSmsMessage } from '@/utils'; import { SaleEstimateMailOptions } from '@/interfaces'; +import { ContactMailNotification } from '@/services/MailNotification/ContactMailNotification'; @Service() export class SendSaleEstimateMail { @Inject() private tenancy: HasTenancyService; - @Inject('agenda') - private agenda: any; - @Inject() private estimatePdf: SaleEstimatesPdf; @Inject() private getSaleEstimateService: GetSaleEstimate; + @Inject() + private contactMailNotification: ContactMailNotification; + + @Inject('agenda') + private agenda: any; + /** * Triggers the reminder mail of the given sale estimate. - * @param {number} tenantId - * @param {number} saleEstimateId - * @param {SaleEstimateMailOptions} messageOptions + * @param {number} tenantId - + * @param {number} saleEstimateId - + * @param {SaleEstimateMailOptions} messageOptions - */ public async triggerMail( tenantId: number, @@ -50,51 +52,50 @@ export class SendSaleEstimateMail { * @param {number} estimateId * @param {string} text */ - public formatText = async ( - tenantId: number, - estimateId: number, - text: string - ) => { + public formatterData = async (tenantId: number, estimateId: number) => { const estimate = await this.getSaleEstimateService.getEstimate( tenantId, estimateId ); - return formatSmsMessage(text, { + return { CustomerName: estimate.customer.displayName, EstimateNumber: estimate.estimateNumber, EstimateDate: estimate.formattedEstimateDate, EstimateAmount: estimate.formattedAmount, EstimateExpirationDate: estimate.formattedExpirationDate, - }); - }; - - /** - * 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. + * Retrieves the mail options. + * @param {number} tenantId + * @param {number} saleEstimateId + * @returns + */ + public getMailOptions = async (tenantId: number, saleEstimateId: number) => { + const { SaleEstimate } = this.tenancy.models(tenantId); + + const saleEstimate = await SaleEstimate.query() + .findById(saleEstimateId) + .throwIfNotFound(); + + const formatterData = await this.formatterData(tenantId, saleEstimateId); + + const mailOptions = await this.contactMailNotification.getMailOptions( + tenantId, + saleEstimate.customerId, + DEFAULT_ESTIMATE_REMINDER_MAIL_SUBJECT, + DEFAULT_ESTIMATE_REMINDER_MAIL_CONTENT, + formatterData + ); + return { + ...mailOptions, + data: formatterData, + }; + }; + + /** + * Sends the mail notification of the given sale estimate. * @param {number} tenantId * @param {number} saleEstimateId * @param {SaleEstimateMailOptions} messageOptions @@ -104,34 +105,31 @@ export class SendSaleEstimateMail { saleEstimateId: number, messageOptions: SaleEstimateMailOptions ) { - const defaultMessageOpts = await this.getDefaultMailOpts( + const localMessageOpts = await this.getMailOptions( tenantId, saleEstimateId ); - const parsedMessageOpts = { - ...defaultMessageOpts, + const messageOpts = { + ...localMessageOpts, ...messageOptions, }; - const formatter = R.curry(this.formatText)(tenantId, saleEstimateId); - const subject = await formatter(parsedMessageOpts.subject); - const body = await formatter(parsedMessageOpts.body); - const attachments = []; + const mail = new Mail() + .setSubject(messageOpts.subject) + .setTo(messageOpts.to) + .setContent(messageOpts.body); - if (parsedMessageOpts.to) { + if (messageOpts.to) { const estimatePdfBuffer = await this.estimatePdf.getSaleEstimatePdf( tenantId, saleEstimateId ); - attachments.push({ - filename: 'estimate.pdf', - content: estimatePdfBuffer, - }); + mail.setAttachments([ + { + filename: messageOpts.data?.EstimateNumber || 'estimate.pdf', + content: estimatePdfBuffer, + }, + ]); } - await new Mail() - .setSubject(subject) - .setTo(parsedMessageOpts.to) - .setContent(body) - .setAttachments(attachments) - .send(); + await mail.send(); } } diff --git a/packages/server/src/services/Sales/Invoices/SaleInvoicesApplication.ts b/packages/server/src/services/Sales/Invoices/SaleInvoicesApplication.ts index 9b3c19d33..b175d9546 100644 --- a/packages/server/src/services/Sales/Invoices/SaleInvoicesApplication.ts +++ b/packages/server/src/services/Sales/Invoices/SaleInvoicesApplication.ts @@ -300,10 +300,7 @@ export class SaleInvoiceApplication { * @returns {} */ public getSaleInvoiceMailReminder(tenantId: number, saleInvoiceId: number) { - return this.getSaleInvoiceReminderService.getInvoiceMailReminder( - tenantId, - saleInvoiceId - ); + return this.sendInvoiceReminderService.getMailOpts(tenantId, saleInvoiceId); } /** @@ -345,14 +342,11 @@ export class SaleInvoiceApplication { /** * Retrieves the default mail options of the given sale invoice. - * @param {number} tenantId - * @param {number} saleInvoiceid + * @param {number} tenantId + * @param {number} saleInvoiceid * @returns {Promise} */ public getSaleInvoiceMail(tenantId: number, saleInvoiceid: number) { - return this.sendInvoiceReminderService.getDefaultMailOpts( - tenantId, - saleInvoiceid - ); + return this.sendSaleInvoiceMailService.getMailOpts(tenantId, saleInvoiceid); } } diff --git a/packages/server/src/services/Sales/Invoices/SendInvoiceInvoiceMailCommon.ts b/packages/server/src/services/Sales/Invoices/SendInvoiceInvoiceMailCommon.ts new file mode 100644 index 000000000..97be8de87 --- /dev/null +++ b/packages/server/src/services/Sales/Invoices/SendInvoiceInvoiceMailCommon.ts @@ -0,0 +1,115 @@ +import { Inject, Service } from 'typedi'; +import { isEmpty } from 'lodash'; +import { SaleInvoiceMailOptions } from '@/interfaces'; +import HasTenancyService from '@/services/Tenancy/TenancyService'; +import { + DEFAULT_INVOICE_MAIL_CONTENT, + DEFAULT_INVOICE_MAIL_SUBJECT, +} from './constants'; +import { GetSaleInvoice } from './GetSaleInvoice'; +import { Tenant } from '@/system/models'; +import { ServiceError } from '@/exceptions'; +import { ContactMailNotification } from '@/services/MailNotification/ContactMailNotification'; + +@Service() +export class SendSaleInvoiceMailCommon { + @Inject() + private tenancy: HasTenancyService; + + @Inject() + private getSaleInvoiceService: GetSaleInvoice; + + @Inject() + private contactMailNotification: ContactMailNotification; + + /** + * Retrieves the mail options. + * @param {number} tenantId - Tenant id. + * @param {number} invoiceId - Invoice id. + * @param {string} defaultSubject - Subject text. + * @param {string} defaultBody - Subject body. + * @returns {} + */ + public async getMailOpts( + tenantId: number, + invoiceId: number, + defaultSubject: string = DEFAULT_INVOICE_MAIL_SUBJECT, + defaultBody: string = DEFAULT_INVOICE_MAIL_CONTENT + ): Promise { + const { SaleInvoice } = this.tenancy.models(tenantId); + + const saleInvoice = await SaleInvoice.query() + .findById(invoiceId) + .throwIfNotFound(); + + const formatterData = await this.formatText(tenantId, invoiceId); + + return this.contactMailNotification.getMailOptions( + tenantId, + saleInvoice.customerId, + defaultSubject, + defaultBody, + formatterData + ); + } + + /** + * Retrieves the formatted text of the given sale invoice. + * @param {number} tenantId - Tenant id. + * @param {number} invoiceId - Sale invoice id. + * @param {string} text - The given text. + * @returns {Promise} + */ + public formatText = async ( + tenantId: number, + invoiceId: number + ): Promise> => { + const invoice = await this.getSaleInvoiceService.getSaleInvoice( + tenantId, + invoiceId + ); + const organization = await Tenant.query() + .findById(tenantId) + .withGraphFetched('metadata'); + + return { + CompanyName: organization.metadata.name, + CustomerName: invoice.customer.displayName, + InvoiceNumber: invoice.invoiceNo, + InvoiceDueAmount: invoice.dueAmountFormatted, + InvoiceDueDate: invoice.dueDateFormatted, + InvoiceDate: invoice.invoiceDateFormatted, + InvoiceAmount: invoice.totalFormatted, + OverdueDays: invoice.overdueDays, + }; + }; + + /** + * Validates the mail notification options before sending it. + * @param {Partial} mailNotificationOpts + * @throws {ServiceError} + */ + public validateMailNotification( + mailNotificationOpts: Partial + ) { + if (isEmpty(mailNotificationOpts.from)) { + throw new ServiceError(ERRORS.MAIL_FROM_NOT_FOUND); + } + if (isEmpty(mailNotificationOpts.to)) { + throw new ServiceError(ERRORS.MAIL_TO_NOT_FOUND); + } + if (isEmpty(mailNotificationOpts.subject)) { + throw new ServiceError(ERRORS.MAIL_SUBJECT_NOT_FOUND); + } + if (isEmpty(mailNotificationOpts.body)) { + throw new ServiceError(ERRORS.MAIL_BODY_NOT_FOUND); + } + } +} + +const ERRORS = { + MAIL_FROM_NOT_FOUND: 'Mail from address not found', + MAIL_TO_NOT_FOUND: 'Mail to address not found', + MAIL_SUBJECT_NOT_FOUND: 'Mail subject not found', + MAIL_BODY_NOT_FOUND: 'Mail body not found', +}; diff --git a/packages/server/src/services/Sales/Invoices/SendSaleInvoiceMail.ts b/packages/server/src/services/Sales/Invoices/SendSaleInvoiceMail.ts index cc8269c56..a5bc744e1 100644 --- a/packages/server/src/services/Sales/Invoices/SendSaleInvoiceMail.ts +++ b/packages/server/src/services/Sales/Invoices/SendSaleInvoiceMail.ts @@ -1,29 +1,20 @@ import { Inject, Service } from 'typedi'; -import * as R from 'ramda'; -import { SendInvoiceMailDTO } from '@/interfaces'; import Mail from '@/lib/Mail'; -import HasTenancyService from '@/services/Tenancy/TenancyService'; +import { SendInvoiceMailDTO } from '@/interfaces'; import { SaleInvoicePdf } from './SaleInvoicePdf'; +import { SendSaleInvoiceMailCommon } from './SendInvoiceInvoiceMailCommon'; import { DEFAULT_INVOICE_MAIL_CONTENT, DEFAULT_INVOICE_MAIL_SUBJECT, - ERRORS, } from './constants'; -import { ServiceError } from '@/exceptions'; -import { formatSmsMessage } from '@/utils'; -import { GetSaleInvoice } from './GetSaleInvoice'; -import { Tenant } from '@/system/models'; @Service() export class SendSaleInvoiceMail { @Inject() - private tenancy: HasTenancyService; + private invoicePdf: SaleInvoicePdf; @Inject() - private getSaleInvoiceService: GetSaleInvoice; - - @Inject() - private invoicePdf: SaleInvoicePdf; + private invoiceMail: SendSaleInvoiceMailCommon; @Inject('agenda') private agenda: any; @@ -48,56 +39,19 @@ export class SendSaleInvoiceMail { } /** - * Retrieves the default invoice mail options. + * Retrieves the mail options of the given sale invoice. * @param {number} tenantId - * @param {number} invoiceId - * @returns {Promise} + * @param {number} saleInvoiceId + * @returns {Promise} */ - public getDefaultMailOpts = async (tenantId: number, invoiceId: number) => { - const { SaleInvoice } = this.tenancy.models(tenantId); - const saleInvoice = await SaleInvoice.query() - .findById(invoiceId) - .withGraphFetched('customer') - .throwIfNotFound(); - - return { - attachInvoice: true, - subject: DEFAULT_INVOICE_MAIL_SUBJECT, - body: DEFAULT_INVOICE_MAIL_CONTENT, - to: saleInvoice.customer.email, - }; - }; - - /** - * Retrieves the formatted text of the given sale invoice. - * @param {number} tenantId - Tenant id. - * @param {number} invoiceId - Sale invoice id. - * @param {string} text - The given text. - * @returns {Promise} - */ - public textFormatter = async ( - tenantId: number, - invoiceId: number, - text: string - ): Promise => { - const invoice = await this.getSaleInvoiceService.getSaleInvoice( + public async getMailOpts(tenantId: number, saleInvoiceId: number) { + return this.invoiceMail.getMailOpts( tenantId, - invoiceId + saleInvoiceId, + DEFAULT_INVOICE_MAIL_SUBJECT, + DEFAULT_INVOICE_MAIL_CONTENT ); - 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. @@ -111,37 +65,30 @@ export class SendSaleInvoiceMail { saleInvoiceId: number, messageDTO: SendInvoiceMailDTO ) { - const defaultMessageOpts = await this.getDefaultMailOpts( - tenantId, - saleInvoiceId - ); + const defaultMessageOpts = await this.getMailOpts(tenantId, saleInvoiceId); + // Parsed message opts with default options. - const parsedMessageOpts = { + const messageOpts = { ...defaultMessageOpts, ...messageDTO, }; - // 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, saleInvoiceId); - const subject = await formatter(parsedMessageOpts.subject); - const body = await formatter(parsedMessageOpts.body); - const attachments = []; + this.invoiceMail.validateMailNotification(messageOpts); - if (parsedMessageOpts.attachInvoice) { + const mail = new Mail() + .setSubject(messageOpts.subject) + .setTo(messageOpts.to) + .setContent(messageOpts.body); + + if (messageOpts.attachInvoice) { // Retrieves document buffer of the invoice pdf document. const invoicePdfBuffer = await this.invoicePdf.saleInvoicePdf( tenantId, saleInvoiceId ); - attachments.push({ filename: 'invoice.pdf', content: invoicePdfBuffer }); + mail.setAttachments([ + { filename: 'invoice.pdf', content: invoicePdfBuffer }, + ]); } - await new Mail() - .setSubject(subject) - .setTo(parsedMessageOpts.to) - .setContent(body) - .setAttachments(attachments) - .send(); + await mail.send(); } } diff --git a/packages/server/src/services/Sales/Invoices/SendSaleInvoiceMailReminder.ts b/packages/server/src/services/Sales/Invoices/SendSaleInvoiceMailReminder.ts index 886916260..09463bddc 100644 --- a/packages/server/src/services/Sales/Invoices/SendSaleInvoiceMailReminder.ts +++ b/packages/server/src/services/Sales/Invoices/SendSaleInvoiceMailReminder.ts @@ -1,24 +1,15 @@ import { Inject, Service } from 'typedi'; -import * as R from 'ramda'; import { SendInvoiceMailDTO } from '@/interfaces'; import Mail from '@/lib/Mail'; -import HasTenancyService from '@/services/Tenancy/TenancyService'; +import { SaleInvoicePdf } from './SaleInvoicePdf'; +import { SendSaleInvoiceMailCommon } from './SendInvoiceInvoiceMailCommon'; import { DEFAULT_INVOICE_REMINDER_MAIL_CONTENT, DEFAULT_INVOICE_REMINDER_MAIL_SUBJECT, - ERRORS, } from './constants'; -import { SaleInvoicePdf } from './SaleInvoicePdf'; -import { ServiceError } from '@/exceptions'; -import { GetSaleInvoice } from './GetSaleInvoice'; -import { Tenant } from '@/system/models'; -import { formatSmsMessage } from '@/utils'; @Service() export class SendInvoiceMailReminder { - @Inject() - private tenancy: HasTenancyService; - @Inject('agenda') private agenda: any; @@ -26,7 +17,7 @@ export class SendInvoiceMailReminder { private invoicePdf: SaleInvoicePdf; @Inject() - private getSaleInvoiceService: GetSaleInvoice; + private invoiceCommonMail: SendSaleInvoiceMailCommon; /** * Triggers the reminder mail of the given sale invoice. @@ -47,57 +38,19 @@ export class SendInvoiceMailReminder { } /** - * Parses the default message options. + * Retrieves the mail options of the given sale invoice. * @param {number} tenantId - * @param {number} invoiceId - * @returns {Promise} + * @param {number} saleInvoiceId + * @returns {Promise} */ - public async getDefaultMailOpts(tenantId: number, invoiceId: number) { - const { SaleInvoice } = this.tenancy.models(tenantId); - - const saleInvoice = await SaleInvoice.query() - .findById(invoiceId) - .withGraphFetched('customer') - .throwIfNotFound(); - - return { - attachInvoice: true, - subject: DEFAULT_INVOICE_REMINDER_MAIL_SUBJECT, - body: DEFAULT_INVOICE_REMINDER_MAIL_CONTENT, - to: saleInvoice.customer.email, - }; - } - - /** - * Retrieves the formatted text of the given sale invoice. - * @param {number} tenantId - Tenant id. - * @param {number} invoiceId - Sale invoice id. - * @param {string} text - The given text. - * @returns {Promise} - */ - public formatText = async ( - tenantId: number, - invoiceId: number, - text: string - ): Promise => { - const invoice = await this.getSaleInvoiceService.getSaleInvoice( + public async getMailOpts(tenantId: number, saleInvoiceId: number) { + return this.invoiceCommonMail.getMailOpts( tenantId, - invoiceId + saleInvoiceId, + DEFAULT_INVOICE_REMINDER_MAIL_SUBJECT, + DEFAULT_INVOICE_REMINDER_MAIL_CONTENT ); - 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. @@ -111,37 +64,27 @@ export class SendInvoiceMailReminder { saleInvoiceId: number, messageOptions: SendInvoiceMailDTO ) { - const defaultMessageOpts = await this.getDefaultMailOpts( - tenantId, - saleInvoiceId - ); - const parsedMessageOptions = { - ...defaultMessageOpts, + const localMessageOpts = await this.getMailOpts(tenantId, saleInvoiceId); + + const messageOpts = { + ...localMessageOpts, ...messageOptions, }; - // In case there is no email address from the customer or from options, throw an error. - if (!parsedMessageOptions.to) { - throw new ServiceError(ERRORS.NO_INVOICE_CUSTOMER_EMAIL_ADDR); - } - const formatter = R.curry(this.formatText)(tenantId, saleInvoiceId); - const subject = await formatter(parsedMessageOptions.subject); - const body = await formatter(parsedMessageOptions.body); - const attachments = []; + const mail = new Mail() + .setSubject(messageOpts.subject) + .setTo(messageOpts.to) + .setContent(messageOpts.body); - if (parsedMessageOptions.attachInvoice) { + if (messageOpts.attachInvoice) { // Retrieves document buffer of the invoice pdf document. const invoicePdfBuffer = await this.invoicePdf.saleInvoicePdf( tenantId, saleInvoiceId ); - attachments.push({ filename: 'invoice.pdf', content: invoicePdfBuffer }); + mail.setAttachments([ + { filename: 'invoice.pdf', content: invoicePdfBuffer }, + ]); } - const mail = new Mail() - .setSubject(subject) - .setTo(parsedMessageOptions.to) - .setContent(body) - .setAttachments(attachments); - await mail.send(); } } diff --git a/packages/server/src/services/Sales/Invoices/constants.ts b/packages/server/src/services/Sales/Invoices/constants.ts index 79bf67c0a..404b7e613 100644 --- a/packages/server/src/services/Sales/Invoices/constants.ts +++ b/packages/server/src/services/Sales/Invoices/constants.ts @@ -8,6 +8,11 @@ Invoice #{InvoiceNumber}
Due Date : {InvoiceDueDate}
Amount : {InvoiceAmount}

+ +

+Regards
+{CompanyName} +

`; export const DEFAULT_INVOICE_REMINDER_MAIL_SUBJECT = @@ -18,6 +23,11 @@ export const DEFAULT_INVOICE_REMINDER_MAIL_CONTENT = `

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

+ +

+Regards
+{CompanyName} +

`; export const ERRORS = { diff --git a/packages/server/src/services/Sales/PaymentReceives/PaymentReceiveMailNotification.ts b/packages/server/src/services/Sales/PaymentReceives/PaymentReceiveMailNotification.ts index 42069c5f3..79dfe2392 100644 --- a/packages/server/src/services/Sales/PaymentReceives/PaymentReceiveMailNotification.ts +++ b/packages/server/src/services/Sales/PaymentReceives/PaymentReceiveMailNotification.ts @@ -1,17 +1,14 @@ import { Inject, Service } from 'typedi'; -import * as R from 'ramda'; import { IPaymentReceiveMailOpts, SendInvoiceMailDTO } from '@/interfaces'; import Mail from '@/lib/Mail'; import HasTenancyService from '@/services/Tenancy/TenancyService'; import { DEFAULT_PAYMENT_MAIL_CONTENT, DEFAULT_PAYMENT_MAIL_SUBJECT, - ERRORS, } from './constants'; -import { ServiceError } from '@/exceptions'; -import { formatSmsMessage } from '@/utils'; import { Tenant } from '@/system/models'; import { GetPaymentReceive } from './GetPaymentReceive'; +import { ContactMailNotification } from '@/services/MailNotification/ContactMailNotification'; @Service() export class SendPaymentReceiveMailNotification { @@ -21,6 +18,9 @@ export class SendPaymentReceiveMailNotification { @Inject() private getPaymentService: GetPaymentReceive; + @Inject() + private contactMailNotification: ContactMailNotification; + @Inject('agenda') private agenda: any; @@ -49,19 +49,22 @@ export class SendPaymentReceiveMailNotification { * @param {number} invoiceId * @returns {Promise} */ - public getDefaultMailOpts = async (tenantId: number, invoiceId: number) => { + public getMailOptions = async (tenantId: number, invoiceId: number) => { const { PaymentReceive } = this.tenancy.models(tenantId); + const paymentReceive = await PaymentReceive.query() .findById(invoiceId) - .withGraphFetched('customer') .throwIfNotFound(); - return { - attachInvoice: true, - subject: DEFAULT_PAYMENT_MAIL_SUBJECT, - body: DEFAULT_PAYMENT_MAIL_CONTENT, - to: paymentReceive.customer.email, - }; + const formatterData = await this.textFormatter(tenantId, invoiceId); + + return this.contactMailNotification.getMailOptions( + tenantId, + paymentReceive.customerId, + DEFAULT_PAYMENT_MAIL_SUBJECT, + DEFAULT_PAYMENT_MAIL_CONTENT, + formatterData + ); }; /** @@ -73,9 +76,8 @@ export class SendPaymentReceiveMailNotification { */ public textFormatter = async ( tenantId: number, - invoiceId: number, - text: string - ): Promise => { + invoiceId: number + ): Promise> => { const payment = await this.getPaymentService.getPaymentReceive( tenantId, invoiceId @@ -84,13 +86,13 @@ export class SendPaymentReceiveMailNotification { .findById(tenantId) .withGraphFetched('metadata'); - return formatSmsMessage(text, { + return { CompanyName: organization.metadata.name, CustomerName: payment.customer.displayName, PaymentNumber: payment.payment_receive_no, PaymentDate: payment.formattedPaymentDate, PaymentAmount: payment.formattedAmount, - }); + }; }; /** @@ -105,7 +107,7 @@ export class SendPaymentReceiveMailNotification { paymentReceiveId: number, messageDTO: SendInvoiceMailDTO ): Promise { - const defaultMessageOpts = await this.getDefaultMailOpts( + const defaultMessageOpts = await this.getMailOptions( tenantId, paymentReceiveId ); @@ -114,18 +116,10 @@ export class SendPaymentReceiveMailNotification { ...defaultMessageOpts, ...messageDTO, }; - // 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, paymentReceiveId); - const subject = await formatter(parsedMessageOpts.subject); - const body = await formatter(parsedMessageOpts.body); - await new Mail() - .setSubject(subject) + .setSubject(parsedMessageOpts.subject) .setTo(parsedMessageOpts.to) - .setContent(body) + .setContent(parsedMessageOpts.body) .send(); } } diff --git a/packages/server/src/services/Sales/PaymentReceives/PaymentReceivesApplication.ts b/packages/server/src/services/Sales/PaymentReceives/PaymentReceivesApplication.ts index 6092664e6..bf1e2da3f 100644 --- a/packages/server/src/services/Sales/PaymentReceives/PaymentReceivesApplication.ts +++ b/packages/server/src/services/Sales/PaymentReceives/PaymentReceivesApplication.ts @@ -205,10 +205,7 @@ export class PaymentReceivesApplication { * @returns {Promise} */ public getPaymentDefaultMail(tenantId: number, paymentReceiveId: number) { - return this.paymentMailNotify.getDefaultMailOpts( - tenantId, - paymentReceiveId - ); + return this.paymentMailNotify.getMailOptions(tenantId, paymentReceiveId); } /** diff --git a/packages/server/src/services/Sales/Receipts/SaleReceiptApplication.ts b/packages/server/src/services/Sales/Receipts/SaleReceiptApplication.ts index 6fe03b2a1..0f790ec6f 100644 --- a/packages/server/src/services/Sales/Receipts/SaleReceiptApplication.ts +++ b/packages/server/src/services/Sales/Receipts/SaleReceiptApplication.ts @@ -191,12 +191,12 @@ export class SaleReceiptApplication { /** * Retrieves the default mail options of the given sale receipt. - * @param {number} tenantId - * @param {number} saleReceiptId - * @returns + * @param {number} tenantId + * @param {number} saleReceiptId + * @returns */ public getSaleReceiptMail(tenantId: number, saleReceiptId: number) { - return this.saleReceiptNotifyByMailService.getDefaultMailOpts( + return this.saleReceiptNotifyByMailService.getMailOptions( tenantId, saleReceiptId ); diff --git a/packages/server/src/services/Sales/Receipts/SaleReceiptMailNotification.ts b/packages/server/src/services/Sales/Receipts/SaleReceiptMailNotification.ts index 0d93c2b65..20bfc4073 100644 --- a/packages/server/src/services/Sales/Receipts/SaleReceiptMailNotification.ts +++ b/packages/server/src/services/Sales/Receipts/SaleReceiptMailNotification.ts @@ -2,8 +2,6 @@ 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'; @@ -11,8 +9,8 @@ import { DEFAULT_RECEIPT_MAIL_CONTENT, DEFAULT_RECEIPT_MAIL_SUBJECT, } from './constants'; -import { ERRORS } from './constants'; import { SaleReceiptMailOpts } from '@/interfaces'; +import { ContactMailNotification } from '@/services/MailNotification/ContactMailNotification'; @Service() export class SaleReceiptMailNotification { @@ -25,6 +23,9 @@ export class SaleReceiptMailNotification { @Inject() private receiptPdfService: SaleReceiptsPdf; + @Inject() + private contactMailNotification: ContactMailNotification; + @Inject('agenda') private agenda: any; @@ -48,25 +49,28 @@ export class SaleReceiptMailNotification { } /** - * Retrieves the default receipt mail options. + * Retrieves the mail options of the given sale receipt. * @param {number} tenantId - * @param {number} invoiceId - * @returns {Promise} + * @param {number} saleReceiptId + * @returns */ - public getDefaultMailOpts = async (tenantId: number, invoiceId: number) => { + public async getMailOptions(tenantId: number, saleReceiptId: number) { const { SaleReceipt } = this.tenancy.models(tenantId); + const saleReceipt = await SaleReceipt.query() - .findById(invoiceId) - .withGraphFetched('customer') + .findById(saleReceiptId) .throwIfNotFound(); - return { - attachInvoice: true, - subject: DEFAULT_RECEIPT_MAIL_SUBJECT, - body: DEFAULT_RECEIPT_MAIL_CONTENT, - to: saleReceipt.customer.email, - }; - }; + const formattedData = await this.textFormatter(tenantId, saleReceiptId); + + return this.contactMailNotification.getMailOptions( + tenantId, + saleReceipt.customerId, + DEFAULT_RECEIPT_MAIL_SUBJECT, + DEFAULT_RECEIPT_MAIL_CONTENT, + formattedData + ); + } /** * Retrieves the formatted text of the given sale invoice. @@ -77,9 +81,8 @@ export class SaleReceiptMailNotification { */ public textFormatter = async ( tenantId: number, - receiptId: number, - text: string - ): Promise => { + receiptId: number + ): Promise> => { const invoice = await this.getSaleReceiptService.getSaleReceipt( tenantId, receiptId @@ -88,13 +91,13 @@ export class SaleReceiptMailNotification { .findById(tenantId) .withGraphFetched('metadata'); - return formatSmsMessage(text, { + return { CompanyName: organization.metadata.name, CustomerName: invoice.customer.displayName, ReceiptNumber: invoice.receiptNumber, ReceiptDate: invoice.formattedReceiptDate, ReceiptAmount: invoice.formattedAmount, - }); + }; }; /** @@ -109,7 +112,7 @@ export class SaleReceiptMailNotification { saleReceiptId: number, messageOpts: SaleReceiptMailOpts ) { - const defaultMessageOpts = await this.getDefaultMailOpts( + const defaultMessageOpts = await this.getMailOptions( tenantId, saleReceiptId ); @@ -118,14 +121,11 @@ export class SaleReceiptMailNotification { ...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 = []; + + const mail = new Mail() + .setSubject(parsedMessageOpts.subject) + .setTo(parsedMessageOpts.to) + .setContent(parsedMessageOpts.body); if (parsedMessageOpts.attachInvoice) { // Retrieves document buffer of the invoice pdf document. @@ -133,13 +133,10 @@ export class SaleReceiptMailNotification { tenantId, saleReceiptId ); - attachments.push({ filename: 'invoice.pdf', content: receiptPdfBuffer }); + mail.setAttachments([ + { filename: 'invoice.pdf', content: receiptPdfBuffer }, + ]); } - await new Mail() - .setSubject(subject) - .setTo(parsedMessageOpts.to) - .setContent(body) - .setAttachments(attachments) - .send(); + await mail.send(); } }