UNPKG

dt-common-device

Version:

A secure and robust device management library for IoT applications

1,015 lines (1,014 loc) 47.2 kB
"use strict"; var __createBinding = (this && this.__createBinding) || (Object.create ? (function(o, m, k, k2) { if (k2 === undefined) k2 = k; var desc = Object.getOwnPropertyDescriptor(m, k); if (!desc || ("get" in desc ? !m.__esModule : desc.writable || desc.configurable)) { desc = { enumerable: true, get: function() { return m[k]; } }; } Object.defineProperty(o, k2, desc); }) : (function(o, m, k, k2) { if (k2 === undefined) k2 = k; o[k2] = m[k]; })); var __setModuleDefault = (this && this.__setModuleDefault) || (Object.create ? (function(o, v) { Object.defineProperty(o, "default", { enumerable: true, value: v }); }) : function(o, v) { o["default"] = v; }); var __esDecorate = (this && this.__esDecorate) || function (ctor, descriptorIn, decorators, contextIn, initializers, extraInitializers) { function accept(f) { if (f !== void 0 && typeof f !== "function") throw new TypeError("Function expected"); return f; } var kind = contextIn.kind, key = kind === "getter" ? "get" : kind === "setter" ? "set" : "value"; var target = !descriptorIn && ctor ? contextIn["static"] ? ctor : ctor.prototype : null; var descriptor = descriptorIn || (target ? Object.getOwnPropertyDescriptor(target, contextIn.name) : {}); var _, done = false; for (var i = decorators.length - 1; i >= 0; i--) { var context = {}; for (var p in contextIn) context[p] = p === "access" ? {} : contextIn[p]; for (var p in contextIn.access) context.access[p] = contextIn.access[p]; context.addInitializer = function (f) { if (done) throw new TypeError("Cannot add initializers after decoration has completed"); extraInitializers.push(accept(f || null)); }; var result = (0, decorators[i])(kind === "accessor" ? { get: descriptor.get, set: descriptor.set } : descriptor[key], context); if (kind === "accessor") { if (result === void 0) continue; if (result === null || typeof result !== "object") throw new TypeError("Object expected"); if (_ = accept(result.get)) descriptor.get = _; if (_ = accept(result.set)) descriptor.set = _; if (_ = accept(result.init)) initializers.unshift(_); } else if (_ = accept(result)) { if (kind === "field") initializers.unshift(_); else descriptor[key] = _; } } if (target) Object.defineProperty(target, contextIn.name, descriptor); done = true; }; var __runInitializers = (this && this.__runInitializers) || function (thisArg, initializers, value) { var useValue = arguments.length > 2; for (var i = 0; i < initializers.length; i++) { value = useValue ? initializers[i].call(thisArg, value) : initializers[i].call(thisArg); } return useValue ? value : void 0; }; var __importStar = (this && this.__importStar) || (function () { var ownKeys = function(o) { ownKeys = Object.getOwnPropertyNames || function (o) { var ar = []; for (var k in o) if (Object.prototype.hasOwnProperty.call(o, k)) ar[ar.length] = k; return ar; }; return ownKeys(o); }; return function (mod) { if (mod && mod.__esModule) return mod; var result = {}; if (mod != null) for (var k = ownKeys(mod), i = 0; i < k.length; i++) if (k[i] !== "default") __createBinding(result, mod, k[i]); __setModuleDefault(result, mod); return result; }; })(); var __setFunctionName = (this && this.__setFunctionName) || function (f, name, prefix) { if (typeof name === "symbol") name = name.description ? "[".concat(name.description, "]") : ""; return Object.defineProperty(f, "name", { configurable: true, value: prefix ? "".concat(prefix, " ", name) : name }); }; Object.defineProperty(exports, "__esModule", { value: true }); exports.IssueService = void 0; const typedi_1 = __importStar(require("typedi")); const Issue_repository_1 = require("./Issue.repository"); const Issue_model_1 = require("./Issue.model"); const issue_types_1 = require("./issue.types"); const IssueBuilder_1 = require("./IssueBuilder"); const Service_1 = require("../constants/Service"); const Admin_service_1 = require("../entities/admin/Admin.service"); const audit_1 = require("../audit"); const constants_1 = require("../constants"); const dt_pub_sub_1 = require("dt-pub-sub"); let IssueService = (() => { let _classDecorators = [(0, typedi_1.Service)()]; let _classDescriptor; let _classExtraInitializers = []; let _classThis; var IssueService = _classThis = class { constructor() { this.issueRepository = typedi_1.default.get(Issue_repository_1.IssueRepository); } /** * Create an operations issue using IssueBuilder */ async createOperationsIssue(data) { const issueBuilder = IssueBuilder_1.IssueBuilder.createOperationsIssue() .setPropertyId(data.propertyId) .setTitle(data.title) .setDescription(data.description) .setCreatedBy(data.createdBy); if (data.entityId) issueBuilder.setEntityId(data.entityId); if (data.entityType) issueBuilder.setEntityType(data.entityType); if (data.assignedTo) issueBuilder.setAssignedTo(data.assignedTo); if (data.dueDate) issueBuilder.setDueDate(data.dueDate); if (data.zoneId) issueBuilder.setZoneId(data.zoneId); if (data.type) issueBuilder.setType(data.type); return await this.createIssue(issueBuilder); } /** * Create a security issue using IssueBuilder */ async createSecurityIssue(data) { const issueBuilder = IssueBuilder_1.IssueBuilder.createSecurityIssue() .setPropertyId(data.propertyId) .setTitle(data.title) .setDescription(data.description) .setCreatedBy(data.createdBy); if (data.entityId) issueBuilder.setEntityId(data.entityId); if (data.entityType) issueBuilder.setEntityType(data.entityType); if (data.assignedTo) issueBuilder.setAssignedTo(data.assignedTo); if (data.dueDate) issueBuilder.setDueDate(data.dueDate); if (data.zoneId) issueBuilder.setZoneId(data.zoneId); if (data.type) issueBuilder.setType(data.type); return await this.createIssue(issueBuilder); } /** * Create an energy issue using IssueBuilder */ async createEnergyIssue(data) { const issueBuilder = IssueBuilder_1.IssueBuilder.createEnergyIssue() .setPropertyId(data.propertyId) .setTitle(data.title) .setDescription(data.description) .setCreatedBy(data.createdBy); if (data.entityId) issueBuilder.setEntityId(data.entityId); if (data.entityType) issueBuilder.setEntityType(data.entityType); if (data.assignedTo) issueBuilder.setAssignedTo(data.assignedTo); if (data.dueDate) issueBuilder.setDueDate(data.dueDate); if (data.type) issueBuilder.setType(data.type); if (data.zoneId) issueBuilder.setZoneId(data.zoneId); return await this.createIssue(issueBuilder); } /** * Create a device-specific issue using IssueBuilder */ async createDeviceIssue(data) { const issueBuilder = IssueBuilder_1.IssueBuilder.createDeviceIssue(data.entityId, data.propertyId, data.type, data.entitySubType) .setTitle(data.title) .setDescription(data.description) .setCreatedBy(data.createdBy); if (data.category) issueBuilder.setCategory(data.category); if (data.priority) issueBuilder.setPriority(data.priority); if (data.type) issueBuilder.setType(data.type); if (data.assignedTo) issueBuilder.setAssignedTo(data.assignedTo); if (data.dueDate) issueBuilder.setDueDate(data.dueDate); if (data.zoneId) issueBuilder.setZoneId(data.zoneId); return await this.createIssue(issueBuilder); } /** * Create a hub-specific issue using IssueBuilder */ async createHubIssue(data) { const issueBuilder = IssueBuilder_1.IssueBuilder.createHubIssue(data.entityId, data.propertyId, data.type) .setTitle(data.title) .setDescription(data.description) .setCreatedBy(data.createdBy); if (data.category) issueBuilder.setCategory(data.category); if (data.priority) issueBuilder.setPriority(data.priority); if (data.type) issueBuilder.setType(data.type); if (data.assignedTo) issueBuilder.setAssignedTo(data.assignedTo); if (data.dueDate) issueBuilder.setDueDate(data.dueDate); if (data.zoneId) issueBuilder.setZoneId(data.zoneId); return await this.createIssue(issueBuilder); } /** * Create a user-specific issue using IssueBuilder */ async createUserIssue(data) { const issueBuilder = IssueBuilder_1.IssueBuilder.createUserIssue(data.entityId, data.propertyId, data.type) .setTitle(data.title) .setDescription(data.description) .setCreatedBy(data.createdBy); if (data.category) issueBuilder.setCategory(data.category); if (data.priority) issueBuilder.setPriority(data.priority); if (data.type) issueBuilder.setType(data.type); if (data.assignedTo) issueBuilder.setAssignedTo(data.assignedTo); if (data.dueDate) issueBuilder.setDueDate(data.dueDate); if (data.zoneId) issueBuilder.setZoneId(data.zoneId); return await this.createIssue(issueBuilder); } /** * Create issue for device going offline longer than baseline */ async createDeviceOfflineIssue(device, source, reason) { return await this.createDeviceIssue({ entityId: device.deviceId, entityType: issue_types_1.EntityType.DEVICE, entitySubType: device.deviceType.type, propertyId: device.propertyId, zoneId: device.zoneId, title: device.deviceType.type.toLowerCase() === "hub" ? "Hub Offline" : "Device Offline", description: `${device.name} has gone offline. ${reason ? `Reason: ${reason}.` : ""}`, createdBy: source, category: issue_types_1.IssuesCategory.OPERATIONS, priority: issue_types_1.IssuePriority.CRITICAL, type: device.deviceType.type.toLowerCase() === "hub" ? issue_types_1.IssueType.HUB_OFFLINE : issue_types_1.IssueType.DEVICE_OFFLINE, }); } /** * Create issue for device battery level below threshold (READINESS + OPERATIONAL + ENERGY) */ async createDeviceBatteryIssue(device, batteryLevel, threshold, priority, source) { return await this.createDeviceIssue({ entityId: device.deviceId, entityType: issue_types_1.EntityType.DEVICE, entitySubType: device.deviceType.type, propertyId: device.propertyId, zoneId: device.zoneId, title: "Device Battery Low", description: `${device.name} battery level is ${batteryLevel}%, which is below the property threshold of ${threshold}%.`, createdBy: source, category: issue_types_1.IssuesCategory.ENERGY, priority: priority, type: issue_types_1.IssueType.BATTERY_LOW, }); } async createAccountMissingDeviceIssue(connection, propertyId, source) { return await this.createIssue({ entityId: connection.id, entityType: issue_types_1.EntityType.CLOUD_DEVICE_ACCOUNT, entitySubType: connection.provider, propertyId: propertyId, title: "Device Missing", description: `${connection.deviceCount} device(s), missing from the ${connection.provider} account.`, createdBy: source, category: issue_types_1.IssuesCategory.OPERATIONS, priority: issue_types_1.IssuePriority.CRITICAL, type: issue_types_1.IssueType.ACCOUNT_MISSING_DEVICE, }); } async createAccountUnauthorizedIssue(account, source, reason) { return await this.createIssue({ entityId: account.accountId, entityType: account?.accountType, entitySubType: account?.accountSubType, propertyId: account.propertyId, title: "Account re-authorization required", description: `${account.name}'s authorization has expired. Please re-authorize the account. ${reason ? `Reason: ${reason}.` : ""}`, createdBy: source, category: issue_types_1.IssuesCategory.OPERATIONS, priority: issue_types_1.IssuePriority.CRITICAL, type: issue_types_1.IssueType.ACCOUNT_UNAUTHORIZED, }); } /** * Create issue for device malfunction (jammed or not accepting codes) (READINESS + OPERATIONAL) */ async createDeviceMalfunctionIssue(device, source, reason) { return await this.createDeviceIssue({ entityId: device.deviceId, entityType: issue_types_1.EntityType.DEVICE, entitySubType: device.deviceType.type, propertyId: device.propertyId, zoneId: device.zoneId, title: `Device Malfunction`, description: `${device.name} is malfunctioning. ${reason ? `Reason: ${reason}.` : ""}`, createdBy: source, category: issue_types_1.IssuesCategory.OPERATIONS, priority: issue_types_1.IssuePriority.HIGH, type: issue_types_1.IssueType.DEVICE_MALFUNCTION, }); } async createLockJammedIssue(device, source) { return await this.createDeviceIssue({ entityId: device.deviceId, entityType: issue_types_1.EntityType.DEVICE, entitySubType: device.deviceType.type, propertyId: device.propertyId, zoneId: device.zoneId, title: "Lock Jammed", description: `${device.name} is jammed. Requires Immediate Attention.`, createdBy: source, category: issue_types_1.IssuesCategory.OPERATIONS, priority: issue_types_1.IssuePriority.HIGH, type: issue_types_1.IssueType.LOCK_JAMMED, }); } /** * Create a maintenance issue using IssueBuilder */ async createMaintenanceIssue(data) { const issueBuilder = IssueBuilder_1.IssueBuilder.createMaintenanceIssue(data.propertyId, data.type, data.entityId, data.entityType) .setTitle(data.title) .setDescription(data.description) .setCreatedBy(data.createdBy); if (data.type) issueBuilder.setType(data.type); if (data.assignedTo) issueBuilder.setAssignedTo(data.assignedTo); if (data.dueDate) issueBuilder.setDueDate(data.dueDate); if (data.zoneId) issueBuilder.setZoneId(data.zoneId); return await this.createIssue(issueBuilder); } /** * Create a high priority issue using IssueBuilder */ async createHighPriorityIssue(data) { const issueBuilder = IssueBuilder_1.IssueBuilder.createHighPriorityIssue(data.propertyId, data.type, data.entityId, data.entityType) .setTitle(data.title) .setDescription(data.description) .setCreatedBy(data.createdBy); if (data.type) issueBuilder.setType(data.type); if (data.assignedTo) issueBuilder.setAssignedTo(data.assignedTo); if (data.dueDate) issueBuilder.setDueDate(data.dueDate); if (data.zoneId) issueBuilder.setZoneId(data.zoneId); return await this.createIssue(issueBuilder); } /** * Create a new issue with business logic validation * Accepts either a CreateIssueData object or an IssueBuilder instance */ async createIssue(issueData) { try { let processedIssueData; // Handle IssueBuilder instance if (issueData instanceof IssueBuilder_1.IssueBuilder) { processedIssueData = issueData.build(); } else { processedIssueData = issueData; } // Business logic: Validate issue data this.validateIssueData(processedIssueData); // Business logic: Set default priority if not provided if (!processedIssueData.priority) { processedIssueData.priority = this.determineDefaultPriority(processedIssueData.category); } // Business logic: Validate due date is in the future if (processedIssueData.dueDate && processedIssueData.dueDate <= new Date()) { throw new Error("Due date must be in the future"); } if (processedIssueData?.zoneId && !processedIssueData?.zoneName) { const zone = await typedi_1.default.get(Admin_service_1.AdminService).getZone(processedIssueData.zoneId); processedIssueData.zoneName = zone?.name || ""; } const existingIssue = await this.query({ propertyId: processedIssueData.propertyId, zoneId: processedIssueData.zoneId, entityId: processedIssueData.entityId, entityType: processedIssueData.entityType, entitySubType: processedIssueData.entitySubType, type: processedIssueData.type, }); if (existingIssue.length > 0) { (0, audit_1.pushAudit)({ auditType: constants_1.DT_EVENT_TYPES.ISSUE.CREATE.SKIPPED, auditData: { reason: "Issue already exists", propertyId: processedIssueData.propertyId, entityId: processedIssueData.entityId, entityType: processedIssueData.entityType, entitySubType: processedIssueData.entitySubType, type: processedIssueData.type, resource: audit_1.Resource.ISSUE, source: processedIssueData.createdBy, }, }); return null; } const issue = await this.issueRepository.create(processedIssueData); (0, audit_1.pushAudit)({ auditType: constants_1.DT_EVENT_TYPES.ISSUE.CREATE.SUCCESS, auditData: { resource: audit_1.Resource.ISSUE, source: Service_1.Source.USER, propertyId: processedIssueData.propertyId, zoneId: processedIssueData.zoneId, entityId: processedIssueData.entityId, entityType: processedIssueData.entityType, entitySubType: processedIssueData.entitySubType, type: processedIssueData.type, createdBy: processedIssueData.createdBy, createdAt: new Date(), }, }); await dt_pub_sub_1.eventDispatcher.publishEvent(constants_1.DT_EVENT_TYPES.ISSUE.CREATE.SUCCESS, issue, "dt-common-device"); return issue; } catch (error) { (0, audit_1.pushAudit)({ auditType: constants_1.DT_EVENT_TYPES.ISSUE.CREATE.FAILED, auditData: { resource: audit_1.Resource.ISSUE, source: Service_1.Source.USER, propertyId: issueData?.propertyId || "", zoneId: issueData?.zoneId || "", errorMessage: error.message, error: error, createdBy: issueData?.createdBy || "", }, }); throw error; } } /** * Get issue by ID with business logic */ async getIssueById(id, includeDeleted = false) { if (!id) { throw new Error("Issue ID is required"); } const issue = await this.issueRepository.findById(id, includeDeleted); // Business logic: Check if issue is overdue if (issue?.dueDate && issue.dueDate < new Date() && issue.status !== issue_types_1.IssueStatus.RESOLVED) { // You could add a flag or handle overdue logic here console.warn(`Issue ${id} is overdue`); } return issue; } /** * Get all issues with business logic filtering */ async query(filters) { // Business logic: Validate filters this.validateFilters(filters); // Business logic: Apply business rules to filters const enhancedFilters = this.applyBusinessRules(filters); return await this.issueRepository.query(enhancedFilters); } async issueCount(filters) { // Business logic: Apply business rules to filters const enhancedFilters = this.applyBusinessRules(filters); return await this.issueRepository.count(enhancedFilters); } /** * Update an issue with business logic validation */ async updateIssue(id, updateData) { if (!id) { throw new Error("Issue ID is required"); } // Business logic: Validate update data this.validateUpdateData(updateData); // Business logic: Check if issue exists and is not deleted const existingIssue = await this.issueRepository.findById(id); if (!existingIssue) { throw new Error("Issue not found"); } // Business logic: Handle status transitions if (updateData.status) { this.validateStatusTransition(existingIssue.status, updateData.status); } // Business logic: Handle priority changes // if (updateData.priority) { // this.validatePriorityChange(existingIssue.priority, updateData.priority); // } return await this.issueRepository.update(id, updateData); } async deleteIssues(filters, deletedBy, softDelete = true) { try { if (!filters || Object.keys(filters).length === 0) { throw new Error("Filters are required"); } if (!deletedBy) { throw new Error("Deleted by user is required"); } const issues = await this.issueRepository.query(filters); if (softDelete) { for (const issue of issues) { await this.issueRepository.softDelete(issue.id, deletedBy); } } else { for (const issue of issues) { await this.issueRepository.hardDelete(issue.id); } } return true; } catch (error) { throw new Error(`Failed to delete issues: ${error instanceof Error ? error.message : error}`); } } /** * Soft delete an issue with business logic */ async deleteIssue(id, deletedBy) { if (!id || !deletedBy) { throw new Error("Issue ID and deleted by user are required"); } // Business logic: Check if issue can be deleted const issue = await this.issueRepository.findById(id); if (!issue) { throw new Error("Issue not found"); } // Business logic: Prevent deletion of resolved issues (optional rule) if (issue.status === issue_types_1.IssueStatus.RESOLVED) { throw new Error("Cannot delete resolved issues"); } return await this.issueRepository.softDelete(id, deletedBy); } /** * Permanently delete an issue */ async permanentlyDeleteIssue(id) { return await this.issueRepository.hardDelete(id); } /** * Add a comment with business logic */ async addComment(issueId, commentData) { if (!issueId || !commentData.userId || !commentData.content) { throw new Error("Issue ID, user ID, and comment content are required"); } // Business logic: Check if issue exists and is active const issue = await this.issueRepository.findById(issueId); if (!issue) { throw new Error("Issue not found"); } if (issue.isDeleted) { throw new Error("Cannot add comment to deleted issue"); } // Business logic: Update issue status based on comment (optional) const shouldUpdateStatus = this.shouldUpdateStatusOnComment(commentData.content); if (shouldUpdateStatus && issue.status === issue_types_1.IssueStatus.PENDING) { await this.issueRepository.update(issueId, { status: issue_types_1.IssueStatus.IN_PROGRESS, updatedBy: commentData.userId, }); } // Add comment using the model instance methods const issueModel = await Issue_model_1.IssueModel.findById(issueId); if (issueModel) { issueModel.addComment(commentData); return await issueModel.save(); } return null; } /** * Update a comment on an issue */ async updateComment(issueId, commentId, content, userId) { const issueModel = await Issue_model_1.IssueModel.findById(issueId); if (!issueModel) return false; const success = issueModel.updateComment(commentId, content, userId); if (success) { await issueModel.save(); } return success; } /** * Remove a comment from an issue */ async removeComment(issueId, commentId) { const issueModel = await Issue_model_1.IssueModel.findById(issueId); if (!issueModel) return false; const success = issueModel.removeComment(commentId); if (success) { await issueModel.save(); } return success; } /** * Resolve an issue with business logic */ async resolveIssue(id, resolvedBy) { if (!id || !resolvedBy) { throw new Error("Issue ID and resolved by user are required"); } // Business logic: Check if issue can be resolved const issue = await this.issueRepository.findById(id); if (!issue) { throw new Error("Issue not found"); } if (issue.status === issue_types_1.IssueStatus.RESOLVED) { throw new Error("Issue is already resolved"); } if (issue.status === issue_types_1.IssueStatus.CANCELLED) { throw new Error("Cannot resolve cancelled issue"); } // Business logic: Auto-assign if not assigned if (!issue.assignedTo) { await this.issueRepository.update(id, { assignedTo: resolvedBy, updatedBy: resolvedBy, }); } // Resolve the issue using model instance methods const issueModel = await Issue_model_1.IssueModel.findById(id); if (issueModel) { issueModel.resolve(resolvedBy); return await issueModel.save(); } return null; } /** * Reopen a resolved issue */ async reopenIssue(id, reopenedBy) { const issueModel = await Issue_model_1.IssueModel.findById(id); if (!issueModel) return null; issueModel.reopen(reopenedBy); return await issueModel.save(); } /** * Assign an issue with business logic */ async assignIssue(id, userId, assignedBy) { if (!id || !userId || !assignedBy) { throw new Error("Issue ID, assignee user ID, and assigned by user are required"); } // Business logic: Check if issue can be assigned const issue = await this.issueRepository.findById(id); if (!issue) { throw new Error("Issue not found"); } if (issue.status === issue_types_1.IssueStatus.RESOLVED || issue.status === issue_types_1.IssueStatus.CLOSED) { throw new Error("Cannot assign resolved or closed issue"); } // Business logic: Update status to IN_PROGRESS when assigned const updateData = { assignedTo: userId, updatedBy: assignedBy, }; if (issue.status === issue_types_1.IssueStatus.PENDING) { updateData.status = issue_types_1.IssueStatus.IN_PROGRESS; } return await this.issueRepository.update(id, updateData); } /** * Unassign an issue */ async unassignIssue(id, unassignedBy) { const issueModel = await Issue_model_1.IssueModel.findById(id); if (!issueModel) return null; issueModel.unassign(unassignedBy); return await issueModel.save(); } /** * Cancel/Close/Resolve/Ignore/In_Progress/On_Hold an issue based on query * This method will find an issue matching the query and update its status */ async performIssueAction(query, action, updatedBy) { try { if (!updatedBy) { throw new Error("Updated by user is required"); } // Find the issue based on the query const issues = await this.query(query); if (issues.length === 0) { return null; // No issue found matching the query } if (issues.length > 1) { throw new Error(`Multiple issues found matching the query. Please provide more specific criteria.`); } const issue = issues[0]; if ([ issue_types_1.IssueStatus.CANCELLED, issue_types_1.IssueStatus.CLOSED, issue_types_1.IssueStatus.RESOLVED, issue_types_1.IssueStatus.IGNORED, ].includes(issue.status)) { throw new Error(`Issue is already ${issue.status}ed`); } // Determine the new status based on the action let newStatus; let updateData = { updatedBy, updatedAt: new Date(), }; let auditType = ""; switch (action) { case "in_progress": newStatus = issue_types_1.IssueStatus.IN_PROGRESS; auditType = constants_1.DT_EVENT_TYPES.ISSUE.IN_PROGRESS.SUCCESS; break; case "resolve": newStatus = issue_types_1.IssueStatus.RESOLVED; updateData.resolvedAt = new Date(); auditType = constants_1.DT_EVENT_TYPES.ISSUE.RESOLVE.SUCCESS; break; case "close": newStatus = issue_types_1.IssueStatus.CLOSED; auditType = constants_1.DT_EVENT_TYPES.ISSUE.CLOSE.SUCCESS; break; case "cancel": newStatus = issue_types_1.IssueStatus.CANCELLED; auditType = constants_1.DT_EVENT_TYPES.ISSUE.CANCEL.SUCCESS; break; case "on_hold": newStatus = issue_types_1.IssueStatus.ON_HOLD; auditType = constants_1.DT_EVENT_TYPES.ISSUE.ON_HOLD.SUCCESS; break; case "ignore": newStatus = issue_types_1.IssueStatus.IGNORED; auditType = constants_1.DT_EVENT_TYPES.ISSUE.IGNORE.SUCCESS; break; default: throw new Error("Invalid action. Must be 'cancel', 'close', or 'resolve'"); } updateData.status = newStatus; // Update the issue const updatedIssue = await this.updateIssue(issue.id, updateData); if (updatedIssue) { // Log the action for audit purposes //console.log(`Issue ${issue.id} has been ${action}ed by ${updatedBy}`); await dt_pub_sub_1.eventDispatcher.publishEvent(auditType, updatedIssue, "dt-common-device"); await (0, audit_1.pushAudit)({ auditType: auditType, auditData: { resource: audit_1.Resource.ISSUE, source: Service_1.Source.USER, propertyId: issue.propertyId, zoneId: issue.zoneId, oldStatus: issue.status, newStatus, action, updatedBy, updatedAt: new Date(), }, }); } return updatedIssue; } catch (error) { await (0, audit_1.pushAudit)({ auditType: constants_1.DT_EVENT_TYPES.ISSUE.UPDATE.FAILED, auditData: { resource: audit_1.Resource.ISSUE, source: Service_1.Source.USER, propertyId: query.propertyId || "", zoneId: query.zoneId || "", errorMessage: error.message, error: error, updatedBy, updatedAt: new Date(), actionPayload: query, action, }, }); throw error; } } /** * Get issues assigned to a user with business logic */ async getIssuesByAssignee(assignedTo, includeDeleted = false) { if (!assignedTo) { throw new Error("Assignee user ID is required"); } return await this.issueRepository.findByAssignee(assignedTo, includeDeleted); } /** * Get overdue issues with business logic */ async getOverdueIssues(includeDeleted = false) { const overdueIssues = await this.issueRepository.findOverdue(includeDeleted); // Business logic: Log overdue issues for monitoring if (overdueIssues.length > 0) { console.warn(`Found ${overdueIssues.length} overdue issues`); } return overdueIssues; } /** * Get upcoming issues (due within specified days) */ async getUpcomingIssues(days = 7, includeDeleted = false) { return await this.issueRepository.findUpcoming(days, includeDeleted); } /** * Get issue statistics with business logic */ async getIssueStatistics(propertyId, zoneId) { const stats = await this.issueRepository.getStatistics(propertyId, zoneId); // Business logic: Calculate additional metrics const responseTime = this.calculateAverageResponseTime(stats); const resolutionRate = this.calculateResolutionRate(stats); // Log resolution rate for monitoring // console.log(`Resolution rate: ${resolutionRate.toFixed(2)}%`); // console.log(`Response time: ${responseTime.toFixed(2)} days`); // Business logic: Add alerts for critical metrics if (stats.overdue > 0) { console.warn(`Alert: ${stats.overdue} overdue issues detected`); } if (stats.byPriority[issue_types_1.IssuePriority.CRITICAL] > 0) { console.error(`Alert: ${stats.byPriority[issue_types_1.IssuePriority.CRITICAL]} critical issues require immediate attention`); } return stats; } /** * Search issues with business logic */ async searchIssues(searchTerm, filters = {}) { if (!searchTerm || searchTerm.trim().length < 2) { throw new Error("Search term must be at least 2 characters long"); } return await this.issueRepository.search(searchTerm, filters); } /** * Get issues by zone ID */ async getIssuesByZoneId(zoneId, includeDeleted = false) { if (!zoneId) { throw new Error("Zone ID is required"); } return await this.issueRepository.findByZoneId(zoneId, includeDeleted); } /** * Get issues by zone ID and status */ async getIssuesByZoneIdAndStatus(zoneId, status, includeDeleted = false) { if (!zoneId) { throw new Error("Zone ID is required"); } if (!status) { throw new Error("Status is required"); } return await this.issueRepository.findByZoneIdAndStatus(zoneId, status, includeDeleted); } /** * Get issues by multiple zone IDs */ async getIssuesByZoneIds(zoneIds, includeDeleted = false) { if (!zoneIds || zoneIds.length === 0) { throw new Error("Zone IDs array is required and cannot be empty"); } return await this.issueRepository.findByZoneIds(zoneIds, includeDeleted); } // Private business logic methods validateIssueData(data) { if (!data.title || data.title.trim().length < 5) { throw new Error("Issue title must be at least 5 characters long"); } if (!data.description || data.description.trim().length < 10) { throw new Error("Issue description must be at least 10 characters long"); } if (!data.propertyId) { throw new Error("Property ID is required"); } // Zone ID is now optional, so no validation needed if (!data.createdBy) { throw new Error("Created by user ID is required"); } } validateFilters(filters) { if (filters.limit && (filters.limit < 1 || filters.limit > 100)) { throw new Error("Limit must be between 1 and 100"); } if (filters.skip && filters.skip < 0) { throw new Error("Skip must be non-negative"); } // Validate date filters if (filters.startDate && filters.endDate && filters.startDate > filters.endDate) { throw new Error("Start date must be before or equal to end date"); } } validateUpdateData(data) { if (data.title && data.title.trim().length < 5) { throw new Error("Issue title must be at least 5 characters long"); } if (data.description && data.description.trim().length < 10) { throw new Error("Issue description must be at least 10 characters long"); } } validateStatusTransition(currentStatus, newStatus) { const validTransitions = { [issue_types_1.IssueStatus.PENDING]: [ issue_types_1.IssueStatus.IN_PROGRESS, issue_types_1.IssueStatus.RESOLVED, issue_types_1.IssueStatus.CLOSED, issue_types_1.IssueStatus.CANCELLED, issue_types_1.IssueStatus.ON_HOLD, issue_types_1.IssueStatus.IGNORED, ], [issue_types_1.IssueStatus.IN_PROGRESS]: [ issue_types_1.IssueStatus.RESOLVED, issue_types_1.IssueStatus.CLOSED, issue_types_1.IssueStatus.CANCELLED, issue_types_1.IssueStatus.ON_HOLD, issue_types_1.IssueStatus.IGNORED, ], [issue_types_1.IssueStatus.RESOLVED]: [issue_types_1.IssueStatus.CLOSED, issue_types_1.IssueStatus.PENDING], // Reopen [issue_types_1.IssueStatus.CLOSED]: [issue_types_1.IssueStatus.PENDING], // Reopen [issue_types_1.IssueStatus.CANCELLED]: [issue_types_1.IssueStatus.PENDING], // Reopen [issue_types_1.IssueStatus.ON_HOLD]: [issue_types_1.IssueStatus.PENDING, issue_types_1.IssueStatus.IN_PROGRESS], [issue_types_1.IssueStatus.IGNORED]: [issue_types_1.IssueStatus.PENDING, issue_types_1.IssueStatus.IN_PROGRESS], // Reopen from ignored }; if (!validTransitions[currentStatus]?.includes(newStatus)) { throw new Error(`Invalid status transition from ${currentStatus} to ${newStatus}`); } } validatePriorityChange(currentPriority, newPriority) { // Business rule: Only allow priority escalation, not de-escalation for critical issues if (currentPriority === issue_types_1.IssuePriority.CRITICAL && newPriority !== issue_types_1.IssuePriority.CRITICAL) { throw new Error("Cannot de-escalate priority of critical issues"); } } determineDefaultPriority(category) { // Business logic: Determine default priority based on category const categoryPriorities = { [issue_types_1.IssuesCategory.OPERATIONS]: issue_types_1.IssuePriority.HIGH, [issue_types_1.IssuesCategory.SECURITY]: issue_types_1.IssuePriority.CRITICAL, [issue_types_1.IssuesCategory.ENERGY]: issue_types_1.IssuePriority.HIGH, [issue_types_1.IssuesCategory.OTHER]: issue_types_1.IssuePriority.LOW, }; return categoryPriorities[category] || issue_types_1.IssuePriority.LOW; } applyBusinessRules(filters) { // Business logic: Apply additional filters based on business rules const enhancedFilters = { ...filters }; // Always exclude cancelled, resolved, and closed issues unless explicitly requested if (!enhancedFilters.status) { enhancedFilters.status = { $nin: [ issue_types_1.IssueStatus.CANCELLED, issue_types_1.IssueStatus.RESOLVED, issue_types_1.IssueStatus.CLOSED, issue_types_1.IssueStatus.IGNORED, ], }; } return enhancedFilters; } shouldUpdateStatusOnComment(content) { // Business logic: Determine if comment should trigger status change const statusKeywords = [ "working on", "investigating", "fixing", "resolving", ]; return statusKeywords.some((keyword) => content.toLowerCase().includes(keyword)); } calculateAverageResponseTime(stats) { // Business logic: Calculate average response time (placeholder) return 0; } calculateResolutionRate(stats) { // Business logic: Calculate resolution rate if (stats.total === 0) return 0; return ((stats.resolved + stats.closed) / stats.total) * 100; } }; __setFunctionName(_classThis, "IssueService"); (() => { const _metadata = typeof Symbol === "function" && Symbol.metadata ? Object.create(null) : void 0; __esDecorate(null, _classDescriptor = { value: _classThis }, _classDecorators, { kind: "class", name: _classThis.name, metadata: _metadata }, null, _classExtraInitializers); IssueService = _classThis = _classDescriptor.value; if (_metadata) Object.defineProperty(_classThis, Symbol.metadata, { enumerable: true, configurable: true, writable: true, value: _metadata }); __runInitializers(_classThis, _classExtraInitializers); })(); return IssueService = _classThis; })(); exports.IssueService = IssueService;