From b0ce11e3926a303033b1b4bca6d98d0390e23e10 Mon Sep 17 00:00:00 2001 From: isra el Date: Mon, 16 Jun 2025 08:17:16 +0300 Subject: [PATCH] chore(api): improve sms status and error tracking logic --- api/src/gateway/gateway.module.ts | 3 +- api/src/gateway/gateway.service.ts | 4 +- api/src/gateway/schemas/sms.schema.ts | 6 +- .../tasks/sms-status-update.task.spec.ts | 77 +++++++++++++++++++ .../gateway/tasks/sms-status-update.task.ts | 68 ++++++++++++++++ 5 files changed, 153 insertions(+), 5 deletions(-) create mode 100644 api/src/gateway/tasks/sms-status-update.task.spec.ts create mode 100644 api/src/gateway/tasks/sms-status-update.task.ts diff --git a/api/src/gateway/gateway.module.ts b/api/src/gateway/gateway.module.ts index f5cc607..7a38af2 100644 --- a/api/src/gateway/gateway.module.ts +++ b/api/src/gateway/gateway.module.ts @@ -13,6 +13,7 @@ import { BullModule } from '@nestjs/bull' import { ConfigModule } from '@nestjs/config' import { SmsQueueService } from './queue/sms-queue.service' import { SmsQueueProcessor } from './queue/sms-queue.processor' +import { SmsStatusUpdateTask } from './tasks/sms-status-update.task' @Module({ imports: [ @@ -49,7 +50,7 @@ import { SmsQueueProcessor } from './queue/sms-queue.processor' ConfigModule, ], controllers: [GatewayController], - providers: [GatewayService, SmsQueueService, SmsQueueProcessor], + providers: [GatewayService, SmsQueueService, SmsQueueProcessor, SmsStatusUpdateTask], exports: [MongooseModule, GatewayService, SmsQueueService], }) export class GatewayModule {} diff --git a/api/src/gateway/gateway.service.ts b/api/src/gateway/gateway.service.ts index dba2507..33ec036 100644 --- a/api/src/gateway/gateway.service.ts +++ b/api/src/gateway/gateway.service.ts @@ -547,6 +547,7 @@ export class GatewayService { device: device._id, message: dto.message, type: SMSType.RECEIVED, + status: 'received', sender: dto.sender, receivedAt, }) @@ -763,8 +764,9 @@ export class GatewayService { const allHaveSameStatus = allSmsInBatch.every(sms => sms.status.toLowerCase() === normalizedStatus); if (allHaveSameStatus) { + const smsBatchStatus = normalizedStatus === 'failed' ? 'failed' : 'completed'; await this.smsBatchModel.findByIdAndUpdate(dto.smsBatchId, { - $set: { status: normalizedStatus } + $set: { status: smsBatchStatus } }); } } diff --git a/api/src/gateway/schemas/sms.schema.ts b/api/src/gateway/schemas/sms.schema.ts index 10d8303..5f2d88f 100644 --- a/api/src/gateway/schemas/sms.schema.ts +++ b/api/src/gateway/schemas/sms.schema.ts @@ -50,8 +50,8 @@ export class SMS { @Prop({ type: Date }) failedAt: Date - @Prop({ type: Number, required: false }) - errorCode: number + @Prop({ type: String, required: false }) + errorCode: string @Prop({ type: String, required: false }) errorMessage: string @@ -60,7 +60,7 @@ export class SMS { // failureReason: string @Prop({ type: String, default: 'pending' }) - status: 'pending' | 'sent' | 'delivered' | 'failed' + status: 'pending' | 'sent' | 'delivered' | 'failed' | 'unknown' | 'received' // misc metadata for debugging @Prop({ type: Object }) diff --git a/api/src/gateway/tasks/sms-status-update.task.spec.ts b/api/src/gateway/tasks/sms-status-update.task.spec.ts new file mode 100644 index 0000000..a39cf18 --- /dev/null +++ b/api/src/gateway/tasks/sms-status-update.task.spec.ts @@ -0,0 +1,77 @@ +import { Test, TestingModule } from '@nestjs/testing'; +import { getModelToken } from '@nestjs/mongoose'; +import { SmsStatusUpdateTask } from './sms-status-update.task'; +import { SMS } from '../schemas/sms.schema'; +import { SMSBatch } from '../schemas/sms-batch.schema'; +import { Model } from 'mongoose'; + +describe('SmsStatusUpdateTask', () => { + let task: SmsStatusUpdateTask; + let smsModel: Model; + let smsBatchModel: Model; + + beforeEach(async () => { + const module: TestingModule = await Test.createTestingModule({ + providers: [ + SmsStatusUpdateTask, + { + provide: getModelToken(SMS.name), + useValue: { + updateMany: jest.fn().mockResolvedValue({ modifiedCount: 5 }), + }, + }, + { + provide: getModelToken(SMSBatch.name), + useValue: { + updateMany: jest.fn().mockResolvedValue({ modifiedCount: 2 }), + }, + }, + ], + }).compile(); + + task = module.get(SmsStatusUpdateTask); + smsModel = module.get>(getModelToken(SMS.name)); + smsBatchModel = module.get>(getModelToken(SMSBatch.name)); + }); + + it('should be defined', () => { + expect(task).toBeDefined(); + }); + + describe('handlePendingSmsTimeout', () => { + it('should update stale pending SMS messages to unknown status', async () => { + jest.spyOn(smsModel, 'updateMany'); + jest.spyOn(smsBatchModel, 'updateMany'); + + await task.handlePendingSmsTimeout(); + + // Check that SMS model was updated with correct query + expect(smsModel.updateMany).toHaveBeenCalledWith( + expect.objectContaining({ + status: 'pending', + requestedAt: expect.any(Object), + }), + { + $set: { + status: 'unknown', + errorMessage: 'Status update timeout - no response received after 20 minutes', + }, + }, + ); + + // Check that SMSBatch model was updated with correct query + expect(smsBatchModel.updateMany).toHaveBeenCalledWith( + expect.objectContaining({ + status: 'pending', + createdAt: expect.any(Object), + }), + { + $set: { + status: 'unknown', + error: 'Status update timeout - no response received after 20 minutes', + }, + }, + ); + }); + }); +}); \ No newline at end of file diff --git a/api/src/gateway/tasks/sms-status-update.task.ts b/api/src/gateway/tasks/sms-status-update.task.ts new file mode 100644 index 0000000..327eac8 --- /dev/null +++ b/api/src/gateway/tasks/sms-status-update.task.ts @@ -0,0 +1,68 @@ +import { Injectable, Logger } from '@nestjs/common'; +import { Cron, CronExpression } from '@nestjs/schedule'; +import { InjectModel } from '@nestjs/mongoose'; +import { Model } from 'mongoose'; +import { SMS } from '../schemas/sms.schema'; +import { SMSBatch } from '../schemas/sms-batch.schema'; + + +@Injectable() +export class SmsStatusUpdateTask { + private readonly logger = new Logger(SmsStatusUpdateTask.name); + + constructor( + @InjectModel(SMS.name) private smsModel: Model, + @InjectModel(SMSBatch.name) private smsBatchModel: Model, + ) {} + + /** + * Cron job that runs every 5 minutes to update the status of SMS messages + * that have been pending for more than 20 minutes without any status updates. + */ + @Cron(CronExpression.EVERY_5_MINUTES) + async handlePendingSmsTimeout() { + this.logger.log('Running cron job to update stale pending SMS messages'); + + const twentyMinutesAgo = new Date(); + twentyMinutesAgo.setMinutes(twentyMinutesAgo.getMinutes() - 20); + + try { + + const result = await this.smsModel.updateMany( + { + status: 'pending', + requestedAt: { $lt: twentyMinutesAgo }, + }, + { + $set: { + status: 'unknown', + errorMessage: 'Status update timeout - no response received after 20 minutes' + } + } + ); + + + + this.logger.log(`Updated ${result.modifiedCount} SMS messages from 'pending' to 'unknown' status`); + + const batchResult = await this.smsBatchModel.updateMany( + { + status: 'pending', + createdAt: { $lt: twentyMinutesAgo } + }, + { + $set: { + status: 'unknown', + error: 'Status update timeout - no response received after 20 minutes' + } + } + ); + + + + this.logger.log(`Updated ${batchResult.modifiedCount} SMS batches from 'pending' to 'unknown' status`); + } catch (error) { + this.logger.error('Error updating stale pending SMS messages', error); + } + } +} \ No newline at end of file