Skip to content

Commit 20a1be2

Browse files
adding status endpoint for devices
1 parent ab01e54 commit 20a1be2

4 files changed

Lines changed: 96 additions & 6 deletions

File tree

src/devices/devices.controller.ts

Lines changed: 15 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -70,6 +70,21 @@ export class DevicesController {
7070
return this.devicesService.findAll(req.user, req.headers.authorization, skip, take);
7171
}
7272

73+
@Get('status')
74+
@UseGuards(JwtAuthGuard)
75+
@ApiOperation({
76+
summary: 'Returns online vs offline devices for the authenticated user',
77+
description: `
78+
Returns the online vs offline status of all devices for the authenticated user.`,
79+
})
80+
findAllDeviceStatus(@Req() req) {
81+
const parsedSkip = parseInt(req.query.skip, 10);
82+
const parsedTake = parseInt(req.query.take, 10);
83+
const skip = Number.isNaN(parsedSkip) ? 0 : parsedSkip;
84+
const take = Number.isNaN(parsedTake) ? undefined : parsedTake;
85+
return this.devicesService.findAllStatus(req.user, req.headers.authorization);
86+
}
87+
7388
@Get('latest-primary-data')
7489
@UseGuards(JwtAuthGuard)
7590
@ApiParam({ name: 'skip (0)', description: 'Number of records to skip for pagination', required: false })

src/devices/devices.service.ts

Lines changed: 39 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -100,6 +100,45 @@ export class DevicesService {
100100
return data;
101101
}
102102

103+
public async findAllStatus(jwtPayload: any, authHeader: string): Promise<{ online: number; offline: number }> {
104+
const accessToken = this.getAccessToken(authHeader);
105+
const client = this.supabaseService.getClient(accessToken);
106+
const userId = this.getUserId(jwtPayload);
107+
108+
const { data: devices, error: devicesError } = await client
109+
.from('cw_devices')
110+
.select('last_data_updated_at, upload_interval, cw_device_type(default_upload_interval)')
111+
.eq('user_id', userId);
112+
113+
if (devicesError) {
114+
throw new InternalServerErrorException('Failed to fetch devices');
115+
}
116+
117+
if (!devices || devices.length === 0) {
118+
throw new NotFoundException('No devices found');
119+
}
120+
121+
const now = new Date();
122+
123+
let onlineCount = 0;
124+
let offlineCount = 0;
125+
126+
devices.forEach((device) => {
127+
const lastUpdated = new Date(device.last_data_updated_at);
128+
const minutesSinceLastUpdate = (now.getTime() - lastUpdated.getTime()) / (1000 * 60);
129+
const deviceType = Array.isArray(device.cw_device_type)
130+
? device.cw_device_type[0]
131+
: device.cw_device_type;
132+
if (minutesSinceLastUpdate <= (device.upload_interval ? device.upload_interval : (deviceType?.default_upload_interval as number))) {
133+
onlineCount++;
134+
} else {
135+
offlineCount++;
136+
}
137+
});
138+
139+
return { online: onlineCount, offline: offlineCount };
140+
}
141+
103142
public async findData(
104143
jwtPayload: any,
105144
devEui: string,

src/payments/dto/create-checkout-session.dto.ts

Lines changed: 10 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -54,17 +54,23 @@ export class CreateCheckoutSessionDto {
5454

5555
@ApiPropertyOptional({
5656
type: 'object',
57-
additionalProperties: true,
57+
additionalProperties: {
58+
oneOf: [{ type: 'string' }, { type: 'number' }, { type: 'boolean' }],
59+
},
5860
description: 'Metadata copied to order/subscription.',
61+
example: { plan: 'pro', seats: 5, trial: true },
5962
})
60-
metadata?: Record<string, unknown>;
63+
metadata?: Record<string, string | number | boolean>;
6164

6265
@ApiPropertyOptional({
6366
type: 'object',
64-
additionalProperties: true,
67+
additionalProperties: {
68+
oneOf: [{ type: 'string' }, { type: 'number' }, { type: 'boolean' }],
69+
},
6570
description: 'Metadata copied to customer when created.',
71+
example: { source: 'dashboard', region: 'us-east-1' },
6672
})
67-
customer_metadata?: Record<string, unknown>;
73+
customer_metadata?: Record<string, string | number | boolean>;
6874

6975
@ApiPropertyOptional({ default: true })
7076
allow_discount_codes?: boolean;

src/payments/payments.service.ts

Lines changed: 32 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -10,6 +10,8 @@ import { ConfigService } from '@nestjs/config';
1010
import { CreateCheckoutSessionDto } from './dto/create-checkout-session.dto';
1111
import { CreateCustomerPortalSessionDto } from './dto/create-customer-portal-session.dto';
1212

13+
type CheckoutMetadataValue = string | number | boolean;
14+
1315
@Injectable()
1416
export class PaymentsService {
1517
constructor(private readonly configService: ConfigService) {}
@@ -60,10 +62,12 @@ export class PaymentsService {
6062
if (createCheckoutSessionDto.customer_billing_address) {
6163
payload.customer_billing_address = createCheckoutSessionDto.customer_billing_address;
6264
}
63-
if (createCheckoutSessionDto.metadata) {
65+
if (createCheckoutSessionDto.metadata !== undefined) {
66+
this.assertCheckoutMetadata('metadata', createCheckoutSessionDto.metadata);
6467
payload.metadata = createCheckoutSessionDto.metadata;
6568
}
66-
if (createCheckoutSessionDto.customer_metadata) {
69+
if (createCheckoutSessionDto.customer_metadata !== undefined) {
70+
this.assertCheckoutMetadata('customer_metadata', createCheckoutSessionDto.customer_metadata);
6771
payload.customer_metadata = createCheckoutSessionDto.customer_metadata;
6872
}
6973
if (typeof createCheckoutSessionDto.allow_discount_codes === 'boolean') {
@@ -184,6 +188,32 @@ export class PaymentsService {
184188
return token;
185189
}
186190

191+
private assertCheckoutMetadata(fieldName: string, value: unknown): void {
192+
if (!value || typeof value !== 'object' || Array.isArray(value)) {
193+
throw new BadRequestException(
194+
`${fieldName} must be an object with string, number, or boolean values`,
195+
);
196+
}
197+
198+
for (const [key, entryValue] of Object.entries(value)) {
199+
if (!this.isValidCheckoutMetadataValue(entryValue)) {
200+
throw new BadRequestException(
201+
`${fieldName}.${key} must be a string, number, or boolean`,
202+
);
203+
}
204+
}
205+
}
206+
207+
private isValidCheckoutMetadataValue(value: unknown): value is CheckoutMetadataValue {
208+
if (typeof value === 'string' || typeof value === 'boolean') {
209+
return true;
210+
}
211+
if (typeof value === 'number') {
212+
return Number.isFinite(value);
213+
}
214+
return false;
215+
}
216+
187217
private getPolarApiBaseUrl(): string {
188218
const configuredBaseUrl = this.configService.get<string>('POLAR_API_URL')?.trim();
189219
const baseUrl =

0 commit comments

Comments
 (0)