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.
Files changed (132) hide show
  1. package/TROUBLESHOOTING.md +184 -0
  2. package/dist/config/config.d.ts +9 -2
  3. package/dist/config/config.js +97 -14
  4. package/dist/constants/Event.d.ts +75 -0
  5. package/dist/constants/Event.js +78 -0
  6. package/dist/db/db.d.ts +1 -0
  7. package/dist/db/db.js +18 -2
  8. package/dist/device/local/entities/AlertBuilder.d.ts +87 -0
  9. package/dist/device/local/entities/AlertBuilder.example.d.ts +11 -0
  10. package/dist/device/local/entities/AlertBuilder.example.js +117 -0
  11. package/dist/device/local/entities/AlertBuilder.js +179 -0
  12. package/dist/device/local/entities/IssueBuilder.d.ts +109 -0
  13. package/dist/device/local/entities/IssueBuilder.example.d.ts +16 -0
  14. package/dist/device/local/entities/IssueBuilder.example.js +196 -0
  15. package/dist/device/local/entities/IssueBuilder.js +237 -0
  16. package/dist/device/local/entities/index.d.ts +2 -0
  17. package/dist/device/local/entities/index.js +7 -0
  18. package/dist/device/local/interfaces/IDevice.d.ts +10 -9
  19. package/dist/device/local/interfaces/IDevice.js +7 -0
  20. package/dist/device/local/models/Alert.model.d.ts +28 -0
  21. package/dist/device/local/models/Alert.model.js +222 -0
  22. package/dist/device/local/models/Issue.model.d.ts +28 -0
  23. package/dist/device/local/models/Issue.model.js +260 -0
  24. package/dist/device/local/repository/Alert.repository.d.ts +106 -0
  25. package/dist/device/local/repository/Alert.repository.js +374 -0
  26. package/dist/device/local/repository/Device.repository.d.ts +10 -2
  27. package/dist/device/local/repository/Device.repository.js +153 -30
  28. package/dist/device/local/repository/Hub.repository.d.ts +1 -1
  29. package/dist/device/local/repository/Hub.repository.js +60 -18
  30. package/dist/device/local/repository/Issue.repository.d.ts +113 -0
  31. package/dist/device/local/repository/Issue.repository.js +401 -0
  32. package/dist/device/local/repository/Schedule.repository.d.ts +1 -1
  33. package/dist/device/local/repository/Schedule.repository.js +14 -18
  34. package/dist/device/local/services/Alert.service.d.ts +135 -5
  35. package/dist/device/local/services/Alert.service.js +471 -7
  36. package/dist/device/local/services/AlertService.example.d.ts +55 -0
  37. package/dist/device/local/services/AlertService.example.js +148 -0
  38. package/dist/device/local/services/Device.service.d.ts +8 -5
  39. package/dist/device/local/services/Device.service.js +58 -40
  40. package/dist/device/local/services/Issue.service.d.ts +168 -0
  41. package/dist/device/local/services/Issue.service.js +642 -0
  42. package/dist/device/local/services/IssueService.example.d.ts +68 -0
  43. package/dist/device/local/services/IssueService.example.js +177 -0
  44. package/dist/device/local/services/index.d.ts +7 -5
  45. package/dist/device/local/services/index.js +21 -11
  46. package/dist/events/BaseEventHandler.d.ts +43 -0
  47. package/dist/events/BaseEventHandler.js +111 -0
  48. package/dist/events/BaseEventTransformer.d.ts +26 -0
  49. package/dist/events/BaseEventTransformer.js +72 -0
  50. package/dist/events/DeviceEventHandler.d.ts +15 -0
  51. package/dist/events/DeviceEventHandler.js +152 -0
  52. package/dist/events/DeviceEventTransformerFactory.d.ts +27 -0
  53. package/dist/events/DeviceEventTransformerFactory.js +116 -0
  54. package/dist/events/EventHandler.d.ts +11 -0
  55. package/dist/events/EventHandler.js +106 -0
  56. package/dist/events/EventHandlerOrchestrator.d.ts +35 -0
  57. package/dist/events/EventHandlerOrchestrator.js +141 -0
  58. package/dist/events/EventProcessingService.d.ts +43 -0
  59. package/dist/events/EventProcessingService.js +243 -0
  60. package/dist/events/InternalEventSubscription.d.ts +44 -0
  61. package/dist/events/InternalEventSubscription.js +152 -0
  62. package/dist/events/index.d.ts +9 -0
  63. package/dist/events/index.js +21 -0
  64. package/dist/events/interfaces/DeviceEvent.d.ts +48 -0
  65. package/dist/events/interfaces/DeviceEvent.js +2 -0
  66. package/dist/events/interfaces/IEventHandler.d.ts +23 -0
  67. package/dist/events/interfaces/IEventHandler.js +2 -0
  68. package/dist/events/interfaces/IEventTransformer.d.ts +7 -0
  69. package/dist/events/interfaces/IEventTransformer.js +2 -0
  70. package/dist/events/interfaces/IInternalEvent.d.ts +42 -0
  71. package/dist/events/interfaces/IInternalEvent.js +2 -0
  72. package/dist/events/interfaces/index.d.ts +4 -0
  73. package/dist/events/interfaces/index.js +20 -0
  74. package/dist/index.d.ts +6 -2
  75. package/dist/index.js +9 -2
  76. package/dist/types/alert.types.d.ts +57 -0
  77. package/dist/types/alert.types.js +22 -0
  78. package/dist/types/config.types.d.ts +15 -4
  79. package/dist/types/index.d.ts +2 -0
  80. package/dist/types/index.js +2 -0
  81. package/dist/types/issue.types.d.ts +90 -0
  82. package/dist/types/issue.types.js +40 -0
  83. package/dist/utils/http-utils.d.ts +13 -0
  84. package/dist/utils/http-utils.js +117 -0
  85. package/package.json +2 -1
  86. package/src/config/config.ts +117 -14
  87. package/src/{device/local/events/Events.ts → constants/Event.ts} +34 -13
  88. package/src/db/db.ts +14 -5
  89. package/src/device/local/entities/AlertBuilder.example.ts +126 -0
  90. package/src/device/local/entities/AlertBuilder.ts +202 -0
  91. package/src/device/local/entities/IssueBuilder.example.ts +210 -0
  92. package/src/device/local/entities/IssueBuilder.ts +263 -0
  93. package/src/device/local/entities/README.md +173 -0
  94. package/src/device/local/entities/index.ts +2 -0
  95. package/src/device/local/interfaces/IDevice.ts +11 -9
  96. package/src/device/local/models/Alert.model.md +319 -0
  97. package/src/device/local/models/Alert.model.ts +283 -0
  98. package/src/device/local/models/Issue.model.md +386 -0
  99. package/src/device/local/models/Issue.model.ts +350 -0
  100. package/src/device/local/models/README.md +312 -0
  101. package/src/device/local/repository/Alert.repository.ts +465 -0
  102. package/src/device/local/repository/Device.repository.ts +241 -32
  103. package/src/device/local/repository/Hub.repository.ts +74 -18
  104. package/src/device/local/repository/Issue.repository.ts +517 -0
  105. package/src/device/local/repository/Schedule.repository.ts +28 -22
  106. package/src/device/local/services/Alert.service.ts +617 -5
  107. package/src/device/local/services/AlertService.example.ts +229 -0
  108. package/src/device/local/services/Device.service.ts +70 -50
  109. package/src/device/local/services/Issue.service.ts +872 -0
  110. package/src/device/local/services/IssueService.example.ts +307 -0
  111. package/src/device/local/services/index.ts +7 -5
  112. package/src/events/BaseEventHandler.ts +145 -0
  113. package/src/events/BaseEventTransformer.ts +97 -0
  114. package/src/events/DeviceEventHandler.ts +211 -0
  115. package/src/events/DeviceEventTransformerFactory.ts +77 -0
  116. package/src/{device/local/events → events}/EventHandler.ts +19 -15
  117. package/src/events/EventHandlerOrchestrator.ts +119 -0
  118. package/src/events/EventProcessingService.ts +248 -0
  119. package/src/events/InternalEventSubscription.ts +219 -0
  120. package/src/events/index.ts +9 -0
  121. package/src/events/interfaces/DeviceEvent.ts +56 -0
  122. package/src/events/interfaces/IEventHandler.ts +28 -0
  123. package/src/events/interfaces/IEventTransformer.ts +8 -0
  124. package/src/events/interfaces/IInternalEvent.ts +47 -0
  125. package/src/events/interfaces/index.ts +4 -0
  126. package/src/index.ts +9 -2
  127. package/src/types/alert.types.ts +64 -0
  128. package/src/types/config.types.ts +17 -4
  129. package/src/types/index.ts +2 -0
  130. package/src/types/issue.types.ts +98 -0
  131. package/src/utils/http-utils.ts +143 -0
  132. 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
+ }