chore: add send announce sms
This commit is contained in:
@@ -12,13 +12,14 @@ import { DanakServicesModule } from "../danak-services/danak-services.module";
|
|||||||
import { UsersModule } from "../users/users.module";
|
import { UsersModule } from "../users/users.module";
|
||||||
import { UserAnnouncement } from "./entities/user-announcement.entity";
|
import { UserAnnouncement } from "./entities/user-announcement.entity";
|
||||||
import { UserAnnouncementRepository } from "./repositories/user-announcement.repository";
|
import { UserAnnouncementRepository } from "./repositories/user-announcement.repository";
|
||||||
|
import { NotificationModule } from "../notifications/notifications.module";
|
||||||
@Module({
|
@Module({
|
||||||
imports: [
|
imports: [
|
||||||
TypeOrmModule.forFeature([Announcement, UserAnnouncement]),
|
TypeOrmModule.forFeature([Announcement, UserAnnouncement]),
|
||||||
BullModule.registerQueue({ name: ANNOUNCEMENT.ANNOUNCEMENT_QUEUE_NAME }),
|
BullModule.registerQueue({ name: ANNOUNCEMENT.ANNOUNCEMENT_QUEUE_NAME }),
|
||||||
DanakServicesModule,
|
DanakServicesModule,
|
||||||
UsersModule,
|
UsersModule,
|
||||||
|
NotificationModule,
|
||||||
],
|
],
|
||||||
providers: [AnnouncementService, AnnouncementRepository, UserAnnouncementRepository, AnnouncementProcessor],
|
providers: [AnnouncementService, AnnouncementRepository, UserAnnouncementRepository, AnnouncementProcessor],
|
||||||
controllers: [AnnouncementController],
|
controllers: [AnnouncementController],
|
||||||
|
|||||||
@@ -4,17 +4,25 @@ import { Job } from "bullmq";
|
|||||||
import { DataSource } from "typeorm";
|
import { DataSource } from "typeorm";
|
||||||
|
|
||||||
import { WorkerProcessor } from "../../../common/queues/worker.processor";
|
import { WorkerProcessor } from "../../../common/queues/worker.processor";
|
||||||
|
import { IAnnouncementNotificationData } from "../../notifications/interfaces/ISendNotificationData";
|
||||||
|
import { NotificationQueue } from "../../notifications/queue/notification.queue";
|
||||||
import { SubscriptionPlan } from "../../subscriptions/entities/subscription.entity";
|
import { SubscriptionPlan } from "../../subscriptions/entities/subscription.entity";
|
||||||
import { UserSubscription } from "../../subscriptions/entities/user-subscription.entity";
|
import { UserSubscription } from "../../subscriptions/entities/user-subscription.entity";
|
||||||
import { User } from "../../users/entities/user.entity";
|
import { User } from "../../users/entities/user.entity";
|
||||||
|
import { UsersService } from "../../users/providers/users.service";
|
||||||
import { ANNOUNCEMENT } from "../constants";
|
import { ANNOUNCEMENT } from "../constants";
|
||||||
|
import { Announcement } from "../entities/announcement.entity";
|
||||||
import { UserAnnouncement } from "../entities/user-announcement.entity";
|
import { UserAnnouncement } from "../entities/user-announcement.entity";
|
||||||
import { ISendAnnouncement } from "../interfaces/ISendAnnouncement";
|
import { ISendAnnouncement } from "../interfaces/ISendAnnouncement";
|
||||||
|
|
||||||
@Processor(ANNOUNCEMENT.ANNOUNCEMENT_QUEUE_NAME)
|
@Processor(ANNOUNCEMENT.ANNOUNCEMENT_QUEUE_NAME)
|
||||||
export class AnnouncementProcessor extends WorkerProcessor {
|
export class AnnouncementProcessor extends WorkerProcessor {
|
||||||
protected readonly logger = new Logger(AnnouncementProcessor.name);
|
protected readonly logger = new Logger(AnnouncementProcessor.name);
|
||||||
constructor(private readonly dataSource: DataSource) {
|
constructor(
|
||||||
|
private readonly dataSource: DataSource,
|
||||||
|
private readonly notificationQueue: NotificationQueue,
|
||||||
|
private readonly usersService: UsersService,
|
||||||
|
) {
|
||||||
super();
|
super();
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -23,7 +31,7 @@ export class AnnouncementProcessor extends WorkerProcessor {
|
|||||||
|
|
||||||
switch (job.name) {
|
switch (job.name) {
|
||||||
case ANNOUNCEMENT.ANNOUNCEMENT_SEND_JOB_NAME:
|
case ANNOUNCEMENT.ANNOUNCEMENT_SEND_JOB_NAME:
|
||||||
return this.sendAnnouncement(job, token);
|
return await this.sendAnnouncement(job, token);
|
||||||
case ANNOUNCEMENT.ANNOUNCEMENT_PUBLISH_JOB_NAME:
|
case ANNOUNCEMENT.ANNOUNCEMENT_PUBLISH_JOB_NAME:
|
||||||
return this.publishAnnouncement(job, token);
|
return this.publishAnnouncement(job, token);
|
||||||
default:
|
default:
|
||||||
@@ -36,11 +44,12 @@ export class AnnouncementProcessor extends WorkerProcessor {
|
|||||||
this.logger.log(`Sending announcement: ${job.data.announcementId} to users. ${token}`);
|
this.logger.log(`Sending announcement: ${job.data.announcementId} to users. ${token}`);
|
||||||
|
|
||||||
const queryRunner = this.dataSource.createQueryRunner();
|
const queryRunner = this.dataSource.createQueryRunner();
|
||||||
await queryRunner.connect();
|
|
||||||
await queryRunner.startTransaction();
|
|
||||||
const data = job.data;
|
const data = job.data;
|
||||||
|
|
||||||
try {
|
try {
|
||||||
|
await queryRunner.connect();
|
||||||
|
await queryRunner.startTransaction();
|
||||||
// Create a query builder to fetch unique users
|
// Create a query builder to fetch unique users
|
||||||
const userQueryBuilder = queryRunner.manager.createQueryBuilder(User, "user").select("user.id").distinct(true);
|
const userQueryBuilder = queryRunner.manager.createQueryBuilder(User, "user").select("user.id").distinct(true);
|
||||||
|
|
||||||
@@ -89,8 +98,26 @@ export class AnnouncementProcessor extends WorkerProcessor {
|
|||||||
|
|
||||||
await queryRunner.manager.insert(UserAnnouncement, userAnnouncements);
|
await queryRunner.manager.insert(UserAnnouncement, userAnnouncements);
|
||||||
|
|
||||||
|
// Fetch announcement details for notification
|
||||||
|
const announcement = await queryRunner.manager.findOne(Announcement, { where: { id: job.data.announcementId } });
|
||||||
|
if (!announcement) throw new Error("Announcement not found");
|
||||||
|
|
||||||
|
// Send notification to each user
|
||||||
|
for (const userId of users) {
|
||||||
|
const user = await this.usersService.findOneByIdWithQueryRunner(userId, queryRunner);
|
||||||
|
const notifData: IAnnouncementNotificationData = {
|
||||||
|
userPhone: user.phone,
|
||||||
|
userEmail: user.email,
|
||||||
|
title: announcement.title,
|
||||||
|
description: announcement.content,
|
||||||
|
date: announcement.publishAt ?? new Date(),
|
||||||
|
};
|
||||||
|
await this.notificationQueue.addAnnouncementNotification(userId, notifData);
|
||||||
|
}
|
||||||
|
|
||||||
this.logger.log(`Successfully sent announcement ${announcementId} to ${users.length} users`);
|
this.logger.log(`Successfully sent announcement ${announcementId} to ${users.length} users`);
|
||||||
await queryRunner.commitTransaction();
|
await queryRunner.commitTransaction();
|
||||||
|
return true;
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
this.logger.error(`Failed to send announcement:`, error);
|
this.logger.error(`Failed to send announcement:`, error);
|
||||||
await queryRunner.rollbackTransaction();
|
await queryRunner.rollbackTransaction();
|
||||||
|
|||||||
@@ -7,4 +7,9 @@ export const NOTIFICATION = Object.freeze({
|
|||||||
SEND_NOTIFICATION_JOB_ATTEMPTS: 3, // retry 3 times
|
SEND_NOTIFICATION_JOB_ATTEMPTS: 3, // retry 3 times
|
||||||
SEND_NOTIFICATION_JOB_BACKOFF: 5 * 1000, // retry after 5 seconds
|
SEND_NOTIFICATION_JOB_BACKOFF: 5 * 1000, // retry after 5 seconds
|
||||||
SEND_NOTIFICATION_JOB_TIMEOUT: 10000, // timeout after 10 seconds
|
SEND_NOTIFICATION_JOB_TIMEOUT: 10000, // timeout after 10 seconds
|
||||||
|
|
||||||
|
SEND_NOTIFICATION_JOB_CONCURRENCY: 2,
|
||||||
|
SEND_NOTIFICATION_JOB_LOCK_DURATION: 30000,
|
||||||
|
SEND_NOTIFICATION_JOB_STALLED_INTERVAL: 30000,
|
||||||
|
SEND_NOTIFICATION_JOB_MAX_STALLED_COUNT: 2,
|
||||||
});
|
});
|
||||||
|
|||||||
@@ -42,10 +42,10 @@ type NotificationJobData = {
|
|||||||
};
|
};
|
||||||
|
|
||||||
@Processor(NOTIFICATION.QUEUE_NAME, {
|
@Processor(NOTIFICATION.QUEUE_NAME, {
|
||||||
concurrency: 5,
|
concurrency: NOTIFICATION.SEND_NOTIFICATION_JOB_CONCURRENCY,
|
||||||
lockDuration: 30000, // 30 seconds lock duration
|
lockDuration: NOTIFICATION.SEND_NOTIFICATION_JOB_LOCK_DURATION,
|
||||||
stalledInterval: 30000, // Check for stalled jobs every 30 seconds
|
stalledInterval: NOTIFICATION.SEND_NOTIFICATION_JOB_STALLED_INTERVAL,
|
||||||
maxStalledCount: 2, // Allow 2 stalls before failing
|
maxStalledCount: NOTIFICATION.SEND_NOTIFICATION_JOB_MAX_STALLED_COUNT,
|
||||||
})
|
})
|
||||||
export class NotificationProcessor extends WorkerProcessor {
|
export class NotificationProcessor extends WorkerProcessor {
|
||||||
protected readonly logger = new Logger(NotificationProcessor.name);
|
protected readonly logger = new Logger(NotificationProcessor.name);
|
||||||
@@ -60,136 +60,105 @@ export class NotificationProcessor extends WorkerProcessor {
|
|||||||
|
|
||||||
async process(job: Job<NotificationJobData>, token?: string) {
|
async process(job: Job<NotificationJobData>, token?: string) {
|
||||||
this.logger.log(`Processing notification job: ${job.id} ${token ? `with token: ${token}` : ""}`);
|
this.logger.log(`Processing notification job: ${job.id} ${token ? `with token: ${token}` : ""}`);
|
||||||
|
|
||||||
const queryRunner = this.dataSource.createQueryRunner();
|
const queryRunner = this.dataSource.createQueryRunner();
|
||||||
|
let heartbeat: NodeJS.Timeout | undefined;
|
||||||
|
|
||||||
|
//
|
||||||
|
|
||||||
try {
|
try {
|
||||||
await queryRunner.connect();
|
await queryRunner.connect();
|
||||||
await queryRunner.startTransaction();
|
await queryRunner.startTransaction();
|
||||||
|
heartbeat = setInterval(() => job.updateProgress(50), 10000);
|
||||||
|
|
||||||
const heartbeat = setInterval(() => {
|
//
|
||||||
job.updateProgress(50);
|
|
||||||
}, 10000); // Update progress every 10 seconds
|
|
||||||
|
|
||||||
const { type, recipientId, data } = job.data;
|
const { type, recipientId, data } = job.data;
|
||||||
|
switch (type) {
|
||||||
try {
|
// Admin Notifications
|
||||||
switch (type) {
|
case NotifType.NEW_BLOG_COMMENT:
|
||||||
// Admin Notifications
|
await this.notificationsService.createNewBlogCommentNotification(recipientId, data as IBlogCommentNotificationData, queryRunner);
|
||||||
case NotifType.NEW_BLOG_COMMENT:
|
break;
|
||||||
await this.notificationsService.createNewBlogCommentNotification(
|
case NotifType.NEW_SERVICE_REVIEW:
|
||||||
recipientId,
|
await this.notificationsService.createNewServiceReviewNotification(
|
||||||
data as IBlogCommentNotificationData,
|
recipientId,
|
||||||
queryRunner,
|
data as IServiceReviewNotificationData,
|
||||||
);
|
queryRunner,
|
||||||
break;
|
);
|
||||||
case NotifType.NEW_SERVICE_REVIEW:
|
break;
|
||||||
await this.notificationsService.createNewServiceReviewNotification(
|
case NotifType.NEW_CUSTOMER:
|
||||||
recipientId,
|
await this.notificationsService.createNewCustomerNotification(recipientId, data as INewCustomerNotificationData, queryRunner);
|
||||||
data as IServiceReviewNotificationData,
|
break;
|
||||||
queryRunner,
|
case NotifType.NEW_SUBSCRIPTION:
|
||||||
);
|
await this.notificationsService.createNewSubscriptionNotification(
|
||||||
break;
|
recipientId,
|
||||||
case NotifType.NEW_CUSTOMER:
|
data as INewSubscriptionNotificationData,
|
||||||
await this.notificationsService.createNewCustomerNotification(recipientId, data as INewCustomerNotificationData, queryRunner);
|
queryRunner,
|
||||||
break;
|
);
|
||||||
case NotifType.NEW_SUBSCRIPTION:
|
break;
|
||||||
await this.notificationsService.createNewSubscriptionNotification(
|
case NotifType.NEW_CRITICISM:
|
||||||
recipientId,
|
await this.notificationsService.createNewCriticismNotification(recipientId, data as INewCriticismNotificationData, queryRunner);
|
||||||
data as INewSubscriptionNotificationData,
|
break;
|
||||||
queryRunner,
|
case NotifType.NEW_TICKET:
|
||||||
);
|
await this.notificationsService.createNewTicketGlobalNotification(recipientId, data as INewTicketNotificationData, queryRunner);
|
||||||
break;
|
break;
|
||||||
case NotifType.NEW_CRITICISM:
|
// User Notifications
|
||||||
await this.notificationsService.createNewCriticismNotification(recipientId, data as INewCriticismNotificationData, queryRunner);
|
case NotifType.USER_LOGIN:
|
||||||
break;
|
await this.notificationsService.createLoginNotification(recipientId, data);
|
||||||
case NotifType.NEW_TICKET:
|
break;
|
||||||
await this.notificationsService.createNewTicketGlobalNotification(recipientId, data as INewTicketNotificationData, queryRunner);
|
case NotifType.ANNOUNCEMENT:
|
||||||
break;
|
await this.notificationsService.createAnnouncementNotification(recipientId, data as IAnnouncementNotificationData, queryRunner);
|
||||||
|
break;
|
||||||
// User Notifications
|
// Wallet Notifications
|
||||||
case NotifType.USER_LOGIN:
|
case NotifType.WALLET_CHARGE:
|
||||||
await this.notificationsService.createLoginNotification(recipientId, data);
|
await this.notificationsService.createWalletChargeNotification(recipientId, data as IWalletNotificationData, queryRunner);
|
||||||
break;
|
break;
|
||||||
case NotifType.ANNOUNCEMENT:
|
case NotifType.WALLET_DEDUCTION:
|
||||||
await this.notificationsService.createAnnouncementNotification(recipientId, data as IAnnouncementNotificationData, queryRunner);
|
await this.notificationsService.createWalletDeductionNotification(recipientId, data as IWalletNotificationData, queryRunner);
|
||||||
break;
|
break;
|
||||||
|
// Ticket Notifications
|
||||||
// Wallet Notifications
|
case NotifType.ANSWER_TICKET:
|
||||||
case NotifType.WALLET_CHARGE:
|
await this.notificationsService.createAnswerTicketNotification(recipientId, data as ITicketNotificationData, queryRunner);
|
||||||
await this.notificationsService.createWalletChargeNotification(recipientId, data as IWalletNotificationData, queryRunner);
|
break;
|
||||||
break;
|
case NotifType.CREATE_TICKET:
|
||||||
case NotifType.WALLET_DEDUCTION:
|
await this.notificationsService.createTicketNotification(recipientId, data as ITicketNotificationData, queryRunner);
|
||||||
await this.notificationsService.createWalletDeductionNotification(recipientId, data as IWalletNotificationData, queryRunner);
|
break;
|
||||||
break;
|
case NotifType.ASSIGN_TICKET:
|
||||||
|
await this.notificationsService.createAssignTicketNotificationForAdmin(recipientId, data as ITicketNotificationData, queryRunner);
|
||||||
// Ticket Notifications
|
break;
|
||||||
case NotifType.ANSWER_TICKET:
|
// Invoice Notifications
|
||||||
await this.notificationsService.createAnswerTicketNotification(recipientId, data as ITicketNotificationData, queryRunner);
|
case NotifType.CREATE_INVOICE:
|
||||||
break;
|
await this.notificationsService.createInvoiceCreationNotification(recipientId, data as IInvoiceNotificationData, queryRunner);
|
||||||
case NotifType.CREATE_TICKET:
|
break;
|
||||||
await this.notificationsService.createTicketNotification(recipientId, data as ITicketNotificationData, queryRunner);
|
case NotifType.BILL_INVOICE_REMINDER:
|
||||||
break;
|
await this.notificationsService.createBillInvoiceReminderNotification(recipientId, data as IInvoiceNotificationData, queryRunner);
|
||||||
case NotifType.ASSIGN_TICKET:
|
break;
|
||||||
await this.notificationsService.createAssignTicketNotificationForAdmin(
|
case NotifType.BILL_INVOICE:
|
||||||
recipientId,
|
await this.notificationsService.createBillInvoiceNotification(recipientId, data as IInvoiceNotificationData, queryRunner);
|
||||||
data as ITicketNotificationData,
|
break;
|
||||||
queryRunner,
|
case NotifType.APPROVE_INVOICE:
|
||||||
);
|
await this.notificationsService.createApprovedInvoiceNotification(recipientId, data as IInvoiceNotificationData, queryRunner);
|
||||||
break;
|
break;
|
||||||
|
case NotifType.INVOICE_OVERDUE:
|
||||||
// Invoice Notifications
|
await this.notificationsService.createInvoiceOverdueNotification(recipientId, data as IInvoiceNotificationData, queryRunner);
|
||||||
case NotifType.CREATE_INVOICE:
|
break;
|
||||||
await this.notificationsService.createInvoiceCreationNotification(recipientId, data as IInvoiceNotificationData, queryRunner);
|
case NotifType.RECURRING_INVOICE:
|
||||||
break;
|
await this.notificationsService.createRecurringInvoiceNotification(recipientId, data as IInvoiceNotificationData, queryRunner);
|
||||||
case NotifType.BILL_INVOICE_REMINDER:
|
break;
|
||||||
await this.notificationsService.createBillInvoiceReminderNotification(
|
// Service Notifications
|
||||||
recipientId,
|
case NotifType.BLOCK_SERVICE:
|
||||||
data as IInvoiceNotificationData,
|
await this.notificationsService.createBlockServiceNotification(recipientId, data as ISubscriptionNotificationData, queryRunner);
|
||||||
queryRunner,
|
break;
|
||||||
);
|
// Payment Notifications
|
||||||
break;
|
case NotifType.PAYMENT_REMINDER:
|
||||||
case NotifType.BILL_INVOICE:
|
await this.notificationsService.createPaymentReminderNotification(recipientId, data as IPaymentNotificationData, queryRunner);
|
||||||
await this.notificationsService.createBillInvoiceNotification(recipientId, data as IInvoiceNotificationData, queryRunner);
|
break;
|
||||||
break;
|
case NotifType.PAYMENT_CANCELLATION:
|
||||||
case NotifType.APPROVE_INVOICE:
|
await this.notificationsService.createPaymentCancellationNotification(recipientId, data as IPaymentNotificationData, queryRunner);
|
||||||
await this.notificationsService.createApprovedInvoiceNotification(recipientId, data as IInvoiceNotificationData, queryRunner);
|
break;
|
||||||
break;
|
default:
|
||||||
case NotifType.INVOICE_OVERDUE:
|
this.logger.warn(`Unknown notification type: ${type}`);
|
||||||
await this.notificationsService.createInvoiceOverdueNotification(recipientId, data as IInvoiceNotificationData, queryRunner);
|
|
||||||
break;
|
|
||||||
case NotifType.RECURRING_INVOICE:
|
|
||||||
await this.notificationsService.createRecurringInvoiceNotification(recipientId, data as IInvoiceNotificationData, queryRunner);
|
|
||||||
break;
|
|
||||||
|
|
||||||
// Service Notifications
|
|
||||||
case NotifType.BLOCK_SERVICE:
|
|
||||||
await this.notificationsService.createBlockServiceNotification(recipientId, data as ISubscriptionNotificationData, queryRunner);
|
|
||||||
break;
|
|
||||||
|
|
||||||
// Payment Notifications
|
|
||||||
case NotifType.PAYMENT_REMINDER:
|
|
||||||
await this.notificationsService.createPaymentReminderNotification(recipientId, data as IPaymentNotificationData, queryRunner);
|
|
||||||
break;
|
|
||||||
case NotifType.PAYMENT_CANCELLATION:
|
|
||||||
await this.notificationsService.createPaymentCancellationNotification(
|
|
||||||
recipientId,
|
|
||||||
data as IPaymentNotificationData,
|
|
||||||
queryRunner,
|
|
||||||
);
|
|
||||||
break;
|
|
||||||
|
|
||||||
default:
|
|
||||||
this.logger.warn(`Unknown notification type: ${type}`);
|
|
||||||
}
|
|
||||||
|
|
||||||
await queryRunner.commitTransaction();
|
|
||||||
clearInterval(heartbeat);
|
|
||||||
return true;
|
|
||||||
} catch (error) {
|
|
||||||
clearInterval(heartbeat);
|
|
||||||
throw error;
|
|
||||||
}
|
}
|
||||||
|
await queryRunner.commitTransaction();
|
||||||
|
return true;
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
this.logger.error(
|
this.logger.error(
|
||||||
`Failed to process notification: ${error instanceof Error ? error.message : "Unknown error"}`,
|
`Failed to process notification: ${error instanceof Error ? error.message : "Unknown error"}`,
|
||||||
@@ -198,6 +167,7 @@ export class NotificationProcessor extends WorkerProcessor {
|
|||||||
await queryRunner.rollbackTransaction();
|
await queryRunner.rollbackTransaction();
|
||||||
throw error;
|
throw error;
|
||||||
} finally {
|
} finally {
|
||||||
|
if (heartbeat) clearInterval(heartbeat);
|
||||||
await queryRunner.release();
|
await queryRunner.release();
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -87,7 +87,6 @@ export class NotificationQueue {
|
|||||||
async addWalletChargeNotification(recipientId: string, data: IWalletNotificationData) {
|
async addWalletChargeNotification(recipientId: string, data: IWalletNotificationData) {
|
||||||
await this.addNotificationJob(NotifType.WALLET_CHARGE, recipientId, data);
|
await this.addNotificationJob(NotifType.WALLET_CHARGE, recipientId, data);
|
||||||
}
|
}
|
||||||
//TODO:USER THIS
|
|
||||||
|
|
||||||
async addWalletDeductionNotification(recipientId: string, data: IWalletNotificationData) {
|
async addWalletDeductionNotification(recipientId: string, data: IWalletNotificationData) {
|
||||||
await this.addNotificationJob(NotifType.WALLET_DEDUCTION, recipientId, data);
|
await this.addNotificationJob(NotifType.WALLET_DEDUCTION, recipientId, data);
|
||||||
|
|||||||
Reference in New Issue
Block a user