diff --git a/packages/server/src/api/controllers/Sales/PaymentReceives.ts b/packages/server/src/api/controllers/Sales/PaymentReceives.ts index 74657a703..0bef1e60d 100644 --- a/packages/server/src/api/controllers/Sales/PaymentReceives.ts +++ b/packages/server/src/api/controllers/Sales/PaymentReceives.ts @@ -4,9 +4,8 @@ import { body, check, param, query, ValidationChain } from 'express-validator'; import { AbilitySubject, IPaymentReceiveDTO, - IPaymentReceiveMailOpts, - // IPaymentReceiveMailOpts, PaymentReceiveAction, + PaymentReceiveMailOptsDTO, } from '@/interfaces'; import BaseController from '@/api/controllers/BaseController'; import asyncMiddleware from '@/api/middleware/asyncMiddleware'; @@ -541,9 +540,12 @@ export default class PaymentReceivesController extends BaseController { ) => { const { tenantId } = req; const { id: paymentReceiveId } = req.params; - const paymentMailDTO: IPaymentReceiveMailOpts = this.matchedBodyData(req, { - includeOptionals: false, - }); + const paymentMailDTO: PaymentReceiveMailOptsDTO = this.matchedBodyData( + req, + { + includeOptionals: false, + } + ); try { await this.paymentReceiveApplication.notifyPaymentByMail( tenantId, @@ -574,7 +576,7 @@ export default class PaymentReceivesController extends BaseController { const { id: paymentReceiveId } = req.params; try { - const data = await this.paymentReceiveApplication.getPaymentDefaultMail( + const data = await this.paymentReceiveApplication.getPaymentMailOptions( tenantId, paymentReceiveId ); diff --git a/packages/server/src/api/controllers/Sales/SalesEstimates.ts b/packages/server/src/api/controllers/Sales/SalesEstimates.ts index f78719de9..b34c1ccf2 100644 --- a/packages/server/src/api/controllers/Sales/SalesEstimates.ts +++ b/packages/server/src/api/controllers/Sales/SalesEstimates.ts @@ -5,7 +5,7 @@ import { AbilitySubject, ISaleEstimateDTO, SaleEstimateAction, - SaleEstimateMailOptions, + SaleEstimateMailOptionsDTO, } from '@/interfaces'; import BaseController from '@/api/controllers/BaseController'; import asyncMiddleware from '@/api/middleware/asyncMiddleware'; @@ -513,10 +513,12 @@ export default class SalesEstimatesController extends BaseController { ) => { const { tenantId } = req; const { id: invoiceId } = req.params; - const saleEstimateDTO: SaleEstimateMailOptions = this.matchedBodyData(req, { - includeOptionals: false, - }); - + const saleEstimateDTO: SaleEstimateMailOptionsDTO = this.matchedBodyData( + req, + { + includeOptionals: false, + } + ); try { await this.saleEstimatesApplication.sendSaleEstimateMail( tenantId, diff --git a/packages/server/src/api/controllers/Sales/SalesReceipts.ts b/packages/server/src/api/controllers/Sales/SalesReceipts.ts index c8635fe3c..6151561f8 100644 --- a/packages/server/src/api/controllers/Sales/SalesReceipts.ts +++ b/packages/server/src/api/controllers/Sales/SalesReceipts.ts @@ -3,7 +3,7 @@ import { body, check, param, query } from 'express-validator'; import { Inject, Service } from 'typedi'; import asyncMiddleware from '@/api/middleware/asyncMiddleware'; import BaseController from '../BaseController'; -import { ISaleReceiptDTO, SaleReceiptMailOpts } from '@/interfaces/SaleReceipt'; +import { ISaleReceiptDTO, SaleReceiptMailOpts, SaleReceiptMailOptsDTO } from '@/interfaces/SaleReceipt'; import { ServiceError } from '@/exceptions'; import DynamicListingService from '@/services/DynamicListing/DynamicListService'; import CheckPolicies from '@/api/middleware/CheckPolicies'; @@ -54,7 +54,7 @@ export default class SalesReceiptsController extends BaseController { body('from').isString().optional(), body('to').isString().optional(), body('body').isString().optional(), - body('attach_invoice').optional().isBoolean().toBoolean(), + body('attach_receipt').optional().isBoolean().toBoolean(), ], this.validationResult, asyncMiddleware(this.sendSaleReceiptMail.bind(this)), @@ -439,7 +439,7 @@ export default class SalesReceiptsController extends BaseController { ) => { const { tenantId } = req; const { id: receiptId } = req.params; - const receiptMailDTO: SaleReceiptMailOpts = this.matchedBodyData(req, { + const receiptMailDTO: SaleReceiptMailOptsDTO = this.matchedBodyData(req, { includeOptionals: false, }); diff --git a/packages/server/src/interfaces/Mailable.ts b/packages/server/src/interfaces/Mailable.ts index 36cc3c81f..5682f2529 100644 --- a/packages/server/src/interfaces/Mailable.ts +++ b/packages/server/src/interfaces/Mailable.ts @@ -1,9 +1,17 @@ +export type IMailAttachment = MailAttachmentPath | MailAttachmentContent; + +export interface MailAttachmentPath { + filename: string; + path: string; + cid: string; +} +export interface MailAttachmentContent { + filename: string; + content: Buffer; +} export interface IMailable { - constructor( - view: string, - data?: { [key: string]: string | number }, - ); + constructor(view: string, data?: { [key: string]: string | number }); send(): Promise; build(): void; setData(data: { [key: string]: string | number }): IMailable; @@ -13,4 +21,27 @@ export interface IMailable { setView(view: string): IMailable; render(data?: { [key: string]: string | number }): string; getViewContent(): string; -} \ No newline at end of file +} + +export interface AddressItem { + label: string; + mail: string; + primary?: boolean; +} + +export interface CommonMailOptions { + toAddresses: AddressItem[]; + fromAddresses: AddressItem[]; + from: string; + to: string | string[]; + subject: string; + body: string; + data?: Record; +} + +export interface CommonMailOptionsDTO { + to?: string | string[]; + from?: string; + subject?: string; + body?: string; +} diff --git a/packages/server/src/interfaces/PaymentReceive.ts b/packages/server/src/interfaces/PaymentReceive.ts index c919182ae..2926d923c 100644 --- a/packages/server/src/interfaces/PaymentReceive.ts +++ b/packages/server/src/interfaces/PaymentReceive.ts @@ -1,5 +1,9 @@ import { Knex } from 'knex'; -import { ISystemUser } from '@/interfaces'; +import { + CommonMailOptions, + CommonMailOptionsDTO, + ISystemUser, +} from '@/interfaces'; import { ILedgerEntry } from './Ledger'; import { ISaleInvoice } from './SaleInvoice'; @@ -19,7 +23,7 @@ export interface IPaymentReceive { createdAt: Date; updatedAt: Date; localAmount?: number; - branchId?: number + branchId?: number; } export interface IPaymentReceiveCreateDTO { customerId: number; @@ -166,6 +170,6 @@ export type IPaymentReceiveGLCommonEntry = Pick< | 'branchId' >; -export interface IPaymentReceiveMailOpts { - -} \ No newline at end of file +export interface PaymentReceiveMailOpts extends CommonMailOptions {} + +export interface PaymentReceiveMailOptsDTO extends CommonMailOptionsDTO {} diff --git a/packages/server/src/interfaces/SaleEstimate.ts b/packages/server/src/interfaces/SaleEstimate.ts index 3a503e1fd..171c8a0d1 100644 --- a/packages/server/src/interfaces/SaleEstimate.ts +++ b/packages/server/src/interfaces/SaleEstimate.ts @@ -1,6 +1,7 @@ import { Knex } from 'knex'; import { IItemEntry, IItemEntryDTO } from './ItemEntry'; import { IDynamicListFilterDTO } from '@/interfaces/DynamicFilter'; +import { CommonMailOptions, CommonMailOptionsDTO } from './Mailable'; export interface ISaleEstimate { id?: number; @@ -125,10 +126,10 @@ export interface ISaleEstimateApprovedEvent { trx: Knex.Transaction; } -export interface SaleEstimateMailOptions { - to: string; - from: string; - subject: string; - body: string; - attachInvoice?: boolean; +export interface SaleEstimateMailOptions extends CommonMailOptions { + attachEstimate?: boolean; +} + +export interface SaleEstimateMailOptionsDTO extends CommonMailOptionsDTO { + attachEstimate?: boolean; } \ No newline at end of file diff --git a/packages/server/src/interfaces/SaleInvoice.ts b/packages/server/src/interfaces/SaleInvoice.ts index d660b17ca..394319e86 100644 --- a/packages/server/src/interfaces/SaleInvoice.ts +++ b/packages/server/src/interfaces/SaleInvoice.ts @@ -1,5 +1,6 @@ import { Knex } from 'knex'; -import { ISystemUser, IAccount, ITaxTransaction, AddressItem } from '@/interfaces'; +import { ISystemUser, IAccount, ITaxTransaction } from '@/interfaces'; +import { CommonMailOptions, CommonMailOptionsDTO } from './Mailable'; import { IDynamicListFilter } from '@/interfaces/DynamicFilter'; import { IItemEntry, IItemEntryDTO } from './ItemEntry'; @@ -187,21 +188,11 @@ export enum SaleInvoiceAction { NotifyBySms = 'NotifyBySms', } -export interface SaleInvoiceMailOptions { - toAddresses: AddressItem[]; - fromAddresses: AddressItem[]; - from: string; - to: string | string[]; - subject: string; - body: string; +export interface SaleInvoiceMailOptions extends CommonMailOptions { attachInvoice: boolean; } -export interface SendInvoiceMailDTO { - to: string | string[]; - from: string; - subject: string; - body: string; +export interface SendInvoiceMailDTO extends CommonMailOptionsDTO { attachInvoice?: boolean; } diff --git a/packages/server/src/interfaces/SaleReceipt.ts b/packages/server/src/interfaces/SaleReceipt.ts index 102513f7e..1e8ffa98e 100644 --- a/packages/server/src/interfaces/SaleReceipt.ts +++ b/packages/server/src/interfaces/SaleReceipt.ts @@ -1,5 +1,6 @@ import { Knex } from 'knex'; import { IItemEntry } from './ItemEntry'; +import { CommonMailOptions, CommonMailOptionsDTO } from './Mailable'; export interface ISaleReceipt { id?: number; @@ -135,6 +136,10 @@ export interface ISaleReceiptDeletingPayload { trx: Knex.Transaction; } -export interface SaleReceiptMailOpts { - -} \ No newline at end of file +export interface SaleReceiptMailOpts extends CommonMailOptions { + attachReceipt: boolean; +} + +export interface SaleReceiptMailOptsDTO extends CommonMailOptionsDTO { + attachReceipt?: boolean; +} diff --git a/packages/server/src/lib/Mail/index.ts b/packages/server/src/lib/Mail/index.ts index dd79c934b..015ca02a8 100644 --- a/packages/server/src/lib/Mail/index.ts +++ b/packages/server/src/lib/Mail/index.ts @@ -2,18 +2,13 @@ import fs from 'fs'; import Mustache from 'mustache'; import { Container } from 'typedi'; import path from 'path'; -import { IMailable } from '@/interfaces'; - -interface IMailAttachment { - filename: string; - path: string; - cid: string; -} +import { IMailAttachment } from '@/interfaces'; export default class Mail { view: string; - subject: string; - to: string; + subject: string = ''; + content: string = ''; + to: string | string[]; from: string = `${process.env.MAIL_FROM_NAME} ${process.env.MAIL_FROM_ADDRESS}`; data: { [key: string]: string | number }; attachments: IMailAttachment[]; @@ -21,16 +16,24 @@ export default class Mail { /** * Mail options. */ - private get mailOptions() { + public get mailOptions() { return { to: this.to, from: this.from, subject: this.subject, - html: this.render(this.data), + html: this.html, attachments: this.attachments, }; } + /** + * Retrieves the html content of the mail. + * @returns {string} + */ + public get html() { + return this.view ? Mail.render(this.view, this.data) : this.content; + } + /** * Sends the given mail to the target address. */ @@ -52,7 +55,7 @@ export default class Mail { * Set send mail to address. * @param {string} to - */ - setTo(to: string) { + setTo(to: string | string[]) { this.to = to; return this; } @@ -62,11 +65,16 @@ export default class Mail { * @param {string} from * @return {} */ - private setFrom(from: string) { + setFrom(from: string) { this.from = from; return this; } + /** + * Set attachments to the mail. + * @param {IMailAttachment[]} attachments + * @returns {Mail} + */ setAttachments(attachments: IMailAttachment[]) { this.attachments = attachments; return this; @@ -95,21 +103,26 @@ export default class Mail { return this; } + setContent(content: string) { + this.content = content; + return this; + } + /** * Renders the view template with the given data. * @param {object} data * @return {string} */ - render(data): string { - const viewContent = this.getViewContent(); + static render(view: string, data: Record): string { + const viewContent = Mail.getViewContent(view); return Mustache.render(viewContent, data); } /** * Retrieve view content from the view directory. */ - private getViewContent(): string { - const filePath = path.join(global.__views_dir, `/${this.view}`); + static getViewContent(view: string): string { + const filePath = path.join(global.__views_dir, `/${view}`); return fs.readFileSync(filePath, 'utf8'); } } diff --git a/packages/server/src/services/MailNotification/ContactMailNotification.ts b/packages/server/src/services/MailNotification/ContactMailNotification.ts index 21c745a96..e1e733a79 100644 --- a/packages/server/src/services/MailNotification/ContactMailNotification.ts +++ b/packages/server/src/services/MailNotification/ContactMailNotification.ts @@ -1,9 +1,9 @@ import { Inject, Service } from 'typedi'; -import * as R from 'ramda'; -import { SaleInvoiceMailOptions } from '@/interfaces'; +import { CommonMailOptions } from '@/interfaces'; import HasTenancyService from '@/services/Tenancy/TenancyService'; import { MailTenancy } from '@/services/MailTenancy/MailTenancy'; import { formatSmsMessage } from '@/utils'; +import { Tenant } from '@/system/models'; @Service() export class ContactMailNotification { @@ -15,8 +15,10 @@ export class ContactMailNotification { /** * Parses the default message options. - * @param {number} tenantId - * @param {number} invoiceId + * @param {number} tenantId - + * @param {number} invoiceId - + * @param {string} subject - + * @param {string} body - * @returns {Promise} */ public async getDefaultMailOptions( @@ -24,9 +26,11 @@ export class ContactMailNotification { contactId: number, subject: string = '', body: string = '' - ): Promise { - const { Contact, Customer } = this.tenancy.models(tenantId); - const contact = await Customer.query().findById(contactId).throwIfNotFound(); + ): Promise { + const { Customer } = this.tenancy.models(tenantId); + const contact = await Customer.query() + .findById(contactId) + .throwIfNotFound(); const toAddresses = contact.contactAddresses; const fromAddresses = await this.mailTenancy.senders(tenantId); @@ -48,10 +52,12 @@ export class ContactMailNotification { } /** - * Retrieves the mail options. - * @param {number} - * @param {number} invoiceId - * @returns {} + * Retrieves the mail options of the given contact. + * @param {number} tenantId - Tenant id. + * @param {number} invoiceId - Invoice id. + * @param {string} defaultSubject - Default subject text. + * @param {string} defaultBody - Default body text. + * @returns {Promise} */ public async getMailOptions( tenantId: number, @@ -59,15 +65,20 @@ export class ContactMailNotification { defaultSubject?: string, defaultBody?: string, formatterData?: Record - ): Promise { + ): Promise { const mailOpts = await this.getDefaultMailOptions( tenantId, contactId, defaultSubject, defaultBody ); - const subject = formatSmsMessage(mailOpts.subject, formatterData); - const body = formatSmsMessage(mailOpts.body, formatterData); + const commonFormatArgs = await this.getCommonFormatArgs(tenantId); + const formatArgs = { + ...commonFormatArgs, + ...formatterData, + }; + const subject = formatSmsMessage(mailOpts.subject, formatArgs); + const body = formatSmsMessage(mailOpts.body, formatArgs); return { ...mailOpts, @@ -75,4 +86,21 @@ export class ContactMailNotification { body, }; } + + /** + * Retrieves the common format args. + * @param {number} tenantId + * @returns {Promise>} + */ + public async getCommonFormatArgs( + tenantId: number + ): Promise> { + const organization = await Tenant.query() + .findById(tenantId) + .withGraphFetched('metadata'); + + return { + CompanyName: organization.metadata.name, + }; + } } diff --git a/packages/server/src/services/MailNotification/constants.ts b/packages/server/src/services/MailNotification/constants.ts new file mode 100644 index 000000000..95b720d70 --- /dev/null +++ b/packages/server/src/services/MailNotification/constants.ts @@ -0,0 +1,6 @@ +export 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/MailNotification/utils.ts b/packages/server/src/services/MailNotification/utils.ts new file mode 100644 index 000000000..b9e37b297 --- /dev/null +++ b/packages/server/src/services/MailNotification/utils.ts @@ -0,0 +1,33 @@ +import { isEmpty } from 'lodash'; +import { ServiceError } from '@/exceptions'; +import { CommonMailOptions, CommonMailOptionsDTO } from '@/interfaces'; +import { ERRORS } from './constants'; + +/** + * Merges the mail options with incoming options. + * @param {Partial} mailOptions + * @param {Partial} overridedOptions + * @throws {ServiceError} + */ +export function parseAndValidateMailOptions( + mailOptions: Partial, + overridedOptions: Partial +) { + const mergedMessageOptions = { + ...mailOptions, + ...overridedOptions, + }; + if (isEmpty(mergedMessageOptions.from)) { + throw new ServiceError(ERRORS.MAIL_FROM_NOT_FOUND); + } + if (isEmpty(mergedMessageOptions.to)) { + throw new ServiceError(ERRORS.MAIL_TO_NOT_FOUND); + } + if (isEmpty(mergedMessageOptions.subject)) { + throw new ServiceError(ERRORS.MAIL_SUBJECT_NOT_FOUND); + } + if (isEmpty(mergedMessageOptions.body)) { + throw new ServiceError(ERRORS.MAIL_BODY_NOT_FOUND); + } + return mergedMessageOptions; +} diff --git a/packages/server/src/services/Sales/Estimates/SaleEstimatesApplication.ts b/packages/server/src/services/Sales/Estimates/SaleEstimatesApplication.ts index 1ceb2bbcc..f1c7b3cdf 100644 --- a/packages/server/src/services/Sales/Estimates/SaleEstimatesApplication.ts +++ b/packages/server/src/services/Sales/Estimates/SaleEstimatesApplication.ts @@ -8,6 +8,7 @@ import { ISaleEstimateDTO, ISalesEstimatesFilter, SaleEstimateMailOptions, + SaleEstimateMailOptionsDTO, } from '@/interfaces'; import { EditSaleEstimate } from './EditSaleEstimate'; import { DeleteSaleEstimate } from './DeleteSaleEstimate'; @@ -224,8 +225,8 @@ export class SaleEstimatesApplication { public sendSaleEstimateMail( tenantId: number, saleEstimateId: number, - saleEstimateMailOpts: SaleEstimateMailOptions - ) { + saleEstimateMailOpts: SaleEstimateMailOptionsDTO + ): Promise { return this.sendEstimateMailService.triggerMail( tenantId, saleEstimateId, @@ -235,11 +236,14 @@ export class SaleEstimatesApplication { /** * Retrieves the default mail options of the given sale estimate. - * @param {number} tenantId - * @param {number} saleEstimateId - * @returns {} + * @param {number} tenantId + * @param {number} saleEstimateId + * @returns {Promise} */ - public getSaleEstimateMail(tenantId: number, saleEstimateId: number) { + public getSaleEstimateMail( + tenantId: number, + saleEstimateId: number + ): Promise { 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 5777ab55a..258496306 100644 --- a/packages/server/src/services/Sales/Estimates/SendSaleEstimateMail.ts +++ b/packages/server/src/services/Sales/Estimates/SendSaleEstimateMail.ts @@ -7,8 +7,12 @@ import { } from './constants'; import { SaleEstimatesPdf } from './SaleEstimatesPdf'; import { GetSaleEstimate } from './GetSaleEstimate'; -import { SaleEstimateMailOptions } from '@/interfaces'; +import { + SaleEstimateMailOptions, + SaleEstimateMailOptionsDTO, +} from '@/interfaces'; import { ContactMailNotification } from '@/services/MailNotification/ContactMailNotification'; +import { parseAndValidateMailOptions } from '@/services/MailNotification/utils'; @Service() export class SendSaleEstimateMail { @@ -31,13 +35,14 @@ export class SendSaleEstimateMail { * Triggers the reminder mail of the given sale estimate. * @param {number} tenantId - * @param {number} saleEstimateId - - * @param {SaleEstimateMailOptions} messageOptions - + * @param {SaleEstimateMailOptionsDTO} messageOptions - + * @returns {Promise} */ public async triggerMail( tenantId: number, saleEstimateId: number, - messageOptions: SaleEstimateMailOptions - ) { + messageOptions: SaleEstimateMailOptionsDTO + ): Promise { const payload = { tenantId, saleEstimateId, @@ -48,9 +53,9 @@ export class SendSaleEstimateMail { /** * Formates the text of the mail. - * @param {number} tenantId - * @param {number} estimateId - * @param {string} text + * @param {number} tenantId - Tenant id. + * @param {number} estimateId - Estimate id. + * @returns {Promise>} */ public formatterData = async (tenantId: number, estimateId: number) => { const estimate = await this.getSaleEstimateService.getEstimate( @@ -70,9 +75,12 @@ export class SendSaleEstimateMail { * Retrieves the mail options. * @param {number} tenantId * @param {number} saleEstimateId - * @returns + * @returns {Promise} */ - public getMailOptions = async (tenantId: number, saleEstimateId: number) => { + public getMailOptions = async ( + tenantId: number, + saleEstimateId: number + ): Promise => { const { SaleEstimate } = this.tenancy.models(tenantId); const saleEstimate = await SaleEstimate.query() @@ -91,6 +99,7 @@ export class SendSaleEstimateMail { return { ...mailOptions, data: formatterData, + attachEstimate: true }; }; @@ -99,26 +108,28 @@ export class SendSaleEstimateMail { * @param {number} tenantId * @param {number} saleEstimateId * @param {SaleEstimateMailOptions} messageOptions + * @returns {Promise} */ public async sendMail( tenantId: number, saleEstimateId: number, - messageOptions: SaleEstimateMailOptions - ) { + messageOptions: SaleEstimateMailOptionsDTO + ): Promise { const localMessageOpts = await this.getMailOptions( tenantId, saleEstimateId ); - const messageOpts = { - ...localMessageOpts, - ...messageOptions, - }; + // Overrides and validates the given mail options. + const messageOpts = parseAndValidateMailOptions( + localMessageOpts, + messageOptions + ); const mail = new Mail() .setSubject(messageOpts.subject) .setTo(messageOpts.to) .setContent(messageOpts.body); - if (messageOpts.to) { + if (messageOpts.attachEstimate) { const estimatePdfBuffer = await this.estimatePdf.getSaleEstimatePdf( tenantId, saleEstimateId diff --git a/packages/server/src/services/Sales/Invoices/SaleInvoicesApplication.ts b/packages/server/src/services/Sales/Invoices/SaleInvoicesApplication.ts index b175d9546..bc3f8c24b 100644 --- a/packages/server/src/services/Sales/Invoices/SaleInvoicesApplication.ts +++ b/packages/server/src/services/Sales/Invoices/SaleInvoicesApplication.ts @@ -300,7 +300,10 @@ export class SaleInvoiceApplication { * @returns {} */ public getSaleInvoiceMailReminder(tenantId: number, saleInvoiceId: number) { - return this.sendInvoiceReminderService.getMailOpts(tenantId, saleInvoiceId); + return this.sendInvoiceReminderService.getMailOption( + tenantId, + saleInvoiceId + ); } /** @@ -347,6 +350,9 @@ export class SaleInvoiceApplication { * @returns {Promise} */ public getSaleInvoiceMail(tenantId: number, saleInvoiceid: number) { - return this.sendSaleInvoiceMailService.getMailOpts(tenantId, saleInvoiceid); + return this.sendSaleInvoiceMailService.getMailOption( + tenantId, + saleInvoiceid + ); } } diff --git a/packages/server/src/services/Sales/Invoices/SendInvoiceInvoiceMailCommon.ts b/packages/server/src/services/Sales/Invoices/SendInvoiceInvoiceMailCommon.ts index 97be8de87..52ef46a59 100644 --- a/packages/server/src/services/Sales/Invoices/SendInvoiceInvoiceMailCommon.ts +++ b/packages/server/src/services/Sales/Invoices/SendInvoiceInvoiceMailCommon.ts @@ -1,15 +1,12 @@ import { Inject, Service } from 'typedi'; -import { isEmpty } from 'lodash'; import { SaleInvoiceMailOptions } from '@/interfaces'; import HasTenancyService from '@/services/Tenancy/TenancyService'; +import { GetSaleInvoice } from './GetSaleInvoice'; +import { ContactMailNotification } from '@/services/MailNotification/ContactMailNotification'; 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 { @@ -28,9 +25,9 @@ export class SendSaleInvoiceMailCommon { * @param {number} invoiceId - Invoice id. * @param {string} defaultSubject - Subject text. * @param {string} defaultBody - Subject body. - * @returns {} + * @returns {Promise} */ - public async getMailOpts( + public async getMailOption( tenantId: number, invoiceId: number, defaultSubject: string = DEFAULT_INVOICE_MAIL_SUBJECT, @@ -44,13 +41,17 @@ export class SendSaleInvoiceMailCommon { const formatterData = await this.formatText(tenantId, invoiceId); - return this.contactMailNotification.getMailOptions( + const mailOptions = await this.contactMailNotification.getMailOptions( tenantId, saleInvoice.customerId, defaultSubject, defaultBody, formatterData ); + return { + ...mailOptions, + attachInvoice: true, + }; } /** @@ -68,12 +69,8 @@ export class SendSaleInvoiceMailCommon { 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, @@ -83,33 +80,4 @@ export class SendSaleInvoiceMailCommon { 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 a5bc744e1..05db4f73e 100644 --- a/packages/server/src/services/Sales/Invoices/SendSaleInvoiceMail.ts +++ b/packages/server/src/services/Sales/Invoices/SendSaleInvoiceMail.ts @@ -7,6 +7,7 @@ import { DEFAULT_INVOICE_MAIL_CONTENT, DEFAULT_INVOICE_MAIL_SUBJECT, } from './constants'; +import { parseAndValidateMailOptions } from '@/services/MailNotification/utils'; @Service() export class SendSaleInvoiceMail { @@ -44,8 +45,8 @@ export class SendSaleInvoiceMail { * @param {number} saleInvoiceId * @returns {Promise} */ - public async getMailOpts(tenantId: number, saleInvoiceId: number) { - return this.invoiceMail.getMailOpts( + public async getMailOption(tenantId: number, saleInvoiceId: number) { + return this.invoiceMail.getMailOption( tenantId, saleInvoiceId, DEFAULT_INVOICE_MAIL_SUBJECT, @@ -65,15 +66,15 @@ export class SendSaleInvoiceMail { saleInvoiceId: number, messageDTO: SendInvoiceMailDTO ) { - const defaultMessageOpts = await this.getMailOpts(tenantId, saleInvoiceId); - - // Parsed message opts with default options. - const messageOpts = { - ...defaultMessageOpts, - ...messageDTO, - }; - this.invoiceMail.validateMailNotification(messageOpts); - + const defaultMessageOpts = await this.getMailOption( + tenantId, + saleInvoiceId + ); + // Merge message opts with default options and validate the incoming options. + const messageOpts = parseAndValidateMailOptions( + defaultMessageOpts, + messageDTO + ); const mail = new Mail() .setSubject(messageOpts.subject) .setTo(messageOpts.to) diff --git a/packages/server/src/services/Sales/Invoices/SendSaleInvoiceMailReminder.ts b/packages/server/src/services/Sales/Invoices/SendSaleInvoiceMailReminder.ts index 09463bddc..b5389a8a0 100644 --- a/packages/server/src/services/Sales/Invoices/SendSaleInvoiceMailReminder.ts +++ b/packages/server/src/services/Sales/Invoices/SendSaleInvoiceMailReminder.ts @@ -43,8 +43,8 @@ export class SendInvoiceMailReminder { * @param {number} saleInvoiceId * @returns {Promise} */ - public async getMailOpts(tenantId: number, saleInvoiceId: number) { - return this.invoiceCommonMail.getMailOpts( + public async getMailOption(tenantId: number, saleInvoiceId: number) { + return this.invoiceCommonMail.getMailOption( tenantId, saleInvoiceId, DEFAULT_INVOICE_REMINDER_MAIL_SUBJECT, @@ -64,7 +64,7 @@ export class SendInvoiceMailReminder { saleInvoiceId: number, messageOptions: SendInvoiceMailDTO ) { - const localMessageOpts = await this.getMailOpts(tenantId, saleInvoiceId); + const localMessageOpts = await this.getMailOption(tenantId, saleInvoiceId); const messageOpts = { ...localMessageOpts, diff --git a/packages/server/src/services/Sales/PaymentReceives/PaymentReceiveMailNotification.ts b/packages/server/src/services/Sales/PaymentReceives/PaymentReceiveMailNotification.ts index 79dfe2392..acb1ea7a1 100644 --- a/packages/server/src/services/Sales/PaymentReceives/PaymentReceiveMailNotification.ts +++ b/packages/server/src/services/Sales/PaymentReceives/PaymentReceiveMailNotification.ts @@ -1,14 +1,18 @@ import { Inject, Service } from 'typedi'; -import { IPaymentReceiveMailOpts, SendInvoiceMailDTO } from '@/interfaces'; +import { + PaymentReceiveMailOpts, + PaymentReceiveMailOptsDTO, + SendInvoiceMailDTO, +} from '@/interfaces'; import Mail from '@/lib/Mail'; import HasTenancyService from '@/services/Tenancy/TenancyService'; import { DEFAULT_PAYMENT_MAIL_CONTENT, DEFAULT_PAYMENT_MAIL_SUBJECT, } from './constants'; -import { Tenant } from '@/system/models'; import { GetPaymentReceive } from './GetPaymentReceive'; import { ContactMailNotification } from '@/services/MailNotification/ContactMailNotification'; +import { parseAndValidateMailOptions } from '@/services/MailNotification/utils'; @Service() export class SendPaymentReceiveMailNotification { @@ -28,13 +32,14 @@ export class SendPaymentReceiveMailNotification { * Sends the mail of the given payment receive. * @param {number} tenantId * @param {number} paymentReceiveId - * @param {SendInvoiceMailDTO} messageDTO + * @param {PaymentReceiveMailOptsDTO} messageDTO + * @returns {Promise} */ public async triggerMail( tenantId: number, paymentReceiveId: number, - messageDTO: IPaymentReceiveMailOpts - ) { + messageDTO: PaymentReceiveMailOptsDTO + ): Promise { const payload = { tenantId, paymentReceiveId, @@ -45,18 +50,21 @@ export class SendPaymentReceiveMailNotification { /** * Retrieves the default payment mail options. - * @param {number} tenantId - * @param {number} invoiceId - * @returns {Promise} + * @param {number} tenantId - Tenant id. + * @param {number} paymentReceiveId - Payment receive id. + * @returns {Promise} */ - public getMailOptions = async (tenantId: number, invoiceId: number) => { + public getMailOptions = async ( + tenantId: number, + paymentId: number + ): Promise => { const { PaymentReceive } = this.tenancy.models(tenantId); const paymentReceive = await PaymentReceive.query() - .findById(invoiceId) + .findById(paymentId) .throwIfNotFound(); - const formatterData = await this.textFormatter(tenantId, invoiceId); + const formatterData = await this.textFormatter(tenantId, paymentId); return this.contactMailNotification.getMailOptions( tenantId, @@ -82,12 +90,7 @@ export class SendPaymentReceiveMailNotification { tenantId, invoiceId ); - const organization = await Tenant.query() - .findById(tenantId) - .withGraphFetched('metadata'); - return { - CompanyName: organization.metadata.name, CustomerName: payment.customer.displayName, PaymentNumber: payment.payment_receive_no, PaymentDate: payment.formattedPaymentDate, @@ -112,10 +115,10 @@ export class SendPaymentReceiveMailNotification { paymentReceiveId ); // Parsed message opts with default options. - const parsedMessageOpts = { - ...defaultMessageOpts, - ...messageDTO, - }; + const parsedMessageOpts = parseAndValidateMailOptions( + defaultMessageOpts, + messageDTO + ); await new Mail() .setSubject(parsedMessageOpts.subject) .setTo(parsedMessageOpts.to) diff --git a/packages/server/src/services/Sales/PaymentReceives/PaymentReceivesApplication.ts b/packages/server/src/services/Sales/PaymentReceives/PaymentReceivesApplication.ts index bf1e2da3f..0d5669bf8 100644 --- a/packages/server/src/services/Sales/PaymentReceives/PaymentReceivesApplication.ts +++ b/packages/server/src/services/Sales/PaymentReceives/PaymentReceivesApplication.ts @@ -4,10 +4,10 @@ import { IPaymentReceive, IPaymentReceiveCreateDTO, IPaymentReceiveEditDTO, - IPaymentReceiveMailOpts, IPaymentReceiveSmsDetails, IPaymentReceivesFilter, ISystemUser, + PaymentReceiveMailOptsDTO, } from '@/interfaces'; import { Inject, Service } from 'typedi'; import { CreatePaymentReceive } from './CreatePaymentReceive'; @@ -189,8 +189,8 @@ export class PaymentReceivesApplication { public notifyPaymentByMail( tenantId: number, paymentReceiveId: number, - messageOpts: IPaymentReceiveMailOpts - ) { + messageOpts: PaymentReceiveMailOptsDTO + ): Promise { return this.paymentMailNotify.triggerMail( tenantId, paymentReceiveId, @@ -204,7 +204,7 @@ export class PaymentReceivesApplication { * @param {number} paymentReceiveId * @returns {Promise} */ - public getPaymentDefaultMail(tenantId: number, paymentReceiveId: number) { + public getPaymentMailOptions(tenantId: number, paymentReceiveId: number) { 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 0f790ec6f..459d9c62e 100644 --- a/packages/server/src/services/Sales/Receipts/SaleReceiptApplication.ts +++ b/packages/server/src/services/Sales/Receipts/SaleReceiptApplication.ts @@ -6,6 +6,7 @@ import { ISaleReceipt, ISalesReceiptsFilter, SaleReceiptMailOpts, + SaleReceiptMailOptsDTO, } from '@/interfaces'; import { EditSaleReceipt } from './EditSaleReceipt'; import { GetSaleReceipt } from './GetSaleReceipt'; @@ -176,12 +177,13 @@ export class SaleReceiptApplication { * Sends the receipt mail of the given sale receipt. * @param {number} tenantId * @param {number} saleReceiptId + * @returns {Promise} */ public sendSaleReceiptMail( tenantId: number, saleReceiptId: number, - messageOpts: SaleReceiptMailOpts - ) { + messageOpts: SaleReceiptMailOptsDTO + ): Promise { return this.saleReceiptNotifyByMailService.triggerMail( tenantId, saleReceiptId, @@ -193,9 +195,12 @@ export class SaleReceiptApplication { * Retrieves the default mail options of the given sale receipt. * @param {number} tenantId * @param {number} saleReceiptId - * @returns + * @returns {Promise} */ - public getSaleReceiptMail(tenantId: number, saleReceiptId: number) { + public getSaleReceiptMail( + tenantId: number, + saleReceiptId: number + ): Promise { 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 20bfc4073..572bed2f8 100644 --- a/packages/server/src/services/Sales/Receipts/SaleReceiptMailNotification.ts +++ b/packages/server/src/services/Sales/Receipts/SaleReceiptMailNotification.ts @@ -1,7 +1,5 @@ -import * as R from 'ramda'; import HasTenancyService from '@/services/Tenancy/TenancyService'; import { Inject, Service } from 'typedi'; -import { Tenant } from '@/system/models'; import Mail from '@/lib/Mail'; import { GetSaleReceipt } from './GetSaleReceipt'; import { SaleReceiptsPdf } from './SaleReceiptsPdfService'; @@ -9,8 +7,9 @@ import { DEFAULT_RECEIPT_MAIL_CONTENT, DEFAULT_RECEIPT_MAIL_SUBJECT, } from './constants'; -import { SaleReceiptMailOpts } from '@/interfaces'; +import { SaleReceiptMailOpts, SaleReceiptMailOptsDTO } from '@/interfaces'; import { ContactMailNotification } from '@/services/MailNotification/ContactMailNotification'; +import { parseAndValidateMailOptions } from '@/services/MailNotification/utils'; @Service() export class SaleReceiptMailNotification { @@ -32,13 +31,13 @@ export class SaleReceiptMailNotification { /** * Sends the receipt mail of the given sale receipt. * @param {number} tenantId - * @param {number} saleInvoiceId - * @param {SendInvoiceMailDTO} messageDTO + * @param {number} saleReceiptId + * @param {SaleReceiptMailOptsDTO} messageDTO */ public async triggerMail( tenantId: number, saleReceiptId: number, - messageOpts: SaleReceiptMailOpts + messageOpts: SaleReceiptMailOptsDTO ) { const payload = { tenantId, @@ -52,9 +51,12 @@ export class SaleReceiptMailNotification { * Retrieves the mail options of the given sale receipt. * @param {number} tenantId * @param {number} saleReceiptId - * @returns + * @returns {Promise} */ - public async getMailOptions(tenantId: number, saleReceiptId: number) { + public async getMailOptions( + tenantId: number, + saleReceiptId: number + ): Promise { const { SaleReceipt } = this.tenancy.models(tenantId); const saleReceipt = await SaleReceipt.query() @@ -63,17 +65,21 @@ export class SaleReceiptMailNotification { const formattedData = await this.textFormatter(tenantId, saleReceiptId); - return this.contactMailNotification.getMailOptions( + const mailOpts = await this.contactMailNotification.getMailOptions( tenantId, saleReceipt.customerId, DEFAULT_RECEIPT_MAIL_SUBJECT, DEFAULT_RECEIPT_MAIL_CONTENT, formattedData ); + return { + ...mailOpts, + attachReceipt: true, + }; } /** - * Retrieves the formatted text of the given sale invoice. + * Retrieves the formatted text of the given sale receipt. * @param {number} tenantId - Tenant id. * @param {number} receiptId - Sale receipt id. * @param {string} text - The given text. @@ -83,58 +89,52 @@ export class SaleReceiptMailNotification { tenantId: number, receiptId: number ): Promise> => { - const invoice = await this.getSaleReceiptService.getSaleReceipt( + const receipt = await this.getSaleReceiptService.getSaleReceipt( tenantId, receiptId ); - const organization = await Tenant.query() - .findById(tenantId) - .withGraphFetched('metadata'); - return { - CompanyName: organization.metadata.name, - CustomerName: invoice.customer.displayName, - ReceiptNumber: invoice.receiptNumber, - ReceiptDate: invoice.formattedReceiptDate, - ReceiptAmount: invoice.formattedAmount, + CustomerName: receipt.customer.displayName, + ReceiptNumber: receipt.receiptNumber, + ReceiptDate: receipt.formattedReceiptDate, + ReceiptAmount: receipt.formattedAmount, }; }; /** - * Triggers the mail invoice. - * @param {number} tenantId - * @param {number} saleInvoiceId - * @param {SendInvoiceMailDTO} messageDTO + * Triggers the mail notification of the given sale receipt. + * @param {number} tenantId - Tenant id. + * @param {number} saleReceiptId - Sale receipt id. + * @param {SaleReceiptMailOpts} messageDTO - Overrided message options. * @returns {Promise} */ public async sendMail( tenantId: number, saleReceiptId: number, - messageOpts: SaleReceiptMailOpts + messageOpts: SaleReceiptMailOptsDTO ) { const defaultMessageOpts = await this.getMailOptions( tenantId, saleReceiptId ); - // Parsed message opts with default options. - const parsedMessageOpts = { - ...defaultMessageOpts, - ...messageOpts, - }; - + // Merges message opts with default options. + const parsedMessageOpts = parseAndValidateMailOptions( + defaultMessageOpts, + messageOpts + ); const mail = new Mail() .setSubject(parsedMessageOpts.subject) .setTo(parsedMessageOpts.to) .setContent(parsedMessageOpts.body); - if (parsedMessageOpts.attachInvoice) { - // Retrieves document buffer of the invoice pdf document. + if (parsedMessageOpts.attachReceipt) { + // Retrieves document buffer of the receipt pdf document. const receiptPdfBuffer = await this.receiptPdfService.saleReceiptPdf( tenantId, saleReceiptId ); mail.setAttachments([ - { filename: 'invoice.pdf', content: receiptPdfBuffer }, + { filename: 'receipt.pdf', content: receiptPdfBuffer }, ]); } await mail.send(); diff --git a/packages/server/src/services/Sales/Receipts/constants.ts b/packages/server/src/services/Sales/Receipts/constants.ts index ae1a2e388..084af9214 100644 --- a/packages/server/src/services/Sales/Receipts/constants.ts +++ b/packages/server/src/services/Sales/Receipts/constants.ts @@ -1,5 +1,5 @@ export const DEFAULT_RECEIPT_MAIL_SUBJECT = - 'Invoice {InvoiceNumber} from {CompanyName}'; + 'Receipt {ReceiptNumber} from {CompanyName}'; export const DEFAULT_RECEIPT_MAIL_CONTENT = `

Dear {CustomerName}

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

diff --git a/packages/webapp/src/containers/Sales/PaymentReceives/PaymentMailDialog/PaymentMailDialogBoot.tsx b/packages/webapp/src/containers/Sales/PaymentReceives/PaymentMailDialog/PaymentMailDialogBoot.tsx index c70a28896..aa08bd2e1 100644 --- a/packages/webapp/src/containers/Sales/PaymentReceives/PaymentMailDialog/PaymentMailDialogBoot.tsx +++ b/packages/webapp/src/containers/Sales/PaymentReceives/PaymentMailDialog/PaymentMailDialogBoot.tsx @@ -29,6 +29,7 @@ function PaymentMailDialogBoot({ const provider = { mailOptions, isMailOptionsLoading, + paymentReceiveId }; return ( diff --git a/packages/webapp/src/containers/Sales/PaymentReceives/PaymentMailDialog/PaymentMailDialogForm.tsx b/packages/webapp/src/containers/Sales/PaymentReceives/PaymentMailDialog/PaymentMailDialogForm.tsx index 075c2ee8b..bf0aa578b 100644 --- a/packages/webapp/src/containers/Sales/PaymentReceives/PaymentMailDialog/PaymentMailDialogForm.tsx +++ b/packages/webapp/src/containers/Sales/PaymentReceives/PaymentMailDialog/PaymentMailDialogForm.tsx @@ -28,7 +28,7 @@ export function PaymentMailDialogFormRoot({ // #withDialogActions closeDialog, }) { - const { mailOptions, paymentId } = usePaymentMailDialogBoot(); + const { mailOptions, paymentReceiveId } = usePaymentMailDialogBoot(); const { mutateAsync: sendPaymentMail } = useSendPaymentReceiveMail(); const initialValues = transformMailFormToInitialValues( @@ -43,7 +43,7 @@ export function PaymentMailDialogFormRoot({ const reqValues = transformMailFormToRequest(values); setSubmitting(true); - sendPaymentMail([paymentId, reqValues]) + sendPaymentMail([paymentReceiveId, reqValues]) .then(() => { AppToaster.show({ message: 'The mail notification has been sent successfully.',