feat: send mail notifications of sale transactions

This commit is contained in:
Ahmed Bouhuolia
2023-12-24 21:53:37 +02:00
parent 13c6e7a62d
commit 6356cb5e63
9 changed files with 195 additions and 36 deletions

View File

@@ -1,9 +1,11 @@
import { Inject, Service } from 'typedi';
import { Router, Request, Response, NextFunction } from 'express';
import { check, param, query, ValidationChain } from 'express-validator';
import { body, check, param, query, ValidationChain } from 'express-validator';
import {
AbilitySubject,
IPaymentReceiveDTO,
IPaymentReceiveMailOpts,
// IPaymentReceiveMailOpts,
PaymentReceiveAction,
} from '@/interfaces';
import BaseController from '@/api/controllers/BaseController';
@@ -117,6 +119,19 @@ export default class PaymentReceivesController extends BaseController {
asyncMiddleware(this.deletePaymentReceive.bind(this)),
this.handleServiceErrors
);
router.post(
'/:id/mail',
[
...this.paymentReceiveValidation,
body('subject').isString().optional(),
body('from').isString().optional(),
body('to').isString().optional(),
body('body').isString().optional(),
body('attach_invoice').optional().isBoolean().toBoolean(),
],
this.sendPaymentReceiveByMail.bind(this),
this.handleServiceErrors
);
return router;
}
@@ -416,27 +431,26 @@ export default class PaymentReceivesController extends BaseController {
const { id: paymentReceiveId } = req.params;
try {
const paymentReceive =
await this.paymentReceiveApplication.getPaymentReceive(
tenantId,
paymentReceiveId
);
const ACCEPT_TYPE = {
APPLICATION_PDF: 'application/pdf',
APPLICATION_JSON: 'application/json',
};
res.format({
[ACCEPT_TYPE.APPLICATION_JSON]: () => {
[ACCEPT_TYPE.APPLICATION_JSON]: async () => {
const paymentReceive =
await this.paymentReceiveApplication.getPaymentReceive(
tenantId,
paymentReceiveId
);
return res.status(200).send({
payment_receive: this.transfromToResponse(paymentReceive),
payment_receive: paymentReceive,
});
},
[ACCEPT_TYPE.APPLICATION_PDF]: async () => {
const pdfContent =
await this.paymentReceiveApplication.getPaymentReceivePdf(
tenantId,
paymentReceive
paymentReceiveId
);
res.set({
'Content-Type': 'application/pdf',
@@ -507,6 +521,38 @@ export default class PaymentReceivesController extends BaseController {
}
};
/**
* Sends mail invoice of the given sale invoice.
* @param {Request} req
* @param {Response} res
* @param {NextFunction} next
* @returns
*/
public sendPaymentReceiveByMail = async (
req: Request,
res: Response,
next: NextFunction
) => {
const { tenantId } = req;
const { id: paymentReceiveId } = req.params;
const paymentMailDTO: IPaymentReceiveMailOpts = this.matchedBodyData(req, {
includeOptionals: false,
});
try {
await this.paymentReceiveApplication.notifyPaymentByMail(
tenantId,
paymentReceiveId,
paymentMailDTO
);
return res.status(200).send({
code: 200,
message: 'The payment notification has been sent successfully.',
});
} catch (error) {
next(error);
}
};
/**
* Handles service errors.
* @param error
@@ -514,7 +560,7 @@ export default class PaymentReceivesController extends BaseController {
* @param res
* @param next
*/
handleServiceErrors(
private handleServiceErrors(
error: Error,
req: Request,
res: Response,

View File

@@ -1,10 +1,11 @@
import { Router, Request, Response, NextFunction } from 'express';
import { check, param, query } from 'express-validator';
import { body, check, param, query } from 'express-validator';
import { Inject, Service } from 'typedi';
import {
AbilitySubject,
ISaleEstimateDTO,
SaleEstimateAction,
SaleEstimateMailOptions,
} from '@/interfaces';
import BaseController from '@/api/controllers/BaseController';
import asyncMiddleware from '@/api/middleware/asyncMiddleware';
@@ -121,6 +122,20 @@ export default class SalesEstimatesController extends BaseController {
this.handleServiceErrors,
this.dynamicListService.handlerErrorsToResponse
);
router.post(
'/:id/mail',
[
...this.validateSpecificEstimateSchema,
body('subject').isString().optional(),
body('from').isString().optional(),
body('to').isString().optional(),
body('body').isString().optional(),
body('attach_invoice').optional().isBoolean().toBoolean(),
],
this.validationResult,
asyncMiddleware(this.sendSaleEstimateMail.bind(this)),
this.handleServiceErrors
);
return router;
}
@@ -362,22 +377,22 @@ export default class SalesEstimatesController extends BaseController {
const { tenantId } = req;
try {
const estimate = await this.saleEstimatesApplication.getSaleEstimate(
tenantId,
estimateId
);
// Response formatter.
res.format({
// JSON content type.
[ACCEPT_TYPE.APPLICATION_JSON]: () => {
return res.status(200).send(this.transfromToResponse({ estimate }));
[ACCEPT_TYPE.APPLICATION_JSON]: async () => {
const estimate = await this.saleEstimatesApplication.getSaleEstimate(
tenantId,
estimateId
);
return res.status(200).send({ estimate });
},
// PDF content type.
[ACCEPT_TYPE.APPLICATION_PDF]: async () => {
const pdfContent =
await this.saleEstimatesApplication.getSaleEstimatePdf(
tenantId,
estimate
estimateId
);
res.set({
'Content-Type': 'application/pdf',
@@ -478,6 +493,38 @@ export default class SalesEstimatesController extends BaseController {
}
};
/**
* Send the sale estimate mail.
* @param {Request} req
* @param {Response} res
* @param {NextFunction} next
*/
private sendSaleEstimateMail = async (
req: Request,
res: Response,
next: NextFunction
) => {
const { tenantId } = req;
const { id: invoiceId } = req.params;
const saleEstimateDTO: SaleEstimateMailOptions = this.matchedBodyData(req, {
includeOptionals: false,
});
try {
await this.saleEstimatesApplication.sendSaleEstimateMail(
tenantId,
invoiceId,
saleEstimateDTO
);
return res.status(200).send({
code: 200,
message: 'The sale estimate mail has been sent successfully.',
});
} catch (error) {
next(error);
}
};
/**
* Handles service errors.
* @param {Error} error

View File

@@ -1,9 +1,9 @@
import { Router, Request, Response, NextFunction } from 'express';
import { check, param, query } from 'express-validator';
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 } from '@/interfaces/SaleReceipt';
import { ISaleReceiptDTO, SaleReceiptMailOpts } from '@/interfaces/SaleReceipt';
import { ServiceError } from '@/exceptions';
import DynamicListingService from '@/services/DynamicListing/DynamicListService';
import CheckPolicies from '@/api/middleware/CheckPolicies';
@@ -46,6 +46,20 @@ export default class SalesReceiptsController extends BaseController {
this.saleReceiptSmsDetails,
this.handleServiceErrors
);
router.post(
'/:id/mail',
[
...this.specificReceiptValidationSchema,
body('subject').isString().optional(),
body('from').isString().optional(),
body('to').isString().optional(),
body('body').isString().optional(),
body('attach_invoice').optional().isBoolean().toBoolean(),
],
this.validationResult,
asyncMiddleware(this.sendSaleReceiptMail.bind(this)),
this.handleServiceErrors
);
router.post(
'/:id',
CheckPolicies(SaleReceiptAction.Edit, AbilitySubject.SaleReceipt),
@@ -314,26 +328,24 @@ export default class SalesReceiptsController extends BaseController {
* @param {Response} res
* @param {NextFunction} next
*/
async getSaleReceipt(req: Request, res: Response, next: NextFunction) {
public async getSaleReceipt(req: Request, res: Response, next: NextFunction) {
const { id: saleReceiptId } = req.params;
const { tenantId } = req;
try {
const saleReceipt = await this.saleReceiptsApplication.getSaleReceipt(
tenantId,
saleReceiptId
);
res.format({
'application/json': () => {
return res
.status(200)
.send(this.transfromToResponse({ saleReceipt }));
'application/json': async () => {
const saleReceipt = await this.saleReceiptsApplication.getSaleReceipt(
tenantId,
saleReceiptId
);
return res.status(200).send({ saleReceipt });
},
'application/pdf': async () => {
const pdfContent =
await this.saleReceiptsApplication.getSaleReceiptPdf(
tenantId,
saleReceipt
saleReceiptId
);
res.set({
'Content-Type': 'application/pdf',
@@ -405,6 +417,39 @@ export default class SalesReceiptsController extends BaseController {
}
};
/**
* Sends mail notification of the given sale receipt.
* @param {Request} req
* @param {Response} res
* @param {NextFunction} next
*/
public sendSaleReceiptMail = async (
req: Request,
res: Response,
next: NextFunction
) => {
const { tenantId } = req;
const { id: receiptId } = req.params;
const receiptMailDTO: SaleReceiptMailOpts = this.matchedBodyData(req, {
includeOptionals: false,
});
try {
await this.saleReceiptsApplication.sendSaleReceiptMail(
tenantId,
receiptId,
receiptMailDTO
);
return res.status(200).send({
code: 200,
message:
'The sale receipt notification via sms has been sent successfully.',
});
} catch (error) {
next(error);
}
};
/**
* Handles service errors.
* @param {Error} error

View File

@@ -165,3 +165,7 @@ export type IPaymentReceiveGLCommonEntry = Pick<
| 'createdAt'
| 'branchId'
>;
export interface IPaymentReceiveMailOpts {
}

View File

@@ -124,3 +124,11 @@ export interface ISaleEstimateApprovedEvent {
saleEstimate: ISaleEstimate;
trx: Knex.Transaction;
}
export interface SaleEstimateMailOptions {
to: string;
from: string;
subject: string;
body: string;
attachInvoice?: boolean;
}

View File

@@ -134,3 +134,7 @@ export interface ISaleReceiptDeletingPayload {
oldSaleReceipt: ISaleReceipt;
trx: Knex.Transaction;
}
export interface SaleReceiptMailOpts {
}

View File

@@ -7,6 +7,9 @@ import OrganizationSetupJob from 'jobs/OrganizationSetup';
import OrganizationUpgrade from 'jobs/OrganizationUpgrade';
import { SendSaleInvoiceMailJob } from '@/services/Sales/Invoices/SendSaleInvoiceMailJob';
import { SendSaleInvoiceReminderMailJob } from '@/services/Sales/Invoices/SendSaleInvoiceMailReminderJob';
import { SendSaleEstimateMailJob } from '@/services/Sales/Estimates/SendSaleEstimateMailJob';
import { SaleReceiptMailNotificationJob } from '@/services/Sales/Receipts/SaleReceiptMailNotificationJob';
import { PaymentReceiveMailNotificationJob } from '@/services/Sales/PaymentReceives/PaymentReceiveMailNotificationJob';
export default ({ agenda }: { agenda: Agenda }) => {
new ResetPasswordMailJob(agenda);
@@ -17,6 +20,9 @@ export default ({ agenda }: { agenda: Agenda }) => {
new OrganizationUpgrade(agenda);
new SendSaleInvoiceMailJob(agenda);
new SendSaleInvoiceReminderMailJob(agenda);
new SendSaleEstimateMailJob(agenda);
new SaleReceiptMailNotificationJob(agenda);
new PaymentReceiveMailNotificationJob(agenda);
agenda.start();
};

View File

@@ -29,12 +29,12 @@ export class SendSaleEstimateMail {
* Triggers the reminder mail of the given sale estimate.
* @param {number} tenantId
* @param {number} saleEstimateId
* @param messageOptions
* @param {SaleEstimateMailOptions} messageOptions
*/
public async triggerMail(
tenantId: number,
saleEstimateId: number,
messageOptions: any
messageOptions: SaleEstimateMailOptions
) {
const payload = {
tenantId,
@@ -114,7 +114,6 @@ export class SendSaleEstimateMail {
...messageOptions,
};
const formatter = R.curry(this.formatText)(tenantId, saleEstimateId);
const toEmail = parsedMessageOpts.to;
const subject = await formatter(parsedMessageOpts.subject);
const body = await formatter(parsedMessageOpts.body);
const attachments = [];
@@ -131,7 +130,7 @@ export class SendSaleEstimateMail {
}
await new Mail()
.setSubject(subject)
.setTo(toEmail)
.setTo(parsedMessageOpts.to)
.setContent(body)
.setAttachments(attachments)
.send();

View File

@@ -329,7 +329,7 @@ export class SaleInvoiceApplication {
* @param {number} tenantId
* @param {number} saleInvoiceId
* @param {SendInvoiceMailDTO} messageDTO
* @returns
* @returns {Promise<void>}
*/
public sendSaleInvoiceMail(
tenantId: number,