tango-app-api-analysis-traffic 3.8.1-alpha.2 → 3.8.1-alpha.20

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/app.js ADDED
@@ -0,0 +1,46 @@
1
+ import express from 'express';
2
+ import { analysisTrafficRouter, mobileTrafficAnalysisRouter, nobRouter, revopRouter } from './index.js';
3
+
4
+ import dotenv from 'dotenv';
5
+ import { logger } from 'tango-app-api-middleware';
6
+ import { connectdb } from './config/database/database.js';
7
+ import responseMiddleware from './config/response/response.js';
8
+ import errorMiddleware from './config/response/error.js';
9
+ import pkg from 'body-parser';
10
+ import { swaggerConfig } from './config/swagger/swagger.js';
11
+ import swagger from 'swagger-ui-express';
12
+ import cors from 'cors';
13
+
14
+ const { json, urlencoded } = pkg;
15
+ const env=dotenv.config();
16
+
17
+ const app = express();
18
+ const PORT = process.env.PORT || 3000;
19
+
20
+
21
+ app.use( json( { limit: '500mb' } ) );
22
+ app.use(
23
+ urlencoded( {
24
+ extended: true,
25
+ } ),
26
+ );
27
+
28
+ app.use( cors() );
29
+ app.use( responseMiddleware );
30
+ app.use( errorMiddleware );
31
+
32
+ if ( env.error ) {
33
+ logger.error( '.env not found' );
34
+ process.exit( 1 );
35
+ }
36
+ app.use( '/api-docs', swagger.serve, swagger.setup( swaggerConfig ) );
37
+
38
+ app.use( '/v3/trafficAnalysis', analysisTrafficRouter );
39
+ app.use( '/v3/mobile/trafficAnalysis', mobileTrafficAnalysisRouter );
40
+ app.use( '/v3/nob', nobRouter );
41
+ app.use( '/v3/revops', revopRouter );
42
+
43
+ app.listen( PORT, () => {
44
+ logger.info( `server is running on port= ${PORT} ` );
45
+ connectdb();
46
+ } );
package/package.json CHANGED
@@ -1,11 +1,11 @@
1
1
  {
2
2
  "name": "tango-app-api-analysis-traffic",
3
- "version": "3.8.1-alpha.2",
3
+ "version": "3.8.1-alpha.20",
4
4
  "description": "Traffic Analysis",
5
- "main": "index.js",
5
+ "main": "app.js",
6
6
  "type": "module",
7
7
  "scripts": {
8
- "start": "nodemon --exec \"eslint --fix . && node index.js\""
8
+ "start": "nodemon --exec \"eslint --fix . && node app.js\""
9
9
  },
10
10
  "engines": {
11
11
  "node": ">=18.10.0"
@@ -23,8 +23,8 @@
23
23
  "mongodb": "^6.8.0",
24
24
  "nodemon": "^3.1.4",
25
25
  "swagger-ui-express": "^5.0.1",
26
- "tango-api-schema": "^2.2.201",
27
- "tango-app-api-middleware": "^3.1.88",
26
+ "tango-api-schema": "^2.2.203",
27
+ "tango-app-api-middleware": "^3.1.92",
28
28
  "winston": "^3.13.1",
29
29
  "winston-daily-rotate-file": "^5.0.0"
30
30
  },
@@ -1,7 +1,7 @@
1
1
  import { logger, insertOpenSearchData, getOpenSearchData, updateOpenSearchData } from 'tango-app-api-middleware';
2
2
  import { findOnerevopConfig } from '../services/revopConfig.service.js';
3
3
  import * as clientService from '../services/clients.services.js';
4
- import { upsertWithScript } from 'tango-app-api-middleware/src/utils/openSearch.js';
4
+ import { bulkUpdate, upsertWithScript } from 'tango-app-api-middleware/src/utils/openSearch.js';
5
5
  import dayjs from 'dayjs';
6
6
  // Lamda Service Call //
7
7
  async function LamdaServiceCall( url, data ) {
@@ -74,6 +74,7 @@ export async function revoptagging( req, res ) {
74
74
  } else {
75
75
  item.createdAt = new Date();
76
76
  item.updatedAt = new Date();
77
+ item.type='tagging';
77
78
  await insertOpenSearchData( openSearch.revops, item );
78
79
  }
79
80
  }
@@ -229,12 +230,25 @@ export async function storeProcessedData( req, res ) {
229
230
  ],
230
231
  };
