mirror of
https://github.com/bigcapitalhq/bigcapital.git
synced 2026-02-19 22:30:31 +00:00
feat: send mail notifications of sale receipts
This commit is contained in:
@@ -5,6 +5,7 @@ import {
|
|||||||
IPaginationMeta,
|
IPaginationMeta,
|
||||||
ISaleReceipt,
|
ISaleReceipt,
|
||||||
ISalesReceiptsFilter,
|
ISalesReceiptsFilter,
|
||||||
|
SaleReceiptMailOpts,
|
||||||
} from '@/interfaces';
|
} from '@/interfaces';
|
||||||
import { EditSaleReceipt } from './EditSaleReceipt';
|
import { EditSaleReceipt } from './EditSaleReceipt';
|
||||||
import { GetSaleReceipt } from './GetSaleReceipt';
|
import { GetSaleReceipt } from './GetSaleReceipt';
|
||||||
@@ -13,6 +14,7 @@ import { GetSaleReceipts } from './GetSaleReceipts';
|
|||||||
import { CloseSaleReceipt } from './CloseSaleReceipt';
|
import { CloseSaleReceipt } from './CloseSaleReceipt';
|
||||||
import { SaleReceiptsPdf } from './SaleReceiptsPdfService';
|
import { SaleReceiptsPdf } from './SaleReceiptsPdfService';
|
||||||
import { SaleReceiptNotifyBySms } from './SaleReceiptNotifyBySms';
|
import { SaleReceiptNotifyBySms } from './SaleReceiptNotifyBySms';
|
||||||
|
import { SaleReceiptMailNotification } from './SaleReceiptMailNotification';
|
||||||
|
|
||||||
@Service()
|
@Service()
|
||||||
export class SaleReceiptApplication {
|
export class SaleReceiptApplication {
|
||||||
@@ -40,6 +42,9 @@ export class SaleReceiptApplication {
|
|||||||
@Inject()
|
@Inject()
|
||||||
private saleReceiptNotifyBySmsService: SaleReceiptNotifyBySms;
|
private saleReceiptNotifyBySmsService: SaleReceiptNotifyBySms;
|
||||||
|
|
||||||
|
@Inject()
|
||||||
|
private saleReceiptNotifyByMailService: SaleReceiptMailNotification;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Creates a new sale receipt with associated entries.
|
* Creates a new sale receipt with associated entries.
|
||||||
* @param {number} tenantId
|
* @param {number} tenantId
|
||||||
@@ -166,4 +171,21 @@ export class SaleReceiptApplication {
|
|||||||
saleReceiptId
|
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
|
||||||
|
);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -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();
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -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);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
}
|
||||||
@@ -1,6 +1,7 @@
|
|||||||
import { Inject, Service } from 'typedi';
|
import { Inject, Service } from 'typedi';
|
||||||
import { TemplateInjectable } from '@/services/TemplateInjectable/TemplateInjectable';
|
import { TemplateInjectable } from '@/services/TemplateInjectable/TemplateInjectable';
|
||||||
import { ChromiumlyTenancy } from '@/services/ChromiumlyTenancy/ChromiumlyTenancy';
|
import { ChromiumlyTenancy } from '@/services/ChromiumlyTenancy/ChromiumlyTenancy';
|
||||||
|
import { GetSaleReceipt } from './GetSaleReceipt';
|
||||||
|
|
||||||
@Service()
|
@Service()
|
||||||
export class SaleReceiptsPdf {
|
export class SaleReceiptsPdf {
|
||||||
@@ -10,11 +11,20 @@ export class SaleReceiptsPdf {
|
|||||||
@Inject()
|
@Inject()
|
||||||
private templateInjectable: TemplateInjectable;
|
private templateInjectable: TemplateInjectable;
|
||||||
|
|
||||||
|
@Inject()
|
||||||
|
private getSaleReceiptService: GetSaleReceipt;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Retrieve sale invoice pdf content.
|
* Retrieves sale invoice pdf content.
|
||||||
* @param {} saleInvoice -
|
* @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(
|
const htmlContent = await this.templateInjectable.render(
|
||||||
tenantId,
|
tenantId,
|
||||||
'modules/receipt-regular',
|
'modules/receipt-regular',
|
||||||
|
|||||||
@@ -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 = {
|
export const ERRORS = {
|
||||||
SALE_RECEIPT_NOT_FOUND: 'SALE_RECEIPT_NOT_FOUND',
|
SALE_RECEIPT_NOT_FOUND: 'SALE_RECEIPT_NOT_FOUND',
|
||||||
DEPOSIT_ACCOUNT_NOT_FOUND: 'DEPOSIT_ACCOUNT_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_IS_ALREADY_CLOSED: 'SALE_RECEIPT_IS_ALREADY_CLOSED',
|
||||||
SALE_RECEIPT_NO_IS_REQUIRED: 'SALE_RECEIPT_NO_IS_REQUIRED',
|
SALE_RECEIPT_NO_IS_REQUIRED: 'SALE_RECEIPT_NO_IS_REQUIRED',
|
||||||
CUSTOMER_HAS_SALES_INVOICES: 'CUSTOMER_HAS_SALES_INVOICES',
|
CUSTOMER_HAS_SALES_INVOICES: 'CUSTOMER_HAS_SALES_INVOICES',
|
||||||
|
NO_INVOICE_CUSTOMER_EMAIL_ADDR: 'NO_INVOICE_CUSTOMER_EMAIL_ADDR'
|
||||||
};
|
};
|
||||||
|
|
||||||
export const DEFAULT_VIEW_COLUMNS = [];
|
export const DEFAULT_VIEW_COLUMNS = [];
|
||||||
|
|||||||
Reference in New Issue
Block a user