dt-common-device 1.3.0 → 2.0.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/TROUBLESHOOTING.md +184 -0
- package/dist/config/config.d.ts +9 -2
- package/dist/config/config.js +97 -14
- package/dist/constants/Event.d.ts +75 -0
- package/dist/constants/Event.js +78 -0
- package/dist/db/db.d.ts +1 -0
- package/dist/db/db.js +18 -2
- package/dist/device/local/entities/AlertBuilder.d.ts +87 -0
- package/dist/device/local/entities/AlertBuilder.example.d.ts +11 -0
- package/dist/device/local/entities/AlertBuilder.example.js +117 -0
- package/dist/device/local/entities/AlertBuilder.js +179 -0
- package/dist/device/local/entities/IssueBuilder.d.ts +109 -0
- package/dist/device/local/entities/IssueBuilder.example.d.ts +16 -0
- package/dist/device/local/entities/IssueBuilder.example.js +196 -0
- package/dist/device/local/entities/IssueBuilder.js +237 -0
- package/dist/device/local/entities/index.d.ts +2 -0
- package/dist/device/local/entities/index.js +7 -0
- package/dist/device/local/interfaces/IDevice.d.ts +10 -9
- package/dist/device/local/interfaces/IDevice.js +7 -0
- package/dist/device/local/models/Alert.model.d.ts +28 -0
- package/dist/device/local/models/Alert.model.js +222 -0
- package/dist/device/local/models/Issue.model.d.ts +28 -0
- package/dist/device/local/models/Issue.model.js +260 -0
- package/dist/device/local/repository/Alert.repository.d.ts +106 -0
- package/dist/device/local/repository/Alert.repository.js +374 -0
- package/dist/device/local/repository/Device.repository.d.ts +10 -2
- package/dist/device/local/repository/Device.repository.js +153 -30
- package/dist/device/local/repository/Hub.repository.d.ts +1 -1
- package/dist/device/local/repository/Hub.repository.js +60 -18
- package/dist/device/local/repository/Issue.repository.d.ts +113 -0
- package/dist/device/local/repository/Issue.repository.js +401 -0
- package/dist/device/local/repository/Schedule.repository.d.ts +1 -1
- package/dist/device/local/repository/Schedule.repository.js +14 -18
- package/dist/device/local/services/Alert.service.d.ts +135 -5
- package/dist/device/local/services/Alert.service.js +471 -7
- package/dist/device/local/services/AlertService.example.d.ts +55 -0
- package/dist/device/local/services/AlertService.example.js +148 -0
- package/dist/device/local/services/Device.service.d.ts +8 -5
- package/dist/device/local/services/Device.service.js +58 -40
- package/dist/device/local/services/Issue.service.d.ts +168 -0
- package/dist/device/local/services/Issue.service.js +642 -0
- package/dist/device/local/services/IssueService.example.d.ts +68 -0
- package/dist/device/local/services/IssueService.example.js +177 -0
- package/dist/device/local/services/index.d.ts +7 -5
- package/dist/device/local/services/index.js +21 -11
- package/dist/events/BaseEventHandler.d.ts +43 -0
- package/dist/events/BaseEventHandler.js +111 -0
- package/dist/events/BaseEventTransformer.d.ts +26 -0
- package/dist/events/BaseEventTransformer.js +72 -0
- package/dist/events/DeviceEventHandler.d.ts +15 -0
- package/dist/events/DeviceEventHandler.js +152 -0
- package/dist/events/DeviceEventTransformerFactory.d.ts +27 -0
- package/dist/events/DeviceEventTransformerFactory.js +116 -0
- package/dist/events/EventHandler.d.ts +11 -0
- package/dist/events/EventHandler.js +106 -0
- package/dist/events/EventHandlerOrchestrator.d.ts +35 -0
- package/dist/events/EventHandlerOrchestrator.js +141 -0
- package/dist/events/EventProcessingService.d.ts +43 -0
- package/dist/events/EventProcessingService.js +243 -0
- package/dist/events/InternalEventSubscription.d.ts +44 -0
- package/dist/events/InternalEventSubscription.js +152 -0
- package/dist/events/index.d.ts +9 -0
- package/dist/events/index.js +21 -0
- package/dist/events/interfaces/DeviceEvent.d.ts +48 -0
- package/dist/events/interfaces/DeviceEvent.js +2 -0
- package/dist/events/interfaces/IEventHandler.d.ts +23 -0
- package/dist/events/interfaces/IEventHandler.js +2 -0
- package/dist/events/interfaces/IEventTransformer.d.ts +7 -0
- package/dist/events/interfaces/IEventTransformer.js +2 -0
- package/dist/events/interfaces/IInternalEvent.d.ts +42 -0
- package/dist/events/interfaces/IInternalEvent.js +2 -0
- package/dist/events/interfaces/index.d.ts +4 -0
- package/dist/events/interfaces/index.js +20 -0
- package/dist/index.d.ts +6 -2
- package/dist/index.js +9 -2
- package/dist/types/alert.types.d.ts +57 -0
- package/dist/types/alert.types.js +22 -0
- package/dist/types/config.types.d.ts +15 -4
- package/dist/types/index.d.ts +2 -0
- package/dist/types/index.js +2 -0
- package/dist/types/issue.types.d.ts +90 -0
- package/dist/types/issue.types.js +40 -0
- package/dist/utils/http-utils.d.ts +13 -0
- package/dist/utils/http-utils.js +117 -0
- package/package.json +2 -1
- package/src/config/config.ts +117 -14
- package/src/{device/local/events/Events.ts → constants/Event.ts} +34 -13
- package/src/db/db.ts +14 -5
- package/src/device/local/entities/AlertBuilder.example.ts +126 -0
- package/src/device/local/entities/AlertBuilder.ts +202 -0
- package/src/device/local/entities/IssueBuilder.example.ts +210 -0
- package/src/device/local/entities/IssueBuilder.ts +263 -0
- package/src/device/local/entities/README.md +173 -0
- package/src/device/local/entities/index.ts +2 -0
- package/src/device/local/interfaces/IDevice.ts +11 -9
- package/src/device/local/models/Alert.model.md +319 -0
- package/src/device/local/models/Alert.model.ts +283 -0
- package/src/device/local/models/Issue.model.md +386 -0
- package/src/device/local/models/Issue.model.ts +350 -0
- package/src/device/local/models/README.md +312 -0
- package/src/device/local/repository/Alert.repository.ts +465 -0
- package/src/device/local/repository/Device.repository.ts +241 -32
- package/src/device/local/repository/Hub.repository.ts +74 -18
- package/src/device/local/repository/Issue.repository.ts +517 -0
- package/src/device/local/repository/Schedule.repository.ts +28 -22
- package/src/device/local/services/Alert.service.ts +617 -5
- package/src/device/local/services/AlertService.example.ts +229 -0
- package/src/device/local/services/Device.service.ts +70 -50
- package/src/device/local/services/Issue.service.ts +872 -0
- package/src/device/local/services/IssueService.example.ts +307 -0
- package/src/device/local/services/index.ts +7 -5
- package/src/events/BaseEventHandler.ts +145 -0
- package/src/events/BaseEventTransformer.ts +97 -0
- package/src/events/DeviceEventHandler.ts +211 -0
- package/src/events/DeviceEventTransformerFactory.ts +77 -0
- package/src/{device/local/events → events}/EventHandler.ts +19 -15
- package/src/events/EventHandlerOrchestrator.ts +119 -0
- package/src/events/EventProcessingService.ts +248 -0
- package/src/events/InternalEventSubscription.ts +219 -0
- package/src/events/index.ts +9 -0
- package/src/events/interfaces/DeviceEvent.ts +56 -0
- package/src/events/interfaces/IEventHandler.ts +28 -0
- package/src/events/interfaces/IEventTransformer.ts +8 -0
- package/src/events/interfaces/IInternalEvent.ts +47 -0
- package/src/events/interfaces/index.ts +4 -0
- package/src/index.ts +9 -2
- package/src/types/alert.types.ts +64 -0
- package/src/types/config.types.ts +17 -4
- package/src/types/index.ts +2 -0
- package/src/types/issue.types.ts +98 -0
- package/src/utils/http-utils.ts +143 -0
- package/src/device/local/events/index.ts +0 -2
|
@@ -0,0 +1,872 @@
|
|
|
1
|
+
import { Service } from "typedi";
|
|
2
|
+
import { IssueRepository } from "../repository/Issue.repository";
|
|
3
|
+
import { IssueModel, IIssueDocument } from "../models/Issue.model";
|
|
4
|
+
import {
|
|
5
|
+
CreateIssueData,
|
|
6
|
+
UpdateIssueData,
|
|
7
|
+
AddCommentData,
|
|
8
|
+
IssueStatus,
|
|
9
|
+
IssuePriority,
|
|
10
|
+
IssuesCategory,
|
|
11
|
+
EntityType,
|
|
12
|
+
} from "../../../types/issue.types";
|
|
13
|
+
import { IssueBuilder } from "../entities/IssueBuilder";
|
|
14
|
+
|
|
15
|
+
@Service()
|
|
16
|
+
export class IssueService {
|
|
17
|
+
constructor(private readonly issueRepository: IssueRepository) {}
|
|
18
|
+
|
|
19
|
+
/**
|
|
20
|
+
* Create a readiness issue using IssueBuilder
|
|
21
|
+
*/
|
|
22
|
+
async createReadinessIssue(
|
|
23
|
+
propertyId: string,
|
|
24
|
+
title: string,
|
|
25
|
+
description: string,
|
|
26
|
+
createdBy: string,
|
|
27
|
+
entityId?: string,
|
|
28
|
+
entityType?: EntityType,
|
|
29
|
+
assignedTo?: string,
|
|
30
|
+
dueDate?: Date
|
|
31
|
+
): Promise<IIssueDocument> {
|
|
32
|
+
const issueBuilder = IssueBuilder.createReadinessIssue()
|
|
33
|
+
.setPropertyId(propertyId)
|
|
34
|
+
.setTitle(title)
|
|
35
|
+
.setDescription(description)
|
|
36
|
+
.setCreatedBy(createdBy);
|
|
37
|
+
|
|
38
|
+
if (entityId) issueBuilder.setEntityId(entityId);
|
|
39
|
+
if (entityType) issueBuilder.setEntityType(entityType);
|
|
40
|
+
if (assignedTo) issueBuilder.setAssignedTo(assignedTo);
|
|
41
|
+
if (dueDate) issueBuilder.setDueDate(dueDate);
|
|
42
|
+
|
|
43
|
+
return await this.createIssue(issueBuilder);
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
/**
|
|
47
|
+
* Create an operations issue using IssueBuilder
|
|
48
|
+
*/
|
|
49
|
+
async createOperationsIssue(
|
|
50
|
+
propertyId: string,
|
|
51
|
+
title: string,
|
|
52
|
+
description: string,
|
|
53
|
+
createdBy: string,
|
|
54
|
+
entityId?: string,
|
|
55
|
+
entityType?: EntityType,
|
|
56
|
+
assignedTo?: string,
|
|
57
|
+
dueDate?: Date
|
|
58
|
+
): Promise<IIssueDocument> {
|
|
59
|
+
const issueBuilder = IssueBuilder.createOperationsIssue()
|
|
60
|
+
.setPropertyId(propertyId)
|
|
61
|
+
.setTitle(title)
|
|
62
|
+
.setDescription(description)
|
|
63
|
+
.setCreatedBy(createdBy);
|
|
64
|
+
|
|
65
|
+
if (entityId) issueBuilder.setEntityId(entityId);
|
|
66
|
+
if (entityType) issueBuilder.setEntityType(entityType);
|
|
67
|
+
if (assignedTo) issueBuilder.setAssignedTo(assignedTo);
|
|
68
|
+
if (dueDate) issueBuilder.setDueDate(dueDate);
|
|
69
|
+
|
|
70
|
+
return await this.createIssue(issueBuilder);
|
|
71
|
+
}
|
|
72
|
+
|
|
73
|
+
/**
|
|
74
|
+
* Create a security issue using IssueBuilder
|
|
75
|
+
*/
|
|
76
|
+
async createSecurityIssue(
|
|
77
|
+
propertyId: string,
|
|
78
|
+
title: string,
|
|
79
|
+
description: string,
|
|
80
|
+
createdBy: string,
|
|
81
|
+
entityId?: string,
|
|
82
|
+
entityType?: EntityType,
|
|
83
|
+
assignedTo?: string,
|
|
84
|
+
dueDate?: Date
|
|
85
|
+
): Promise<IIssueDocument> {
|
|
86
|
+
const issueBuilder = IssueBuilder.createSecurityIssue()
|
|
87
|
+
.setPropertyId(propertyId)
|
|
88
|
+
.setTitle(title)
|
|
89
|
+
.setDescription(description)
|
|
90
|
+
.setCreatedBy(createdBy);
|
|
91
|
+
|
|
92
|
+
if (entityId) issueBuilder.setEntityId(entityId);
|
|
93
|
+
if (entityType) issueBuilder.setEntityType(entityType);
|
|
94
|
+
if (assignedTo) issueBuilder.setAssignedTo(assignedTo);
|
|
95
|
+
if (dueDate) issueBuilder.setDueDate(dueDate);
|
|
96
|
+
|
|
97
|
+
return await this.createIssue(issueBuilder);
|
|
98
|
+
}
|
|
99
|
+
|
|
100
|
+
/**
|
|
101
|
+
* Create an energy issue using IssueBuilder
|
|
102
|
+
*/
|
|
103
|
+
async createEnergyIssue(
|
|
104
|
+
propertyId: string,
|
|
105
|
+
title: string,
|
|
106
|
+
description: string,
|
|
107
|
+
createdBy: string,
|
|
108
|
+
entityId?: string,
|
|
109
|
+
entityType?: EntityType,
|
|
110
|
+
assignedTo?: string,
|
|
111
|
+
dueDate?: Date
|
|
112
|
+
): Promise<IIssueDocument> {
|
|
113
|
+
const issueBuilder = IssueBuilder.createEnergyIssue()
|
|
114
|
+
.setPropertyId(propertyId)
|
|
115
|
+
.setTitle(title)
|
|
116
|
+
.setDescription(description)
|
|
117
|
+
.setCreatedBy(createdBy);
|
|
118
|
+
|
|
119
|
+
if (entityId) issueBuilder.setEntityId(entityId);
|
|
120
|
+
if (entityType) issueBuilder.setEntityType(entityType);
|
|
121
|
+
if (assignedTo) issueBuilder.setAssignedTo(assignedTo);
|
|
122
|
+
if (dueDate) issueBuilder.setDueDate(dueDate);
|
|
123
|
+
|
|
124
|
+
return await this.createIssue(issueBuilder);
|
|
125
|
+
}
|
|
126
|
+
|
|
127
|
+
/**
|
|
128
|
+
* Create a device-specific issue using IssueBuilder
|
|
129
|
+
*/
|
|
130
|
+
async createDeviceIssue(
|
|
131
|
+
deviceId: string,
|
|
132
|
+
propertyId: string,
|
|
133
|
+
title: string,
|
|
134
|
+
description: string,
|
|
135
|
+
createdBy: string,
|
|
136
|
+
category?: IssuesCategory,
|
|
137
|
+
priority?: IssuePriority,
|
|
138
|
+
assignedTo?: string,
|
|
139
|
+
dueDate?: Date
|
|
140
|
+
): Promise<IIssueDocument> {
|
|
141
|
+
const issueBuilder = IssueBuilder.createDeviceIssue(deviceId, propertyId)
|
|
142
|
+
.setTitle(title)
|
|
143
|
+
.setDescription(description)
|
|
144
|
+
.setCreatedBy(createdBy);
|
|
145
|
+
|
|
146
|
+
if (category) issueBuilder.setCategory(category);
|
|
147
|
+
if (priority) issueBuilder.setPriority(priority);
|
|
148
|
+
if (assignedTo) issueBuilder.setAssignedTo(assignedTo);
|
|
149
|
+
if (dueDate) issueBuilder.setDueDate(dueDate);
|
|
150
|
+
|
|
151
|
+
return await this.createIssue(issueBuilder);
|
|
152
|
+
}
|
|
153
|
+
|
|
154
|
+
/**
|
|
155
|
+
* Create a hub-specific issue using IssueBuilder
|
|
156
|
+
*/
|
|
157
|
+
async createHubIssue(
|
|
158
|
+
hubId: string,
|
|
159
|
+
propertyId: string,
|
|
160
|
+
title: string,
|
|
161
|
+
description: string,
|
|
162
|
+
createdBy: string,
|
|
163
|
+
category?: IssuesCategory,
|
|
164
|
+
priority?: IssuePriority,
|
|
165
|
+
assignedTo?: string,
|
|
166
|
+
dueDate?: Date
|
|
167
|
+
): Promise<IIssueDocument> {
|
|
168
|
+
const issueBuilder = IssueBuilder.createHubIssue(hubId, propertyId)
|
|
169
|
+
.setTitle(title)
|
|
170
|
+
.setDescription(description)
|
|
171
|
+
.setCreatedBy(createdBy);
|
|
172
|
+
|
|
173
|
+
if (category) issueBuilder.setCategory(category);
|
|
174
|
+
if (priority) issueBuilder.setPriority(priority);
|
|
175
|
+
if (assignedTo) issueBuilder.setAssignedTo(assignedTo);
|
|
176
|
+
if (dueDate) issueBuilder.setDueDate(dueDate);
|
|
177
|
+
|
|
178
|
+
return await this.createIssue(issueBuilder);
|
|
179
|
+
}
|
|
180
|
+
|
|
181
|
+
/**
|
|
182
|
+
* Create a user-specific issue using IssueBuilder
|
|
183
|
+
*/
|
|
184
|
+
async createUserIssue(
|
|
185
|
+
userId: string,
|
|
186
|
+
propertyId: string,
|
|
187
|
+
title: string,
|
|
188
|
+
description: string,
|
|
189
|
+
createdBy: string,
|
|
190
|
+
category?: IssuesCategory,
|
|
191
|
+
priority?: IssuePriority,
|
|
192
|
+
assignedTo?: string,
|
|
193
|
+
dueDate?: Date
|
|
194
|
+
): Promise<IIssueDocument> {
|
|
195
|
+
const issueBuilder = IssueBuilder.createUserIssue(userId, propertyId)
|
|
196
|
+
.setTitle(title)
|
|
197
|
+
.setDescription(description)
|
|
198
|
+
.setCreatedBy(createdBy);
|
|
199
|
+
|
|
200
|
+
if (category) issueBuilder.setCategory(category);
|
|
201
|
+
if (priority) issueBuilder.setPriority(priority);
|
|
202
|
+
if (assignedTo) issueBuilder.setAssignedTo(assignedTo);
|
|
203
|
+
if (dueDate) issueBuilder.setDueDate(dueDate);
|
|
204
|
+
|
|
205
|
+
return await this.createIssue(issueBuilder);
|
|
206
|
+
}
|
|
207
|
+
|
|
208
|
+
/**
|
|
209
|
+
* Create a maintenance issue using IssueBuilder
|
|
210
|
+
*/
|
|
211
|
+
async createMaintenanceIssue(
|
|
212
|
+
propertyId: string,
|
|
213
|
+
title: string,
|
|
214
|
+
description: string,
|
|
215
|
+
createdBy: string,
|
|
216
|
+
entityId?: string,
|
|
217
|
+
entityType?: EntityType,
|
|
218
|
+
assignedTo?: string,
|
|
219
|
+
dueDate?: Date
|
|
220
|
+
): Promise<IIssueDocument> {
|
|
221
|
+
const issueBuilder = IssueBuilder.createMaintenanceIssue(propertyId, entityId, entityType)
|
|
222
|
+
.setTitle(title)
|
|
223
|
+
.setDescription(description)
|
|
224
|
+
.setCreatedBy(createdBy);
|
|
225
|
+
|
|
226
|
+
if (assignedTo) issueBuilder.setAssignedTo(assignedTo);
|
|
227
|
+
if (dueDate) issueBuilder.setDueDate(dueDate);
|
|
228
|
+
|
|
229
|
+
return await this.createIssue(issueBuilder);
|
|
230
|
+
}
|
|
231
|
+
|
|
232
|
+
/**
|
|
233
|
+
* Create an urgent issue using IssueBuilder
|
|
234
|
+
*/
|
|
235
|
+
async createUrgentIssue(
|
|
236
|
+
propertyId: string,
|
|
237
|
+
title: string,
|
|
238
|
+
description: string,
|
|
239
|
+
createdBy: string,
|
|
240
|
+
entityId?: string,
|
|
241
|
+
entityType?: EntityType,
|
|
242
|
+
assignedTo?: string,
|
|
243
|
+
dueDate?: Date
|
|
244
|
+
): Promise<IIssueDocument> {
|
|
245
|
+
const issueBuilder = IssueBuilder.createUrgentIssue(propertyId, entityId, entityType)
|
|
246
|
+
.setTitle(title)
|
|
247
|
+
.setDescription(description)
|
|
248
|
+
.setCreatedBy(createdBy);
|
|
249
|
+
|
|
250
|
+
if (assignedTo) issueBuilder.setAssignedTo(assignedTo);
|
|
251
|
+
if (dueDate) issueBuilder.setDueDate(dueDate);
|
|
252
|
+
|
|
253
|
+
return await this.createIssue(issueBuilder);
|
|
254
|
+
}
|
|
255
|
+
|
|
256
|
+
/**
|
|
257
|
+
* Create a new issue with business logic validation
|
|
258
|
+
* Accepts either a CreateIssueData object or an IssueBuilder instance
|
|
259
|
+
*/
|
|
260
|
+
async createIssue(issueData: CreateIssueData | IssueBuilder): Promise<IIssueDocument> {
|
|
261
|
+
let processedIssueData: CreateIssueData;
|
|
262
|
+
|
|
263
|
+
// Handle IssueBuilder instance
|
|
264
|
+
if (issueData instanceof IssueBuilder) {
|
|
265
|
+
processedIssueData = issueData.build();
|
|
266
|
+
} else {
|
|
267
|
+
processedIssueData = issueData;
|
|
268
|
+
}
|
|
269
|
+
|
|
270
|
+
// Business logic: Validate issue data
|
|
271
|
+
this.validateIssueData(processedIssueData);
|
|
272
|
+
|
|
273
|
+
// Business logic: Set default priority if not provided
|
|
274
|
+
if (!processedIssueData.priority) {
|
|
275
|
+
processedIssueData.priority = this.determineDefaultPriority(processedIssueData.category);
|
|
276
|
+
}
|
|
277
|
+
|
|
278
|
+
// Business logic: Validate due date is in the future
|
|
279
|
+
if (processedIssueData.dueDate && processedIssueData.dueDate <= new Date()) {
|
|
280
|
+
throw new Error("Due date must be in the future");
|
|
281
|
+
}
|
|
282
|
+
|
|
283
|
+
return await this.issueRepository.create(processedIssueData);
|
|
284
|
+
}
|
|
285
|
+
|
|
286
|
+
/**
|
|
287
|
+
* Get issue by ID with business logic
|
|
288
|
+
*/
|
|
289
|
+
async getIssueById(
|
|
290
|
+
id: string,
|
|
291
|
+
includeDeleted = false
|
|
292
|
+
): Promise<IIssueDocument | null> {
|
|
293
|
+
if (!id) {
|
|
294
|
+
throw new Error("Issue ID is required");
|
|
295
|
+
}
|
|
296
|
+
|
|
297
|
+
const issue = await this.issueRepository.findById(id, includeDeleted);
|
|
298
|
+
|
|
299
|
+
// Business logic: Check if issue is overdue
|
|
300
|
+
if (
|
|
301
|
+
issue?.dueDate &&
|
|
302
|
+
issue.dueDate < new Date() &&
|
|
303
|
+
issue.status !== IssueStatus.RESOLVED
|
|
304
|
+
) {
|
|
305
|
+
// You could add a flag or handle overdue logic here
|
|
306
|
+
console.warn(`Issue ${id} is overdue`);
|
|
307
|
+
}
|
|
308
|
+
|
|
309
|
+
return issue;
|
|
310
|
+
}
|
|
311
|
+
|
|
312
|
+
/**
|
|
313
|
+
* Get all issues with business logic filtering
|
|
314
|
+
*/
|
|
315
|
+
async getIssues(
|
|
316
|
+
filters: {
|
|
317
|
+
propertyId?: string;
|
|
318
|
+
assignedTo?: string;
|
|
319
|
+
status?: IssueStatus;
|
|
320
|
+
priority?: IssuePriority;
|
|
321
|
+
category?: IssuesCategory;
|
|
322
|
+
entityType?: EntityType;
|
|
323
|
+
entityId?: string;
|
|
324
|
+
includeDeleted?: boolean;
|
|
325
|
+
limit?: number;
|
|
326
|
+
skip?: number;
|
|
327
|
+
} = {}
|
|
328
|
+
): Promise<IIssueDocument[]> {
|
|
329
|
+
// Business logic: Validate filters
|
|
330
|
+
this.validateFilters(filters);
|
|
331
|
+
|
|
332
|
+
// Business logic: Apply business rules to filters
|
|
333
|
+
const enhancedFilters = this.applyBusinessRules(filters);
|
|
334
|
+
|
|
335
|
+
return await this.issueRepository.findAll(enhancedFilters);
|
|
336
|
+
}
|
|
337
|
+
|
|
338
|
+
/**
|
|
339
|
+
* Update an issue with business logic validation
|
|
340
|
+
*/
|
|
341
|
+
async updateIssue(
|
|
342
|
+
id: string,
|
|
343
|
+
updateData: UpdateIssueData
|
|
344
|
+
): Promise<IIssueDocument | null> {
|
|
345
|
+
if (!id) {
|
|
346
|
+
throw new Error("Issue ID is required");
|
|
347
|
+
}
|
|
348
|
+
|
|
349
|
+
// Business logic: Validate update data
|
|
350
|
+
this.validateUpdateData(updateData);
|
|
351
|
+
|
|
352
|
+
// Business logic: Check if issue exists and is not deleted
|
|
353
|
+
const existingIssue = await this.issueRepository.findById(id);
|
|
354
|
+
if (!existingIssue) {
|
|
355
|
+
throw new Error("Issue not found");
|
|
356
|
+
}
|
|
357
|
+
|
|
358
|
+
// Business logic: Handle status transitions
|
|
359
|
+
if (updateData.status) {
|
|
360
|
+
this.validateStatusTransition(existingIssue.status, updateData.status);
|
|
361
|
+
}
|
|
362
|
+
|
|
363
|
+
// Business logic: Handle priority changes
|
|
364
|
+
if (updateData.priority) {
|
|
365
|
+
this.validatePriorityChange(existingIssue.priority, updateData.priority);
|
|
366
|
+
}
|
|
367
|
+
|
|
368
|
+
return await this.issueRepository.update(id, updateData);
|
|
369
|
+
}
|
|
370
|
+
|
|
371
|
+
/**
|
|
372
|
+
* Soft delete an issue with business logic
|
|
373
|
+
*/
|
|
374
|
+
async deleteIssue(id: string, deletedBy: string): Promise<boolean> {
|
|
375
|
+
if (!id || !deletedBy) {
|
|
376
|
+
throw new Error("Issue ID and deleted by user are required");
|
|
377
|
+
}
|
|
378
|
+
|
|
379
|
+
// Business logic: Check if issue can be deleted
|
|
380
|
+
const issue = await this.issueRepository.findById(id);
|
|
381
|
+
if (!issue) {
|
|
382
|
+
throw new Error("Issue not found");
|
|
383
|
+
}
|
|
384
|
+
|
|
385
|
+
// Business logic: Prevent deletion of resolved issues (optional rule)
|
|
386
|
+
if (issue.status === IssueStatus.RESOLVED) {
|
|
387
|
+
throw new Error("Cannot delete resolved issues");
|
|
388
|
+
}
|
|
389
|
+
|
|
390
|
+
return await this.issueRepository.softDelete(id, deletedBy);
|
|
391
|
+
}
|
|
392
|
+
|
|
393
|
+
/**
|
|
394
|
+
* Permanently delete an issue
|
|
395
|
+
*/
|
|
396
|
+
async permanentlyDeleteIssue(id: string): Promise<boolean> {
|
|
397
|
+
return await this.issueRepository.hardDelete(id);
|
|
398
|
+
}
|
|
399
|
+
|
|
400
|
+
/**
|
|
401
|
+
* Add a comment with business logic
|
|
402
|
+
*/
|
|
403
|
+
async addComment(
|
|
404
|
+
issueId: string,
|
|
405
|
+
commentData: AddCommentData
|
|
406
|
+
): Promise<IIssueDocument | null> {
|
|
407
|
+
if (!issueId || !commentData.userId || !commentData.content) {
|
|
408
|
+
throw new Error("Issue ID, user ID, and comment content are required");
|
|
409
|
+
}
|
|
410
|
+
|
|
411
|
+
// Business logic: Check if issue exists and is active
|
|
412
|
+
const issue = await this.issueRepository.findById(issueId);
|
|
413
|
+
if (!issue) {
|
|
414
|
+
throw new Error("Issue not found");
|
|
415
|
+
}
|
|
416
|
+
|
|
417
|
+
if (issue.isDeleted) {
|
|
418
|
+
throw new Error("Cannot add comment to deleted issue");
|
|
419
|
+
}
|
|
420
|
+
|
|
421
|
+
// Business logic: Update issue status based on comment (optional)
|
|
422
|
+
const shouldUpdateStatus = this.shouldUpdateStatusOnComment(
|
|
423
|
+
commentData.content
|
|
424
|
+
);
|
|
425
|
+
|
|
426
|
+
if (shouldUpdateStatus && issue.status === IssueStatus.PENDING) {
|
|
427
|
+
await this.issueRepository.update(issueId, {
|
|
428
|
+
status: IssueStatus.IN_PROGRESS,
|
|
429
|
+
updatedBy: commentData.userId,
|
|
430
|
+
});
|
|
431
|
+
}
|
|
432
|
+
|
|
433
|
+
// Add comment using the model instance methods
|
|
434
|
+
const issueModel = await IssueModel.findById(issueId);
|
|
435
|
+
if (issueModel) {
|
|
436
|
+
issueModel.addComment(commentData);
|
|
437
|
+
return await issueModel.save();
|
|
438
|
+
}
|
|
439
|
+
|
|
440
|
+
return null;
|
|
441
|
+
}
|
|
442
|
+
|
|
443
|
+
/**
|
|
444
|
+
* Update a comment on an issue
|
|
445
|
+
*/
|
|
446
|
+
async updateComment(
|
|
447
|
+
issueId: string,
|
|
448
|
+
commentId: string,
|
|
449
|
+
content: string,
|
|
450
|
+
userId: string
|
|
451
|
+
): Promise<boolean> {
|
|
452
|
+
const issueModel = await IssueModel.findById(issueId);
|
|
453
|
+
if (!issueModel) return false;
|
|
454
|
+
|
|
455
|
+
const success = issueModel.updateComment(commentId, content, userId);
|
|
456
|
+
if (success) {
|
|
457
|
+
await issueModel.save();
|
|
458
|
+
}
|
|
459
|
+
return success;
|
|
460
|
+
}
|
|
461
|
+
|
|
462
|
+
/**
|
|
463
|
+
* Remove a comment from an issue
|
|
464
|
+
*/
|
|
465
|
+
async removeComment(issueId: string, commentId: string): Promise<boolean> {
|
|
466
|
+
const issueModel = await IssueModel.findById(issueId);
|
|
467
|
+
if (!issueModel) return false;
|
|
468
|
+
|
|
469
|
+
const success = issueModel.removeComment(commentId);
|
|
470
|
+
if (success) {
|
|
471
|
+
await issueModel.save();
|
|
472
|
+
}
|
|
473
|
+
return success;
|
|
474
|
+
}
|
|
475
|
+
|
|
476
|
+
/**
|
|
477
|
+
* Resolve an issue with business logic
|
|
478
|
+
*/
|
|
479
|
+
async resolveIssue(
|
|
480
|
+
id: string,
|
|
481
|
+
resolvedBy: string
|
|
482
|
+
): Promise<IIssueDocument | null> {
|
|
483
|
+
if (!id || !resolvedBy) {
|
|
484
|
+
throw new Error("Issue ID and resolved by user are required");
|
|
485
|
+
}
|
|
486
|
+
|
|
487
|
+
// Business logic: Check if issue can be resolved
|
|
488
|
+
const issue = await this.issueRepository.findById(id);
|
|
489
|
+
if (!issue) {
|
|
490
|
+
throw new Error("Issue not found");
|
|
491
|
+
}
|
|
492
|
+
|
|
493
|
+
if (issue.status === IssueStatus.RESOLVED) {
|
|
494
|
+
throw new Error("Issue is already resolved");
|
|
495
|
+
}
|
|
496
|
+
|
|
497
|
+
if (issue.status === IssueStatus.CANCELLED) {
|
|
498
|
+
throw new Error("Cannot resolve cancelled issue");
|
|
499
|
+
}
|
|
500
|
+
|
|
501
|
+
// Business logic: Auto-assign if not assigned
|
|
502
|
+
if (!issue.assignedTo) {
|
|
503
|
+
await this.issueRepository.update(id, {
|
|
504
|
+
assignedTo: resolvedBy,
|
|
505
|
+
updatedBy: resolvedBy,
|
|
506
|
+
});
|
|
507
|
+
}
|
|
508
|
+
|
|
509
|
+
// Resolve the issue using model instance methods
|
|
510
|
+
const issueModel = await IssueModel.findById(id);
|
|
511
|
+
if (issueModel) {
|
|
512
|
+
issueModel.resolve(resolvedBy);
|
|
513
|
+
return await issueModel.save();
|
|
514
|
+
}
|
|
515
|
+
|
|
516
|
+
return null;
|
|
517
|
+
}
|
|
518
|
+
|
|
519
|
+
/**
|
|
520
|
+
* Reopen a resolved issue
|
|
521
|
+
*/
|
|
522
|
+
async reopenIssue(
|
|
523
|
+
id: string,
|
|
524
|
+
reopenedBy: string
|
|
525
|
+
): Promise<IIssueDocument | null> {
|
|
526
|
+
const issueModel = await IssueModel.findById(id);
|
|
527
|
+
if (!issueModel) return null;
|
|
528
|
+
|
|
529
|
+
issueModel.reopen(reopenedBy);
|
|
530
|
+
return await issueModel.save();
|
|
531
|
+
}
|
|
532
|
+
|
|
533
|
+
/**
|
|
534
|
+
* Assign an issue with business logic
|
|
535
|
+
*/
|
|
536
|
+
async assignIssue(
|
|
537
|
+
id: string,
|
|
538
|
+
userId: string,
|
|
539
|
+
assignedBy: string
|
|
540
|
+
): Promise<IIssueDocument | null> {
|
|
541
|
+
if (!id || !userId || !assignedBy) {
|
|
542
|
+
throw new Error(
|
|
543
|
+
"Issue ID, assignee user ID, and assigned by user are required"
|
|
544
|
+
);
|
|
545
|
+
}
|
|
546
|
+
|
|
547
|
+
// Business logic: Check if issue can be assigned
|
|
548
|
+
const issue = await this.issueRepository.findById(id);
|
|
549
|
+
if (!issue) {
|
|
550
|
+
throw new Error("Issue not found");
|
|
551
|
+
}
|
|
552
|
+
|
|
553
|
+
if (
|
|
554
|
+
issue.status === IssueStatus.RESOLVED ||
|
|
555
|
+
issue.status === IssueStatus.CLOSED
|
|
556
|
+
) {
|
|
557
|
+
throw new Error("Cannot assign resolved or closed issue");
|
|
558
|
+
}
|
|
559
|
+
|
|
560
|
+
// Business logic: Update status to IN_PROGRESS when assigned
|
|
561
|
+
const updateData: UpdateIssueData = {
|
|
562
|
+
assignedTo: userId,
|
|
563
|
+
updatedBy: assignedBy,
|
|
564
|
+
};
|
|
565
|
+
|
|
566
|
+
if (issue.status === IssueStatus.PENDING) {
|
|
567
|
+
updateData.status = IssueStatus.IN_PROGRESS;
|
|
568
|
+
}
|
|
569
|
+
|
|
570
|
+
return await this.issueRepository.update(id, updateData);
|
|
571
|
+
}
|
|
572
|
+
|
|
573
|
+
/**
|
|
574
|
+
* Unassign an issue
|
|
575
|
+
*/
|
|
576
|
+
async unassignIssue(
|
|
577
|
+
id: string,
|
|
578
|
+
unassignedBy: string
|
|
579
|
+
): Promise<IIssueDocument | null> {
|
|
580
|
+
const issueModel = await IssueModel.findById(id);
|
|
581
|
+
if (!issueModel) return null;
|
|
582
|
+
|
|
583
|
+
issueModel.unassign(unassignedBy);
|
|
584
|
+
return await issueModel.save();
|
|
585
|
+
}
|
|
586
|
+
|
|
587
|
+
/**
|
|
588
|
+
* Get issues by property with business logic
|
|
589
|
+
*/
|
|
590
|
+
async getIssuesByProperty(
|
|
591
|
+
propertyId: string,
|
|
592
|
+
includeDeleted = false
|
|
593
|
+
): Promise<IIssueDocument[]> {
|
|
594
|
+
if (!propertyId) {
|
|
595
|
+
throw new Error("Property ID is required");
|
|
596
|
+
}
|
|
597
|
+
|
|
598
|
+
return await this.issueRepository.findByProperty(
|
|
599
|
+
propertyId,
|
|
600
|
+
includeDeleted
|
|
601
|
+
);
|
|
602
|
+
}
|
|
603
|
+
|
|
604
|
+
/**
|
|
605
|
+
* Get issues assigned to a user with business logic
|
|
606
|
+
*/
|
|
607
|
+
async getIssuesByAssignee(
|
|
608
|
+
assignedTo: string,
|
|
609
|
+
includeDeleted = false
|
|
610
|
+
): Promise<IIssueDocument[]> {
|
|
611
|
+
if (!assignedTo) {
|
|
612
|
+
throw new Error("Assignee user ID is required");
|
|
613
|
+
}
|
|
614
|
+
|
|
615
|
+
return await this.issueRepository.findByAssignee(
|
|
616
|
+
assignedTo,
|
|
617
|
+
includeDeleted
|
|
618
|
+
);
|
|
619
|
+
}
|
|
620
|
+
|
|
621
|
+
/**
|
|
622
|
+
* Get issues by entity
|
|
623
|
+
*/
|
|
624
|
+
async getIssuesByEntity(
|
|
625
|
+
entityId: string,
|
|
626
|
+
entityType: EntityType,
|
|
627
|
+
includeDeleted = false
|
|
628
|
+
): Promise<IIssueDocument[]> {
|
|
629
|
+
return await this.issueRepository.findByEntity(
|
|
630
|
+
entityId,
|
|
631
|
+
entityType,
|
|
632
|
+
includeDeleted
|
|
633
|
+
);
|
|
634
|
+
}
|
|
635
|
+
|
|
636
|
+
/**
|
|
637
|
+
* Get issues by status
|
|
638
|
+
*/
|
|
639
|
+
async getIssuesByStatus(
|
|
640
|
+
status: IssueStatus,
|
|
641
|
+
includeDeleted = false
|
|
642
|
+
): Promise<IIssueDocument[]> {
|
|
643
|
+
return await this.issueRepository.findByStatus(status, includeDeleted);
|
|
644
|
+
}
|
|
645
|
+
|
|
646
|
+
/**
|
|
647
|
+
* Get issues by priority
|
|
648
|
+
*/
|
|
649
|
+
async getIssuesByPriority(
|
|
650
|
+
priority: IssuePriority,
|
|
651
|
+
includeDeleted = false
|
|
652
|
+
): Promise<IIssueDocument[]> {
|
|
653
|
+
return await this.issueRepository.findByPriority(priority, includeDeleted);
|
|
654
|
+
}
|
|
655
|
+
|
|
656
|
+
/**
|
|
657
|
+
* Get overdue issues with business logic
|
|
658
|
+
*/
|
|
659
|
+
async getOverdueIssues(includeDeleted = false): Promise<IIssueDocument[]> {
|
|
660
|
+
const overdueIssues = await this.issueRepository.findOverdue(
|
|
661
|
+
includeDeleted
|
|
662
|
+
);
|
|
663
|
+
|
|
664
|
+
// Business logic: Log overdue issues for monitoring
|
|
665
|
+
if (overdueIssues.length > 0) {
|
|
666
|
+
console.warn(`Found ${overdueIssues.length} overdue issues`);
|
|
667
|
+
}
|
|
668
|
+
|
|
669
|
+
return overdueIssues;
|
|
670
|
+
}
|
|
671
|
+
|
|
672
|
+
/**
|
|
673
|
+
* Get upcoming issues (due within specified days)
|
|
674
|
+
*/
|
|
675
|
+
async getUpcomingIssues(
|
|
676
|
+
days: number = 7,
|
|
677
|
+
includeDeleted = false
|
|
678
|
+
): Promise<IIssueDocument[]> {
|
|
679
|
+
return await this.issueRepository.findUpcoming(days, includeDeleted);
|
|
680
|
+
}
|
|
681
|
+
|
|
682
|
+
/**
|
|
683
|
+
* Get issue statistics with business logic
|
|
684
|
+
*/
|
|
685
|
+
async getIssueStatistics(propertyId?: string): Promise<{
|
|
686
|
+
total: number;
|
|
687
|
+
pending: number;
|
|
688
|
+
inProgress: number;
|
|
689
|
+
resolved: number;
|
|
690
|
+
closed: number;
|
|
691
|
+
overdue: number;
|
|
692
|
+
byPriority: Record<IssuePriority, number>;
|
|
693
|
+
byCategory: Record<IssuesCategory, number>;
|
|
694
|
+
}> {
|
|
695
|
+
const stats = await this.issueRepository.getStatistics(propertyId);
|
|
696
|
+
|
|
697
|
+
// Business logic: Calculate additional metrics
|
|
698
|
+
const responseTime = this.calculateAverageResponseTime(stats);
|
|
699
|
+
const resolutionRate = this.calculateResolutionRate(stats);
|
|
700
|
+
|
|
701
|
+
// Log resolution rate for monitoring
|
|
702
|
+
console.log(`Resolution rate: ${resolutionRate.toFixed(2)}%`);
|
|
703
|
+
console.log(`Response time: ${responseTime.toFixed(2)} days`);
|
|
704
|
+
|
|
705
|
+
// Business logic: Add alerts for critical metrics
|
|
706
|
+
if (stats.overdue > 0) {
|
|
707
|
+
console.warn(`Alert: ${stats.overdue} overdue issues detected`);
|
|
708
|
+
}
|
|
709
|
+
|
|
710
|
+
if (stats.byPriority[IssuePriority.CRITICAL] > 0) {
|
|
711
|
+
console.error(
|
|
712
|
+
`Alert: ${
|
|
713
|
+
stats.byPriority[IssuePriority.CRITICAL]
|
|
714
|
+
} critical issues require immediate attention`
|
|
715
|
+
);
|
|
716
|
+
}
|
|
717
|
+
|
|
718
|
+
return stats;
|
|
719
|
+
}
|
|
720
|
+
|
|
721
|
+
/**
|
|
722
|
+
* Search issues with business logic
|
|
723
|
+
*/
|
|
724
|
+
async searchIssues(
|
|
725
|
+
searchTerm: string,
|
|
726
|
+
filters: {
|
|
727
|
+
propertyId?: string;
|
|
728
|
+
includeDeleted?: boolean;
|
|
729
|
+
limit?: number;
|
|
730
|
+
skip?: number;
|
|
731
|
+
} = {}
|
|
732
|
+
): Promise<IIssueDocument[]> {
|
|
733
|
+
if (!searchTerm || searchTerm.trim().length < 2) {
|
|
734
|
+
throw new Error("Search term must be at least 2 characters long");
|
|
735
|
+
}
|
|
736
|
+
|
|
737
|
+
return await this.issueRepository.search(searchTerm, filters);
|
|
738
|
+
}
|
|
739
|
+
|
|
740
|
+
// Private business logic methods
|
|
741
|
+
|
|
742
|
+
private validateIssueData(data: CreateIssueData): void {
|
|
743
|
+
if (!data.title || data.title.trim().length < 5) {
|
|
744
|
+
throw new Error("Issue title must be at least 5 characters long");
|
|
745
|
+
}
|
|
746
|
+
|
|
747
|
+
if (!data.description || data.description.trim().length < 10) {
|
|
748
|
+
throw new Error("Issue description must be at least 10 characters long");
|
|
749
|
+
}
|
|
750
|
+
|
|
751
|
+
if (!data.propertyId) {
|
|
752
|
+
throw new Error("Property ID is required");
|
|
753
|
+
}
|
|
754
|
+
|
|
755
|
+
if (!data.createdBy) {
|
|
756
|
+
throw new Error("Created by user ID is required");
|
|
757
|
+
}
|
|
758
|
+
}
|
|
759
|
+
|
|
760
|
+
private validateFilters(filters: any): void {
|
|
761
|
+
if (filters.limit && (filters.limit < 1 || filters.limit > 100)) {
|
|
762
|
+
throw new Error("Limit must be between 1 and 100");
|
|
763
|
+
}
|
|
764
|
+
|
|
765
|
+
if (filters.skip && filters.skip < 0) {
|
|
766
|
+
throw new Error("Skip must be non-negative");
|
|
767
|
+
}
|
|
768
|
+
}
|
|
769
|
+
|
|
770
|
+
private validateUpdateData(data: UpdateIssueData): void {
|
|
771
|
+
if (data.title && data.title.trim().length < 5) {
|
|
772
|
+
throw new Error("Issue title must be at least 5 characters long");
|
|
773
|
+
}
|
|
774
|
+
|
|
775
|
+
if (data.description && data.description.trim().length < 10) {
|
|
776
|
+
throw new Error("Issue description must be at least 10 characters long");
|
|
777
|
+
}
|
|
778
|
+
}
|
|
779
|
+
|
|
780
|
+
private validateStatusTransition(
|
|
781
|
+
currentStatus: IssueStatus,
|
|
782
|
+
newStatus: IssueStatus
|
|
783
|
+
): void {
|
|
784
|
+
const validTransitions: Record<IssueStatus, IssueStatus[]> = {
|
|
785
|
+
[IssueStatus.PENDING]: [
|
|
786
|
+
IssueStatus.IN_PROGRESS,
|
|
787
|
+
IssueStatus.CANCELLED,
|
|
788
|
+
IssueStatus.ON_HOLD,
|
|
789
|
+
],
|
|
790
|
+
[IssueStatus.IN_PROGRESS]: [
|
|
791
|
+
IssueStatus.RESOLVED,
|
|
792
|
+
IssueStatus.CANCELLED,
|
|
793
|
+
IssueStatus.ON_HOLD,
|
|
794
|
+
],
|
|
795
|
+
[IssueStatus.RESOLVED]: [IssueStatus.CLOSED, IssueStatus.PENDING], // Reopen
|
|
796
|
+
[IssueStatus.CLOSED]: [IssueStatus.PENDING], // Reopen
|
|
797
|
+
[IssueStatus.CANCELLED]: [IssueStatus.PENDING], // Reopen
|
|
798
|
+
[IssueStatus.ON_HOLD]: [IssueStatus.PENDING, IssueStatus.IN_PROGRESS],
|
|
799
|
+
};
|
|
800
|
+
|
|
801
|
+
if (!validTransitions[currentStatus]?.includes(newStatus)) {
|
|
802
|
+
throw new Error(
|
|
803
|
+
`Invalid status transition from ${currentStatus} to ${newStatus}`
|
|
804
|
+
);
|
|
805
|
+
}
|
|
806
|
+
}
|
|
807
|
+
|
|
808
|
+
private validatePriorityChange(
|
|
809
|
+
currentPriority: IssuePriority,
|
|
810
|
+
newPriority: IssuePriority
|
|
811
|
+
): void {
|
|
812
|
+
// Business rule: Only allow priority escalation, not de-escalation for critical issues
|
|
813
|
+
if (
|
|
814
|
+
currentPriority === IssuePriority.CRITICAL &&
|
|
815
|
+
newPriority !== IssuePriority.CRITICAL
|
|
816
|
+
) {
|
|
817
|
+
throw new Error("Cannot de-escalate priority of critical issues");
|
|
818
|
+
}
|
|
819
|
+
}
|
|
820
|
+
|
|
821
|
+
private determineDefaultPriority(category: IssuesCategory): IssuePriority {
|
|
822
|
+
// Business logic: Determine default priority based on category
|
|
823
|
+
const categoryPriorities: Record<IssuesCategory, IssuePriority> = {
|
|
824
|
+
[IssuesCategory.READINESS]: IssuePriority.MEDIUM,
|
|
825
|
+
[IssuesCategory.OPERATIONS]: IssuePriority.HIGH,
|
|
826
|
+
[IssuesCategory.SECURITY]: IssuePriority.CRITICAL,
|
|
827
|
+
[IssuesCategory.ENERGY]: IssuePriority.LOW,
|
|
828
|
+
[IssuesCategory.OTHER]: IssuePriority.MEDIUM,
|
|
829
|
+
};
|
|
830
|
+
|
|
831
|
+
return categoryPriorities[category] || IssuePriority.MEDIUM;
|
|
832
|
+
}
|
|
833
|
+
|
|
834
|
+
private applyBusinessRules(filters: any): any {
|
|
835
|
+
// Business logic: Apply additional filters based on business rules
|
|
836
|
+
const enhancedFilters = { ...filters };
|
|
837
|
+
|
|
838
|
+
// Example: Always exclude cancelled issues unless explicitly requested
|
|
839
|
+
if (
|
|
840
|
+
!enhancedFilters.status ||
|
|
841
|
+
enhancedFilters.status !== IssueStatus.CANCELLED
|
|
842
|
+
) {
|
|
843
|
+
enhancedFilters.status = { $ne: IssueStatus.CANCELLED };
|
|
844
|
+
}
|
|
845
|
+
|
|
846
|
+
return enhancedFilters;
|
|
847
|
+
}
|
|
848
|
+
|
|
849
|
+
private shouldUpdateStatusOnComment(content: string): boolean {
|
|
850
|
+
// Business logic: Determine if comment should trigger status change
|
|
851
|
+
const statusKeywords = [
|
|
852
|
+
"working on",
|
|
853
|
+
"investigating",
|
|
854
|
+
"fixing",
|
|
855
|
+
"resolving",
|
|
856
|
+
];
|
|
857
|
+
return statusKeywords.some((keyword) =>
|
|
858
|
+
content.toLowerCase().includes(keyword)
|
|
859
|
+
);
|
|
860
|
+
}
|
|
861
|
+
|
|
862
|
+
private calculateAverageResponseTime(stats: any): number {
|
|
863
|
+
// Business logic: Calculate average response time (placeholder)
|
|
864
|
+
return 0;
|
|
865
|
+
}
|
|
866
|
+
|
|
867
|
+
private calculateResolutionRate(stats: any): number {
|
|
868
|
+
// Business logic: Calculate resolution rate
|
|
869
|
+
if (stats.total === 0) return 0;
|
|
870
|
+
return ((stats.resolved + stats.closed) / stats.total) * 100;
|
|
871
|
+
}
|
|
872
|
+
}
|