fix: sending mail jobs

This commit is contained in:
Ahmed Bouhuolia
2025-12-05 00:07:26 +02:00
parent 32d74b0413
commit c3dc26a1e4
11 changed files with 100 additions and 31 deletions

View File

@@ -1,12 +1,11 @@
import { JOB_REF, Process, Processor } from '@nestjs/bull'; import { JOB_REF, Process, Processor } from '@nestjs/bull';
import { Job } from 'bull'; import { Job } from 'bull';
import { Inject, Scope } from '@nestjs/common';
import { ClsService, UseCls } from 'nestjs-cls';
import { import {
SEND_PAYMENT_RECEIVED_MAIL_JOB, SEND_PAYMENT_RECEIVED_MAIL_JOB,
SEND_PAYMENT_RECEIVED_MAIL_QUEUE, SEND_PAYMENT_RECEIVED_MAIL_QUEUE,
} from '../constants'; } from '../constants';
import { Inject, Scope } from '@nestjs/common';
import { REQUEST } from '@nestjs/core';
import { ClsService } from 'nestjs-cls';
import { SendPaymentReceiveMailNotification } from '../commands/PaymentReceivedMailNotification'; import { SendPaymentReceiveMailNotification } from '../commands/PaymentReceivedMailNotification';
import { SendPaymentReceivedMailPayload } from '../types/PaymentReceived.types'; import { SendPaymentReceivedMailPayload } from '../types/PaymentReceived.types';
@@ -21,9 +20,10 @@ export class SendPaymentReceivedMailProcessor {
@Inject(JOB_REF) @Inject(JOB_REF)
private readonly jobRef: Job<SendPaymentReceivedMailPayload>, private readonly jobRef: Job<SendPaymentReceivedMailPayload>,
) {} ) { }
@Process(SEND_PAYMENT_RECEIVED_MAIL_JOB) @Process(SEND_PAYMENT_RECEIVED_MAIL_JOB)
@UseCls()
async handleSendMail() { async handleSendMail() {
const { messageOptions, paymentReceivedId, organizationId, userId } = const { messageOptions, paymentReceivedId, organizationId, userId } =
this.jobRef.data; this.jobRef.data;
@@ -37,7 +37,8 @@ export class SendPaymentReceivedMailProcessor {
messageOptions, messageOptions,
); );
} catch (error) { } catch (error) {
console.log(error); console.error('Failed to process payment received mail job:', error);
throw error;
} }
} }
} }

View File