231
232
 
232
-
233
233
  const getData = await getOpenSearchData( openSearch.footfall, getQuery );
234
- const processedData = getData?.body?.hits?.hits?.[0]?._source;
235
- const previousData = getData?.body?.hits?.hits?.[1]?._source;
236
- const result=( ( processedData?.footfall_count - previousData?.footfall_count )/previousData?.footfall_count )*100;
237
- return res.sendSuccess( { footfallCount: processedData?.footfall_count, footfallCountTrend: Math.round( result ), downtime: processedData?.down_time } );
234
+ const hits = getData?.body?.hits?.hits || [];
235
+
236
+ const processedData = hits.find( ( d ) => d._id === dateString )?._source || null;
237
+ const previousData = hits.find( ( d ) => d._id === dateStringPrevious )?._source || null;
238
+
239
+ let footfallCountTrend = 0;
240
+
241
+ if ( processedData && previousData && previousData.footfall_count ) {
242
+ footfallCountTrend = Math.round(
243
+ ( ( processedData.footfall_count - previousData.footfall_count ) / previousData.footfall_count ) * 100,
244
+ );
245
+ }
246
+
247
+ return res.sendSuccess( {
248
+ footfallCount: processedData?.footfall_count || 0,
249
+ footfallCountTrend,
250
+ downtime: processedData?.down_time || 0,
251
+ } );
238
252
  } catch ( error ) {
239
253
  logger.error( { message: error, data: req.query, function: 'storeProcessedData' } );
240
254
  const err = error.message || 'Internal Server Error';
@@ -244,21 +258,68 @@ export async function storeProcessedData( req, res ) {
244
258
 
245
259
  export async function footFallImages( req, res ) {
246
260
  try {
261
+ const revop = JSON.parse( process.env.URL );
247
262
  const inputData = req.query;
248
263
  inputData.clientId = inputData.storeId.split( '-' )[0];
249
264
  inputData.storeId=[ inputData.storeId ];
250
265
 
251
- const LamdaURL = 'https://cncmzszloku7y3bewxbpiy6nkm0yunqe.lambda-url.ap-south-1.on.aws/';
252
- let resultData = await LamdaServiceCall( LamdaURL, inputData );
253
- if ( resultData ) {
254
- if ( resultData.status_code == '200' ) {
255
- return res.sendSuccess( { ...resultData, ticketStatus: '' } );
256
- } else {
257
- return res.sendError( 'No Content', 204 );
258
- }
259
- } else {
260
- return res.sendError( 'No Content', 204 );
261
- }
266
+ const opensearch = JSON.parse( process.env.OPENSEARCH );
267
+ const query = {
268
+ 'query': {
269
+ 'bool': {
270
+ must: [
271
+ {
272
+ 'term': {
273
+ 'storeId.keyword': inputData?.storeId[0],
274
+ },
275
+ },
276
+
277
+ {
278
+ 'term': {
279
+ 'dateString': inputData?.dateString,
280
+ },
281
+ },
282
+ {
283
+ 'term': {
284
+ 'ticketName.keyword': 'footfall-directory',
285
+ },
286
+ },
287
+ ],
288
+ },
289
+ },
290
+ '_source': [ 'dateString', 'storeId', 'duplicateCount', 'footfallCount', 'employeeCount', 'houseKeepingCount', 'status', 'ticketId', 'comments', 'userName', 'role', 'createdAt', 'email', 'houseKeepingACCount', 'duplicateACCount', 'employeeACCount', 'approverEmail', 'approverRole', 'approverUserName' ],
291
+
292
+ };
293
+
294
+ const getData = await getOpenSearchData( opensearch.footfallDirectory, query );
295
+ const ticketDetails = getData?.body?.hits?.hits[0];
296
+ let temp = [];
297
+ ticketDetails?._source? temp.push( ticketDetails?._source ) :null;
298
+ // temp[0].status = 'open';
299
+ if ( ticketDetails?._source?.status == 'closed' ) {
300
+ delete temp[0].status;
301
+ temp.push( { ...ticketDetails?._source, status: 'closed' } );
302
+
303
+ temp[1].userName = getData?.body?.hits?.hits?.[0]?._source?.approverUserName;
304
+ temp[1].email = getData?.body?.hits?.hits?.[0]?._source?.approverEmail;
305
+ temp[1].role = getData?.body?.hits?.hits?.[0]?._source?.approverRole;
306
+ temp[1].employeeCount = getData?.body?.hits?.hits?.[0]?._source?.employeeACCount;
307
+ temp[1].houseKeepingCount = getData?.body?.hits?.hits?.[0]?._source?.houseKeepingACCount;
308
+ temp[1].duplicateCount = getData?.body?.hits?.hits?.[0]?._source?.duplicateACCount;
309
+ }
310
+ const LamdaURL = revop.getImages;
311
+ let resultData = await LamdaServiceCall( LamdaURL, inputData );
312
+ logger.info( { resultData: resultData } );
313
+ if ( resultData ) {
314
+ temp.length? temp[0].status = 'open': null;
315
+ if ( resultData.status_code == '200' ) {
316
+ return res.sendSuccess( { ...resultData, ticketStatus: temp?.length > 0? temp : null, config: req?.store?.revopTagging } );
317
+ } else {
318
+ return res.sendError( 'No Content', 204 );
319
+ }
320
+ } else {
321
+ return res.sendError( 'No Content', 204 );
322
+ }
262
323
  } catch ( error ) {
263
324
  logger.error( { message: error, data: req.query, function: 'storeProcessedData' } );
264
325
  const err = error.message || 'Internal Server Error';
@@ -281,9 +342,11 @@ export async function tagTempId( req, res ) {
281
342
  entryTime: inputData.entryTime,
282
343
  exitTime: inputData.exitTime,
283
344
  filePath: inputData.filePath,
284
- status: 'submitted',
345
+ status: inputData?.revopsType == 'non-tagging' ?'':'submitted',
285
346
  description: '',
286
- dublicateImage: inputData?.dublicateImage?.length>0? inputData?.dublicateImage :[],
347
+ isChecked: inputData.isChecked,
348
+ duplicateImage: inputData?.duplicateImage?.length>0? inputData?.duplicateImage :[],
349
+ type: 'tagging-reflect',
287
350
  createdAt: new Date(),
288
351
  updatedAt: new Date(),
289
352
 
@@ -297,15 +360,17 @@ export async function tagTempId( req, res ) {
297
360
  ctx._source.filePath = params.filePath;
298
361
  ctx._source.status = params.status;
299
362
  ctx._source.description = params.description;
300
- ctx._source.dublicateImage = params.dublicateImage;
363
+ ctx._source.isChecked = params.isChecked;
364
+ ctx._source.duplicateImage = params.duplicateImage;
301
365
  ctx._source.updatedAt = params.updatedAt;
366
+ ctx._source.timeRange = params.timeRange;
302
367
  if (ctx._source.createdAt == null) {
303
368
  ctx._source.createdAt = params.createdAt;
304
369
  ctx._source.clientId = params.clientId;
305
370
  ctx._source.storeId = params.storeId;
306
371
  ctx._source.tempId = params.tempId;
307
372
  ctx._source.dateString = params.dateString;
308
- ctx._source.timeRange = params.timeRange;
373
+ ctx._source.type = "tagging-reflect";
309
374
  ctx._source.processType = params.processType;
310
375
  }
311
376
  `,
@@ -313,7 +378,53 @@ export async function tagTempId( req, res ) {
313
378
  };
314
379
  const id = `${inputData.storeId}_${inputData.dateString}_${inputData.timeRange}_${inputData.tempId}`;
315
380
  await upsertWithScript( openSearch.revop, id, { script, upsert: upsertRecord } );
316
- return res.sendSuccess( `tempId ${inputData.tempId} has been tagged successfully` );
381
+ if ( inputData?.duplicateImage?.length> 0 ) {
382
+ let bulkBody = [];
383
+ for ( let item of inputData?.duplicateImage ) {
384
+ const insertRecord = {
385
+ clientId: inputData.storeId.split( '-' )[0],
386
+ storeId: inputData.storeId,
387
+ tempId: item.tempId,
388
+ dateString: inputData.dateString,
389
+ timeRange: item.timeRange,
390
+ isChecked: item.isChecked,
391
+ processType: inputData.processType,
392
+ revopsType: item.revopsType,
393
+ entryTime: item.entryTime,
394
+ exitTime: inputData.exitTime,
395
+ filePath: item.filePath,
396
+ status: item?.revopsType == 'non-tagging' ?'':'submitted',
397
+ description: '',
398
+ duplicateImage: [],
399
+ type: 'tagging-reflect',
400
+ createdAt: new Date(),
401
+ updatedAt: new Date(),
402
+
403
+ };
404
+ const id =`${inputData.storeId}_${inputData.dateString}_${item.timeRange}_${item.tempId}`;
405
+ const updatedDuplicateImages = {
406
+ ...insertRecord,
407
+ parent: inputData.tempId,
408
+ };
409
+ bulkBody.push(
410
+ { update: { _index: openSearch.revop, _id: id } },
411
+ { doc: updatedDuplicateImages, doc_as_upsert: true },
412
+ );
413
+ }
414
+ if ( bulkBody.length > 0 ) {
415
+ const res1 = await bulkUpdate( bulkBody );
416
+ if ( res1?.errors ) {
417
+ logger.error( 'Bulk update errors:', res1.items );
418
+ return { success: false, errors: res1.items };
419
+ } else {
420
+ logger.info( { msg: 'res1' } );
421
+ return res.sendSuccess( `tempId ${inputData.tempId} has been tagged successfully` );
422
+ // return { success: true };
423
+ }
424
+ }
425
+ } else {
426
+ return res.sendSuccess( `tempId ${inputData.tempId} has been tagged successfully` );
427
+ }
317
428
  } catch ( error ) {
318
429
  logger.error( { message: error, data: req.query, function: 'tagTempId' } );
319
430
  const err = error.message || 'Internal Server Error';
@@ -847,6 +847,7 @@ export async function headerClustersV2( req, res ) {
847
847
  try {
848
848
  let requestData = req.body;
849
849
  let getUserEmail = req.user.email;
850
+ let clientId = req?.user?.clientId;
850
851
  let getUserType = req.user.userType;
851
852
  let getRole = req.user.role;
852
853
  let clusterNames=[];
@@ -862,9 +863,9 @@ export async function headerClustersV2( req, res ) {
862
863
  clusterNames = await getclusterList( requestData.clientId, getUserType, getRole, req );
863
864
  } else if ( getUserType == 'client' ) {
864
865
  if ( getRole == 'superadmin' ) {
865
- clusterNames = await getclusterList( requestData.clientId, getUserType, getRole, req );
866
+ clusterNames = await getclusterList( clientId, getUserType, getRole, req );
866
867
  } else {
867
- clusterNames = await getclusterList( requestData.clientId, getUserType, getRole, req );
868
+ clusterNames = await getclusterList( clientId, getUserType, getRole, req );
868
869
  }
869
870
  }
870
871
  }
@@ -1577,31 +1578,30 @@ async function getLocationStores( userClientId, cityList, req ) {
1577
1578
  }
1578
1579
  async function getclusterList( userClientId, getUserType, getRole, req ) {
1579
1580
  try {
1580
- if ( userClientId && userClientId !='' ) {
1581
- let filter = [
1582
- { clientId: { $eq: userClientId } },
1583
- ];
1581
+ if ( userClientId && userClientId !='' || getUserType === 'tango' ) {
1582
+ let filter = [];
1583
+ if ( userClientId && userClientId !='' ) {
1584
+ filter.push( { clientId: { $eq: userClientId } },
1585
+ );
1586
+ }
1584
1587
  if ( req.body.assignedStores&&req.body.assignedStores.length>0 ) {
1585
1588
  filter.push( { 'stores.storeId': { $in: req.body.assignedStores } } );
1586
1589
  }
1587
1590
  if ( getUserType == 'client'&&getRole!='superadmin' ) {
1588
1591
  filter.push( { 'Teamlead.email': req.user.email } );
1589
1592
  }
1593
+ let clusterQuery = [];
1590
1594
 
1595
+ if ( filter.length > 0 ) {
1596
+ clusterQuery.push( { $match: { $and: filter } } );
1597
+ }
1591
1598
 
1592
- let clusterQuery = [
1593
- {
1594
- $match: {
1595
- $and: filter,
1596
- },
1597
- },
1598
- {
1599
- $group: {
1600
- _id: null,
1601
- clusterName: { $push: '$clusterName' },
1602
- },
1599
+ clusterQuery.push( {
1600
+ $group: {
1601
+ _id: null,
1602
+ clusterName: { $push: '$clusterName' },
1603
1603
  },
1604
- ];
1604
+ } );
1605
1605
  const clusterIds = await aggregateCluster( clusterQuery );
1606
1606
  if ( clusterIds && clusterIds.length>0 && clusterIds[0]?.clusterName.length > 0 ) {
1607
1607
  let uniqueclusterIds = [ ...new Set( clusterIds[0].clusterName ) ];
@@ -32,7 +32,18 @@ export const tagTempIdSchema = joi.object( {
32
32
  tempId: joi.number().required(),
33
33
  revopsType: joi.string().required(),
34
34
  timeRange: joi.string().required(),
35
- dublicateImage: joi.array().optional(),
35
+ isChecked: joi.boolean().required().allow( null ),
36
+ duplicateImage: joi.array().items( joi.object(
37
+ {
38
+ tempId: joi.number().required(),
39
+ timeRange: joi.string().required(),
40
+ entryTime: joi.string().required(),
41
+ exitTime: joi.string().required(),
42
+ filePath: joi.string().required(),
43
+ revopsType: joi.string().required(),
44
+ isChecked: joi.boolean().required().allow( null ),
45
+ },
46
+ ) ).optional(),
36
47
  processType: joi.string().required(),
37
48
  entryTime: joi.string().required(),
38
49
  exitTime: joi.string().required(),
@@ -208,7 +208,7 @@ export const validateperformanceMatrixParams = {
208
208
  };
209
209
 
210
210
  export const validateHeaderSchema = joi.object( {
211
- clientId: joi.string().required(),
211
+ clientId: joi.string().required().allow( '' ),
212
212
  city: joi.array().required(),
213
213
  group: joi.array().required(),
214
214
  country: joi.array().optional().empty(),
@@ -3,6 +3,7 @@ import express from 'express';
3
3
  import { storeProcessedData, getconfig, revoptagging, getrevoptagging, revoptaggingcount, footFallImages, tagTempId, getCategorizedImages } from '../controllers/revop.controller.js';
4
4
  import { isAllowedSessionHandler, validate } from 'tango-app-api-middleware';
5
5
  import { footfallImagesValid, getCategorizedImagesValid, storeProcessedDataValid, tagTempIdValid } from '../dtos/revop.dtos.js';
6
+ import { deletetagedDuplicate, getTaggingConfig, mappingConfig } from '../validations/revop.validation.js';
6
7
 
7
8
  export const revopRouter = express.Router();
8
9
 
@@ -14,8 +15,8 @@ revopRouter
14
15
 
15
16
  // new enhnacemnet (for footfall directory)
16
17
  .get( '/store-processed-data', isAllowedSessionHandler, validate( storeProcessedDataValid ), storeProcessedData )
17
- .get( '/footfall-images', isAllowedSessionHandler, validate( footfallImagesValid ), footFallImages )
18
- .post( '/tag-tempId', isAllowedSessionHandler, validate( tagTempIdValid ), tagTempId )
18
+ .get( '/footfall-images', isAllowedSessionHandler, validate( footfallImagesValid ), getTaggingConfig, footFallImages )
19
+ .post( '/tag-tempId', isAllowedSessionHandler, validate( tagTempIdValid ), deletetagedDuplicate, mappingConfig, tagTempId )
19
20
 
20
21
 
21
22
  .post( '/get-categorized-images', isAllowedSessionHandler, validate( getCategorizedImagesValid ), getCategorizedImages );
@@ -0,0 +1,145 @@
1
+ import { getOpenSearchCount, logger } from 'tango-app-api-middleware';
2
+ import { findOneStore } from '../services/stores.service.js';
3
+ import { deleteByQuery } from 'tango-app-api-middleware/src/utils/openSearch.js';
4
+
5
+ export async function getTaggingConfig( req, res, next ) {
6
+ try {
7
+ const inputData= req.body;
8
+ const getData = await findOneStore( { storeId: inputData.storeId }, { revopTagging: 1 } );
9
+ req.store = getData;
10
+ next();
11
+ } catch ( error ) {
12
+ logger.error( { error: error, message: req.body, function: 'traffic-revop-getTaggingConfig' } );
13
+ next();
14
+ }
15
+ }
16
+
17
+
18
+ export async function getFootfallCount( req, res, next ) {
19
+ try {
20
+ const openSearch = JSON.parse( process.env.OPENSEARCH );
21
+ const inputData = req.query;
22
+ const dateString = `${inputData.storeId}_${inputData.dateString}`;
23
+ const getQuery = {
24
+ query: {
25
+ terms: {
26
+ _id: [ dateString ],
27
+ },
28
+ },
29
+ _source: [ 'footfall', 'date_string', 'store_id', 'down_time', 'footfall_count' ],
30
+ sort: [
31
+ {
32
+ date_iso: {
33
+ order: 'desc',
34
+ },
35
+ },
36
+ ],
37
+ };
38
+
39
+ const getData = await getOpenSearchData( openSearch.footfall, getQuery );
40
+ const hits = getData?.body?.hits?.hits || [];
41
+
42
+ const processedData = hits.find( ( d ) => d._id === dateString )?._source || null;
43
+ req.Footfall =processedData;
44
+ next();
45
+ } catch ( error ) {
46
+ logger.error( { error: error, message: req.body, function: 'traffic-revop-getFootfallCount' } );
47
+ next();
48
+ }
49
+ }
50
+
51
+ export async function mappingConfig( req, res, next ) {
52
+ try {
53
+ const inputData = req.body;
54
+ const openSearch = JSON.parse( process.env.OPENSEARCH );
55
+ // const footfall = req?.footfall?.footfall_count;
56
+ if ( inputData.revopsType == 'employee' ) {
57
+ const getQuery = {
58
+ query: {
59
+ bool: {
60
+ must: [
61
+ {
62
+ term: {
63
+ 'storeId.keyword': inputData.storeId,
64
+ },
65
+ },
66
+ {
67
+ term: {
68
+ 'dateString': inputData.dateString,
69
+ },
70
+ },
71
+ {
72
+ term: {
73
+ 'revopsType.keyword': inputData.revopsType,
74
+ },
75
+ },
76
+ ],
77
+ },
78
+ },
79
+ };
80
+
81
+ const getData = await getOpenSearchCount( openSearch.revop, getQuery );
82
+ if ( getData && getData?.body?.count >= 2 ) {
83
+ return res.sendError( 'Select up to 2 items only', 400 );
84
+ } else {
85
+ next();
86
+ }
87
+ } else if ( inputData.revopsType == 'house-keeping' ) {
88
+ const getQuery = {
89
+ query: {
90
+ bool: {
91
+ must: [
92
+ {
93
+ term: {
94
+ 'storeId.keyword': inputData.storeId,
95
+ },
96
+ },
97
+ {
98
+ term: {
99
+ 'dateString': inputData.dateString,
100
+ },
101
+ },
102
+ {
103
+ term: {
104
+ 'revopsType.keyword': inputData.revopsType,
105
+ },
106
+ },
107
+ ],
108
+ },
109
+ },
110
+ };
111
+ const getData = await getOpenSearchCount( openSearch.revop, getQuery );
112
+ if ( getData && getData?.body?.count >= 1 ) {
113
+ return res.sendError( 'Select up to 1 items only', 400 );
114
+ } else {
115
+ next();
116
+ }
117
+ } else {
118
+ next();
119
+ }
120
+ } catch ( error ) {
121
+ logger.error( { error: error, message: req.body, function: 'traffic-revop-getTaggingConfig' } );
122
+ next();
123
+ }
124
+ }
125
+
126
+ export async function deletetagedDuplicate( req, res, next ) {
127
+ try {
128
+ const inputData = req.body;
129
+ const openSearch = JSON.parse( process.env.OPENSEARCH );
130
+ const getQuery = {
131
+ query: {
132
+ term: {
133
+ parent: inputData?.tempId,
134
+ },
135
+ },
136
+
137
+ };
138
+ await deleteByQuery( openSearch.revop, getQuery );
139
+ next();
140
+ } catch ( error ) {
141
+ logger.error( { error: error, message: req.body, function: 'traffic-revop-deletetagedDuplicate' } );
142
+ next();
143
+ }
144
+ }
145
+