diff --git a/api/src/billing/billing.service.ts b/api/src/billing/billing.service.ts index f4dbc50..1dde1c4 100644 --- a/api/src/billing/billing.service.ts +++ b/api/src/billing/billing.service.ts @@ -16,7 +16,6 @@ import { User, UserDocument } from '../users/schemas/user.schema' import { CheckoutResponseDTO, PlanDTO } from './billing.dto' import { SMSDocument } from '../gateway/schemas/sms.schema' import { SMS } from '../gateway/schemas/sms.schema' -import { Device, DeviceDocument } from '../gateway/schemas/device.schema' import { validateEvent } from '@polar-sh/sdk/webhooks' import { PolarWebhookPayload, @@ -41,7 +40,6 @@ export class BillingService { private subscriptionModel: Model, @InjectModel(User.name) private userModel: Model, @InjectModel(SMS.name) private smsModel: Model, - @InjectModel(Device.name) private deviceModel: Model, @InjectModel(PolarWebhookPayload.name) private polarWebhookPayloadModel: Model, @InjectModel(CheckoutSession.name) @@ -69,17 +67,13 @@ export class BillingService { }) .populate('plan') - // Get user's devices and usage data - const userDevices = await this.deviceModel.find({ user: user._id }, '_id') - const deviceIds = userDevices.map((d) => d._id) - const processedSmsToday = await this.smsModel.countDocuments({ - device: { $in: deviceIds }, + user: user._id, createdAt: { $gte: new Date(new Date().setHours(0, 0, 0, 0)) }, }) const processedSmsLastMonth = await this.smsModel.countDocuments({ - device: { $in: deviceIds }, + user: user._id, createdAt: { $gte: new Date(new Date().setMonth(new Date().getMonth() - 1)), }, @@ -568,16 +562,12 @@ export class BillingService { let hasReachedLimit = false let message = '' - // Get user's devices and then count SMS - const userDevices = await this.deviceModel.find({ user: user._id }, '_id') - const deviceIds = userDevices.map((d) => d._id) - const processedSmsToday = await this.smsModel.countDocuments({ - device: { $in: deviceIds }, + user: user._id, createdAt: { $gte: new Date(new Date().setHours(0, 0, 0, 0)) }, }) const processedSmsLastMonth = await this.smsModel.countDocuments({ - device: { $in: deviceIds }, + user: user._id, createdAt: { $gte: new Date(new Date().setMonth(new Date().getMonth() - 1)), }, @@ -724,19 +714,13 @@ export class BillingService { const effectiveLimits = this.getEffectiveLimits(subscription, plan) - // First get all devices belonging to the user - const userDevices = await this.deviceModel - .find({ user: new Types.ObjectId(userId) }) - .select('_id') - const deviceIds = userDevices.map((device) => device._id) - const processedSmsToday = await this.smsModel.countDocuments({ - device: { $in: deviceIds }, + user: new Types.ObjectId(userId), createdAt: { $gte: new Date(new Date().setHours(0, 0, 0, 0)) }, }) const processedSmsLastMonth = await this.smsModel.countDocuments({ - device: { $in: deviceIds }, + user: new Types.ObjectId(userId), createdAt: { $gte: new Date(new Date().setMonth(new Date().getMonth() - 1)), }, diff --git a/api/src/gateway/gateway.module.ts b/api/src/gateway/gateway.module.ts index 7c38850..96432bf 100644 --- a/api/src/gateway/gateway.module.ts +++ b/api/src/gateway/gateway.module.ts @@ -1,6 +1,7 @@ import { forwardRef, Module } from '@nestjs/common' import { MongooseModule } from '@nestjs/mongoose' import { Device, DeviceSchema } from './schemas/device.schema' +import { DeviceTombstone, DeviceTombstoneSchema } from './schemas/device-tombstone.schema' import { GatewayController } from './gateway.controller' import { GatewayService } from './gateway.service' import { AuthModule } from '../auth/auth.module' @@ -23,6 +24,10 @@ import { HeartbeatCheckTask } from './tasks/heartbeat-check.task' name: Device.name, schema: DeviceSchema, }, + { + name: DeviceTombstone.name, + schema: DeviceTombstoneSchema, + }, { name: SMS.name, schema: SMSSchema, diff --git a/api/src/gateway/gateway.service.spec.ts b/api/src/gateway/gateway.service.spec.ts index b5b9d1e..44bda8e 100644 --- a/api/src/gateway/gateway.service.spec.ts +++ b/api/src/gateway/gateway.service.spec.ts @@ -3,6 +3,7 @@ import { GatewayService } from './gateway.service' import { AuthModule } from '../auth/auth.module' import { getModelToken } from '@nestjs/mongoose' import { Device, DeviceDocument } from './schemas/device.schema' +import { DeviceTombstone } from './schemas/device-tombstone.schema' import { SMS } from './schemas/sms.schema' import { SMSBatch } from './schemas/sms-batch.schema' import { AuthService } from '../auth/auth.service' @@ -29,6 +30,7 @@ jest.mock('firebase-admin', () => ({ describe('GatewayService', () => { let service: GatewayService let deviceModel: Model + let deviceTombstoneModel: Model let smsModel: Model let smsBatchModel: Model let authService: AuthService @@ -60,6 +62,10 @@ describe('GatewayService', () => { findByIdAndUpdate: jest.fn(), } + const mockDeviceTombstoneModel = { + updateOne: jest.fn(), + } + const mockAuthService = { getUserApiKeys: jest.fn(), } @@ -85,6 +91,10 @@ describe('GatewayService', () => { provide: getModelToken(Device.name), useValue: mockDeviceModel, }, + { + provide: getModelToken(DeviceTombstone.name), + useValue: mockDeviceTombstoneModel, + }, { provide: getModelToken(SMS.name), useValue: mockSmsModel, @@ -115,6 +125,9 @@ describe('GatewayService', () => { service = module.get(GatewayService) deviceModel = module.get>(getModelToken(Device.name)) + deviceTombstoneModel = module.get>( + getModelToken(DeviceTombstone.name), + ) smsModel = module.get>(getModelToken(SMS.name)) smsBatchModel = module.get>(getModelToken(SMSBatch.name)) authService = module.get(AuthService) @@ -299,16 +312,18 @@ describe('GatewayService', () => { }) describe('deleteDevice', () => { - const mockDeviceId = 'device123' + const mockDeviceId = '507f1f77bcf86cd799439011' const mockDevice = { _id: mockDeviceId, model: 'Pixel 6' } - it('should return empty object when device exists', async () => { + it('should tombstone and delete when device exists', async () => { mockDeviceModel.findById.mockResolvedValue(mockDevice) const result = await service.deleteDevice(mockDeviceId) expect(mockDeviceModel.findById).toHaveBeenCalledWith(mockDeviceId) - expect(result).toEqual({}) + expect(mockDeviceTombstoneModel.updateOne).toHaveBeenCalled() + expect(mockDeviceModel.findByIdAndDelete).toHaveBeenCalledWith(mockDeviceId) + expect(result).toEqual({ success: true }) }) it('should throw an error if device does not exist', async () => { diff --git a/api/src/gateway/gateway.service.ts b/api/src/gateway/gateway.service.ts index 1a76c94..f6b59fa 100644 --- a/api/src/gateway/gateway.service.ts +++ b/api/src/gateway/gateway.service.ts @@ -3,6 +3,7 @@ import { InjectModel } from '@nestjs/mongoose' import { Device, DeviceDocument } from './schemas/device.schema' import { Model, Types } from 'mongoose' import * as firebaseAdmin from 'firebase-admin' +import { DeviceTombstone, DeviceTombstoneDocument } from './schemas/device-tombstone.schema' import { ReceivedSMSDTO, RegisterDeviceInputDTO, @@ -28,6 +29,8 @@ import { SmsQueueService } from './queue/sms-queue.service' export class GatewayService { constructor( @InjectModel(Device.name) private deviceModel: Model, + @InjectModel(DeviceTombstone.name) + private deviceTombstoneModel: Model, @InjectModel(SMS.name) private smsModel: Model, @InjectModel(SMSBatch.name) private smsBatchModel: Model, private authService: AuthService, @@ -141,8 +144,21 @@ export class GatewayService { ) } - return {} - // return await this.deviceModel.findByIdAndDelete(deviceId) + await this.deviceTombstoneModel.updateOne( + { deviceId: new Types.ObjectId(deviceId) }, + { + $setOnInsert: { + deviceId: new Types.ObjectId(deviceId), + userId: device.user, + deletedAt: new Date(), + }, + }, + { upsert: true }, + ) + + await this.deviceModel.findByIdAndDelete(deviceId) + + return { success: true } } private calculateDelayFromScheduledAt(scheduledAt?: string): number | undefined { @@ -256,6 +272,7 @@ export class GatewayService { try { smsBatch = await this.smsBatchModel.create({ + user: device.user, device: device._id, message, recipientCount: recipients.length, @@ -278,6 +295,7 @@ export class GatewayService { for (let recipient of recipients) { recipient = recipient.replace(/\s+/g, "") const sms = await this.smsModel.create({ + user: device.user, device: device._id, smsBatch: smsBatch._id, message: message, @@ -465,6 +483,7 @@ export class GatewayService { const { messageTemplate, messages } = body const smsBatch = await this.smsBatchModel.create({ + user: device.user, device: device._id, message: messageTemplate, recipientCount: messages @@ -504,6 +523,7 @@ export class GatewayService { for (let recipient of recipients) { recipient = recipient.replace(/\s+/g, "") smsDocumentsToInsert.push({ + user: device.user, device: device._id, smsBatch: smsBatch._id, message: message, @@ -765,6 +785,7 @@ export class GatewayService { } const sms = await this.smsModel.create({ + user: device.user, device: device._id, message: dto.message, type: SMSType.RECEIVED, diff --git a/api/src/gateway/schemas/device-tombstone.schema.ts b/api/src/gateway/schemas/device-tombstone.schema.ts new file mode 100644 index 0000000..52c6d4d --- /dev/null +++ b/api/src/gateway/schemas/device-tombstone.schema.ts @@ -0,0 +1,25 @@ +import { Prop, Schema, SchemaFactory } from '@nestjs/mongoose' +import { Document, Types } from 'mongoose' +import { User } from '../../users/schemas/user.schema' + +export type DeviceTombstoneDocument = DeviceTombstone & Document + +@Schema({ timestamps: true }) +export class DeviceTombstone { + _id?: Types.ObjectId + + @Prop({ type: Types.ObjectId, required: true, unique: true, index: true }) + deviceId: Types.ObjectId + + @Prop({ type: Types.ObjectId, ref: User.name, required: true, index: true }) + userId: User | Types.ObjectId + + @Prop({ type: Date, required: true }) + deletedAt: Date +} + +export const DeviceTombstoneSchema = + SchemaFactory.createForClass(DeviceTombstone) + +DeviceTombstoneSchema.index({ userId: 1, deletedAt: -1 }) + diff --git a/api/src/gateway/schemas/sms-batch.schema.ts b/api/src/gateway/schemas/sms-batch.schema.ts index 90820ee..4bc942b 100644 --- a/api/src/gateway/schemas/sms-batch.schema.ts +++ b/api/src/gateway/schemas/sms-batch.schema.ts @@ -1,6 +1,7 @@ import { Prop, Schema, SchemaFactory } from '@nestjs/mongoose' import { Document, Types } from 'mongoose' import { Device } from './device.schema' +import { User } from '../../users/schemas/user.schema' export type SMSBatchDocument = SMSBatch & Document @@ -8,6 +9,9 @@ export type SMSBatchDocument = SMSBatch & Document export class SMSBatch { _id?: Types.ObjectId + @Prop({ type: Types.ObjectId, ref: User.name, required: true, index: true }) + user: User | Types.ObjectId + @Prop({ type: Types.ObjectId, ref: Device.name }) device: Device @@ -53,3 +57,5 @@ export class SMSBatch { } export const SMSBatchSchema = SchemaFactory.createForClass(SMSBatch) + +SMSBatchSchema.index({ user: 1, createdAt: -1 }) diff --git a/api/src/gateway/schemas/sms.schema.ts b/api/src/gateway/schemas/sms.schema.ts index 932f8e1..99ed6fb 100644 --- a/api/src/gateway/schemas/sms.schema.ts +++ b/api/src/gateway/schemas/sms.schema.ts @@ -2,6 +2,7 @@ import { Prop, Schema, SchemaFactory } from '@nestjs/mongoose' import { Document, Types } from 'mongoose' import { Device } from './device.schema' import { SMSBatch } from './sms-batch.schema' +import { User } from '../../users/schemas/user.schema' export type SMSDocument = SMS & Document @@ -9,6 +10,9 @@ export type SMSDocument = SMS & Document export class SMS { _id?: Types.ObjectId + @Prop({ type: Types.ObjectId, ref: User.name, required: true, index: true }) + user: User | Types.ObjectId + @Prop({ type: Types.ObjectId, ref: Device.name, required: true }) device: Device | Types.ObjectId @@ -84,3 +88,4 @@ export const SMSSchema = SchemaFactory.createForClass(SMS) SMSSchema.index({ device: 1, type: 1, receivedAt: -1 }) +SMSSchema.index({ user: 1, createdAt: -1, type: 1 })