mirror of
https://github.com/vernu/textbee.git
synced 2026-02-20 07:34:00 -05:00
feat(api): track sms status
This commit is contained in:
@@ -25,6 +25,7 @@ import {
|
||||
RetrieveSMSResponseDTO,
|
||||
SendBulkSMSInputDTO,
|
||||
SendSMSInputDTO,
|
||||
UpdateSMSStatusDTO,
|
||||
} from './gateway.dto'
|
||||
import { GatewayService } from './gateway.service'
|
||||
import { CanModifyDevice } from './guards/can-modify-device.guard'
|
||||
@@ -149,4 +150,15 @@ export class GatewayController {
|
||||
const result = await this.gatewayService.getMessages(deviceId, type, page, limit);
|
||||
return result;
|
||||
}
|
||||
|
||||
@ApiOperation({ summary: 'Update SMS status' })
|
||||
@UseGuards(AuthGuard, CanModifyDevice)
|
||||
@Patch('/devices/:id/sms-status')
|
||||
async updateSMSStatus(
|
||||
@Param('id') deviceId: string,
|
||||
@Body() dto: UpdateSMSStatusDTO,
|
||||
) {
|
||||
const data = await this.gatewayService.updateSMSStatus(deviceId, dto);
|
||||
return { data };
|
||||
}
|
||||
}
|
||||
|
||||
@@ -240,3 +240,62 @@ export class RetrieveSMSResponseDTO {
|
||||
})
|
||||
meta?: PaginationMetaDTO
|
||||
}
|
||||
|
||||
export class UpdateSMSStatusDTO {
|
||||
@ApiProperty({
|
||||
type: String,
|
||||
required: true,
|
||||
description: 'The ID of the SMS',
|
||||
})
|
||||
smsId: string
|
||||
|
||||
@ApiProperty({
|
||||
type: String,
|
||||
required: true,
|
||||
description: 'The ID of the SMS batch',
|
||||
})
|
||||
smsBatchId: string
|
||||
|
||||
@ApiProperty({
|
||||
type: String,
|
||||
required: true,
|
||||
description: 'The status of the SMS (sent, delivered, failed)',
|
||||
enum: ['sent', 'delivered', 'failed'],
|
||||
})
|
||||
status: string
|
||||
|
||||
@ApiProperty({
|
||||
type: Number,
|
||||
required: false,
|
||||
description: 'The time the message was sent (in milliseconds)',
|
||||
})
|
||||
sentAtInMillis?: number
|
||||
|
||||
@ApiProperty({
|
||||
type: Number,
|
||||
required: false,
|
||||
description: 'The time the message was delivered (in milliseconds)',
|
||||
})
|
||||
deliveredAtInMillis?: number
|
||||
|
||||
@ApiProperty({
|
||||
type: Number,
|
||||
required: false,
|
||||
description: 'The time the message failed (in milliseconds)',
|
||||
})
|
||||
failedAtInMillis?: number
|
||||
|
||||
@ApiProperty({
|
||||
type: String,
|
||||
required: false,
|
||||
description: 'Error code if the message failed',
|
||||
})
|
||||
errorCode?: string
|
||||
|
||||
@ApiProperty({
|
||||
type: String,
|
||||
required: false,
|
||||
description: 'Error message if the message failed',
|
||||
})
|
||||
errorMessage?: string
|
||||
}
|
||||
|
||||
@@ -9,6 +9,7 @@ import {
|
||||
RetrieveSMSDTO,
|
||||
SendBulkSMSInputDTO,
|
||||
SendSMSInputDTO,
|
||||
UpdateSMSStatusDTO,
|
||||
} from './gateway.dto'
|
||||
import { User } from '../users/schemas/user.schema'
|
||||
import { AuthService } from '../auth/auth.service'
|
||||
@@ -690,6 +691,98 @@ export class GatewayService {
|
||||
}
|
||||
}
|
||||
|
||||
async updateSMSStatus(deviceId: string, dto: UpdateSMSStatusDTO): Promise<any> {
|
||||
|
||||
const device = await this.deviceModel.findById(deviceId);
|
||||
|
||||
if (!device) {
|
||||
throw new HttpException(
|
||||
{
|
||||
success: false,
|
||||
error: 'Device not found',
|
||||
},
|
||||
HttpStatus.NOT_FOUND,
|
||||
);
|
||||
}
|
||||
|
||||
const sms = await this.smsModel.findById(dto.smsId);
|
||||
|
||||
if (!sms) {
|
||||
throw new HttpException(
|
||||
{
|
||||
success: false,
|
||||
error: 'SMS not found',
|
||||
},
|
||||
HttpStatus.NOT_FOUND,
|
||||
);
|
||||
}
|
||||
|
||||
// Verify the SMS belongs to this device
|
||||
if (sms.device.toString() !== deviceId) {
|
||||
throw new HttpException(
|
||||
{
|
||||
success: false,
|
||||
error: 'SMS does not belong to this device',
|
||||
},
|
||||
HttpStatus.FORBIDDEN,
|
||||
);
|
||||
}
|
||||
|
||||
// Normalize status to uppercase for comparison
|
||||
const normalizedStatus = dto.status.toUpperCase();
|
||||
|
||||
const updateData: any = {
|
||||
status: normalizedStatus, // Store normalized status
|
||||
};
|
||||
|
||||
// Update timestamps based on status
|
||||
if (normalizedStatus === 'SENT' && dto.sentAtInMillis) {
|
||||
updateData.sentAt = new Date(dto.sentAtInMillis);
|
||||
} else if (normalizedStatus === 'DELIVERED' && dto.deliveredAtInMillis) {
|
||||
updateData.deliveredAt = new Date(dto.deliveredAtInMillis);
|
||||
} else if (normalizedStatus === 'FAILED' && dto.failedAtInMillis) {
|
||||
updateData.failedAt = new Date(dto.failedAtInMillis);
|
||||
updateData.errorCode = dto.errorCode;
|
||||
updateData.errorMessage = dto.errorMessage || 'Unknown error';
|
||||
}
|
||||
|
||||
// Update the SMS
|
||||
await this.smsModel.findByIdAndUpdate(dto.smsId, { $set: updateData });
|
||||
|
||||
// Check if all SMS in batch have the same status, then update batch status
|
||||
if (dto.smsBatchId) {
|
||||
const smsBatch = await this.smsBatchModel.findById(dto.smsBatchId);
|
||||
if (smsBatch) {
|
||||
const allSmsInBatch = await this.smsModel.find({ smsBatch: dto.smsBatchId });
|
||||
|
||||
// Check if all SMS in batch have the same status (case insensitive)
|
||||
const allHaveSameStatus = allSmsInBatch.every(sms => sms.status.toLowerCase() === normalizedStatus);
|
||||
|
||||
if (allHaveSameStatus) {
|
||||
await this.smsBatchModel.findByIdAndUpdate(dto.smsBatchId, {
|
||||
$set: { status: normalizedStatus }
|
||||
});
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Trigger webhook event for SMS status update
|
||||
try {
|
||||
this.webhookService.deliverNotification({
|
||||
sms,
|
||||
user: device.user,
|
||||
event: WebhookEvent.SMS_STATUS_UPDATED,
|
||||
});
|
||||
} catch (error) {
|
||||
console.error('Failed to trigger webhook event:', error);
|
||||
}
|
||||
|
||||
return {
|
||||
success: true,
|
||||
message: 'SMS status updated successfully',
|
||||
};
|
||||
}
|
||||
|
||||
async getStatsForUser(user: User) {
|
||||
const devices = await this.deviceModel.find({ user: user._id })
|
||||
const apiKeys = await this.authService.getUserApiKeys(user)
|
||||
|
||||
@@ -41,6 +41,7 @@ export class SmsQueueService {
|
||||
batches.push(fcmMessages.slice(i, i + this.maxSmsBatchSize))
|
||||
}
|
||||
|
||||
let delayMultiplier = 1;
|
||||
for (const batch of batches) {
|
||||
await this.smsQueue.add(
|
||||
'send-sms',
|
||||
@@ -52,7 +53,7 @@ export class SmsQueueService {
|
||||
{
|
||||
priority: 1, // TODO: Make this dynamic based on users subscription plan
|
||||
attempts: 1,
|
||||
delay: 1000, // 1 second
|
||||
delay: 1000 * delayMultiplier++,
|
||||
backoff: {
|
||||
type: 'exponential',
|
||||
delay: 5000, // 5 seconds
|
||||
|
||||
@@ -32,8 +32,8 @@ export class SMSBatch {
|
||||
@Prop({ type: Number, default: 0 })
|
||||
failureCount: number
|
||||
|
||||
@Prop({ type: String, default: 'pending', enum: ['pending', 'processing', 'completed', 'partial_success', 'failed'] })
|
||||
status: string
|
||||
@Prop({ type: String, default: 'PENDING' })
|
||||
status: 'PENDING' | 'PROCESSING' | 'COMPLETED' | 'PARTIAL_SUCCESS' | 'FAILED'
|
||||
|
||||
@Prop({ type: String })
|
||||
error: string
|
||||
|
||||
@@ -49,15 +49,18 @@ export class SMS {
|
||||
|
||||
@Prop({ type: Date })
|
||||
failedAt: Date
|
||||
|
||||
@Prop({ type: Number, required: false })
|
||||
errorCode: number
|
||||
|
||||
@Prop({ type: String, required: false })
|
||||
errorMessage: string
|
||||
|
||||
// @Prop({ type: String })
|
||||
// failureReason: string
|
||||
|
||||
@Prop({ type: String, default: 'pending', enum: ['pending', 'sent', 'delivered', 'failed'] })
|
||||
status: string
|
||||
|
||||
@Prop({ type: String })
|
||||
error: string
|
||||
@Prop({ type: String, default: 'PENDING' })
|
||||
status: 'PENDING' | 'SENT' | 'DELIVERED' | 'FAILED'
|
||||
|
||||
// misc metadata for debugging
|
||||
@Prop({ type: Object })
|
||||
|
||||
@@ -1,3 +1,4 @@
|
||||
export enum WebhookEvent {
|
||||
MESSAGE_RECEIVED = 'MESSAGE_RECEIVED',
|
||||
SMS_STATUS_UPDATED = 'SMS_STATUS_UPDATED',
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user