import { Router } from "express"; import { ApiError } from "../shared/errors"; import { requireDeviceToken } from "../shared/middleware/auth"; import { successResponse } from "../shared/middleware/errorMiddleware"; import { getDeviceById, patchDevice } from "../shared/store/deviceStore"; import { createDeviceHeartbeat } from "../shared/store/heartbeatStore"; import { acknowledgeDeviceCommand } from "../shared/store/deviceCommandStore"; const router = Router(); function normalizeNumberOrNull(value) { if (typeof value === "string") { const parsed = Number(value); if (!Number.isNaN(parsed) && Number.isFinite(parsed)) { return parsed; } return null; } if (typeof value === "number" && Number.isFinite(value)) { return value; } return null; } function normalizeSignalStrength(value) { const normalized = normalizeNumberOrNull(value); if (normalized === null) { return null; } if (normalized < 0 || normalized > 100) { throw new Error("NETWORK_STRENGTH_OUT_OF_RANGE"); } return normalized; } function normalizeBatteryLevel(value) { const normalized = normalizeNumberOrNull(value); if (normalized === null) { return null; } if (normalized < 0 || normalized > 100) { throw new Error("BATTERY_LEVEL_OUT_OF_RANGE"); } return normalized; } router.post("/heartbeat", requireDeviceToken, async (req, res, next) => { const payload = req.body; if (!payload || !payload.device_id) { return next(new ApiError("BAD_REQUEST", "device_id is required", 400)); } const device = await getDeviceById(payload.device_id); if (!device) { return next(new ApiError("NOT_FOUND", "device not found", 404)); } const eventTs = payload.timestamp ? new Date(payload.timestamp) : new Date(); if (Number.isNaN(eventTs.getTime())) { return next(new ApiError("BAD_REQUEST", "timestamp must be valid ISO datetime", 400)); } let payloadNetworkStrength; let payloadBattery; try { payloadNetworkStrength = normalizeSignalStrength(payload.network_strength); payloadBattery = normalizeBatteryLevel(payload.battery_level); } catch (error) { if (error instanceof Error && error.message === "NETWORK_STRENGTH_OUT_OF_RANGE") { return next(new ApiError("BAD_REQUEST", "network_strength must be between 0 and 100", 400)); } if (error instanceof Error && error.message === "BATTERY_LEVEL_OUT_OF_RANGE") { return next(new ApiError("BAD_REQUEST", "battery_level must be between 0 and 100", 400)); } return next(error); } const heartbeat = await createDeviceHeartbeat({ device_id: payload.device_id, timestamp: eventTs.toISOString(), firmware_version: payload.firmware_version, network_strength: payloadNetworkStrength, battery_level: payloadBattery, state: payload.state, payload_json: { network_strength_raw: payload.network_strength, battery_level_raw: payload.battery_level, state: payload.state, firmware_version: payload.firmware_version, timestamp: payload.timestamp, request_id: req.requestId } }); await patchDevice(payload.device_id, { last_seen_at: heartbeat.timestamp, firmware_version: payload.firmware_version || device.firmware_version }); res.json(successResponse(req, { heartbeat_id: heartbeat.id, device_id: heartbeat.device_id, request_id: req.requestId, server_time: heartbeat.received_at })); }); router.post("/commands/ack", requireDeviceToken, async (req, res, next) => { const payload = req.body; if (!payload || !payload.command_id || !payload.device_id || !payload.status) { return next(new ApiError("BAD_REQUEST", "command_id, device_id, status are required", 400)); } if (!["delivered", "failed", "timeout"].includes(payload.status)) { return next(new ApiError("BAD_REQUEST", "status must be delivered, failed, or timeout", 400)); } const device = await getDeviceById(payload.device_id); if (!device) { return next(new ApiError("NOT_FOUND", "device not found", 404)); } const updated = await acknowledgeDeviceCommand({ device_id: device.id, command_id: payload.command_id, status: payload.status, reason: payload.reason, result_payload: payload.result_payload }); if (!updated) { return next(new ApiError("NOT_FOUND", "command not found", 404)); } res.json(successResponse(req, { command_id: updated.id, device_id: updated.device_id, status: updated.status, acknowledged_at: updated.acknowledged_at })); }); export default router;