rez_core 2.2.166 → 2.2.168

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 (66) hide show
  1. package/dist/app.module.js +2 -0
  2. package/dist/app.module.js.map +1 -1
  3. package/dist/module/communication/controller/communication.controller.d.ts +4 -1
  4. package/dist/module/communication/controller/communication.controller.js +23 -3
  5. package/dist/module/communication/controller/communication.controller.js.map +1 -1
  6. package/dist/module/communication/entity/communication-config.entity.js +1 -1
  7. package/dist/module/communication/entity/communication-config.entity.js.map +1 -1
  8. package/dist/module/communication/entity/communication-hub.entity.js +1 -1
  9. package/dist/module/communication/entity/communication-hub.entity.js.map +1 -1
  10. package/dist/module/communication/service/communication.service.d.ts +5 -1
  11. package/dist/module/communication/service/communication.service.js +13 -3
  12. package/dist/module/communication/service/communication.service.js.map +1 -1
  13. package/dist/module/communication/strategies/email/gmail-api.strategy.d.ts +3 -0
  14. package/dist/module/communication/strategies/email/gmail-api.strategy.js +33 -12
  15. package/dist/module/communication/strategies/email/gmail-api.strategy.js.map +1 -1
  16. package/dist/module/communication/strategies/gmail.strategy.d.ts +3 -0
  17. package/dist/module/communication/strategies/gmail.strategy.js +17 -7
  18. package/dist/module/communication/strategies/gmail.strategy.js.map +1 -1
  19. package/dist/module/listmaster/service/list-master.service.js +1 -10
  20. package/dist/module/listmaster/service/list-master.service.js.map +1 -1
  21. package/dist/module/meta/service/entity-service-impl.service.js +0 -1
  22. package/dist/module/meta/service/entity-service-impl.service.js.map +1 -1
  23. package/dist/module/notification/controller/notification.controller.d.ts +17 -0
  24. package/dist/module/notification/controller/notification.controller.js +48 -0
  25. package/dist/module/notification/controller/notification.controller.js.map +1 -0
  26. package/dist/module/notification/entity/notification.entity.d.ts +8 -0
  27. package/dist/module/notification/entity/notification.entity.js +41 -0
  28. package/dist/module/notification/entity/notification.entity.js.map +1 -0
  29. package/dist/module/notification/firebase-admin.config.d.ts +2 -0
  30. package/dist/module/notification/firebase-admin.config.js +10 -0
  31. package/dist/module/notification/firebase-admin.config.js.map +1 -0
  32. package/dist/module/notification/firebase-admin.json +13 -0
  33. package/dist/module/notification/notification.module.js +6 -3
  34. package/dist/module/notification/notification.module.js.map +1 -1
  35. package/dist/module/notification/service/firebase.service.d.ts +11 -0
  36. package/dist/module/notification/service/firebase.service.js +40 -0
  37. package/dist/module/notification/service/firebase.service.js.map +1 -0
  38. package/dist/module/workflow/entity/task-data.entity.d.ts +2 -0
  39. package/dist/module/workflow/entity/task-data.entity.js +9 -1
  40. package/dist/module/workflow/entity/task-data.entity.js.map +1 -1
  41. package/dist/module/workflow/repository/task.repository.js +6 -1
  42. package/dist/module/workflow/repository/task.repository.js.map +1 -1
  43. package/dist/module/workflow/service/task.service.d.ts +1 -0
  44. package/dist/module/workflow/service/task.service.js +40 -0
  45. package/dist/module/workflow/service/task.service.js.map +1 -1
  46. package/dist/tsconfig.build.tsbuildinfo +1 -1
  47. package/package.json +4 -1
  48. package/src/app.module.ts +2 -0
  49. package/src/module/communication/controller/communication.controller.ts +19 -1
  50. package/src/module/communication/entity/communication-config.entity.ts +1 -1
  51. package/src/module/communication/entity/communication-hub.entity.ts +1 -1
  52. package/src/module/communication/service/communication.service.ts +21 -2
  53. package/src/module/communication/strategies/email/gmail-api.strategy.ts +37 -13
  54. package/src/module/communication/strategies/gmail.strategy.ts +15 -9
  55. package/src/module/listmaster/service/list-master.service.ts +1 -15
  56. package/src/module/meta/service/entity-service-impl.service.ts +0 -1
  57. package/src/module/notification/controller/notification.controller.ts +25 -0
  58. package/src/module/notification/entity/notification.entity.ts +20 -0
  59. package/src/module/notification/firebase-admin.config.ts +8 -0
  60. package/src/module/notification/firebase-admin.json +13 -0
  61. package/src/module/notification/notification.module.ts +6 -3
  62. package/src/module/notification/service/firebase.service.ts +31 -0
  63. package/src/module/workflow/entity/task-data.entity.ts +7 -1
  64. package/src/module/workflow/repository/task.repository.ts +10 -2
  65. package/src/module/workflow/service/task.service.ts +49 -0
  66. package/tsconfig.json +5 -4
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "rez_core",
3
- "version": "2.2.166",
3
+ "version": "2.2.168",
4
4
  "description": "",
