rez_core 2.2.253 → 2.2.255
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/module/communication/communication.module.js +2 -0
- package/dist/module/communication/communication.module.js.map +1 -1
- package/dist/module/communication/factories/telephone.factory.d.ts +3 -1
- package/dist/module/communication/factories/telephone.factory.js +9 -3
- package/dist/module/communication/factories/telephone.factory.js.map +1 -1
- package/dist/module/communication/strategies/telephone/tubelight-voice.strategy.d.ts +15 -0
- package/dist/module/communication/strategies/telephone/tubelight-voice.strategy.js +104 -0
- package/dist/module/communication/strategies/telephone/tubelight-voice.strategy.js.map +1 -0
- package/dist/module/meta/controller/entity.controller.js +11 -6
- package/dist/module/meta/controller/entity.controller.js.map +1 -1
- package/dist/module/user/service/user.service.d.ts +2 -1
- package/dist/module/user/service/user.service.js +2 -0
- package/dist/module/user/service/user.service.js.map +1 -1
- package/dist/module/workflow/entity/action-data.entity.d.ts +1 -0
- package/dist/module/workflow/entity/action-data.entity.js +4 -0
- package/dist/module/workflow/entity/action-data.entity.js.map +1 -1
- package/dist/module/workflow/entity/action.entity.d.ts +1 -0
- package/dist/module/workflow/entity/action.entity.js +4 -0
- package/dist/module/workflow/entity/action.entity.js.map +1 -1
- package/dist/module/workflow-automation/service/workflow-automation-engine.service.d.ts +1 -1
- package/dist/module/workflow-automation/service/workflow-automation-engine.service.js +13 -5
- package/dist/module/workflow-automation/service/workflow-automation-engine.service.js.map +1 -1
- package/dist/module/workflow-automation/service/workflow-automation.service.d.ts +34 -2
- package/dist/module/workflow-automation/service/workflow-automation.service.js +136 -15
- package/dist/module/workflow-automation/service/workflow-automation.service.js.map +1 -1
- package/dist/tsconfig.build.tsbuildinfo +1 -1
- package/package.json +1 -1
- package/src/module/communication/communication.module.ts +2 -0
- package/src/module/communication/factories/telephone.factory.ts +6 -1
- package/src/module/communication/strategies/telephone/tubelight-voice.strategy.ts +122 -0
- package/src/module/meta/controller/entity.controller.ts +51 -42
- package/src/module/user/service/user.service.ts +2 -1
- package/src/module/workflow/entity/action-data.entity.ts +3 -0
- package/src/module/workflow/entity/action.entity.ts +3 -0
- package/src/module/workflow-automation/service/workflow-automation-engine.service.ts +53 -26
- package/src/module/workflow-automation/service/workflow-automation.service.ts +223 -25
package/package.json
CHANGED
|
@@ -22,6 +22,7 @@ import { KnowlarityStrategy as KnowlaritySMSStrategy } from './strategies/sms/kn
|
|
|
22
22
|
import { WhatsAppCloudStrategy } from './strategies/whatsapp/whatsapp-cloud.strategy';
|
|
23
23
|
import { KnowlarityVoiceStrategy } from './strategies/telephone/knowlarity-voice.strategy';
|
|
24
24
|
import { OzonetelVoiceStrategy } from './strategies/telephone/ozonetel-voice.strategy';
|
|
25
|
+
import { TubelightVoiceStrategy } from './strategies/telephone/tubelight-voice.strategy';
|
|
25
26
|
|
|
26
27
|
// New unified strategies
|
|
27
28
|
import { OutlookStrategy } from './strategies/email/outlook.strategy';
|
|
@@ -75,6 +76,7 @@ import { WebhookController } from './controller/webhook.controller';
|
|
|
75
76
|
WhatsAppCloudStrategy,
|
|
76
77
|
KnowlarityVoiceStrategy,
|
|
77
78
|
OzonetelVoiceStrategy,
|
|
79
|
+
TubelightVoiceStrategy,
|
|
78
80
|
|
|
79
81
|
// New unified strategies
|
|
80
82
|
OutlookStrategy,
|
|
@@ -3,12 +3,14 @@ import { BaseFactory } from './base.factory';
|
|
|
3
3
|
import { CommunicationStrategy } from '../strategies/communication.strategy';
|
|
4
4
|
import { KnowlarityStrategy as TelephoneKnowlarityStrategy } from '../strategies/telephone/knowlarity-multi.strategy';
|
|
5
5
|
import { OzonetelVoiceStrategy } from '../strategies/telephone/ozonetel-voice.strategy';
|
|
6
|
+
import { TubelightVoiceStrategy } from '../strategies/telephone/tubelight-voice.strategy';
|
|
6
7
|
|
|
7
8
|
@Injectable()
|
|
8
9
|
export class TelephoneFactory implements BaseFactory {
|
|
9
10
|
constructor(
|
|
10
11
|
private knowlarityStrategy: TelephoneKnowlarityStrategy,
|
|
11
12
|
private ozonetelVoiceStrategy: OzonetelVoiceStrategy,
|
|
13
|
+
private tubelightVoiceStrategy: TubelightVoiceStrategy,
|
|
12
14
|
) {}
|
|
13
15
|
|
|
14
16
|
createProvider(service: string, provider: string): CommunicationStrategy {
|
|
@@ -19,6 +21,8 @@ export class TelephoneFactory implements BaseFactory {
|
|
|
19
21
|
return this.knowlarityStrategy;
|
|
20
22
|
case 'third_party_ozonetel':
|
|
21
23
|
return this.ozonetelVoiceStrategy;
|
|
24
|
+
case 'third_party_tubelight':
|
|
25
|
+
return this.tubelightVoiceStrategy;
|
|
22
26
|
|
|
23
27
|
default:
|
|
24
28
|
throw new Error(
|
|
@@ -30,7 +34,8 @@ export class TelephoneFactory implements BaseFactory {
|
|
|
30
34
|
getSupportedCombinations(): Array<{ service: string; provider: string }> {
|
|
31
35
|
return [
|
|
32
36
|
{ service: 'THIRD_PARTY', provider: 'knowlarity' },
|
|
33
|
-
{ service: 'THIRD_PARTY', provider: 'ozonetel' }
|
|
37
|
+
{ service: 'THIRD_PARTY', provider: 'ozonetel' },
|
|
38
|
+
{ service: 'THIRD_PARTY', provider: 'tubelight' }
|
|
34
39
|
];
|
|
35
40
|
}
|
|
36
41
|
|
|
@@ -0,0 +1,122 @@
|
|
|
1
|
+
import { Injectable } from '@nestjs/common';
|
|
2
|
+
import axios from 'axios';
|
|
3
|
+
import {
|
|
4
|
+
CommunicationResult,
|
|
5
|
+
CommunicationStrategy,
|
|
6
|
+
} from '../communication.strategy';
|
|
7
|
+
|
|
8
|
+
interface TubelightVoiceConfig {
|
|
9
|
+
userName: string;
|
|
10
|
+
password: string;
|
|
11
|
+
agentVerificationKey: string;
|
|
12
|
+
tenantId: string;
|
|
13
|
+
agentLoginUrl?: string;
|
|
14
|
+
}
|
|
15
|
+
|
|
16
|
+
@Injectable()
|
|
17
|
+
export class TubelightVoiceStrategy implements CommunicationStrategy {
|
|
18
|
+
private readonly baseUrl = 'https://portal.tubelightcommunications.com/voice/api/v1';
|
|
19
|
+
|
|
20
|
+
async sendMessage(
|
|
21
|
+
to: string,
|
|
22
|
+
message: string,
|
|
23
|
+
config: TubelightVoiceConfig,
|
|
24
|
+
): Promise<CommunicationResult> {
|
|
25
|
+
if (!this.validateConfig(config)) {
|
|
26
|
+
return {
|
|
27
|
+
success: false,
|
|
28
|
+
provider: 'tubelight',
|
|
29
|
+
service: 'THIRD_PARTY',
|
|
30
|
+
error: 'Invalid Tubelight Voice configuration',
|
|
31
|
+
timestamp: new Date(),
|
|
32
|
+
};
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
try {
|
|
36
|
+
const token = await this.authenticate(config);
|
|
37
|
+
if (!token) {
|
|
38
|
+
return {
|
|
39
|
+
success: false,
|
|
40
|
+
provider: 'tubelight',
|
|
41
|
+
service: 'THIRD_PARTY',
|
|
42
|
+
error: 'Failed to authenticate with Tubelight',
|
|
43
|
+
timestamp: new Date(),
|
|
44
|
+
};
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
const url = `${this.baseUrl}/user/sso/agent/outbound-call`;
|
|
48
|
+
await axios.post(
|
|
49
|
+
url,
|
|
50
|
+
{
|
|
51
|
+
agentVerificationKey: config.agentVerificationKey,
|
|
52
|
+
customerNumber: to,
|
|
53
|
+
},
|
|
54
|
+
{
|
|
55
|
+
headers: {
|
|
56
|
+
'X-TENANT-ID': config.tenantId,
|
|
57
|
+
'Content-Type': 'application/json',
|
|
58
|
+
Authorization: `Bearer ${token}`,
|
|
59
|
+
},
|
|
60
|
+
},
|
|
61
|
+
);
|
|
62
|
+
|
|
63
|
+
return {
|
|
64
|
+
success: true,
|
|
65
|
+
messageId: `tubelight-voice-${Date.now()}`,
|
|
66
|
+
provider: 'tubelight',
|
|
67
|
+
service: 'THIRD_PARTY',
|
|
68
|
+
timestamp: new Date(),
|
|
69
|
+
};
|
|
70
|
+
} catch (error) {
|
|
71
|
+
let errorMsg = 'Unknown error';
|
|
72
|
+
if (axios.isAxiosError(error)) {
|
|
73
|
+
errorMsg = error.response?.data?.message || error.message;
|
|
74
|
+
} else if (error instanceof Error) {
|
|
75
|
+
errorMsg = error.message;
|
|
76
|
+
}
|
|
77
|
+
return {
|
|
78
|
+
success: false,
|
|
79
|
+
provider: 'tubelight',
|
|
80
|
+
service: 'THIRD_PARTY',
|
|
81
|
+
error: errorMsg,
|
|
82
|
+
timestamp: new Date(),
|
|
83
|
+
};
|
|
84
|
+
}
|
|
85
|
+
}
|
|
86
|
+
|
|
87
|
+
private async authenticate(
|
|
88
|
+
config: TubelightVoiceConfig,
|
|
89
|
+
): Promise<string | null> {
|
|
90
|
+
try {
|
|
91
|
+
const response = await axios.post(
|
|
92
|
+
`${this.baseUrl}/auth/login`,
|
|
93
|
+
{
|
|
94
|
+
user_name: config.userName,
|
|
95
|
+
password: config.password,
|
|
96
|
+
},
|
|
97
|
+
{
|
|
98
|
+
headers: {
|
|
99
|
+
'Content-Type': 'application/json',
|
|
100
|
+
'X-TENANT-ID': config.tenantId,
|
|
101
|
+
},
|
|
102
|
+
},
|
|
103
|
+
);
|
|
104
|
+
|
|
105
|
+
const data = response.data as { bearer_token?: string; access_token?: string; token?: string };
|
|
106
|
+
return data?.bearer_token || data?.access_token || data?.token || null;
|
|
107
|
+
} catch (error) {
|
|
108
|
+
console.error('Failed to authenticate with Tubelight:', error);
|
|
109
|
+
return null;
|
|
110
|
+
}
|
|
111
|
+
}
|
|
112
|
+
|
|
113
|
+
validateConfig(config: Partial<TubelightVoiceConfig>): boolean {
|
|
114
|
+
return !!(
|
|
115
|
+
config &&
|
|
116
|
+
config.userName &&
|
|
117
|
+
config.password &&
|
|
118
|
+
config.agentVerificationKey &&
|
|
119
|
+
config.tenantId
|
|
120
|
+
);
|
|
121
|
+
}
|
|
122
|
+
}
|
|
@@ -142,47 +142,47 @@ export class EntityController {
|
|
|
142
142
|
@Req() req: Request & { user: any },
|
|
143
143
|
) {
|
|
144
144
|
const loggedInUser = req.user.userData;
|
|
145
|
-
const appcode =
|
|
146
|
-
|
|
145
|
+
const appcode = loggedInUser.appcode;
|
|
146
|
+
|
|
147
147
|
if (!entityType) {
|
|
148
|
-
throw new BadRequestException(
|
|
149
|
-
`Query parameter "entity_type" is required`,
|
|
150
|
-
);
|
|
148
|
+
throw new BadRequestException(`Query parameter "entity_type" is required`);
|
|
151
149
|
}
|
|
152
|
-
|
|
150
|
+
|
|
153
151
|
const entityMaster = await this.entityMasterService.getEntityData(
|
|
154
152
|
entityType,
|
|
155
153
|
loggedInUser,
|
|
156
154
|
);
|
|
157
|
-
|
|
158
|
-
|
|
159
|
-
|
|
160
|
-
|
|
161
|
-
|
|
155
|
+
|
|
156
|
+
const entityService = await this.reflectionHelper.getBean<EntityServiceImpl>(
|
|
157
|
+
entityMaster.entity_service,
|
|
158
|
+
);
|
|
159
|
+
|
|
162
160
|
if (!entityService) {
|
|
163
161
|
throw new InternalServerErrorException(
|
|
164
|
-
`No service found for
|
|
162
|
+
`No service found for entity_type "${entityType}"`,
|
|
165
163
|
);
|
|
166
164
|
}
|
|
167
|
-
|
|
168
|
-
|
|
165
|
+
|
|
166
|
+
// ✅ Create
|
|
167
|
+
const savedData = await entityService.createEntity(
|
|
169
168
|
entityData,
|
|
170
169
|
loggedInUser as UserData,
|
|
171
170
|
null,
|
|
172
171
|
appcode,
|
|
173
172
|
);
|
|
174
|
-
|
|
173
|
+
|
|
174
|
+
// ✅ Run workflow automation (no preUpdateStates for CREATE)
|
|
175
175
|
await this.workflowAutomationEngineService.handleEntityEvent(
|
|
176
|
-
|
|
176
|
+
entityType,
|
|
177
177
|
'CREATE',
|
|
178
178
|
savedData,
|
|
179
|
-
null,
|
|
180
179
|
loggedInUser,
|
|
180
|
+
null,
|
|
181
181
|
);
|
|
182
|
-
|
|
182
|
+
|
|
183
183
|
return savedData;
|
|
184
184
|
}
|
|
185
|
-
|
|
185
|
+
|
|
186
186
|
@Post('update/:id')
|
|
187
187
|
@HttpCode(200)
|
|
188
188
|
async update(
|
|
@@ -192,58 +192,67 @@ export class EntityController {
|
|
|
192
192
|
@Req() req: Request & { user: any },
|
|
193
193
|
) {
|
|
194
194
|
const loggedInUser = req.user.userData;
|
|
195
|
-
|
|
195
|
+
|
|
196
196
|
if (!entityType) {
|
|
197
|
-
throw new BadRequestException(
|
|
198
|
-
'Query parameter "entity_type" is required',
|
|
199
|
-
);
|
|
197
|
+
throw new BadRequestException('Query parameter "entity_type" is required');
|
|
200
198
|
}
|
|
201
|
-
|
|
199
|
+
|
|
202
200
|
const entityMaster = await this.entityMasterService.getEntityData(
|
|
203
201
|
entityType,
|
|
204
202
|
loggedInUser,
|
|
205
203
|
);
|
|
206
|
-
|
|
207
|
-
const entityService =
|
|
208
|
-
|
|
209
|
-
|
|
210
|
-
|
|
211
|
-
|
|
204
|
+
|
|
205
|
+
const entityService = await this.reflectionHelper.getBean<EntityServiceImpl>(
|
|
206
|
+
entityMaster.entity_service,
|
|
207
|
+
);
|
|
208
|
+
|
|
212
209
|
if (!entityService) {
|
|
213
210
|
throw new InternalServerErrorException(
|
|
214
211
|
`No service found for entity_type "${entityType}"`,
|
|
215
212
|
);
|
|
216
213
|
}
|
|
217
|
-
|
|
214
|
+
|
|
215
|
+
// 1️⃣ Get old state
|
|
218
216
|
const existingEntity = await entityService.getEntityData(
|
|
219
217
|
entityType,
|
|
220
218
|
id,
|
|
221
219
|
loggedInUser,
|
|
222
220
|
);
|
|
223
|
-
|
|
224
221
|
if (!existingEntity) {
|
|
225
222
|
throw new NotFoundException(`No entity found for id "${id}"`);
|
|
226
223
|
}
|
|
227
|
-
|
|
228
|
-
|
|
224
|
+
|
|
225
|
+
// 2️⃣ Pre-evaluate criteria
|
|
226
|
+
const workflows = await this.workflowAutomationEngineService['wfService'].getActiveRules(
|
|
227
|
+
entityType,
|
|
228
|
+
'UPDATE',
|
|
229
|
+
);
|
|
230
|
+
const preUpdateStates: Record<number, boolean> = {};
|
|
231
|
+
for (const wf of workflows) {
|
|
232
|
+
preUpdateStates[wf.id] = await this.workflowAutomationEngineService[
|
|
233
|
+
'filterEvaluator'
|
|
234
|
+
].evaluateCriteria(entityType, wf.condition_filter_code, existingEntity.id, loggedInUser);
|
|
235
|
+
}
|
|
236
|
+
|
|
237
|
+
// 3️⃣ Update
|
|
238
|
+
const updatedData = await entityService.updateEntity(
|
|
229
239
|
entityData,
|
|
230
240
|
loggedInUser as UserData,
|
|
231
241
|
);
|
|
232
|
-
|
|
242
|
+
|
|
243
|
+
// 4️⃣ Run workflow automation (pass preUpdateStates)
|
|
233
244
|
await this.workflowAutomationEngineService.handleEntityEvent(
|
|
234
|
-
|
|
245
|
+
entityType,
|
|
235
246
|
'UPDATE',
|
|
236
247
|
updatedData,
|
|
237
|
-
existingEntity,
|
|
238
248
|
loggedInUser,
|
|
249
|
+
preUpdateStates,
|
|
239
250
|
);
|
|
240
|
-
|
|
251
|
+
|
|
241
252
|
return updatedData;
|
|
242
|
-
// return {
|
|
243
|
-
// success: true,
|
|
244
|
-
// data: updatedEntity,
|
|
245
|
-
// };
|
|
246
253
|
}
|
|
254
|
+
|
|
255
|
+
|
|
247
256
|
|
|
248
257
|
@Post('delete/:id')
|
|
249
258
|
@HttpCode(200)
|
|
@@ -31,7 +31,8 @@ import { Action } from 'src/module/workflow-automation/interface/action.interfac
|
|
|
31
31
|
import { ActionHandler } from 'src/module/workflow-automation/interface/action.decorator';
|
|
32
32
|
|
|
33
33
|
@Injectable()
|
|
34
|
-
|
|
34
|
+
@ActionHandler('User')
|
|
35
|
+
export class UserService extends EntityServiceImpl implements Action {
|
|
35
36
|
constructor(
|
|
36
37
|
private userRepository: UserRepository,
|
|
37
38
|
private userRoleMappingService: UserRoleMappingService,
|
|
@@ -20,59 +20,86 @@ export class WorkflowAutomationEngineService {
|
|
|
20
20
|
|
|
21
21
|
/**
|
|
22
22
|
* Called from entity hooks (CREATE / UPDATE / DELETE)
|
|
23
|
+
* @param preUpdateStates optional, used for UPDATE events to pass pre-update criteria results
|
|
23
24
|
*/
|
|
24
|
-
async handleEntityEvent(
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
25
|
+
async handleEntityEvent(
|
|
26
|
+
entityType: string,
|
|
27
|
+
eventType: 'CREATE' | 'UPDATE' | 'DELETE',
|
|
28
|
+
newEntity: any,
|
|
29
|
+
user: any,
|
|
30
|
+
preUpdateStates?: Record<number, boolean> | null,
|
|
31
|
+
) {
|
|
32
|
+
const workflows = await this.wfService.getActiveRules(entityType, eventType);
|
|
33
|
+
|
|
30
34
|
for (const wf of workflows) {
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
+
// Step 1️⃣ Condition / Trigger evaluation
|
|
36
|
+
let triggerMatched = false;
|
|
37
|
+
|
|
38
|
+
if (eventType === 'CREATE') {
|
|
39
|
+
triggerMatched = await this.filterEvaluator.evaluateCriteria(
|
|
40
|
+
entityType,
|
|
41
|
+
wf.condition_filter_code,
|
|
42
|
+
newEntity.id,
|
|
43
|
+
user,
|
|
44
|
+
);
|
|
45
|
+
} else if (eventType === 'UPDATE' && preUpdateStates) {
|
|
46
|
+
const before = preUpdateStates[wf.id] ?? false;
|
|
47
|
+
const after = await this.filterEvaluator.evaluateCriteria(
|
|
48
|
+
entityType,
|
|
49
|
+
wf.condition_filter_code,
|
|
50
|
+
newEntity.id,
|
|
51
|
+
user,
|
|
52
|
+
);
|
|
53
|
+
triggerMatched = before !== after && after === true;
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
// 🔍 Log Step 1 result
|
|
57
|
+
console.log(
|
|
58
|
+
`[Workflow:${wf.id}] Step 1 - Trigger matched:`,
|
|
59
|
+
triggerMatched,
|
|
35
60
|
);
|
|
36
|
-
|
|
37
|
-
if (!
|
|
38
|
-
|
|
61
|
+
|
|
62
|
+
if (!triggerMatched) continue;
|
|
63
|
+
|
|
64
|
+
// Step 2️⃣ Final criteria evaluation
|
|
39
65
|
const criteriaMatched = await this.filterEvaluator.evaluateCriteria(
|
|
40
66
|
entityType,
|
|
41
67
|
wf.criteria_filter_code,
|
|
42
68
|
newEntity.id,
|
|
43
69
|
user,
|
|
44
70
|
);
|
|
45
|
-
|
|
71
|
+
|
|
72
|
+
// 🔍 Log Step 2 result
|
|
73
|
+
console.log(
|
|
74
|
+
`[Workflow:${wf.id}] Step 2 - Criteria matched:`,
|
|
75
|
+
criteriaMatched,
|
|
76
|
+
);
|
|
77
|
+
|
|
46
78
|
if (!criteriaMatched) continue;
|
|
47
|
-
|
|
79
|
+
|
|
80
|
+
// Step 3️⃣ Execute workflow actions
|
|
48
81
|
await this.executeActions(wf.id, newEntity, user);
|
|
49
82
|
}
|
|
50
83
|
}
|
|
84
|
+
|
|
51
85
|
|
|
52
86
|
private async executeActions(
|
|
53
87
|
workflow_automation_id: number,
|
|
54
88
|
entity: any,
|
|
55
89
|
user: any,
|
|
56
90
|
) {
|
|
57
|
-
//
|
|
58
|
-
const actions = await this.wfService.getActionsForRule(
|
|
59
|
-
workflow_automation_id,
|
|
60
|
-
);
|
|
91
|
+
// Load actions for this rule from DB
|
|
92
|
+
const actions = await this.wfService.getActionsForRule(workflow_automation_id);
|
|
61
93
|
console.log(actions, 'actions found');
|
|
62
|
-
// (this should fetch from cr_workflow_automation_action)
|
|
63
94
|
|
|
64
95
|
for (const action of actions) {
|
|
65
96
|
const impl = this.actions.get(String(action.action_decorator)); // action_code = registered name
|
|
66
97
|
if (!impl) {
|
|
67
|
-
console.warn(
|
|
68
|
-
`⚠️ No implementation found for action: ${action.action_decorator}`,
|
|
69
|
-
);
|
|
98
|
+
console.warn(`⚠️ No implementation found for action: ${action.action_decorator}`);
|
|
70
99
|
continue;
|
|
71
100
|
}
|
|
72
101
|
|
|
73
|
-
console.log(
|
|
74
|
-
`🚀 Executing action ${action.action_decorator} for entity ${entity.id}`,
|
|
75
|
-
);
|
|
102
|
+
console.log(`🚀 Executing action ${action.action_decorator} for entity ${entity.id}`);
|
|
76
103
|
await impl.execute({ entity, user, config: action.payload });
|
|
77
104
|
}
|
|
78
105
|
}
|