feat: send mail notifications of payment

This commit is contained in:
Ahmed Bouhuolia
2023-12-24 21:51:23 +02:00
parent b6d99b1d4b
commit 13c6e7a62d
5 changed files with 215 additions and 6 deletions

View File

@@ -1,7 +1,7 @@
import { Inject, Service } from 'typedi';
import { ChromiumlyTenancy } from '@/services/ChromiumlyTenancy/ChromiumlyTenancy';
import { TemplateInjectable } from '@/services/TemplateInjectable/TemplateInjectable';
import { IPaymentReceive } from '@/interfaces';
import { GetPaymentReceive } from './GetPaymentReceive';
@Service()
export default class GetPaymentReceivePdf {
@@ -11,6 +11,9 @@ export default class GetPaymentReceivePdf {
@Inject()
private templateInjectable: TemplateInjectable;
@Inject()
private getPaymentService: GetPaymentReceive;
/**
* Retrieve sale invoice pdf content.
* @param {number} tenantId -
@@ -19,8 +22,12 @@ export default class GetPaymentReceivePdf {
*/
async getPaymentReceivePdf(
tenantId: number,
paymentReceive: IPaymentReceive
paymentReceiveId: number
): Promise<Buffer> {
const paymentReceive = await this.getPaymentService.getPaymentReceive(
tenantId,
paymentReceiveId
);
const htmlContent = await this.templateInjectable.render(
tenantId,
'modules/payment-receive-standard',

View File

@@ -0,0 +1,131 @@
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';
@Service()
export class SendPaymentReceiveMailNotification {
@Inject()
private tenancy: HasTenancyService;
@Inject()
private getPaymentService: GetPaymentReceive;
@Inject('agenda')
private agenda: any;
/**
* Sends the mail of the given payment receive.
* @param {number} tenantId
* @param {number} paymentReceiveId
* @param {SendInvoiceMailDTO} messageDTO
*/
public async triggerMail(
tenantId: number,
paymentReceiveId: number,
messageDTO: IPaymentReceiveMailOpts
) {
const payload = {
tenantId,
paymentReceiveId,
messageDTO,
};
await this.agenda.now('payment-receive-mail-send', payload);
}
/**
* Retrieves the default payment mail options.
* @param {number} tenantId
* @param {number} invoiceId
* @returns {Promise<SendInvoiceMailDTO>}
*/
public getDefaultMailOpts = 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,
};
};
/**
* 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<string>}
*/
public textFormatter = async (
tenantId: number,
invoiceId: number,
text: string
): Promise<string> => {
const payment = await this.getPaymentService.getPaymentReceive(
tenantId,
invoiceId
);
const organization = await Tenant.query()
.findById(tenantId)
.withGraphFetched('metadata');
return formatSmsMessage(text, {
CompanyName: organization.metadata.name,
CustomerName: payment.customer.displayName,
PaymentNumber: payment.invoiceNo,
PaymentDate: payment.dueAmountFormatted,
PaymentAmount: payment.dueAmountFormatted,
});
};
/**
* Triggers the mail invoice.
* @param {number} tenantId
* @param {number} saleInvoiceId
* @param {SendInvoiceMailDTO} messageDTO
* @returns {Promise<void>}
*/
public async sendMail(
tenantId: number,
paymentReceiveId: number,
messageDTO: SendInvoiceMailDTO
): Promise<void> {
const defaultMessageOpts = await this.getDefaultMailOpts(
tenantId,
paymentReceiveId
);
// Parsed message opts with default options.
const parsedMessageOpts = {
...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)
.setTo(parsedMessageOpts.to)
.setContent(body)
.send();
}
}

View File

@@ -0,0 +1,35 @@
import Container, { Service } from 'typedi';
import events from '@/subscribers/events';
import { SendPaymentReceiveMailNotification } from './PaymentReceiveMailNotification';
@Service()
export class PaymentReceiveMailNotificationJob {
/**
* Constructor method.
*/
constructor(agenda) {
agenda.define(
'payment-receive-mail-send',
{ priority: 'high', concurrency: 2 },
this.handler
);
}
/**
* Triggers sending payment notification via mail.
*/
private handler = async (job, done: Function) => {
const { tenantId, paymentReceiveId, messageDTO } = job.attrs.data;
const paymentMail = Container.get(SendPaymentReceiveMailNotification);
console.log(tenantId, paymentReceiveId, messageDTO);
try {
await paymentMail.sendMail(tenantId, paymentReceiveId, messageDTO);
done();
} catch (error) {
console.log(error);
done(error);
}
};
}

View File

@@ -4,6 +4,7 @@ import {
IPaymentReceive,
IPaymentReceiveCreateDTO,
IPaymentReceiveEditDTO,
IPaymentReceiveMailOpts,
IPaymentReceiveSmsDetails,
IPaymentReceivesFilter,
ISystemUser,
@@ -17,7 +18,7 @@ import { GetPaymentReceive } from './GetPaymentReceive';
import { GetPaymentReceiveInvoices } from './GetPaymentReceiveInvoices';
import { PaymentReceiveNotifyBySms } from './PaymentReceiveSmsNotify';
import GetPaymentReceivePdf from './GetPaymentReeceivePdf';
import { PaymentReceive } from '@/models';
import { SendPaymentReceiveMailNotification } from './PaymentReceiveMailNotification';
@Service()
export class PaymentReceivesApplication {
@@ -42,6 +43,9 @@ export class PaymentReceivesApplication {
@Inject()
private paymentSmsNotify: PaymentReceiveNotifyBySms;
@Inject()
private paymentMailNotify: SendPaymentReceiveMailNotification;
@Inject()
private getPaymentReceivePdfService: GetPaymentReceivePdf;
@@ -176,18 +180,37 @@ export class PaymentReceivesApplication {
};
/**
* Retrieve PDF content of the given payment receive.
* Notify customer via mail about payment receive details.
* @param {number} tenantId
* @param {number} paymentReceiveId
* @param {IPaymentReceiveMailOpts} messageOpts
* @returns {Promise<void>}
*/
public notifyPaymentByMail(
tenantId: number,
paymentReceiveId: number,
messageOpts: IPaymentReceiveMailOpts
) {
return this.paymentMailNotify.triggerMail(
tenantId,
paymentReceiveId,
messageOpts
);
}
/**
* Retrieve pdf content of the given payment receive.
* @param {number} tenantId
* @param {PaymentReceive} paymentReceive
* @returns
*/
public getPaymentReceivePdf = (
tenantId: number,
paymentReceive: PaymentReceive
paymentReceiveId: number
) => {
return this.getPaymentReceivePdfService.getPaymentReceivePdf(
tenantId,
paymentReceive
paymentReceiveId
);
};
}

View File

@@ -1,3 +1,15 @@
export const DEFAULT_PAYMENT_MAIL_SUBJECT =
'Invoice {InvoiceNumber} from {CompanyName}';
export const DEFAULT_PAYMENT_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 = {
PAYMENT_RECEIVE_NO_EXISTS: 'PAYMENT_RECEIVE_NO_EXISTS',
PAYMENT_RECEIVE_NOT_EXISTS: 'PAYMENT_RECEIVE_NOT_EXISTS',
@@ -12,6 +24,7 @@ export const ERRORS = {
PAYMENT_CUSTOMER_SHOULD_NOT_UPDATE: 'PAYMENT_CUSTOMER_SHOULD_NOT_UPDATE',
CUSTOMER_HAS_PAYMENT_RECEIVES: 'CUSTOMER_HAS_PAYMENT_RECEIVES',
PAYMENT_ACCOUNT_CURRENCY_INVALID: 'PAYMENT_ACCOUNT_CURRENCY_INVALID',
NO_INVOICE_CUSTOMER_EMAIL_ADDR: 'NO_INVOICE_CUSTOMER_EMAIL_ADDR'
};
export const DEFAULT_VIEWS = [];