dt-common-device 14.0.1 → 14.0.2

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 (59) hide show
  1. package/dist/Integrations/twilio/twilio.service.d.ts +1 -2
  2. package/dist/Integrations/twilio/twilio.service.js +16 -20
  3. package/dist/alerts/alert.types.d.ts +5 -1
  4. package/dist/alerts/alert.types.js +4 -0
  5. package/dist/audit/AuditUtils.js +13 -0
  6. package/dist/audit/IAuditProperties.d.ts +7 -1
  7. package/dist/audit/IAuditProperties.js +1 -0
  8. package/dist/audit/PushAudit.d.ts +17 -1
  9. package/dist/audit/PushAudit.js +51 -9
  10. package/dist/config/config.d.ts +2 -1
  11. package/dist/config/config.js +14 -38
  12. package/dist/config/config.types.d.ts +3 -3
  13. package/dist/config/constants.js +1 -0
  14. package/dist/constants/ConnectionProviders.d.ts +4 -0
  15. package/dist/constants/ConnectionProviders.js +4 -0
  16. package/dist/constants/Event.d.ts +22 -0
  17. package/dist/constants/Event.js +22 -0
  18. package/dist/copilotQueue/utils/queueManager.js +2 -35
  19. package/dist/cronicle/Cronicle.service.d.ts +3 -3
  20. package/dist/cronicle/Cronicle.service.js +4 -3
  21. package/dist/cronicle/ICronicle.interface.d.ts +67 -0
  22. package/dist/db/db.js +32 -28
  23. package/dist/emails/emailService.js +23 -41
  24. package/dist/entities/admin/Admin.repository.d.ts +11 -2
  25. package/dist/entities/admin/Admin.repository.js +73 -2
  26. package/dist/entities/admin/Admin.service.d.ts +2 -1
  27. package/dist/entities/admin/Admin.service.js +12 -1
  28. package/dist/entities/admin/IAdmin.d.ts +40 -0
  29. package/dist/entities/connection/Connection.repository.js +12 -4
  30. package/dist/entities/connection/Connection.service.d.ts +2 -1
  31. package/dist/entities/connection/IConnection.d.ts +4 -1
  32. package/dist/entities/connection/IConnection.js +3 -0
  33. package/dist/entities/device/cloud/interfaces/IRawDevice.d.ts +3 -1
  34. package/dist/entities/device/cloud/interfaces/IRawDevice.js +2 -0
  35. package/dist/entities/device/local/interfaces/IDevice.d.ts +6 -0
  36. package/dist/entities/device/local/repository/Device.repository.d.ts +7 -0
  37. package/dist/entities/device/local/repository/Device.repository.js +11 -0
  38. package/dist/entities/device/local/services/Device.service.d.ts +8 -0
  39. package/dist/entities/device/local/services/Device.service.js +21 -0
  40. package/dist/entities/pms/IPms.d.ts +2 -1
  41. package/dist/entities/pms/IPms.js +1 -0
  42. package/dist/entities/pms/pms.service.js +22 -11
  43. package/dist/entities/property/Property.repository.d.ts +1 -0
  44. package/dist/entities/property/Property.repository.js +5 -0
  45. package/dist/entities/property/Property.service.d.ts +1 -0
  46. package/dist/entities/property/Property.service.js +6 -0
  47. package/dist/index.d.ts +1 -0
  48. package/dist/index.js +2 -0
  49. package/dist/issues/issue.types.d.ts +9 -2
  50. package/dist/issues/issue.types.js +8 -0
  51. package/dist/queue/entities/HybridHttpQueue.d.ts +0 -1
  52. package/dist/queue/entities/HybridHttpQueue.js +3 -5
  53. package/dist/queue/utils/queueUtils.d.ts +2 -2
  54. package/dist/queue/utils/queueUtils.js +16 -25
  55. package/dist/queue/utils/rateLimit.utils.js +19 -1
  56. package/dist/utils/http.utils.d.ts +3 -1
  57. package/dist/utils/http.utils.js +8 -0
  58. package/dist/webhookQueue/services/WebhookQueueService.js +3 -36
  59. package/package.json +3 -2
@@ -17,3 +17,70 @@ export interface ICronicle {
17
17
  enabled?: boolean;
18
18
  notes?: string;
19
19
  }
