import { Op } from 'sequelize'; import crypto from 'crypto'; import { Device, Customer } from '../models/index.js'; import { CommandService } from './commandService.js'; import { utcNow } from '../utils/time.js'; import { toEnumName } from '../utils/enums.js'; function enrollmentCode() { return `EMI-${crypto.randomUUID().replace(/-/g, '').slice(0, 12).toUpperCase()}`; } export const DeviceService = { generateEnrollmentCode: enrollmentCode, async enrollDevice(data) { return Device.create({ imei: data.imei ?? null, imei2: data.imei2 ?? null, serial_number: data.serial_number ?? null, device_model: data.device_model ?? null, manufacturer: data.manufacturer ?? null, android_version: data.android_version ?? null, sdk_version: data.sdk_version ?? null, fcm_token: data.fcm_token ?? null, android_id: data.android_id ?? null, status: 'active', is_online: true, last_seen: utcNow(), enrolled_at: utcNow(), enrollment_code: enrollmentCode(), }); }, getDeviceByImei(imei) { return Device.findOne({ where: { imei } }); }, getDeviceById(id) { return Device.findByPk(id); }, async updateDeviceStatus(deviceId, status, userId = null, reason = null) { const device = await this.getDeviceById(deviceId); if (!device) return [null, null]; const oldStatus = device.status; device.status = status; device.updated_at = utcNow(); let command = null; if (status === 'locked' && oldStatus !== 'locked') { command = await CommandService.createLockCommand(device, userId, reason); } else if (status === 'active' && oldStatus === 'locked') { command = await CommandService.createUnlockCommand(device, userId, reason); } await device.save(); return [device, command]; }, async updateHeartbeat(hb) { let device = null; // Honour the device's own id first so a device that already knows its id // keeps the same record (and command/stream channels) across restarts. const uuidRe = /^[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12}$/i; if (hb.device_id && uuidRe.test(hb.device_id)) { device = await Device.findByPk(hb.device_id); } if (!device && hb.android_id) { device = await Device.findOne({ where: { android_id: hb.android_id } }); } if (!device && hb.imei) device = await this.getDeviceByImei(hb.imei); if (!device && hb.serial_number) { device = await Device.findOne({ where: { serial_number: hb.serial_number } }); } // Self-heal: the device thinks it is enrolled but no record exists (e.g. // the DB was migrated/reset). Re-create it — preserving the device's own id // when valid — so command polling, streaming and location all keep working // without forcing the user to re-enroll. if (!device) { device = await Device.create({ ...(hb.device_id && uuidRe.test(hb.device_id) ? { id: hb.device_id } : {}), imei: hb.imei ?? null, imei2: hb.imei2 ?? null, serial_number: hb.serial_number ?? null, android_id: hb.android_id ?? null, status: 'active', is_online: true, last_seen: utcNow(), enrolled_at: utcNow(), enrollment_code: enrollmentCode(), }); } device.fcm_token = hb.fcm_token; device.is_online = true; device.last_seen = utcNow(); if (hb.android_id && !device.android_id) device.android_id = hb.android_id; if (hb.app_version) device.app_version = hb.app_version; if (hb.device_name) device.device_name = hb.device_name; if (hb.brand) device.brand = hb.brand; if (hb.manufacturer) device.manufacturer = hb.manufacturer; if (hb.device_model) device.device_model = hb.device_model; if (hb.serial_number) device.serial_number = hb.serial_number; if (hb.imei2) device.imei2 = hb.imei2; if (hb.android_version) device.android_version = hb.android_version; if (hb.is_device_owner !== undefined && hb.is_device_owner !== null) device.is_device_owner = hb.is_device_owner; if (hb.is_admin_active !== undefined && hb.is_admin_active !== null) device.is_admin_active = hb.is_admin_active; if (hb.battery_level !== undefined && hb.battery_level !== null) device.battery_level = hb.battery_level; if (hb.is_charging !== undefined && hb.is_charging !== null) device.is_charging = hb.is_charging; if (hb.network_type) device.network_type = hb.network_type; await device.save(); return device; }, async listDevices({ skip = 0, limit = 50, status = null, search = null } = {}) { const where = {}; if (status) where.status = toEnumName(status); if (search) { where[Op.or] = [ { imei: { [Op.like]: `%${search}%` } }, { device_model: { [Op.like]: `%${search}%` } }, { serial_number: { [Op.like]: `%${search}%` } }, ]; } const { count, rows } = await Device.findAndCountAll({ where, order: [['created_at', 'DESC']], offset: skip, limit, }); return [rows, count]; }, }; export const CustomerService = { hashIdNumber(idNumber) { return crypto.createHash('sha256').update(String(idNumber)).digest('hex'); }, createCustomer(data) { return Customer.create({ full_name: data.full_name, phone: data.phone, alternate_phone: data.alternate_phone ?? null, email: data.email ?? null, id_type: data.id_type ?? null, id_hash: data.id_number ? this.hashIdNumber(data.id_number) : null, address: data.address ?? null, city: data.city ?? null, state: data.state ?? null, pincode: data.pincode ?? null, emergency_contact_name: data.emergency_contact_name ?? null, emergency_contact_phone: data.emergency_contact_phone ?? null, emergency_contact_relation: data.emergency_contact_relation ?? null, }); }, getCustomerById(id) { return Customer.findByPk(id); }, getCustomerByPhone(phone) { return Customer.findOne({ where: { phone } }); }, async listCustomers({ skip = 0, limit = 50, search = null } = {}) { const where = {}; if (search) { where[Op.or] = [ { full_name: { [Op.like]: `%${search}%` } }, { phone: { [Op.like]: `%${search}%` } }, { email: { [Op.like]: `%${search}%` } }, ]; } const { count, rows } = await Customer.findAndCountAll({ where, order: [['created_at', 'DESC']], offset: skip, limit, }); return [rows, count]; }, }; export default DeviceService;