@@ -42,6 +42,7 @@ import { GetSaleEstimateMailTemplateService } from './queries/GetSaleEstimateMai
import { SaleEstimateAutoIncrementSubscriber } from './subscribers/SaleEstimateAutoIncrementSubscriber'; import { SaleEstimateAutoIncrementSubscriber } from './subscribers/SaleEstimateAutoIncrementSubscriber';
import { BulkDeleteSaleEstimatesService } from './BulkDeleteSaleEstimates.service'; import { BulkDeleteSaleEstimatesService } from './BulkDeleteSaleEstimates.service';
import { ValidateBulkDeleteSaleEstimatesService } from './ValidateBulkDeleteSaleEstimates.service'; import { ValidateBulkDeleteSaleEstimatesService } from './ValidateBulkDeleteSaleEstimates.service';
import { SendSaleEstimateMailProcess } from './processes/SendSaleEstimateMail.process';
@Module({ @Module({
imports: [ imports: [
@@ -89,6 +90,7 @@ import { ValidateBulkDeleteSaleEstimatesService } from './ValidateBulkDeleteSale
SaleEstimateAutoIncrementSubscriber, SaleEstimateAutoIncrementSubscriber,
BulkDeleteSaleEstimatesService, BulkDeleteSaleEstimatesService,
ValidateBulkDeleteSaleEstimatesService, ValidateBulkDeleteSaleEstimatesService,
SendSaleEstimateMailProcess,
], ],
exports: [ exports: [
SaleEstimatesExportable, SaleEstimatesExportable,

View File

@@ -24,6 +24,7 @@ import { Mail } from '@/modules/Mail/Mail';
import { MailTransporter } from '@/modules/Mail/MailTransporter.service'; import { MailTransporter } from '@/modules/Mail/MailTransporter.service';
import { TenantModelProxy } from '@/modules/System/models/TenantBaseModel'; import { TenantModelProxy } from '@/modules/System/models/TenantBaseModel';
import { GetSaleEstimateMailTemplateService } from '../queries/GetSaleEstimateMailTemplate.service'; import { GetSaleEstimateMailTemplateService } from '../queries/GetSaleEstimateMailTemplate.service';
import { TenancyContext } from '@/modules/Tenancy/TenancyContext.service';
@Injectable() @Injectable()
export class SendSaleEstimateMail { export class SendSaleEstimateMail {
@@ -42,13 +43,14 @@ export class SendSaleEstimateMail {
private readonly getEstimateMailTemplate: GetSaleEstimateMailTemplateService, private readonly getEstimateMailTemplate: GetSaleEstimateMailTemplateService,
private readonly eventPublisher: EventEmitter2, private readonly eventPublisher: EventEmitter2,
private readonly mailTransporter: MailTransporter, private readonly mailTransporter: MailTransporter,
private readonly tenancyContext: TenancyContext,
@Inject(SaleEstimate.name) @Inject(SaleEstimate.name)
private readonly saleEstimateModel: TenantModelProxy<typeof SaleEstimate>, private readonly saleEstimateModel: TenantModelProxy<typeof SaleEstimate>,
@InjectQueue(SendSaleEstimateMailQueue) @InjectQueue(SendSaleEstimateMailQueue)
private readonly sendEstimateMailQueue: Queue, private readonly sendEstimateMailQueue: Queue,
) {} ) { }
/** /**
* Triggers the reminder mail of the given sale estimate. * Triggers the reminder mail of the given sale estimate.
@@ -60,10 +62,19 @@ export class SendSaleEstimateMail {
saleEstimateId: number, saleEstimateId: number,
messageOptions: SaleEstimateMailOptionsDTO, messageOptions: SaleEstimateMailOptionsDTO,
): Promise<void> { ): Promise<void> {
const tenant = await this.tenancyContext.getTenant();
const user = await this.tenancyContext.getSystemUser();
const organizationId = tenant.organizationId;
const userId = user.id;
const payload = { const payload = {
saleEstimateId, saleEstimateId,
messageOptions, messageOptions,
userId,
organizationId,
}; };
await this.sendEstimateMailQueue.add(SendSaleEstimateMailJob, payload); await this.sendEstimateMailQueue.add(SendSaleEstimateMailJob, payload);
// Triggers `onSaleEstimatePreMailSend` event. // Triggers `onSaleEstimatePreMailSend` event.

View File

@@ -1,19 +1,39 @@
import { Process, Processor } from '@nestjs/bull'; import { Process, Processor } from '@nestjs/bull';
import { Job } from 'bull'; import { Job } from 'bull';
import { Inject, Scope } from '@nestjs/common';
import { JOB_REF } from '@nestjs/bull';
import { import {
SendSaleEstimateMailJob, SendSaleEstimateMailJob,
SendSaleEstimateMailQueue, SendSaleEstimateMailQueue,
} from '../types/SaleEstimates.types'; } from '../types/SaleEstimates.types';
import { SendSaleEstimateMail } from '../commands/SendSaleEstimateMail'; import { SendSaleEstimateMail } from '../commands/SendSaleEstimateMail';
import { ClsService, UseCls } from 'nestjs-cls';
@Processor(SendSaleEstimateMailQueue) @Processor({
name: SendSaleEstimateMailQueue,
scope: Scope.REQUEST,
})
export class SendSaleEstimateMailProcess { export class SendSaleEstimateMailProcess {
constructor(private readonly sendEstimateMailService: SendSaleEstimateMail) {} constructor(
private readonly sendEstimateMailService: SendSaleEstimateMail,
private readonly clsService: ClsService,
@Inject(JOB_REF)
private readonly jobRef: Job,
) { }
@Process(SendSaleEstimateMailJob) @Process(SendSaleEstimateMailJob)
async handleSendMail(job: Job) { @UseCls()
const { saleEstimateId, messageOptions } = job.data; async handleSendMail() {
const { saleEstimateId, messageOptions, organizationId, userId } = this.jobRef.data;
await this.sendEstimateMailService.sendMail(saleEstimateId, messageOptions); this.clsService.set('organizationId', organizationId);
this.clsService.set('userId', userId);
try {
await this.sendEstimateMailService.sendMail(saleEstimateId, messageOptions);
} catch (error) {
console.error('Failed to process estimate mail job:', error);
throw error;
}
} }
} }

View File

@@ -99,7 +99,8 @@ export class SaleInvoicesController {
return this.saleInvoiceApplication.createSaleInvoice(saleInvoiceDTO); return this.saleInvoiceApplication.createSaleInvoice(saleInvoiceDTO);
} }
@Put(':id/mail') @Post(':id/mail')
@HttpCode(200)
@ApiOperation({ summary: 'Send the sale invoice mail.' }) @ApiOperation({ summary: 'Send the sale invoice mail.' })
@ApiResponse({ @ApiResponse({
status: 200, status: 200,

View File

@@ -33,7 +33,7 @@ export class SendSaleInvoiceMail {
private readonly tenancyContect: TenancyContext, private readonly tenancyContect: TenancyContext,
@InjectQueue(SendSaleInvoiceQueue) private readonly sendInvoiceQueue: Queue, @InjectQueue(SendSaleInvoiceQueue) private readonly sendInvoiceQueue: Queue,
) {} ) { }
/** /**
* Sends the invoice mail of the given sale invoice. * Sends the invoice mail of the given sale invoice.
@@ -132,7 +132,13 @@ export class SendSaleInvoiceMail {
events.saleInvoice.onMailSend, events.saleInvoice.onMailSend,
eventPayload, eventPayload,
); );
await this.mailTransporter.send(mail);
try {
await this.mailTransporter.send(mail);
} catch (error) {
console.error('Failed to send invoice mail:', error);
throw error;
}
// Triggers the event `onSaleInvoiceSend`. // Triggers the event `onSaleInvoiceSend`.
await this.eventEmitter.emitAsync( await this.eventEmitter.emitAsync(

View File

@@ -18,9 +18,10 @@ export class SendSaleInvoiceMailProcessor {
@Inject(JOB_REF) @Inject(JOB_REF)
private readonly jobRef: Job<SendSaleInvoiceMailJobPayload>, private readonly jobRef: Job<SendSaleInvoiceMailJobPayload>,
private readonly clsService: ClsService, private readonly clsService: ClsService,
) {} ) { }
@Process(SendSaleInvoiceMailJob) @Process(SendSaleInvoiceMailJob)
@UseCls()
async handleSendInvoice() { async handleSendInvoice() {
const { messageOptions, saleInvoiceId, organizationId, userId } = const { messageOptions, saleInvoiceId, organizationId, userId } =
this.jobRef.data; this.jobRef.data;
@@ -31,7 +32,8 @@ export class SendSaleInvoiceMailProcessor {
try { try {
await this.sendSaleInvoiceMail.sendMail(saleInvoiceId, messageOptions); await this.sendSaleInvoiceMail.sendMail(saleInvoiceId, messageOptions);
} catch (error) { } catch (error) {
console.log(error); console.error('Failed to process invoice mail job:', error);
throw error;
} }
} }
} }

View File

@@ -25,7 +25,10 @@ import {
CreateSaleReceiptDto, CreateSaleReceiptDto,
EditSaleReceiptDto, EditSaleReceiptDto,
} from './dtos/SaleReceipt.dto'; } from './dtos/SaleReceipt.dto';
import { ISalesReceiptsFilter } from './types/SaleReceipts.types'; import {
ISalesReceiptsFilter,
SaleReceiptMailOptsDTO,
} from './types/SaleReceipts.types';
import { AcceptType } from '@/constants/accept-type'; import { AcceptType } from '@/constants/accept-type';
import { Response } from 'express'; import { Response } from 'express';
import { SaleReceiptResponseDto } from './dtos/SaleReceiptResponse.dto'; import { SaleReceiptResponseDto } from './dtos/SaleReceiptResponse.dto';
@@ -87,7 +90,7 @@ export class SaleReceiptsController {
return this.saleReceiptApplication.createSaleReceipt(saleReceiptDTO); return this.saleReceiptApplication.createSaleReceipt(saleReceiptDTO);
} }
@Put(':id/mail') @Post(':id/mail')
@HttpCode(200) @HttpCode(200)
@ApiOperation({ summary: 'Send the sale receipt mail.' }) @ApiOperation({ summary: 'Send the sale receipt mail.' })
@ApiParam({ @ApiParam({
@@ -96,8 +99,11 @@ export class SaleReceiptsController {
type: Number, type: Number,
description: 'The sale receipt id', description: 'The sale receipt id',
}) })
sendSaleReceiptMail(@Param('id', ParseIntPipe) id: number) { sendSaleReceiptMail(
return this.saleReceiptApplication.getSaleReceiptMail(id); @Param('id', ParseIntPipe) id: number,
@Body() messageOpts: SaleReceiptMailOptsDTO,
) {
return this.saleReceiptApplication.sendSaleReceiptMail(id, messageOpts);
} }
@Get('state') @Get('state')

View File

@@ -1,24 +1,42 @@
import { Process, Processor } from '@nestjs/bull'; import { Process, Processor } from '@nestjs/bull';
import { Job } from 'bull'; import { Job } from 'bull';
import { SendSaleReceiptMailQueue } from '../constants'; import { Inject, Scope } from '@nestjs/common';
import { JOB_REF } from '@nestjs/bull';
import { SendSaleReceiptMailQueue, SendSaleReceiptMailJob } from '../constants';
import { SaleReceiptMailNotification } from '../commands/SaleReceiptMailNotification'; import { SaleReceiptMailNotification } from '../commands/SaleReceiptMailNotification';
import { SaleReceiptSendMailPayload } from '../types/SaleReceipts.types'; import { SaleReceiptSendMailPayload } from '../types/SaleReceipts.types';
import { ClsService } from 'nestjs-cls'; import { ClsService, UseCls } from 'nestjs-cls';
@Processor(SendSaleReceiptMailQueue) @Processor({
name: SendSaleReceiptMailQueue,
scope: Scope.REQUEST,
})
export class SendSaleReceiptMailProcess { export class SendSaleReceiptMailProcess {
constructor( constructor(
private readonly saleReceiptMailNotification: SaleReceiptMailNotification, private readonly saleReceiptMailNotification: SaleReceiptMailNotification,
private readonly clsService: ClsService, private readonly clsService: ClsService,
) {}
@Process(SendSaleReceiptMailQueue) @Inject(JOB_REF)
async handleSendMailJob(job: Job<SaleReceiptSendMailPayload>) { private readonly jobRef: Job<SaleReceiptSendMailPayload>,
const { messageOpts, saleReceiptId, organizationId, userId } = job.data; ) { }
@Process(SendSaleReceiptMailJob)
@UseCls()
async handleSendMailJob() {
const { messageOpts, saleReceiptId, organizationId, userId } =
this.jobRef.data;
this.clsService.set('organizationId', organizationId); this.clsService.set('organizationId', organizationId);
this.clsService.set('userId', userId); this.clsService.set('userId', userId);
await this.saleReceiptMailNotification.sendMail(saleReceiptId, messageOpts); try {
await this.saleReceiptMailNotification.sendMail(
saleReceiptId,
messageOpts,
);
} catch (error) {
console.error('Failed to process receipt mail job:', error);
throw error;
}
} }
} }

View File

@@ -21,9 +21,10 @@ export class SendInviteUserMailProcessor {
@Inject(JOB_REF) @Inject(JOB_REF)
private readonly jobRef: Job<SendInviteUserMailJobPayload>, private readonly jobRef: Job<SendInviteUserMailJobPayload>,
private readonly clsService: ClsService, private readonly clsService: ClsService,
) {} ) { }
@Process(SendInviteUserMailJob) @Process(SendInviteUserMailJob)
@UseCls()
async handleSendInviteMail() { async handleSendInviteMail() {
const { fromUser, invite, organizationId, userId } = this.jobRef.data; const { fromUser, invite, organizationId, userId } = this.jobRef.data;
@@ -33,7 +34,8 @@ export class SendInviteUserMailProcessor {
try { try {
await this.sendInviteUsersMailService.sendInviteMail(fromUser, invite); await this.sendInviteUsersMailService.sendInviteMail(fromUser, invite);
} catch (error) { } catch (error) {
console.log(error); console.error('Failed to process invite user mail job:', error);
throw error;
} }
} }
} }

View File

@@ -229,7 +229,7 @@ describe('Sale Invoices (e2e)', () => {
.send(requestSaleInvoiceBody()); .send(requestSaleInvoiceBody());
return request(app.getHttpServer()) return request(app.getHttpServer())
.put(`/sale-invoices/${response.body.id}/mail`) .post(`/sale-invoices/${response.body.id}/mail`)
.set('organization-id', orgainzationId) .set('organization-id', orgainzationId)
.set('Authorization', AuthorizationHeader) .set('Authorization', AuthorizationHeader)
.send({ .send({