20
+ export interface CronicleJob {
21
+ id: string;
22
+ title: string;
23
+ category: string;
24
+ plugin: string;
25
+ timeZone?: string;
26
+ timezone?: string;
27
+ enabled: number;
28
+ max_children: number;
29
+ target: string;
30
+ params: {
31
+ timeout: number;
32
+ headers: {
33
+ "Content-Type": string;
34
+ "x-api-key": string;
35
+ "User-Agent": string;
36
+ [key: string]: string;
37
+ };
38
+ url: string;
39
+ method: string;
40
+ data: string;
41
+ follow: number;
42
+ ssl_cert_bypass: number;
43
+ success_match: string;
44
+ error_match: string;
45
+ };
46
+ data: {
47
+ name: string;
48
+ cronJobId: string;
49
+ apiUrl: string;
50
+ method: string;
51
+ target: string;
52
+ schedule: {
53
+ years?: number[];
54
+ months?: number[];
55
+ days?: number[];
56
+ weekdays?: number[];
57
+ hours?: number[];
58
+ minutes?: number[];
59
+ };
60
+ notes: string;
61
+ };
62
+ timing: {
63
+ years?: number[];
64
+ months?: number[];
65
+ days?: number[];
66
+ weekdays?: number[];
67
+ hours?: number[];
68
+ minutes?: number[];
69
+ };
70
+ timeout: number;
71
+ notes: string;
72
+ modified: number;
73
+ created: number;
74
+ api_key: string;
75
+ }
76
+ export interface ListSchedules {
77
+ code: number;
78
+ list: {
79
+ page_size: number;
80
+ first_page: number;
81
+ last_page: number;
82
+ length: number;
83
+ type: string;
84
+ };
85
+ rows: CronicleJob[];
86
+ }
package/dist/db/db.js CHANGED
@@ -11,45 +11,49 @@ exports.getWebhookPostgresClient = getWebhookPostgresClient;
11
11
  const pg_1 = require("pg");
12
12
  const config_1 = require("../config/config");
13
13
  const mongoose_1 = __importDefault(require("mongoose"));
14
+ let adminPool = null;
15
+ let accessPool = null;
16
+ let pmsPool = null;
17
+ let webhookPool = null;
14
18
  function getPostgresClient() {
15
19
  const URI = (0, config_1.getAdminPostgresDbUri)();
16
- let pool = null;
17
- if (!pool) {
18
- pool = new pg_1.Pool({
19
- connectionString: URI,
20
- });
21
- }
22
- return pool;
20
+ adminPool ?? (adminPool = new pg_1.Pool({
21
+ connectionString: URI,
22
+ connectionTimeoutMillis: 5000,
23
+ idleTimeoutMillis: 30000,
24
+ }).on("error", (err) => (0, config_1.getLogger)().error(err, `[Postgres] Admin Pool connection error`)));
25
+ (0, config_1.getLogger)().info(`[Postgres] Admin Pool initialized`);
26
+ return adminPool;
23
27
  }
24
28
  function getAccessPostgresClient() {
25
29
  const URI = (0, config_1.getAccessPostgresDbUri)();
26
- let pool = null;
27
- if (!pool) {
28
- pool = new pg_1.Pool({
29
- connectionString: URI,
30
- });
31
- }
32
- return pool;
30
+ accessPool ?? (accessPool = new pg_1.Pool({
31
+ connectionString: URI,
32
+ connectionTimeoutMillis: 5000,
33
+ idleTimeoutMillis: 30000,
34
+ }).on("error", (err) => (0, config_1.getLogger)().error(err, `[Postgres] Access Pool connection error`)));
35
+ (0, config_1.getLogger)().info(`[Postgres] Access Pool initialized`);
36
+ return accessPool;
33
37
  }
34
38
  function getPmsPostgresClient() {
35
39
  const URI = (0, config_1.getPmsPostgresDbUri)();
36
- let pool = null;
37
- if (!pool) {
38
- pool = new pg_1.Pool({
39
- connectionString: URI,
40
- });
41
- }
42
- return pool;
40
+ pmsPool ?? (pmsPool = new pg_1.Pool({
41
+ connectionString: URI,
42
+ connectionTimeoutMillis: 5000,
43
+ idleTimeoutMillis: 30000,
44
+ }).on("error", (err) => (0, config_1.getLogger)().error(err, `[Postgres] PMS Pool connection error`)));
45
+ (0, config_1.getLogger)().info(`[Postgres] PMS Pool initialized`);
46
+ return pmsPool;
43
47
  }
44
48
  function getWebhookPostgresClient() {
45
49
  const URI = (0, config_1.getWebhookPostgresDbUri)();
46
- let pool = null;
47
- if (!pool) {
48
- pool = new pg_1.Pool({
49
- connectionString: URI,
50
- });
51
- }
52
- return pool;
50
+ webhookPool ?? (webhookPool = new pg_1.Pool({
51
+ connectionString: URI,
52
+ connectionTimeoutMillis: 5000,
53
+ idleTimeoutMillis: 30000,
54
+ }).on("error", (err) => (0, config_1.getLogger)().error(err, `[Postgres] Webhook Pool connection error`)));
55
+ (0, config_1.getLogger)().info(`[Postgres] Webhook Pool initialized`);
56
+ return webhookPool;
53
57
  }