5
5
  "author": "",
6
6
  "private": false,
@@ -31,6 +31,7 @@
31
31
  "@nestjs/jwt": "^11.0.0",
32
32
  "@nestjs/passport": "^11.0.5",
33
33
  "@nestjs/platform-express": "^11.0.20",
34
+ "@nestjs/schedule": "^6.0.0",
34
35
  "@nestjs/swagger": "^11.0.3",
35
36
  "@nestjs/typeorm": "^11.0.0",
36
37
  "250218_nodejs_core": "file:",
@@ -43,6 +44,7 @@
43
44
  "crypto-js": "^4.2.0",
44
45
  "dotenv": "^16.4.7",
45
46
  "exceljs": "^4.4.0",
47
+ "firebase-admin": "^13.5.0",
46
48
  "googleapis": "^159.0.0",
47
49
  "handlebars": "^4.7.8",
48
50
  "ics": "^3.8.1",
@@ -71,6 +73,7 @@
71
73
  "@nestjs/testing": "^11.0.1",
72
74
  "@swc/cli": "^0.6.0",
73
75
  "@swc/core": "^1.10.7",
76
+ "@types/cron": "^2.0.1",
74
77
  "@types/express": "^5.0.0",
75
78
  "@types/jest": "^29.5.14",
76
79
  "@types/multer": "^1.4.12",
package/src/app.module.ts CHANGED
@@ -17,6 +17,7 @@ import { WorkflowModule } from './module/workflow/workflow.module';
17
17
  import { IcsMeetingModule } from './module/ics/ics.module';
18
18
  import { DashboardModule } from './module/dashboard/dashboard.module';
19
19
  import { CommunicationModule } from './module/communication/communication.module';
20
+ import { ScheduleModule } from '@nestjs/schedule';
20
21
 
21
22
  @Module({
22
23
  imports: [
@@ -38,6 +39,7 @@ import { CommunicationModule } from './module/communication/communication.module
38
39
  IcsMeetingModule,
39
40
  DashboardModule,
40
41
  CommunicationModule,
42
+ ScheduleModule.forRoot(),
41
43
  ],
42
44
  })
43
45
  export class AppModule {}
@@ -8,6 +8,7 @@ import {
8
8
  ParseIntPipe,
9
9
  HttpStatus,
10
10
  HttpCode,
11
+ Query,
11
12
  } from '@nestjs/common';
12
13
  import { CommunicationService } from '../service/communication.service';
