feat: send mail notifications of sale receipts

This commit is contained in:
Ahmed Bouhuolia
2023-12-24 21:49:59 +02:00
parent f0e15d43d3
commit b6d99b1d4b
5 changed files with 231 additions and 3 deletions

View File

@@ -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
);
}
}

View File

@@ -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<SendInvoiceMailDTO>}
*/
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<string>}
*/
public textFormatter = async (
tenantId: number,
receiptId: number,
text: string
): Promise<string> => {
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<void>}
*/
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();
}
}

View File

@@ -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);
}
};
}

View File

@@ -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<Buffer>}
*/
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',

View File

@@ -1,3 +1,15 @@
export const DEFAULT_RECEIPT_MAIL_SUBJECT =
'Invoice {InvoiceNumber} from {CompanyName}';
export const DEFAULT_RECEIPT_MAIL_CONTENT = `
<p>Dear {CustomerName}</p>
<p>Thank you for your business, You can view or print your invoice from attachements.</p>
<p>
Invoice <strong>#{InvoiceNumber}</strong><br />
Due Date : <strong>{InvoiceDueDate}</strong><br />
Amount : <strong>{InvoiceAmount}</strong></br />
</p>
`;
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 = [];