54
58
  const connectDatabase = async () => {
55
59
  try {
@@ -236,61 +236,43 @@ let EmailService = (() => {
236
236
  else {
237
237
  imageURL = `https://api-sandbox-new.devicethread.com/dt-logo.png`;
238
238
  }
239
- const CONTACT_US_DETAILS = (0, Email_1.getContactUsDetails)();
240
- const from = CONTACT_US_DETAILS?.FROM_EMAIL_ADDRESS;
241
- if (!process.env.AWS_ACCESS_KEY_ID ||
242
- !process.env.AWS_SECRET_ACCESS_KEY) {
243
- throw new Error("AWS credentials are not set");
244
- }
245
- // Section below is replacing the global variables from template like contact us email, phone and current year.
239
+ const fromEmail = typeof mailData.fromEmail === "string" ? mailData.fromEmail.trim() : "";
246
240
  const currentYear = new Date().getFullYear().toString();
247
- // const logo: any = await GetLogo(ctx);
248
- let finalMessage = message
241
+ const finalMessage = message
249
242
  .replaceAll("{{year}}", currentYear)
250
- .replaceAll("{{contactUsPhone}}", `${CONTACT_US_DETAILS?.PHONE}`)
251
- .replaceAll("{{contactUsEmail}}", `${CONTACT_US_DETAILS?.TO_EMAIL_ADDRESS}`)
243
+ .replaceAll("{{contactUsEmail}}", fromEmail)
252
244
  .replaceAll("{{logo}}", `${imageURL}`);
253
- // .replaceAll('{{logo}}', `${logo}`);
254
- //
255
- const params = {
245
+ (0, config_1.getLogger)().info(`sending email (SES) with params: ${JSON.stringify(toAddr)}`);
246
+ if (ccAddr && Array.isArray(ccAddr) && ccAddr.length > 0) {
247
+ (0, config_1.getLogger)().info(`CC addresses: ${JSON.stringify(ccAddr)}`);
248
+ }
249
+ mailData.from = fromEmail;
250
+ if (pdfBuffer) {
251
+ return;
252
+ }
253
+ await this.sesClient.send(new client_ses_1.SendEmailCommand({
254
+ Source: fromEmail,
256
255
  Destination: {
257
256
  ToAddresses: toAddr,
257
+ ...(ccAddr && Array.isArray(ccAddr) && ccAddr.length > 0
258
+ ? {
259
+ CcAddresses: ccAddr.filter((e) => typeof e === "string" && e.trim()),
260
+ }
261
+ : {}),
258
262
  },
259
263
  Message: {
264
+ Subject: { Data: subject, Charset: "UTF-8" },
260
265
  Body: {
261
- Html: {
262
- Charset: "UTF-8",
263
- Data: finalMessage,
264
- },
265
- },
266
- Subject: {
267
- Charset: "UTF-8",
268
- Data: subject,
266
+ Html: { Data: finalMessage, Charset: "UTF-8" },
269
267
  },
270
268
  },
271
- Source: from, // SENDER_ADDRESS
272
- ReplyToAddresses: [CONTACT_US_DETAILS?.TO_EMAIL_ADDRESS],
273
- };
274
- // Add CC addresses if present
275
- if (ccAddr && Array.isArray(ccAddr) && ccAddr.length > 0) {
276
- params.Destination.CcAddresses = ccAddr;
277
- }
278
- (0, config_1.getLogger)().info(`sending email with params: ${JSON.stringify(toAddr)}`);
279
- if (ccAddr && ccAddr.length > 0) {
280
- (0, config_1.getLogger)().info(`CC addresses: ${JSON.stringify(ccAddr)}`);
281
- }
282
- mailData.from = CONTACT_US_DETAILS?.FROM_EMAIL_ADDRESS;
283
- if (pdfBuffer) {
284
- // this.sendEmailWithAttachments(this.sesClient, mailData, finalMessage, pdfBuffer)
285
- }
286
- else {
287
- await this.sesClient.send(new client_ses_1.SendEmailCommand(params));
288
- }
269
+ ...(fromEmail ? { ReplyToAddresses: [fromEmail] } : {}),
270
+ }));
289
271
  const maskedEmailsList = toAddr.map((email) => (0, Email_1.GetMaskedEmail)(email));
290
272
  (0, config_1.getLogger)().info(`Sending email to: ${maskedEmailsList.join(", ")}`);
291
273
  }
292
274
  catch (error) {
293
- (0, config_1.getLogger)().error("sendMail: Error", error);
275
+ (0, config_1.getLogger)().error("sendMail: Error", JSON.stringify(error));
294
276
  }
295
277
  }
296
278
  };
@@ -1,11 +1,20 @@
1
- import { IAccessGroup, IUser, IZone, IZoneAccessGroup } from "./IAdmin";
1
+ import { IAccessGroup, IDeliverableLocksByAccessGroupResult, IUser, IZone, IZoneAccessGroup, ILocksAndZonesByAccessGroup } from "./IAdmin";
2
2
  export declare class AdminRepository {
3
3
  private readonly deviceRepository;
4
4
  private readonly postgres;
5
5
  private readonly localDeviceService;
6
6
  private readonly redisUtils;
7
7
  constructor();
8
- getZonesByAccessGroupIds(accessGroupIds: string[], propertyId: string): Promise<any[]>;
8
+ /**
9
+ * Raw zones + devices for access groups (all non-HUB devices). Used when callers need the full list (e.g. UI).
10
+ */
11
+ private loadCollectionZonesDevicesByAccessGroupIds;
12
+ getZonesByAccessGroupIds(accessGroupIds: string[], propertyId: string): Promise<ILocksAndZonesByAccessGroup[]>;
13
+ /**
14
+ * ZN-2: same data as {@link getZonesByAccessGroupIds}, but `devices` are only LOCKs that are not
15
+ * under maintenance (zone or device). `maintenanceSkips` is structured data for audit emission.
16
+ */
17
+ getDeliverableLockDevicesByAccessGroupIds(accessGroupIds: string[], propertyId: string): Promise<IDeliverableLocksByAccessGroupResult>;
9
18
  getZonesByAccessGroups(accessGroupIds: string[], type?: string[]): Promise<any[]>;
10
19
  getAccessGroup(accessGroupId: string, propertyId?: string): Promise<IAccessGroup | null>;
11
20
  getZoneAccessGroupByZoneId(zoneId: string): Promise<IZoneAccessGroup[] | null>;
@@ -91,7 +91,13 @@ let AdminRepository = (() => {
91
91
  this.localDeviceService = typedi_1.default.get(services_1.LocalDeviceService);
92
92
  this.redisUtils = typedi_1.default.get(utils_1.RedisUtils);
93
93
  }
94
- async getZonesByAccessGroupIds(accessGroupIds, propertyId) {
94
+ /**
95
+ * Raw zones + devices for access groups (all non-HUB devices). Used when callers need the full list (e.g. UI).
96
+ */
97
+ async loadCollectionZonesDevicesByAccessGroupIds(accessGroupIds, propertyId) {
98
+ if (!accessGroupIds?.length) {
99
+ return [];
100
+ }
95
101
  // If not cached, get the result from the database
96
102
  const result = await this.postgres.query(`SELECT
97
103
  "zc"."id" AS "zoneCollectionMapId",
@@ -100,7 +106,8 @@ let AdminRepository = (() => {
100
106
  "z"."id" AS "zoneId",
101
107
  "z"."name" AS "zoneName",
102
108
  "z"."zoneTypeId",
103
- "zt"."name" AS "zoneTypeName"
109
+ "zt"."name" AS "zoneTypeName",
110
+ "z"."isUnderMaintenance"
104
111
  FROM "dt_zones_collection_map" AS "zc"
105
112
  INNER JOIN "dt_zones" AS "z" ON "zc"."zoneId" = "z"."id"
106
113
  LEFT JOIN "dt_zoneTypes" AS "zt" ON "z"."zoneTypeId" = "zt"."id"
@@ -171,6 +178,14 @@ let AdminRepository = (() => {
171
178
  zoneIds: _zoneIds,
172
179
  excludeDeviceType: interfaces_1.DeviceType.HUB,
173
180
  });
181
+ // Fetch isUnderMaintenance for ALL zone IDs (including child zones not in the collection map)
182
+ const allCollectionIds = collectionZone.map((e) => e.collectionId);
183
+ const [allZoneMaintenanceResult, collectionMaintenanceResult] = await Promise.all([
184
+ this.postgres.query(`SELECT "id", "isUnderMaintenance" FROM "dt_zones" WHERE "id" = ANY($1)`, [_zoneIds]),
185
+ this.postgres.query(`SELECT "id", "isUnderMaintenance" FROM "dt_collections" WHERE "id" = ANY($1)`, [allCollectionIds]),
186
+ ]);
187
+ const zoneMaintenanceMap = new Map(allZoneMaintenanceResult.rows.map((r) => [r.id, r.isUnderMaintenance ?? false]));
188
+ const collectionMaintenanceMap = new Map(collectionMaintenanceResult.rows.map((r) => [r.id, r.isUnderMaintenance ?? false]));
174
189
  const _collectionZone = collectionZone.map((e) => {
175
190
  const zones = e.zoneIds;
176
191
  let devices = [];
@@ -184,18 +199,21 @@ let AdminRepository = (() => {
184
199
  const _devices = devices.concat(device);
185
200
  devices = _devices;
186
201
  // Create zone data for each zone
202
+ // Use zoneMaintenanceMap for isUnderMaintenance so child zones are also covered
187
203
  const zoneInfo = response.find((r) => r.zoneId === element);
188
204
  const zoneData = {
189
205
  zoneId: element,
190
206
  deviceIds: device?.map((d) => d.deviceId), // Get first device ID if available
191
207
  zoneName: zoneInfo?.zoneName || "",
192
208
  zoneType: zoneInfo?.zoneTypeName || null,
209
+ isUnderMaintenance: zoneMaintenanceMap.get(element) ?? false,
193
210
  };
194
211
  zonesData.push(zoneData);
195
212
  });
196
213
  e.devices = devices;
197
214
  e.zones = zonesData;
198
215
  e.parentZone = response;
216
+ e.isUnderMaintenance = collectionMaintenanceMap.get(e.collectionId) ?? false;
199
217
  return e;
200
218
  });
201
219
  const collectionZoneDevices = Array.from(new Set(_collectionZone));
@@ -207,6 +225,59 @@ let AdminRepository = (() => {
207
225
  // );
208
226
  return collectionZoneDevices;
209
227
  }
228
+ async getZonesByAccessGroupIds(accessGroupIds, propertyId) {
229
+ return await this.loadCollectionZonesDevicesByAccessGroupIds(accessGroupIds, propertyId);
230
+ }
231
+ /**
232
+ * ZN-2: same data as {@link getZonesByAccessGroupIds}, but `devices` are only LOCKs that are not
233
+ * under maintenance (zone or device). `maintenanceSkips` is structured data for audit emission.
234
+ */
235
+ async getDeliverableLockDevicesByAccessGroupIds(accessGroupIds, propertyId) {
236
+ if (!accessGroupIds?.length) {
237
+ return { collections: [], maintenanceSkips: [] };
238
+ }
239
+ const raw = await this.loadCollectionZonesDevicesByAccessGroupIds(accessGroupIds, propertyId);
240
+ const maintenanceSkips = [];
241
+ const collections = raw.map((cz) => {
242
+ const inactiveIds = new Set();
243
+ for (const zone of cz.zones || []) {
244
+ if (zone.isUnderMaintenance) {
245
+ (zone.deviceIds || []).forEach((id) => {
246
+ if (id != null && id !== "")
247
+ inactiveIds.add(String(id));
248
+ });
249
+ maintenanceSkips.push({
250
+ accessGroupId: cz.collectionId,
251
+ zoneId: zone.zoneId,
252
+ deviceIds: (zone.deviceIds || [])
253
+ .map((x) => String(x))
254
+ .filter((x) => x !== ""),
255
+ reason: "zone inactive",
256
+ });
257
+ }
258
+ }
259
+ for (const dev of cz.devices || []) {
260
+ if (dev.deviceType?.type !== "LOCK" || !dev.isUnderMaintenance) {
261
+ continue;
262
+ }
263
+ const did = String(dev.deviceId);
264
+ if (inactiveIds.has(did)) {
265
+ continue;
266
+ }
267
+ inactiveIds.add(did);
268
+ maintenanceSkips.push({
269
+ accessGroupId: cz.collectionId,
270
+ zoneId: dev.zoneId,
271
+ deviceIds: [did],
272
+ reason: "device under maintenance",
273
+ });
274
+ }
275
+ const deliverableDevices = (cz.devices || []).filter((d) => d.deviceType?.type === "LOCK" &&
276
+ !inactiveIds.has(String(d.deviceId)));
277
+ return { ...cz, devices: deliverableDevices, skippedDevices: cz.devices.filter((device) => device.deviceType?.type === "LOCK" && inactiveIds.has(String(device.deviceId))) };
278
+ });
279
+ return { collections, maintenanceSkips };
280
+ }
210
281
  async getZonesByAccessGroups(accessGroupIds, type) {
211
282
  // Fetch zone IDs associated with these access groups
212
283
  const zonesIdsQuery = `
@@ -3,7 +3,8 @@ export declare class AdminService {
3
3
  private readonly adminRepository;
4
4
  private readonly redisUtils;
5
5
  constructor();
6
- getZonesByAccessGroupIds(accessGroupIds: string[], propertyId: string): Promise<any[]>;
6
+ getZonesByAccessGroupIds(accessGroupIds: string[], propertyId: string): Promise<import("./IAdmin").ILocksAndZonesByAccessGroup[]>;
7
+ getDeliverableLockDevicesByAccessGroupIds(accessGroupIds: string[], propertyId: string): Promise<import("./IAdmin").IDeliverableLocksByAccessGroupResult>;
7
8
  getZonesByAccessGroups(accessGroupIds: string[], type?: string[]): Promise<any[]>;
8
9
  getAccessGroup(accessGroupId: string, propertyId?: string): Promise<IAccessGroup | null>;
9
10
  getAccessGroupByZoneId(zoneId: string): Promise<IAccessGroup[] | []>;
@@ -95,6 +95,15 @@ let AdminService = (() => {
95
95
  return [];
96
96
  }
97
97
  }
98
+ async getDeliverableLockDevicesByAccessGroupIds(accessGroupIds, propertyId) {
99
+ try {
100
+ return await this.adminRepository.getDeliverableLockDevicesByAccessGroupIds(accessGroupIds, propertyId);
101
+ }
102
+ catch (error) {
103
+ console.log(error);
104
+ return { collections: [], maintenanceSkips: [] };
105
+ }
106
+ }
98
107
  async getZonesByAccessGroups(accessGroupIds, type) {
99
108
  try {
100
109
  return await this.adminRepository.getZonesByAccessGroups(accessGroupIds, type);
@@ -307,7 +316,9 @@ let AdminService = (() => {
307
316
  }
308
317
  async propertyHasSaltoConnection(propertyId) {
309
318
  const connections = await typedi_1.default.get(connection_1.LocalConnectionService).getConnectionsByPropertyId(propertyId);
310
- return connections.every((connection) => connection.connectionProvider === connection_1.ConnectionProvider.SaltoKS ||
319
+ // using 'some' to check, because property may have TV, Thermostats connections as well,
320
+ // In that case 'every' will return false and that breaks the SaltoKS check implementation
321
+ return connections.some((connection) => connection.connectionProvider === connection_1.ConnectionProvider.SaltoKS ||
311
322
  connection.connectionProvider === connection_1.ConnectionProvider.SaltoSpace);
312
323
  }
313
324
  };
@@ -1,3 +1,4 @@
1
+ import { IDevice } from "../device/local/interfaces";
1
2
  export interface IAccessGroup {
2
3
  id: string;
3
4
  propertyId: string;
@@ -48,3 +49,42 @@ export interface IUser {
48
49
  deletedAt?: Date | null;
49
50
  imageURL?: string | null;
50
51
  }
52
+ /** ZN-2 skip metadata for callers that emit maintenance audits (e.g. smart-access-node). */
53
+ export type IZn2MaintenanceSkipReason = "zone inactive" | "device under maintenance";
54
+ export interface IZn2MaintenanceSkip {
55
+ accessGroupId: string;
56
+ zoneId: string;
57
+ deviceIds: string[];
58
+ reason: IZn2MaintenanceSkipReason;
59
+ }
60
+ /** LOCK devices eligible for guest programming after zone + device maintenance rules. */
61
+ export interface IDeliverableLocksByAccessGroupResult {
62
+ collections: {
63
+ collectionId: string;
64
+ devices: IDevice[];
65
+ skippedDevices: IDevice[];
66
+ zones: {
67
+ zoneId: string;
68
+ deviceIds: string[];
69
+ zoneName: string;
70
+ zoneTypeName: string;
71
+ isUnderMaintenance: boolean;
72
+ }[];
73
+ parentZone: any;
74
+ isUnderMaintenance: boolean;
75
+ }[];
76
+ maintenanceSkips: IZn2MaintenanceSkip[];
77
+ }
78
+ export interface ILocksAndZonesByAccessGroup {
79
+ collectionId: string;
80
+ devices: IDevice[];
81
+ zones: {
82
+ zoneId: string;
83
+ deviceIds: string[];
84
+ zoneName: string;
85
+ zoneTypeName: string;
86
+ isUnderMaintenance: boolean;
87
+ }[];
88
+ parentZone: any;
89
+ isUnderMaintenance: boolean;
90
+ }
@@ -83,7 +83,15 @@ let ConnectionRepository = (() => {
83
83
  // Build conditions dynamically based on provided query parameters
84
84
  Object.keys(query).forEach((key) => {
85
85
  const value = query[key];
86
- if (value !== undefined && value !== null) {
86
+ if (value === undefined || value === null)
87
+ return;
88
+ // ✅ Special handling for metaData (JSON)
89
+ if (key === "metaData" && typeof value === "object") {
90
+ conditions.push(`"metaData" @> $${paramIndex}`);
91
+ values.push(JSON.stringify(value));
92
+ paramIndex++;
93
+ }
94
+ else {
87
95
  conditions.push(`"${key}" = $${paramIndex}`);
88
96
  values.push(value);
89
97
  paramIndex++;
@@ -117,9 +125,9 @@ let ConnectionRepository = (() => {
117
125
  .join(", ");
118
126
  const values = Object.values(data);
119
127
  const result = await this.pool.query(`UPDATE dt_connections
120
- SET ${setClause}, "updatedAt" = NOW()
121
- WHERE id = $1 AND "propertyId" = $2
122
- RETURNING *`, [connectionId, propertyId, ...values]);
128
+ SET ${setClause}, "updatedAt" = NOW()
129
+ WHERE id = $1 AND "propertyId" = $2
130
+ RETURNING *`, [connectionId, propertyId, ...values]);
123
131
  if (result.rowCount === 0) {
124
132
  return null;
125
133
  }
@@ -1,7 +1,8 @@
1
1
  import { IConnection } from "./IConnection";
2
+ import { RedisUtils } from "../../utils";
2
3
  export declare class LocalConnectionService {
3
4
  private readonly connectionRepository;
4
- private readonly redisUtils;
5
+ protected readonly redisUtils: RedisUtils;
5
6
  constructor();
6
7
  createConnection(data: Partial<IConnection>): Promise<IConnection>;
7
8
  getConnection(connectionId: string): Promise<IConnection>;
@@ -29,5 +29,8 @@ export declare enum ConnectionProvider {
29
29
  Dusaw = "Dusaw",
30
30
  Lockly = "Lockly",
31
31
  Sifely = "Sifely",
32
- Twilio = "Twilio"
32
+ Twilio = "Twilio",
33
+ Daikin = "Daikin",
34
+ HoneyWell = "HoneyWell",
35
+ UltraLock = "UltraLock"
33
36
  }
@@ -20,4 +20,7 @@ var ConnectionProvider;
20
20
  ConnectionProvider["Lockly"] = "Lockly";
21
21
  ConnectionProvider["Sifely"] = "Sifely";
22
22
  ConnectionProvider["Twilio"] = "Twilio";
23
+ ConnectionProvider["Daikin"] = "Daikin";
24
+ ConnectionProvider["HoneyWell"] = "HoneyWell";
25
+ ConnectionProvider["UltraLock"] = "UltraLock";
23
26
  })(ConnectionProvider || (exports.ConnectionProvider = ConnectionProvider = {}));
@@ -4,7 +4,9 @@ export declare enum DeviceType {
4
4
  LOCK = "LOCK",
5
5
  ELEVATOR_LOCK = "ELEVATOR LOCK",
6
6
  THERMOSTAT = "THERMOSTAT",
7
- TV = "TV"
7
+ TV = "TV",
8
+ LIGHT = "LIGHT",
9
+ ENCODER = "ENCODER"
8
10
  }
9
11
  export interface IRawDevice {
10
12
  deviceId?: string;
@@ -8,4 +8,6 @@ var DeviceType;
8
8
  DeviceType["ELEVATOR_LOCK"] = "ELEVATOR LOCK";
9
9
  DeviceType["THERMOSTAT"] = "THERMOSTAT";
10
10
  DeviceType["TV"] = "TV";
11
+ DeviceType["LIGHT"] = "LIGHT";
12
+ DeviceType["ENCODER"] = "ENCODER";
11
13
  })(DeviceType || (exports.DeviceType = DeviceType = {}));
@@ -1,6 +1,7 @@
1
1
  export interface IDevice {
2
2
  deviceId: string;
3
3
  propertyId: string;
4
+ collectionId?: string;
4
5
  zoneId: string;
5
6
  name: string;
6
7
  hubId: string[];
@@ -48,6 +49,11 @@ export interface IDevice {
48
49
  capabilities?: Record<string, any>;
49
50
  isDeleted?: boolean;
50
51
  deletedAt?: Date;
52
+ /** Persisted maintenance; ZONE-sourced rows mirror parent zone toggles (smart-cloud). */
53
+ isUnderMaintenance?: boolean;
54
+ underMaintenanceSince?: string | Date | null;
55
+ maintenanceReason?: string | null;
56
+ maintenanceSource?: "ZONE" | "DEVICE" | null;
51
57
  }
52
58
  export declare class IStatus {
53
59
  online: boolean;
@@ -8,6 +8,13 @@ export declare class DeviceRepository {
8
8
  getDevice(deviceId: string, withHubDetails?: boolean): Promise<IDevice>;
9
9
  updateDevice(deviceId: string, body: any): Promise<IDevice>;
10
10
  updateDevices(query: any, updateData: any): Promise<any>;
11
+ /** Sync Mongo `devices_v2` when smart-cloud toggles zone maintenance (operational device-service). */
12
+ syncDevicesZoneMaintenance(body: {
13
+ zoneId: string;
14
+ enabled: boolean;
15
+ maintenanceReason?: string | null;
16
+ underMaintenanceSince?: string | null;
17
+ }): Promise<any>;
11
18
  deleteDevice(deviceId: string): Promise<void>;
12
19
  getDevices(deviceIds: string[], withHubDetails?: boolean): Promise<IDevice[]>;
13
20
  getPropertyDevices(propertyId: string, selectDeviceId?: boolean, type?: string, withHubDetails?: boolean): Promise<IDevice[]>;
@@ -96,6 +96,17 @@ let DeviceRepository = (() => {
96
96
  throw new Error(`Failed to update devices: ${error.message}`);
97
97
  }
98
98
  }
99
+ /** Sync Mongo `devices_v2` when smart-cloud toggles zone maintenance (operational device-service). */
100
+ async syncDevicesZoneMaintenance(body) {
101
+ try {
102
+ const response = await this.axiosInstance.put(`/devices/maintenance/zone`, body);
103
+ return response.data;
104
+ }
105
+ catch (error) {
106
+ (0, config_1.getConfig)().LOGGER.error("Failed to sync zone maintenance to devices:", error);
107
+ throw new Error(`Failed to sync zone maintenance: ${error.message || "Unknown error"}`);
108
+ }
109
+ }
99
110
  async deleteDevice(deviceId) {
100
111
  try {
101
112
  await this.axiosInstance.delete(`/devices/${deviceId}`);
@@ -13,6 +13,14 @@ export declare class LocalDeviceService {
13
13
  getDevices(deviceIds: string[], withHubDetails?: boolean): Promise<IDevice[]>;
14
14
  getPropertyDevices(propertyId: string, selectDeviceId?: boolean, type?: string, withHubDetails?: boolean): Promise<IDevice[]>;
15
15
  updateDevice(deviceId: string, body: Partial<IDevice>, auditBody: IAuditProperties): Promise<any>;
16
+ /**
17
+ * When a zone’s maintenance flag changes in smart-cloud, mirror the same rules as `dt_devices`
18
+ * onto operational Mongo (`devices_v2`) via device-service.
19
+ */
20
+ syncDevicesZoneMaintenance(zoneId: string, enabled: boolean, options?: {
21
+ maintenanceReason?: string | null;
22
+ underMaintenanceSince?: string | Date | null;
23
+ }): Promise<any>;
16
24
  updateDevices(query: any, updateData: any): Promise<any>;
17
25
  deleteDevice(deviceId: string, auditBody: IAuditProperties): Promise<any>;
18
26
  getState(deviceId: string): Promise<any>;
@@ -154,6 +154,27 @@ let LocalDeviceService = (() => {
154
154
  await this.deviceRepository.updateDevice(deviceId, body);
155
155
  return await this.eventHandler.onDeviceUpdate(deviceId, body, auditBody);
156
156
  }
157
+ /**
158
+ * When a zone’s maintenance flag changes in smart-cloud, mirror the same rules as `dt_devices`
159
+ * onto operational Mongo (`devices_v2`) via device-service.
160
+ */
161
+ async syncDevicesZoneMaintenance(zoneId, enabled, options) {
162
+ if (!zoneId) {
163
+ throw new Error("Zone ID is required");
164
+ }
165
+ const body = { zoneId, enabled };
166
+ if (enabled && options) {
167
+ if (options.maintenanceReason !== undefined) {
168
+ body.maintenanceReason = options.maintenanceReason;
169
+ }
170
+ const since = options.underMaintenanceSince;
171
+ if (since !== undefined && since !== null) {
172
+ body.underMaintenanceSince =
173
+ since instanceof Date ? since.toISOString() : since;
174
+ }
175
+ }
176
+ return await this.deviceRepository.syncDevicesZoneMaintenance(body);
177
+ }
157
178
  async updateDevices(query, updateData) {
158
179
  if (!query || !updateData) {
159
180
  throw new Error("Query and update data are required");