13
14
  import {
@@ -84,8 +85,15 @@ export class CommunicationController {
84
85
  async getLevelConfigs(
85
86
  @Param('id', ParseIntPipe) levelId: number,
86
87
  @Param('type') levelType: string,
88
+ @Query('communication_config_type') configType?: 'WA' | 'SMS' | 'EMAIL' | 'TELEPHONE',
89
+ @Query('service') service?: 'API' | 'THIRD_PARTY' | 'SMTP',
90
+ @Query('provider') provider?: string,
87
91
  ) {
88
- return this.communicationService.getLevelConfigs(levelId, levelType);
92
+ return this.communicationService.getLevelConfigs(levelId, levelType, {
93
+ communication_config_type: configType,
94
+ service,
95
+ provider,
96
+ });
89
97
  }
90
98
 
91
99
  @Put('config/:id/status')
@@ -100,6 +108,16 @@ export class CommunicationController {
100
108
  return { message: 'Status updated successfully' };
101
109
  }
102
110
 
111
+ @Post('config/:id/update')
112
+ @HttpCode(HttpStatus.OK)
113
+ async activateConfig(
114
+ @Param('id', ParseIntPipe) hubId: number,
115
+ @Param('status', ParseIntPipe) status: number,
116
+ ) {
117
+ await this.communicationService.updateConfigStatus(hubId, status);
118
+ return { message: 'Configuration activated successfully' };
119
+ }
120
+
103
121
  @Post('gmail/oauth/init')
104
122
  async initGmailOAuth(@Body() initDto: GmailOAuthInitDto) {
105
123
  return this.communicationService.initGmailOAuth(
@@ -7,7 +7,7 @@ import {
7
7
  } from 'typeorm';
8
8
  import { IsNotEmpty, IsObject } from 'class-validator';
9
9
 
10
- @Entity({ name: 'communication_config' })
10
+ @Entity({ name: 'cr_communication_config' })
11
11
  export class CommunicationConfig {
12
12
  @PrimaryGeneratedColumn({ name: 'id', type: 'int' })
13
13
  id: number;
@@ -15,7 +15,7 @@ export enum CommunicationConfigType {
15
15
  TELEPHONE = 'TELEPHONE',
16
16
  }
17
17
 
18
- @Entity({ name: 'communication_hub' })
18
+ @Entity({ name: 'cr_communication_hub' })
19
19
  @Index('idx_level_status', ['level_id', 'level_type', 'status'])
20
20
  @Index('idx_config_type', ['communication_config_type'])
21
21
  @Index('idx_service_provider', ['service', 'provider'])
@@ -146,7 +146,7 @@ export class CommunicationService {
146
146
  ): Promise<CommunicationHubWithConfig[]> {
147
147
  let query = this.hubRepository
148
148
  .createQueryBuilder('hub')
149
- .leftJoin('communication_config', 'config', 'config.id = hub.config_id')
149
+ .leftJoin('cr_communication_config', 'config', 'config.id = hub.config_id')
150
150
  .select([
151
151
  'hub.id as hub_id',
152
152
  'hub.level_id as hub_level_id',
@@ -322,9 +322,28 @@ export class CommunicationService {
322
322
  async getLevelConfigs(
323
323
  levelId: number,
324
324
  levelType: string,
325
+ filters?: {
326
+ communication_config_type?: 'WA' | 'SMS' | 'EMAIL' | 'TELEPHONE';
327
+ service?: 'API' | 'THIRD_PARTY' | 'SMTP';
328
+ provider?: string;
329
+ },
325
330
  ): Promise<CommunicationHub[]> {
331
+ const where: any = { level_id: levelId, level_type: levelType };
332
+
333
+ if (filters?.communication_config_type) {
334
+ where.communication_config_type = filters.communication_config_type;
335
+ }
336
+
337
+ if (filters?.service) {
338
+ where.service = filters.service;
339
+ }
340
+
341
+ if (filters?.provider) {
342
+ where.provider = filters.provider;
343
+ }
344
+
326
345
  return this.hubRepository.find({
327
- where: { level_id: levelId, level_type: levelType },
346
+ where,
328
347
  order: { created_at: 'DESC' },
329
348
  });
330
349
  }
@@ -1,4 +1,5 @@
1
1
  import { Injectable } from '@nestjs/common';
2
+ import { ConfigService } from '@nestjs/config';
2
3
  import axios from 'axios';
3
4
  import {
4
5
  CommunicationStrategy,
@@ -7,20 +8,26 @@ import {
7
8
 
8
9
  @Injectable()
9
10
  export class GmailApiStrategy implements CommunicationStrategy {
11
+ constructor(private readonly configService: ConfigService) {}
10
12
  async sendMessage(
11
13
  to: string | string[],
12
14
  message: string,
13
15
  config: any,
14
16
  ): Promise<CommunicationResult> {
15
17
  try {
16
- if (!this.validateConfig(config)) {
17
- throw new Error('Invalid Gmail API configuration');
18
+ if (!config || !config.refreshToken) {
19
+ throw new Error('Invalid Gmail API configuration - missing refresh token');
18
20
  }
19
21
 
20
- const { accessToken, subject, html, cc, bcc } = config;
22
+ const { subject, html, cc, bcc } = config;
21
23
 
22
24
  console.log('---- Using HTTP Gmail API ----');
23
- console.log('accessToken:', accessToken ? 'Present' : 'Missing');
25
+ console.log('Always refreshing token before send...');
26
+
27
+ // ALWAYS refresh token first - no expiration checking
28
+ const freshAccessToken = await this.refreshAccessToken(config);
29
+
30
+ console.log('---- Fresh token obtained, preparing email ----');
24
31
 
25
32
  const toRecipients = Array.isArray(to) ? to.join(', ') : to;
26
33
  const ccRecipients = cc
@@ -63,7 +70,7 @@ export class GmailApiStrategy implements CommunicationStrategy {
63
70
  .replace(/\//g, '_')
64
71
  .replace(/=+$/, '');
65
72
 
66
- // Direct HTTP call to Gmail API
73
+ // Send with fresh token
67
74
  const response = await axios.post(
68
75
  'https://gmail.googleapis.com/gmail/v1/users/me/messages/send',
69
76
  {
@@ -71,7 +78,7 @@ export class GmailApiStrategy implements CommunicationStrategy {
71
78
  },
72
79
  {
73
80
  headers: {
74
- Authorization: `Bearer ${accessToken}`,
81
+ Authorization: `Bearer ${freshAccessToken}`,
75
82
  'Content-Type': 'application/json',
76
83
  },
77
84
  timeout: 30000, // 30 second timeout
@@ -88,6 +95,7 @@ export class GmailApiStrategy implements CommunicationStrategy {
88
95
  provider: 'gmail',
89
96
  service: 'API',
90
97
  timestamp: new Date(),
98
+ refreshedToken: freshAccessToken, // Always return the fresh token for database update
91
99
  };
92
100
  } catch (error) {
93
101
  console.error('---- Gmail HTTP API Error ----');
@@ -111,18 +119,29 @@ export class GmailApiStrategy implements CommunicationStrategy {
111
119
  validateConfig(config: any): boolean {
112
120
  if (!config) return false;
113
121
 
114
- // For HTTP API, we just need accessToken
115
- return config.accessToken && typeof config.accessToken === 'string';
122
+ // For always-refresh strategy, we only need a refresh token
123
+ return config.refreshToken && typeof config.refreshToken === 'string';
116
124
  }
117
125
 
118
126
  async refreshAccessToken(config: any): Promise<string> {
119
127
  try {
120
- const { clientId, clientSecret, refreshToken } = config;
128
+ const { refreshToken } = config;
121
129
 
122
- if (!clientId || !clientSecret || !refreshToken) {
123
- throw new Error('Missing OAuth credentials for token refresh');
130
+ if (!refreshToken) {
131
+ throw new Error('Missing refresh token for token refresh');
124
132
  }
125
133
 
134
+ // Get system OAuth credentials
135
+ const clientId = this.configService.get<string>('CLIENT_ID');
136
+ const clientSecret = this.configService.get<string>('CLIENT_SECRET');
137
+
138
+ if (!clientId || !clientSecret) {
139
+ throw new Error('Gmail OAuth system credentials not configured');
140
+ }
141
+
142
+ console.log('---- Refreshing token with system credentials ----');
143
+
144
+ // Use Google's OAuth endpoint to refresh token with system credentials
126
145
  const response = await axios.post(
127
146
  'https://oauth2.googleapis.com/token',
128
147
  {
@@ -133,12 +152,17 @@ export class GmailApiStrategy implements CommunicationStrategy {
133
152
  },
134
153
  {
135
154
  headers: {
136
- 'Content-Type': 'application/json',
155
+ 'Content-Type': 'application/x-www-form-urlencoded',
137
156
  },
138
157
  },
139
158
  );
140
159
 
141
- return response.data.access_token || '';
160
+ if (!response.data.access_token) {
161
+ throw new Error('No access token received from refresh');
162
+ }
163
+
164
+ console.log('---- Token refreshed successfully ----');
165
+ return response.data.access_token;
142
166
  } catch (error) {
143
167
  console.error(
144
168
  'Failed to refresh access token:',
@@ -1,4 +1,5 @@
1
1
  import { Injectable } from '@nestjs/common';
2
+ import { ConfigService } from '@nestjs/config';
2
3
  import { google } from 'googleapis';
3
4
  import {
4
5
  CommunicationStrategy,
@@ -7,6 +8,7 @@ import {
7
8
 
8
9
  @Injectable()
9
10
  export class GmailStrategy implements CommunicationStrategy {
11
+ constructor(private readonly configService: ConfigService) {}
10
12
  async sendMessage(
11
13
  to: string,
12
14
  message: string,
@@ -21,8 +23,16 @@ export class GmailStrategy implements CommunicationStrategy {
21
23
  throw new Error('Missing required Gmail credentials: refreshToken and email are required');
22
24
  }
23
25
 
24
- // Create a minimal OAuth2 client just for token management
25
- const oauth2Client = new google.auth.OAuth2();
26
+ // Get system OAuth credentials
27
+ const systemClientId = this.configService.get<string>('CLIENT_ID');
28
+ const systemClientSecret = this.configService.get<string>('CLIENT_SECRET');
29
+
30
+ if (!systemClientId || !systemClientSecret) {
31
+ throw new Error('Gmail OAuth system credentials not configured');
32
+ }
33
+
34
+ // Create OAuth2 client with system credentials
35
+ const oauth2Client = new google.auth.OAuth2(systemClientId, systemClientSecret);
26
36
 
27
37
  oauth2Client.setCredentials({
28
38
  refresh_token: refreshToken,
@@ -75,7 +85,7 @@ export class GmailStrategy implements CommunicationStrategy {
75
85
  refreshedToken: config.accessToken !== accessToken ? config.accessToken : undefined,
76
86
  };
77
87
  } catch (error) {
78
- console.log('Gmail strategy error:', error.message);
88
+ console.log('Gmail strategy error:', error);
79
89
  console.log('Config validation:', JSON.stringify({
80
90
  hasRefreshToken: !!config.refreshToken,
81
91
  hasAccessToken: !!config.accessToken,
@@ -93,11 +103,7 @@ export class GmailStrategy implements CommunicationStrategy {
93
103
  }
94
104
 
95
105
  validateConfig(config: any): boolean {
96
- return !!(
97
- config.clientId &&
98
- config.clientSecret &&
99
- config.refreshToken &&
100
- config.email
101
- );
106
+ // Since we use system credentials, we only need refreshToken and email from config
107
+ return !!(config.refreshToken && config.email);
102
108
  }
103
109
  }
@@ -296,16 +296,14 @@ export class ListMasterService {
296
296
  async createEntity(entityData: any, loggedInUser: UserData): Promise<any> {
297
297
  // Trim and validate name and code
298
298
  const name = entityData.name?.trim();
299
- const code = entityData.code?.trim();
300
299
 
301
- if (!name || !code) {
300
+ if (!name) {
302
301
  throw new BadRequestException(
303
302
  'Name and Code are required and cannot be empty',
304
303
  );
305
304
  }
306
305
 
307
306
  entityData.name = name;
308
- entityData.code = code;
309
307
  entityData.is_factory = entityData.is_factory || 0;
310
308
  entityData.source = entityData.source || 'master';
311
309
  entityData.status = entityData.status || 'ACTIVE';
@@ -325,18 +323,6 @@ export class ListMasterService {
325
323
  );
326
324
  }
327
325
 
328
- // Check for duplicate code
329
- const codeExists = await this.listMasterRepo.findOneByCondition({
330
- code,
331
- organization_id: loggedInUser.organization_id,
332
- });
333
-
334
- if (codeExists) {
335
- throw new BadRequestException(
336
- 'A List Master with the same code already exists',
337
- );
338
- }
339
-
340
326
  const createdListMaster = await this.entityServiceImpl.createEntity(
341
327
  entityData,
342
328
  loggedInUser,
@@ -318,7 +318,6 @@ export class EntityServiceImpl implements EntityService<BaseEntity> {
318
318
  if (!entityData.enterprise_id)
319
319
  entityData.enterprise_id = loggedInUser.enterprise_id;
320
320
  }
321
- console.log(entityData, 'WWWWWWWWW');
322
321
  await repoService.update(entityData.id, entityData);
323
322
  return await repoService.findOne({ where: { id: entityData.id } });
324
323
  }
@@ -0,0 +1,25 @@
1
+ import { Body, Controller, Post } from '@nestjs/common';
2
+ import { NotificationsService } from '../service/firebase.service';
3
+
4
+ @Controller('notifications')
5
+ export class NotificationsController {
6
+ constructor(private readonly notificationsService: NotificationsService) {}
7
+
8
+ // Store token from frontend
9
+ @Post('register-token')
10
+ async registerToken(@Body() body: { userId?: string; token: string }) {
11
+ // Optional: associate token with userId if provided
12
+ return this.notificationsService.saveToken(body.userId, body.token);
13
+ }
14
+
15
+ @Post('send')
16
+ async sendNotification(
17
+ @Body() body: { token: string; title: string; message: string },
18
+ ) {
19
+ return this.notificationsService.sendToDevice(
20
+ body.token,
21
+ body.title,
22
+ body.message,
23
+ );
24
+ }
25
+ }
@@ -0,0 +1,20 @@
1
+ import { BaseEntity } from 'src/module/meta/entity/base-entity.entity';
2
+ import { Column, Entity } from 'typeorm';
3
+
4
+ @Entity({ name: 'cr_notification' })
5
+ export class NotificationData extends BaseEntity {
6
+ @Column({ name: 'user_id', type: 'int', nullable: true })
7
+ user_id: number | null;
8
+
9
+ @Column({ name: 'event_type', type: 'varchar', length: 255, nullable: true })
10
+ event_type: string | null;
11
+
12
+ @Column({ name: 'message', type: 'text', nullable: true })
13
+ message: string | null;
14
+
15
+ @Column({ nullable: true })
16
+ mapped_entity_id: number;
17
+
18
+ @Column({ nullable: true })
19
+ mapped_entity_type: string;
20
+ }
@@ -0,0 +1,8 @@
1
+ import * as admin from 'firebase-admin';
2
+ import * as serviceAccount from './firebase-admin.json';
3
+
4
+ admin.initializeApp({
5
+ credential: admin.credential.cert(serviceAccount as admin.ServiceAccount),
6
+ });
7
+
8
+ export const firebaseAdmin = admin;
@@ -0,0 +1,13 @@
1
+ {
2
+ "type": "service_account",
3
+ "project_id": "sample-d7855",
4
+ "private_key_id": "753237e3e7d64a87155b916deda9f3d1cb9cdd52",
5
+ "private_key": "-----BEGIN PRIVATE KEY-----\nMIIEvgIBADANBgkqhkiG9w0BAQEFAASCBKgwggSkAgEAAoIBAQChrgvD+0M3u8EU\ntm3xV27RGuYRSjIpZIRi6I3tH0BTwpOde4DnwZWUQnTEbg/wgBGvFtGS7T4UXaef\ngkhTSRqPhIqEaIVRXf34NOhYTAOxBcvDaG9skQbOzhH9gz8cEG1HyDRCWqmxgpCy\nt0jUig5Ta72t0zSaQKc/H6AhwWF8Yiz+KnwGR837FvKDM+ZN28B21sRNcOciDXHf\nEeh4H4GjBQguM+5aMGFRACclIFztwxwTBXCJ2r/AMxSjCntzOCHimgG6dOtlW/KS\neZqunZjxvbmHZpj3N1sGRJQFHFjRLQSgCdMzwOclJ82AImcOYLJ3w1kKTmts6JlW\nVYvj6jzpAgMBAAECggEAA9xhIYv1ffY41SLKG1JILiwjRFRuQDjLRIVjJj9xzM4/\nsioJ2P7xcHnpyesK7GZShcjYtlZ9/gSChTeaPIrpXHYAzMqmLFw/PVceGUWvbOHD\njOYIiyIndHbNTS1+K1BlG6q3PMJnfPquM+ZWkeS+FNXk0KrTfb9/fu1laIjyqAKy\na8w7IwNkk4iBkKxRc74204TxQsgUpooROuoP9JfoVVL90R1EDFJWnrciTcqB77SS\nAFsPbOY9kZ5WOmUp6ZzkQD434fRRCUis7WdicV4OU0VEQmLX57UxzxNTOZzw8Adn\nnn7nmFXfcF5+ODX1htkTSn84p+qP4CR+a99/4rLqgQKBgQDbbsySP4I0KBhXT+xQ\n8iHpTo8lV71R1t4gboBIQXIHkmlrI9Qy/oOe3yViATp54oH727W/vWm4OEomzpl4\nNrs27HrI65XZcYyRCU6mLEaDx2ZjYYKHLMwx0DdQ9MPn+n1JusadiRN671ZZ3smb\nbtMLqeslmoN43SPDiG2CSNOycQKBgQC8n3RViNpHZJDUQoLxHZ7rpfIa/0v3qpjd\nI3nDxDH8udu2ipsVjqXmGJTBY+NQcWhuvuhYTzH5plnSuD3tBrPnA3VoN7FFDd29\nx+5K60acjgbu0sa9yO4aSAYD7sjkfkxVAKhzIgNWlAzl2zluakq5r3+uGQ+FVvJh\nmo5FxSj9+QKBgBkaBqrgOvvObmJmkSj9WeW/h96Et/KJuuVI3sHlQq8dD5QjCB5B\nQTtGWZdpfo/82lO+YX8qotJhFhJ0Zdf7otT4nl8nm//A3oyk3OtjezmN8OeDexQN\nitpT8FABf/ukivqJNDlHOgRBJsanFrcCKYBXEsA9eba0zWeLzsMto5HxAoGBAIaH\nW5udzcDZkwownd+GdtAvPSvQJchwnjIqmS/tAJH1pSTeWpnXca9YnNAJhBjdqdRC\nyMgjQ8uAv9OwoEorW6hKKTS5c++CYkJ3FBfPEj+adItlPWYipt+Luu6XIiUFhz+h\nBoOHIMZhNYnC/4UmvkENUI1FRnKdfqXqa8qLQh9RAoGBAMxOm2fMUKzLPY8xqp0j\nWmo3ZYMkRRi/BFqVn1F+aFGx0ahW8GVT70qy30rhiNJAkl2QnPnllQ6rD/CBc3UT\n3kJLwTc5BWXP1nKxEOuu33xB5wk4mIzyItFf/eYhYFzsWMpJo2gaKE3iNNUKUP0W\nG2coAWXT2iRSS6OHh9B1bRnQ\n-----END PRIVATE KEY-----\n",
6
+ "client_email": "firebase-adminsdk-fbsvc@sample-d7855.iam.gserviceaccount.com",
7
+ "client_id": "115995668082397151669",
8
+ "auth_uri": "https://accounts.google.com/o/oauth2/auth",
9
+ "token_uri": "https://oauth2.googleapis.com/token",
10
+ "auth_provider_x509_cert_url": "https://www.googleapis.com/oauth2/v1/certs",
11
+ "client_x509_cert_url": "https://www.googleapis.com/robot/v1/metadata/x509/firebase-adminsdk-fbsvc%40sample-d7855.iam.gserviceaccount.com",
12
+ "universe_domain": "googleapis.com"
13
+ }
@@ -12,10 +12,13 @@ import { ConfigModule, ConfigService } from '@nestjs/config';
12
12
  import { AuthModule } from '../auth/auth.module';
13
13
  import { UserModule } from '../user/user.module';
14
14
  import { IcsMeetingModule } from '../ics/ics.module';
15
+ import { NotificationData } from './entity/notification.entity';
16
+ import { NotificationsController } from './controller/notification.controller';
17
+ import { NotificationsService } from './service/firebase.service';
15
18
 
16
19
  @Module({
17
20
  imports: [
18
- TypeOrmModule.forFeature([Otp]),
21
+ TypeOrmModule.forFeature([Otp, NotificationData]),
19
22
  MailerModule.forRootAsync({
20
23
  imports: [ConfigModule],
21
24
  inject: [ConfigService],
@@ -45,8 +48,8 @@ import { IcsMeetingModule } from '../ics/ics.module';
45
48
  UserModule,
46
49
  IcsMeetingModule,
47
50
  ],
48
- providers: [OtpService, OtpRepository, EmailService],
51
+ providers: [OtpService, OtpRepository, EmailService, NotificationsService],
49
52
  exports: [OtpService, EmailService],
50
- controllers: [OtpController],
53
+ controllers: [OtpController, NotificationsController],
51
54
  })
52
55
  export class NotificationModule {}
@@ -0,0 +1,31 @@
1
+ import { Injectable } from '@nestjs/common';
2
+ import { firebaseAdmin } from '../firebase-admin.config';
3
+
4
+ @Injectable()
5
+ export class NotificationsService {
6
+ private tokens: Map<string, string> = new Map(); // store in memory for now
7
+
8
+ async saveToken(userId: string | undefined, token: string) {
9
+ if (userId) {
10
+ this.tokens.set(userId, token);
11
+ }
12
+ return { success: true, token };
13
+ }
14
+
15
+ async sendToDevice(token: string, title: string, body: string) {
16
+ const message = {
17
+ token,
18
+ notification: { title, body },
19
+ };
20
+
21
+ return firebaseAdmin.messaging().send(message);
22
+ }
23
+
24
+ // Helper: send to a registered user by userId
25
+ async sendToUser(userId: string, title: string, body: string) {
26
+ const token = this.tokens.get(userId);
27
+ if (!token) return { error: 'No token found for user' };
28
+
29
+ return this.sendToDevice(token, title, body);
30
+ }
31
+ }
@@ -56,7 +56,7 @@ export class TaskDataEntity extends BaseEntity {
56
56
  @Column({ nullable: true })
57
57
  due_date: Date;
58
58
 
59
- @Column({ nullable: true, type: 'varchar' })
59
+ @Column({ nullable: true, type: 'time' })
60
60
  due_time: string;
61
61
 
62
62
  @Column({ nullable: true, type: 'varchar' })
@@ -67,4 +67,10 @@ export class TaskDataEntity extends BaseEntity {
67
67
 
68
68
  @Column({ nullable: true, type: 'varchar' })
69
69
  category: string;
70
+
71
+ @Column({ nullable: true })
72
+ remainder_date: Date;
73
+
74
+ @Column({ nullable: true, type: 'varchar' })
75
+ remainder_time: string;
70
76
  }
@@ -50,6 +50,13 @@ export class TaskRepository {
50
50
  [act.action_requirement, loggedInUser.organization_id],
51
51
  );
52
52
 
53
+ const now = new Date();
54
+
55
+ const dueDateTime = new Date(now.setDate(now.getDate() + 2));
56
+
57
+ const due_date = dueDateTime.toISOString().split('T')[0];
58
+ const due_time = dueDateTime.toTimeString().split(' ')[0];
59
+
53
60
  const taskData = this.TaskRepository.create({
54
61
  stage_id: act.stage_id,
55
62
  user_id: loggedInUser.id,
@@ -60,12 +67,13 @@ export class TaskRepository {
60
67
  is_mandatory: is_mandatory[0]?.code === 'mandatory' ? true : false,
61
68
  mapped_entity_id,
62
69
  mapped_entity_type,
63
- due_date: new Date(new Date().setDate(new Date().getDate() + 2)),
70
+ due_date,
71
+ due_time,
64
72
  is_system: true,
65
73
  status: todoListMasterItemId[0]?.id,
66
74
  category: act?.action_category_code,
67
75
  task_owner: leadData[0]?.lead_owner,
68
- } as TaskDataEntity);
76
+ });
69
77
  await this.TaskRepository.save(taskData);
70
78
  }
71
79
  }
@@ -8,6 +8,7 @@ import { BaseEntity } from 'src/module/meta/entity/base-entity.entity';
8
8
  import { ActivityLogService } from './activity-log.service';
9
9
  import { ACTIVITY_CATEGORIES } from '../repository/activity-log.repository';
10
10
  import { MediaDataService } from 'src/module/meta/service/media-data.service';
11
+ import { Cron, CronExpression } from '@nestjs/schedule';
11
12
 
12
13
  @Injectable()
13
14
  export class TaskService extends EntityServiceImpl {
@@ -438,4 +439,52 @@ export class TaskService extends EntityServiceImpl {
438
439
  console.log(notePayload);
439
440
  return await super.createEntity(notePayload, loggedInUser);
440
441
  }
442
+
443
+ @Cron(CronExpression.EVERY_30_MINUTES)
444
+ async fetchTasksDueIn30Mins() {
445
+ const now = new Date();
446
+ const in30Min = new Date(now.getTime() + 30 * 60 * 1000);
447
+
448
+ const completedStatus = await this.dataSource.query(
449
+ `SELECT id FROM cr_list_master_items WHERE name = 'completed' AND listtype = 'TKST' LIMIT 1;`,
450
+ );
451
+ const completedId = completedStatus[0]?.id;
452
+
453
+ const tasks = await this.dataSource.query(
454
+ `
455
+ SELECT *
456
+ FROM cr_wf_task_data
457
+ WHERE status != ?
458
+ AND TIMESTAMP(due_date, due_time) BETWEEN ? AND ?
459
+ `,
460
+ [completedId, now, in30Min],
461
+ );
462
+
463
+ if (!tasks.length) {
464
+ console.log('No tasks due in 30 mins');
465
+ return;
466
+ }
467
+
468
+ for (const task of tasks) {
469
+ const message = `Reminder: Task ${task.name} is due at ${task.due_time}`;
470
+
471
+ await this.dataSource.query(
472
+ `
473
+ INSERT INTO cr_notification (user_id, event_type, message, entity_type, organization_id, created_date, mapped_entity_id, mapped_entity_type, level_id, level_type)
474
+ VALUES (?, ?, ?, ?, ?, NOW(), ?, ?, ?, ?)
475
+ `,
476
+ [
477
+ task.task_owner,
478
+ 'Task Remainder',
479
+ message,
480
+ 'NOTF',
481
+ task.organization_id,
482
+ task.mapped_entity_id,
483
+ task.mapped_entity_type,
484
+ task.level_id,
485
+ task.level_type,
486
+ ],
487
+ );
488
+ }
489
+ }
441
490
  }
package/tsconfig.json CHANGED
@@ -16,8 +16,9 @@
16
16
  "forceConsistentCasingInFileNames": true,
17
17
  "noImplicitAny": false,
18
18
  "strictBindCallApply": false,
19
- "noFallthroughCasesInSwitch": false
20
- },
21
- "include": ["src/**/*"],
22
- "exclude": ["node_modules", "dist", "test"]
19
+ "noFallthroughCasesInSwitch": false,
20
+ "resolveJsonModule": true,
21
+ },
22
+ "include": ["src/**/*"],
23
+ "exclude": ["node_modules", "dist", "test"]
23
24
  }