Merge pull request #97 from vernu/improve-status-and-error-tracking

Improve sms status and error tracking logic
This commit is contained in:
Israel Abebe
2025-06-16 08:21:08 +03:00
committed by GitHub
5 changed files with 153 additions and 5 deletions

View File

@@ -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 {}

View File

@@ -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 }
});
}
}

View File

@@ -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 })

View File

@@ -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<SMS>;
let smsBatchModel: Model<SMSBatch>;
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>(SmsStatusUpdateTask);
smsModel = module.get<Model<SMS>>(getModelToken(SMS.name));
smsBatchModel = module.get<Model<SMSBatch>>(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',
},
},
);
});
});
});

View File

@@ -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<SMS>,
@InjectModel(SMSBatch.name) private smsBatchModel: Model<SMSBatch>,
) {}
/**
* 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);
}
}
}