dcos-core-monalisav2-latam 1.0.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (129) hide show
  1. package/README.md +130 -0
  2. package/index.js +350 -0
  3. package/package.json +52 -0
  4. package/src/auth/handler.js +3 -0
  5. package/src/common/MondelezCastOrder.js +449 -0
  6. package/src/common/utils/AuthSecurity.js +46 -0
  7. package/src/common/utils/account-error-handler.js +279 -0
  8. package/src/common/utils/account-error-helper.js +231 -0
  9. package/src/common/utils/account-properties-handler.js +355 -0
  10. package/src/common/utils/api-response.js +62 -0
  11. package/src/common/utils/aws-services.js +186 -0
  12. package/src/common/utils/constants/account-error-codes.json +801 -0
  13. package/src/common/utils/constants.js +37 -0
  14. package/src/common/utils/convert/MondelezClientsItemsCast.js +52 -0
  15. package/src/common/utils/convert/MondelezInventoryItemsCast.js +15 -0
  16. package/src/common/utils/convert/MondelezOrderStatusCast.js +34 -0
  17. package/src/common/utils/convert/MondelezPricesItemsCast.js +37 -0
  18. package/src/common/utils/cron-ftp-get.js +143 -0
  19. package/src/common/utils/data-tables-helper.js +213 -0
  20. package/src/common/utils/date-range-calculator.js +113 -0
  21. package/src/common/utils/delay.js +17 -0
  22. package/src/common/utils/ftp-sftp.js +320 -0
  23. package/src/common/utils/logger.js +126 -0
  24. package/src/common/utils/nodemailerLib.js +61 -0
  25. package/src/common/utils/product-unit-converter.js +168 -0
  26. package/src/common/utils/schemas-utils.js +101 -0
  27. package/src/common/utils/seller-email-sharing-service.js +441 -0
  28. package/src/common/utils/sftp-utils.js +202 -0
  29. package/src/common/utils/status.js +15 -0
  30. package/src/common/utils/util.js +236 -0
  31. package/src/common/utils/validate-state-order.js +35 -0
  32. package/src/common/utils/validateProviders.js +67 -0
  33. package/src/common/utils/validation-data.js +45 -0
  34. package/src/common/utils/vtex/save-hooks.js +65 -0
  35. package/src/common/utils/vtex/save-schemas.js +65 -0
  36. package/src/common/utils/vtex-hook-handler.js +71 -0
  37. package/src/common/validation/AccountCoordinatesValidation.js +350 -0
  38. package/src/common/validation/GeneralErrorValidation.js +11 -0
  39. package/src/common/validation/MainErrorValidation.js +8 -0
  40. package/src/entities/account.js +639 -0
  41. package/src/entities/clients.js +104 -0
  42. package/src/entities/controlprice.js +196 -0
  43. package/src/entities/controlstock.js +206 -0
  44. package/src/entities/cron.js +77 -0
  45. package/src/entities/cronjob.js +71 -0
  46. package/src/entities/orders.js +195 -0
  47. package/src/entities/sftp-inbound.js +88 -0
  48. package/src/entities/sku.js +220 -0
  49. package/src/entities/taxpromotion.js +249 -0
  50. package/src/functions/account/account-get.js +262 -0
  51. package/src/functions/account/account-handler.js +299 -0
  52. package/src/functions/account/clients.js +10 -0
  53. package/src/functions/account/index.js +208 -0
  54. package/src/functions/actions/save-promotions-order-history.js +324 -0
  55. package/src/functions/affiliates/affiliates-hook-consumer.js +87 -0
  56. package/src/functions/affiliates/affiliates-hook-producer.js +45 -0
  57. package/src/functions/clients/clients-audience.js +62 -0
  58. package/src/functions/clients/clients-consumer.js +648 -0
  59. package/src/functions/clients/clients-producer.js +362 -0
  60. package/src/functions/clients/clients-suggested-product-consumer.js +166 -0
  61. package/src/functions/clients/helpers/suggested-product-mdlz.js +233 -0
  62. package/src/functions/clients_peru/email.html +129 -0
  63. package/src/functions/clients_peru/splitfile.js +357 -0
  64. package/src/functions/clients_peru/updateClients.js +1334 -0
  65. package/src/functions/clients_peru/utils.js +243 -0
  66. package/src/functions/cronjobs/cron-jobs-manager.js +40 -0
  67. package/src/functions/cronjobs/cron-jobs.js +171 -0
  68. package/src/functions/crons/cron.js +39 -0
  69. package/src/functions/distributors/distributor-handler.js +81 -0
  70. package/src/functions/distributors/distributor.js +535 -0
  71. package/src/functions/distributors/index.js +60 -0
  72. package/src/functions/financialpolicy/assign-financialpolicy.js +111 -0
  73. package/src/functions/financialpolicy/get-financialpolicy.js +91 -0
  74. package/src/functions/financialpolicy/index.js +28 -0
  75. package/src/functions/inventory/catalog-sync-consumer.js +17 -0
  76. package/src/functions/inventory/catalog-sync-handler.js +311 -0
  77. package/src/functions/inventory/inventory-consumer.js +119 -0
  78. package/src/functions/inventory/inventory-producer.js +197 -0
  79. package/src/functions/multiPresentation/multipre-queue.js +155 -0
  80. package/src/functions/multiPresentation/multipres.js +459 -0
  81. package/src/functions/nodeflow/index.js +83 -0
  82. package/src/functions/nodeflow/nodeflow-cron.js +200 -0
  83. package/src/functions/nodeflow/nodeflow-pub.js +203 -0
  84. package/src/functions/nodeflow/nodeflow-pvt.js +266 -0
  85. package/src/functions/notifications/download-leads-handler.js +67 -0
  86. package/src/functions/notifications/new-leads-notification-consumer.js +17 -0
  87. package/src/functions/notifications/new-leads-notification-handler.js +359 -0
  88. package/src/functions/notifications/order-status-notification-handler.js +482 -0
  89. package/src/functions/notifications/promotion-notification-handler.js +193 -0
  90. package/src/functions/orders/index.js +32 -0
  91. package/src/functions/orders/orders-cancel-handler.js +74 -0
  92. package/src/functions/orders/orders-handler.js +280 -0
  93. package/src/functions/orders/orders-hook-consumer.js +137 -0
  94. package/src/functions/orders/orders-hook-producer.js +170 -0
  95. package/src/functions/orders/orders-notifications-handler.js +137 -0
  96. package/src/functions/orders/orders-status-consumer.js +461 -0
  97. package/src/functions/orders/orders-status-producer.js +443 -0
  98. package/src/functions/prices/index.js +75 -0
  99. package/src/functions/prices/prices-consumer.js +236 -0
  100. package/src/functions/prices/prices-producer.js +323 -0
  101. package/src/functions/prices/promotion-and-tax.js +1284 -0
  102. package/src/functions/routesflow/assign-routeflow-queue.js +77 -0
  103. package/src/functions/schemas/vtex/handle-schemas.js +102 -0
  104. package/src/functions/security/process_gas.js +221 -0
  105. package/src/functions/security/security-handler.js +950 -0
  106. package/src/functions/sftp/sftp-consumer.js +453 -0
  107. package/src/functions/sftpIntegrations/processes/redirectServices.js +184 -0
  108. package/src/functions/sftpIntegrations/processes/validateFileSchema.js +226 -0
  109. package/src/functions/sftpIntegrations/schemas/credential-schema.js +123 -0
  110. package/src/functions/sftpIntegrations/schemas/record-schema.js +131 -0
  111. package/src/functions/sftpIntegrations/schemas/sftp_required_fields.json +3 -0
  112. package/src/functions/sftpIntegrations/sftp-config-producer.js +112 -0
  113. package/src/functions/sftpIntegrations/sftp-consumer.js +700 -0
  114. package/src/functions/sftpIntegrations/test/validateFile.test.js +122 -0
  115. package/src/functions/sftpIntegrations/utils/connect-dynamo.js +29 -0
  116. package/src/functions/sftpIntegrations/utils/split-data.js +25 -0
  117. package/src/functions/utils/index.js +130 -0
  118. package/src/functions/vtex/vtex-helpers.js +694 -0
  119. package/src/integrations/accountErrors/AccountErrorManager.js +437 -0
  120. package/src/integrations/audience/Audience.js +70 -0
  121. package/src/integrations/financialPolicy/FinancialPolicyApi.js +377 -0
  122. package/src/integrations/index.js +0 -0
  123. package/src/integrations/mobilvendor/MobilvendorApi.js +405 -0
  124. package/src/integrations/productmultipresentation/ProductMultiPresentation.js +200 -0
  125. package/src/mdlz/auth/SecretManagerApi.js +77 -0
  126. package/src/mdlz/client/MdlzApi.js +70 -0
  127. package/src/vtex/clients/ProvidersApi.js +51 -0
  128. package/src/vtex/clients/VtexApi.js +511 -0
  129. package/src/vtex/models/VtexOrder.js +87 -0
@@ -0,0 +1,950 @@
1
+ const { parse } = require('aws-multipart-parser');
2
+ const validate = require('validate.js');
3
+ const AWSServices = require('../../common/utils/aws-services');
4
+ const ApiResponse = require('../../common/utils/api-response');
5
+ const Util = require('../../common/utils/util.js');
6
+ const SecretManagerApi = require('../../mdlz/auth/SecretManagerApi.js');
7
+ const VtexApi = require('../../vtex/clients/VtexApi');
8
+ const Logger = require("../../common/utils/logger");
9
+
10
+ const MANAGER_STORE_KEYS = process.env.AUTHORIZATION_MANAGER_KEY;
11
+ const REMOVE_USER = true;
12
+
13
+ /**
14
+ * Función principal que maneja las solicitudes entrantes.
15
+ * @param {Object} event - Evento de AWS Lambda.
16
+ * @returns {Object} - Respuesta API con resultado del proceso.
17
+ */
18
+ const producer = async (event) => {
19
+ // Datos iniciales de la respuesta a la petición
20
+ let statusCode = 200, responseMessage = { message: '' };
21
+
22
+ try {
23
+ // Determinar el tipo de operación basado en la ruta
24
+ const operationType = getOperationType(event);
25
+
26
+ // Conexión a la API de Secret Manager
27
+ const configAuth = await AWSServices.getSecretValue(MANAGER_STORE_KEYS);
28
+ const auth = JSON.parse(configAuth);
29
+
30
+ // Rama de ejecución según tipo de operación
31
+ switch (operationType) {
32
+ case 'accounts':
33
+ return await processAccountsRequest(event, auth);
34
+ case 'users':
35
+ return await processUsersRequest(event, auth);
36
+ default:
37
+ return await processUsersRequest(event, auth);
38
+ }
39
+ } catch (ex) {
40
+ Logger.error('Error process request: ', ex);
41
+
42
+ statusCode = 400;
43
+ const { message } = ex;
44
+ try {
45
+ responseMessage = JSON.parse(message);
46
+ } catch (parseEx) {
47
+ responseMessage.message = `Process went wrong: ${message}`;
48
+ }
49
+ }
50
+
51
+ return ApiResponse.response(statusCode, responseMessage);
52
+ };
53
+
54
+ /**
55
+ * Determina el tipo de operación basado en la ruta de la petición
56
+ * @param {Object} event - Evento de AWS Lambda
57
+ * @returns {String} - Tipo de operación: 'users' o 'accounts'
58
+ */
59
+ const getOperationType = (event) => {
60
+ const path = event.resource || '';
61
+ switch (path) {
62
+ case '/batch/users':
63
+ return 'users';
64
+ case '/batch/accounts':
65
+ return 'accounts';
66
+ default:
67
+ return 'users';
68
+ }
69
+ };
70
+
71
+ /**
72
+ * Procesa la solicitud para gestionar cuentas
73
+ * @param {Object} event - Evento de AWS Lambda
74
+ * @param {Object} auth - Configuración de autenticación
75
+ * @returns {Object} - Respuesta API con resultado del proceso
76
+ */
77
+ async function processAccountsRequest(event, auth) {
78
+ let statusCode = 200;
79
+ let responseMessage = { message: '', dataSuccess: [], dataErrors: [] };
80
+
81
+ try {
82
+ // Consulta de los valores almacenados en el Secret Manager
83
+ const { accountsData, isAvailable, authorizedUsers, availableExtensions } = await getSecretDataAccount(auth);
84
+
85
+ // Extraer datos del formulario
86
+ const formData = getFormData(event, availableExtensions);
87
+ const { email: formDataEmail, file: formDataFile } = formData;
88
+
89
+ // Validar autorización del remitente
90
+ if (!validateSenderAuthorizationAdmin(formDataEmail, authorizedUsers, isAvailable)) {
91
+ return ApiResponse.response(403, {
92
+ message: `El usuario '${formDataEmail}' no está autorizado para ejecutar este proceso`
93
+ });
94
+ }
95
+
96
+ // Procesar el archivo CSV para accounts
97
+ const result = await processAccountsCsv(formDataFile, accountsData, auth);
98
+
99
+ responseMessage.message = "Proceso de cuentas finalizado";
100
+ responseMessage.dataSuccess = result.dataSuccess;
101
+ responseMessage.dataErrors = result.dataErrors;
102
+
103
+ return ApiResponse.response(statusCode, responseMessage);
104
+ } catch (error) {
105
+ Logger.error('Error procesando cuentas:', error);
106
+ return ApiResponse.response(400, {
107
+ message: `Error procesando cuentas: ${error.message}`
108
+ });
109
+ }
110
+ }
111
+
112
+ /**
113
+ * Procesa la solicitud para gestionar usuarios
114
+ * @param {Object} event - Evento de AWS Lambda
115
+ * @param {Object} auth - Configuración de autenticación
116
+ * @returns {Object} - Respuesta API con resultado del proceso
117
+ */
118
+ async function processUsersRequest(event, auth) {
119
+ let statusCode = 200;
120
+ let responseMessage = { message: '', dataSuccess: [], dataErrors: [] };
121
+
122
+ try {
123
+ // Consulta de los valores almacenados en el Secret Manager
124
+ const { accountsData, isAvailable, authorizedUsers, availableExtensions } = await getSecretData(auth);
125
+ const { limitUsersProcess = 50 } = auth;
126
+
127
+ // Extraer datos del formulario
128
+ const formData = getFormData(event, availableExtensions);
129
+ const { email: formDataEmail, file: formDataFile } = formData;
130
+
131
+ // Validar permisos del usuario
132
+ let permissions = { '*': isAvailable };
133
+ if (Object.keys(authorizedUsers).length && formDataEmail && authorizedUsers.hasOwnProperty(formDataEmail)) {
134
+ permissions = { ...permissions, ...authorizedUsers[formDataEmail] };
135
+ }
136
+
137
+ // Validar si el usuario tiene permisos para ejecutar el proceso
138
+ const { '*': executeProcess, ...remainPermissions } = permissions;
139
+ if (!executeProcess && Object.keys(remainPermissions).length == 0) {
140
+ throw new Error(`The user '${formDataEmail}' isn't authorized for execute the process`);
141
+ }
142
+
143
+ // Obtener propiedades de validación del CSV
144
+ const { separatorColumn, constraints } = getCsvValidationProperties(accountsData, executeProcess, remainPermissions);
145
+
146
+ // Obtener filas del CSV
147
+ const { csvRows, headerRow } = getCsvRowsData(formDataFile, limitUsersProcess);
148
+
149
+ // Validar encabezado del CSV
150
+ const csvColumns = Object.keys(constraints);
151
+ if (!new RegExp(`^\\w+(?:${separatorColumn}\\w+)*$`).test(headerRow)) {
152
+ throw new Error(`CSV header '${headerRow}' must contain the column separator '${separatorColumn}'`);
153
+ }
154
+
155
+ // Validar columnas del CSV
156
+ const csvHeaderKeys = arrayToJson(headerRow.split(separatorColumn), []);
157
+ for (let key of csvColumns) {
158
+ if (csvHeaderKeys.hasOwnProperty(key)) {
159
+ delete csvHeaderKeys[key];
160
+ }
161
+ }
162
+
163
+ // Validar columnas inválidas
164
+ const invalidColumns = Object.keys(csvHeaderKeys);
165
+ if (invalidColumns.length) {
166
+ throw new Error(`CSV header contains invalid columns: '${invalidColumns.join(separatorColumn)}'`);
167
+ }
168
+
169
+ // Validar filas del CSV
170
+ let promises = [], checkUsers = {}, accountsIssue = {};
171
+ const dataSuccess = [], dataErrors = [];
172
+ validateRowsData(promises, checkUsers, csvRows, csvColumns, separatorColumn, constraints, dataErrors);
173
+
174
+ // Consultar usuarios en VTEX si es necesario
175
+ await findUsersInVtex(accountsData, checkUsers, accountsIssue);
176
+
177
+ if (promises.length) {
178
+ let result = await Promise.allSettled(promises.map(userRow => {
179
+ return new Promise(async (resolve) => {
180
+ const { jsonData } = userRow;
181
+ let { roles } = jsonData;
182
+ if (!roles && REMOVE_USER) {
183
+ await removeUser(accountsData, resolve, userRow, auth, checkUsers, accountsIssue);
184
+ } else {
185
+ await updateUserRoles(accountsData, resolve, userRow, auth);
186
+ }
187
+ });
188
+ })).catch(Logger.error);
189
+
190
+ for (let res of result) {
191
+ const { index, error, message } = res.value;
192
+ if (error) {
193
+ dataErrors[index] = `Line: ${index + 1}. Error: ${error}`;
194
+ } else {
195
+ dataSuccess[index] = `Line: ${index + 1}. Message: ${message}`;
196
+ }
197
+ }
198
+ }
199
+
200
+ responseMessage.message = 'Process finished';
201
+ responseMessage.dataSuccess = dataSuccess.filter(message => message);
202
+ responseMessage.dataErrors = dataErrors.filter(message => message);
203
+
204
+ return ApiResponse.response(statusCode, responseMessage);
205
+ } catch (error) {
206
+ Logger.error('Error procesando usuarios:', error);
207
+ return ApiResponse.response(400, {
208
+ message: `Error procesando usuarios: ${error.message}`
209
+ });
210
+ }
211
+ }
212
+
213
+ /**
214
+ * Valida si el remitente está autorizado para ejecutar el proceso
215
+ * @param {string} email - Correo del remitente
216
+ * @param {object} authorizedUsers - Objeto con usuarios autorizados
217
+ * @param {boolean} isAvailable - Indica si el sistema está disponible globalmente
218
+ * @returns {boolean} - Indica si el remitente está autorizado
219
+ */
220
+ function validateSenderAuthorization(email, authorizedUsers, isAvailable) {
221
+ if (!email) return false;
222
+ if (isAvailable) return true;
223
+ if (authorizedUsers.hasOwnProperty(email)) {
224
+ const permissions = authorizedUsers[email];
225
+ if (permissions.hasOwnProperty('*') && permissions['*'] === true) {
226
+ return true;
227
+ }
228
+ for (const account in permissions) {
229
+ if (permissions[account] === true) {
230
+ return true;
231
+ }
232
+ }
233
+ }
234
+ return false;
235
+ }
236
+
237
+ /**
238
+ * Valida si el remitente está autorizado para ejecutar el proceso (solo admin con permiso global)
239
+ * @param {string} email - Correo del remitente
240
+ * @param {object} authorizedUsers - Objeto con usuarios autorizados
241
+ * @param {boolean} isAvailable - Indica si el sistema está disponible globalmente
242
+ * @returns {boolean} - Indica si el remitente está autorizado
243
+ */
244
+ function validateSenderAuthorizationAdmin(email, authorizedUsers, isAvailable) {
245
+ // Si no hay email, no está autorizado
246
+ if (!email) return false;
247
+ // Si el sistema está disponible globalmente, todos están autorizados
248
+ if (isAvailable) return true;
249
+ // Verificar si el usuario existe en la lista de autorizados
250
+ if (authorizedUsers.hasOwnProperty(email)) {
251
+ const permissions = authorizedUsers[email];
252
+ // Solo está autorizado si tiene el permiso global (*) explícitamente en true
253
+ return permissions['*'] === true;
254
+ }
255
+ // No está autorizado por defecto
256
+ return false;
257
+ }
258
+
259
+ /**
260
+ * Obtiene los datos enviados al cuerpo de la petición.
261
+ * @param {Object} event - Objeto con datos de la petición a la función lambda.
262
+ * @param {Array} availableExtensions - Lista de extensiones permitidas.
263
+ * @returns {Object} - Objeto con los datos del form-data.
264
+ */
265
+ function getFormData(event, availableExtensions) {
266
+ const formData = parse(event, true);
267
+
268
+ let errors = validate(formData, {
269
+ 'file': { presence: { allowEmpty: false } },
270
+ 'file.type': { presence: { allowEmpty: false } },
271
+ 'file.filename': { presence: { allowEmpty: false } },
272
+ 'file.contentType': {
273
+ presence: { allowEmpty: false },
274
+ inclusion: {
275
+ within: availableExtensions,
276
+ message: `^El archivo debe tener una extensión .csv o .txt. Verifique el formato de su archivo e inténtelo nuevamente.`
277
+ }
278
+ },
279
+ 'file.content': { presence: { allowEmpty: false } },
280
+ 'email': { presence: { allowEmpty: true }, email: true }
281
+ });
282
+
283
+ if (errors) {
284
+ let errorsData = {};
285
+ for (let key in errors) {
286
+ const [message = ''] = errors[key];
287
+ errorsData[key] = message;
288
+ }
289
+ throw new Error(JSON.stringify(errorsData));
290
+ }
291
+
292
+ return formData;
293
+ }
294
+
295
+ /**
296
+ * Consulta los valores configuradas en Secret Manager.
297
+ * @param {Object} auth - Configuración de autenticación
298
+ * @returns {Object} - Datos de configuración obtenidos
299
+ */
300
+ async function getSecretDataAccount(auth) {
301
+ const { tokenSecretManagerAPI, urlSecretManagerAPI, secretName } = auth;
302
+ const secretManagerApi = new SecretManagerApi(`Bearer ${tokenSecretManagerAPI}`, urlSecretManagerAPI);
303
+ const responseSecret = await secretManagerApi.fetch(`${secretName}`, { method: 'GET' }, true);
304
+ const { status: smStatus, statusText: smStatusText, data: smData } = responseSecret;
305
+
306
+ // Validar respuesta del Secret Manager
307
+ if (!(smStatus >= 200 && smStatus < 300)) {
308
+ throw new Error(`Can't get information from SM API`);
309
+ }
310
+
311
+ if (!smData?.[0]?.value) {
312
+ throw new Error(`Information from SM API not defined`);
313
+ }
314
+
315
+ const {
316
+ isAvailable = false,
317
+ authorizedUsers = {},
318
+ availableExtensions = ['text/csv', 'text/plain'],
319
+ accounts = []
320
+ } = smData[0].value;
321
+
322
+ // 1. Procesar cuentas: Mantener como ARRAY para usar .some()
323
+ const accountsData = accounts
324
+ .filter(account =>
325
+ account.accountName &&
326
+ account.vtexAppkey &&
327
+ account.vtexAppToken
328
+ );
329
+
330
+ // 2. Limpiar permisos redundantes
331
+ Object.keys(authorizedUsers).forEach(email => {
332
+ const hasWildcard = authorizedUsers[email].hasOwnProperty('*');
333
+ const { '*': wildcardUser, ...remainPerms } = authorizedUsers[email];
334
+
335
+ Object.keys(remainPerms).forEach(account => {
336
+ if (remainPerms[account] === (hasWildcard ? wildcardUser : isAvailable)) {
337
+ delete authorizedUsers[email][account];
338
+ }
339
+ });
340
+ });
341
+
342
+ return {
343
+ accountsData, // Ahora es un array filtrado y mapeado
344
+ isAvailable,
345
+ authorizedUsers,
346
+ availableExtensions
347
+ };
348
+ }
349
+
350
+ /**
351
+ * Consulta los valores configuradas en Secret Manager.
352
+ * @param {*} auth
353
+ */
354
+ async function getSecretData(auth) {
355
+ const { tokenSecretManagerAPI, urlSecretManagerAPI, secretName } = auth;
356
+ const secretManagerApi = new SecretManagerApi(`Bearer ${tokenSecretManagerAPI}`, urlSecretManagerAPI);
357
+ const responseSecret = await secretManagerApi.fetch(`${secretName}`, { method: 'GET' }, true);
358
+ const { status: smStatus, statusText: smStatusText, data: smData } = responseSecret;
359
+
360
+ // Se valida si la consulta de los secret fue exitosa
361
+ if (!(smStatus >= 200 && smStatus < 300)) {
362
+ throw new Error(`Can't get information from SM API`);
363
+ }
364
+
365
+ if (!smData?.[0]?.value) {
366
+ throw new Error(`Information from SM API not defined`);
367
+ }
368
+
369
+ const accountsData = {};
370
+ const { isAvailable = false, authorizedUsers = {}, availableExtensions = ['text/csv', 'text/plain'], accounts = [] } = smData[0].value;
371
+
372
+ // Se agrupa los datos de las cuentas por el nombre (accountName)
373
+ for (let account of accounts) {
374
+ const { accountName, vtexAppkey, vtexAppToken } = account;
375
+ if (accountName && !accountsData[accountName]) {
376
+ // Instancia de conexión a la API de VTEX para el account 'accountName'
377
+ accountsData[accountName] = new VtexApi(accountName, vtexAppkey, vtexAppToken);
378
+ }
379
+ }
380
+
381
+ // Se eliminan las redundancias que puede tener la parametrización de permisos
382
+ for (let email in authorizedUsers) {
383
+ const hasWildcard = authorizedUsers[email].hasOwnProperty('*');
384
+ const { '*': wildcardUser, ...remainPerms } = authorizedUsers[email];
385
+ for (let account in remainPerms) {
386
+ if (remainPerms[account] == hasWildcard ? wildcardUser : isAvailable) {
387
+ delete authorizedUsers[email][account];
388
+ }
389
+ }
390
+ }
391
+
392
+ return { accountsData, isAvailable, authorizedUsers, availableExtensions };
393
+ }
394
+
395
+ /**
396
+ * Procesa el archivo CSV para configurar accounts
397
+ * @param {object} fileData - Objeto con datos del archivo subido
398
+ * @param {object} accountsData - Datos de las cuentas existentes
399
+ * @param {object} auth - Configuración de autenticación
400
+ * @returns {object} - Resultados del procesamiento
401
+ */
402
+ async function processAccountsCsv(fileData, accountsData, auth) {
403
+ const result = {
404
+ dataSuccess: [],
405
+ dataErrors: []
406
+ };
407
+
408
+ try {
409
+ // 1. Validación básica del contenido del archivo
410
+ let csvDataString = Buffer.from(fileData.content).toString();
411
+ if (!csvDataString || !csvDataString.trim()) {
412
+ throw new Error("El contenido del CSV está vacío o no es válido");
413
+ }
414
+
415
+ const csvRows = csvDataString.trim().split(/\r?\n/);
416
+
417
+ // 2. Validación de headers
418
+ if (csvRows.length === 0) {
419
+ throw new Error("El archivo no contiene datos");
420
+ }
421
+
422
+ const headerRow = csvRows.shift();
423
+
424
+ // 3. Validación de existencia de headers
425
+ if (!headerRow) {
426
+ throw new Error("El archivo no contiene encabezados. Debe incluir la primera línea con los nombres de columna.");
427
+ }
428
+
429
+ // 4. Validación del delimitador
430
+ if (!headerRow.includes(';')) {
431
+ throw new Error("El archivo CSV debe estar delimitado por ';'. Verifique el formato e inténtelo nuevamente.");
432
+ }
433
+
434
+ // 5. Validación de headers esperados
435
+ const headers = headerRow.split(';').map(h => h.trim().toLowerCase());
436
+ const expectedHeaders = ['action', 'accountname', 'appkey', 'apptoken'];
437
+ const missingHeaders = expectedHeaders.filter(h => !headers.includes(h));
438
+
439
+ if (missingHeaders.length > 0) {
440
+ throw new Error(`Encabezados incorrectos. Faltan: ${missingHeaders.join(', ')}. Formato esperado: ACTION;ACCOUNTNAME;APPKEY;APPTOKEN`);
441
+ }
442
+
443
+ // Mapeo de índices de columnas
444
+ const columnIndices = {};
445
+ headers.forEach((header, index) => {
446
+ columnIndices[header] = index;
447
+ });
448
+
449
+ // Obtener configuración actual del Secret Manager
450
+ const { currentConfig, secretManagerApi } = await getCurrentSecretManagerConfig(auth);
451
+
452
+ if (!Array.isArray(currentConfig.value.accounts)) {
453
+ currentConfig.value.accounts = [];
454
+ }
455
+
456
+ // Procesar cada fila del CSV
457
+ for (let [index, row] of csvRows.entries()) {
458
+ try {
459
+ row = row.trim();
460
+ if (!row) {
461
+ result.dataErrors.push(`Línea ${index + 2}: Fila vacía (se esperaban datos)`);
462
+ continue;
463
+ }
464
+
465
+ const rowData = row.split(';');
466
+
467
+ // 6. Validación de campos vacíos
468
+ const action = (rowData[columnIndices.action]?.trim() || 'add').toLowerCase();
469
+ const accountName = rowData[columnIndices.accountname]?.trim();
470
+ const appKey = rowData[columnIndices.appkey]?.trim();
471
+ const appToken = rowData[columnIndices.apptoken]?.trim();
472
+
473
+ // Validar campos obligatorios
474
+ const errors = [];
475
+ if (!accountName) errors.push('ACCOUNTNAME no puede estar vacío');
476
+ if (!appKey) errors.push('APPKEY no puede estar vacío');
477
+ if (!appToken) errors.push('APPTOKEN no puede estar vacío');
478
+ if (!['add', 'delete'].includes(action)) {
479
+ errors.push(`ACTION '${action}' no válida (solo ADD/DELETE)`);
480
+ }
481
+
482
+ if (errors.length > 0) {
483
+ result.dataErrors.push(`Línea ${index + 2}: ${errors.join('; ')}`);
484
+ continue;
485
+ }
486
+
487
+ // Procesar acción
488
+ const accountData = {
489
+ accountName: accountName.toLowerCase(),
490
+ vtexAppkey: appKey,
491
+ vtexAppToken: appToken
492
+ };
493
+
494
+ if (action === 'add') {
495
+ // Eliminar cuenta existente si ya existe
496
+ currentConfig.value.accounts = currentConfig.value.accounts.filter(
497
+ acc => acc.accountName.toLowerCase() !== accountData.accountName
498
+ );
499
+ currentConfig.value.accounts.push(accountData);
500
+ result.dataSuccess.push(`Línea ${index + 2}: Cuenta '${accountData.accountName}' agregada/actualizada correctamente`);
501
+ } else if (action === 'delete') {
502
+ const initialCount = currentConfig.value.accounts.length;
503
+ currentConfig.value.accounts = currentConfig.value.accounts.filter(
504
+ acc => acc.accountName.toLowerCase() !== accountData.accountName
505
+ );
506
+ if (initialCount === currentConfig.value.accounts.length) {
507
+ result.dataErrors.push(`Línea ${index + 2}: Cuenta '${accountData.accountName}' no encontrada para eliminar`);
508
+ } else {
509
+ result.dataSuccess.push(`Línea ${index + 2}: Cuenta '${accountData.accountName}' eliminada correctamente`);
510
+ }
511
+ }
512
+ } catch (error) {
513
+ result.dataErrors.push(`Línea ${index + 2}: Error procesando - ${error.message}`);
514
+ }
515
+ }
516
+
517
+ // Actualizar Secret Manager solo si no hay errores críticos
518
+ if (result.dataErrors.length === 0 || result.dataSuccess.length > 0) {
519
+ const updateResponse = await secretManagerApi.fetch(`${currentConfig.id}/${currentConfig.name}`, {
520
+ method: 'PUT',
521
+ data: {
522
+ id: currentConfig.id,
523
+ name: currentConfig.name,
524
+ value: currentConfig.value
525
+ }
526
+ }, true);
527
+
528
+ if (updateResponse.status < 200 || updateResponse.status >= 300) {
529
+ throw new Error(`Error al actualizar Secret Manager: ${updateResponse.statusText}`);
530
+ }
531
+ }
532
+
533
+ } catch (error) {
534
+ // Capturar errores de validación globales
535
+ throw new Error(`Error procesando CSV: ${error.message}`);
536
+ }
537
+
538
+ return result;
539
+ }
540
+
541
+ /**
542
+ * Obtiene la configuración actual del Secret Manager
543
+ * @param {object} auth - Configuración de autenticación
544
+ * @returns {object} - Configuración actual y instancia de SecretManagerApi
545
+ */
546
+ async function getCurrentSecretManagerConfig(auth) {
547
+ const { tokenSecretManagerAPI, urlSecretManagerAPI, secretName } = auth;
548
+ const secretManagerApi = new SecretManagerApi(`Bearer ${tokenSecretManagerAPI}`, urlSecretManagerAPI);
549
+ const response = await secretManagerApi.fetch(`${secretName}`, { method: 'GET' }, true);
550
+
551
+ if (response.status < 200 || response.status >= 300) {
552
+ throw new Error(`Error al obtener información del Secret Manager: ${response.statusText}`);
553
+ }
554
+
555
+ if (!response.data?.[0]?.value) {
556
+ throw new Error("No se encontró configuración en Secret Manager");
557
+ }
558
+
559
+ // Manejar tanto string JSON como objeto
560
+ let secretValue = response.data[0].value;
561
+ if (typeof secretValue === 'string') {
562
+ try {
563
+ secretValue = JSON.parse(secretValue);
564
+ } catch (e) {
565
+ throw new Error("El valor del secret no es un JSON válido");
566
+ }
567
+ }
568
+
569
+ // Asegurar que accounts es un array
570
+ if (!Array.isArray(secretValue.accounts)) {
571
+ secretValue.accounts = [];
572
+ }
573
+
574
+ return {
575
+ currentConfig: {
576
+ id: response.data[0].id || secretName,
577
+ name: response.data[0].name || secretName,
578
+ value: secretValue
579
+ },
580
+ secretManagerApi
581
+ };
582
+ }
583
+
584
+ /**
585
+ * Obtiene el contenido del archivo CSV enviado en la petición.
586
+ * @param {object} fileData - Objeto con los datos del archivo subido.
587
+ * @param {Number} limitUsersProcess - Número máximo de filas que debe tener el archivo.
588
+ * @returns {Object} - Fila de encabezado, y filas restantes del archivo leido.
589
+ */
590
+ function getCsvRowsData(fileData, limitUsersProcess) {
591
+ // Se obtiene el contenido del archivo subido
592
+ let csvDataString = Buffer.from(fileData.content).toString();
593
+ if (csvDataString) {
594
+ csvDataString = csvDataString.trim();
595
+ }
596
+
597
+ if (!csvDataString) {
598
+ throw new Error(`CSV content is empty or is invalid`);
599
+ }
600
+
601
+ // Se obtiene las filas del archivo
602
+ const csvRows = csvDataString.split(/\r?\n/), headerRow = csvRows.shift();
603
+
604
+ // Se valida que existan filas en el contenido
605
+ if (csvRows.length == 0) {
606
+ throw new Error(`CSV content is required`);
607
+ }
608
+
609
+ // Se valida que el N° de filas no supere el valor máximo establecido
610
+ if (csvRows.length > limitUsersProcess) {
611
+ throw new Error(`Limit process exceed to upload and update users. Maximum users: ${limitUsersProcess}`);
612
+ }
613
+
614
+ return { csvRows, headerRow };
615
+ }
616
+
617
+ /**
618
+ * Obtiene las propiedades a usar en la validación del contenido del archivo csv.
619
+ * @param {object} accountsData - Lista de accounts registradas en SecretManager.
620
+ * @param {Boolean} hasPermission - Valor que indica si se tiene permisos para ejecutar la actualización con todas los accounts.
621
+ * @param {object} remainPermissions - Objeto con lista de accounts exentos de la condicional 'hasPermission'.
622
+ * @returns {Object} - Caracter * del separador de columnas (separatorColumn) y reglas de validación (constraints).
623
+ */
624
+ function getCsvValidationProperties(accountsData, hasPermission, remainPermissions) {
625
+ let exclusion = [];
626
+ for (let account in accountsData) {
627
+ if (remainPermissions.hasOwnProperty(account)) {
628
+ if (!remainPermissions[account]) {
629
+ exclusion.push(account);
630
+ }
631
+ } else if (!hasPermission) {
632
+ exclusion.push(account);
633
+ }
634
+ }
635
+
636
+ const separatorColumn = ';', constraints = {
637
+ 'accountname': {
638
+ 'presence': { allowEmpty: false },
639
+ 'inclusion': {
640
+ 'within': Object.keys(accountsData),
641
+ 'message': `^'%{value}' isn't in the accounts configuration list`
642
+ },
643
+ 'exclusion': {
644
+ 'within': exclusion,
645
+ 'message': `^User isn't authorized for process roles in account '%{value}'`
646
+ }
647
+ },
648
+ 'email': {
649
+ 'presence': { allowEmpty: false },
650
+ 'email': {
651
+ 'message': `'%{value}' is not valid`
652
+ }
653
+ },
654
+ 'roles': {
655
+ 'presence': { allowEmpty: true },
656
+ 'format': {
657
+ 'pattern': /^\d*(,\d+)*$/,
658
+ 'message': `'%{value}' is not a valid comma separated id values`
659
+ }
660
+ }
661
+ };
662
+
663
+ return { separatorColumn, constraints };
664
+ }
665
+
666
+ /**
667
+ * Realiza la validación de datos para cada una de las filas del archivo
668
+ * @param {array} promises - Array donde se inserta los datos válidos.
669
+ * @param {object} checkUsers - Objeto donde se consolida los usuarios a eliminar.
670
+ * @param {array} csvRows - Filas del archivo csv leido.
671
+ * @param {array} csvColumns - Lista de columnas del archivo.
672
+ * @param {string} separatorColumn - Separador de columnas.
673
+ * @param {object} constraints - Reglas de validación del archivo.
674
+ * @param {array} dataErrors - Lista donde se concatena el mensaje de error para cada línea del archivo.
675
+ */
676
+ function validateRowsData(promises, checkUsers, csvRows, csvColumns, separatorColumn, constraints, dataErrors) {
677
+ for (let [index, row] of csvRows.entries()) {
678
+ row = row.trim();
679
+ if (row) {
680
+ if (new RegExp(`^.*(?:${separatorColumn}.*){${csvColumns.length - 1}}$`).test(row)) {
681
+ let rowData = row.split(separatorColumn);
682
+ const jsonData = arrayToJson(csvColumns, rowData);
683
+ let errors = validate(jsonData, constraints);
684
+ if (errors) {
685
+ let reportErrors = [];
686
+ for (let err in errors) {
687
+ const [message = ''] = errors[err];
688
+ if (message) {
689
+ reportErrors.push(message);
690
+ }
691
+ }
692
+ dataErrors[index] = `Line: ${index + 1}. Error: ${reportErrors.join(';')}`;
693
+ } else {
694
+ promises.push({ index, jsonData });
695
+
696
+ let { accountname, email, roles } = jsonData;
697
+ if (!roles && REMOVE_USER) {
698
+ if (accountname && !checkUsers.hasOwnProperty(accountname)) {
699
+ checkUsers[accountname] = {};
700
+ }
701
+ if (email && !checkUsers[accountname]?.hasOwnProperty(email)) {
702
+ checkUsers[accountname][email] = null;
703
+ }
704
+ }
705
+ }
706
+ } else {
707
+ dataErrors[index] = `Line: ${index + 1}. Error: row data '${row}' does not have the columns separator (${separatorColumn}) and/or does not have the columns number required (${csvColumns.length})`;
708
+ }
709
+ } else {
710
+ dataErrors[index] = `Line: ${index + 1}. Error: Line is empty`;
711
+ }
712
+ }
713
+ }
714
+
715
+ /**
716
+ * Convierte un array a un objeto JSON usando las cabeceras como claves.
717
+ * @param {Array} headers - Cabeceras que serán usadas como claves
718
+ * @param {Array} items - Valores a asignar a cada clave
719
+ * @returns {Object} - Objeto JSON con los datos
720
+ */
721
+ function arrayToJson(headers, items) {
722
+ let objectData = {};
723
+ headers.forEach((key, index) => {
724
+ objectData[key] = items?.[index]?.trim() ?? null;
725
+ });
726
+ return objectData;
727
+ }
728
+
729
+ /**
730
+ * Actualiza los roles asociados a un determinado usuario.
731
+ * @param {object} accountsData - Datos de cuentas
732
+ * @param {function} resolve - Función para resolver la promesa
733
+ * @param {object} userRow - Objeto de datos de la fila
734
+ * @param {object} auth - Configuración de autenticación
735
+ */
736
+ async function updateUserRoles(accountsData, resolve, userRow, auth) {
737
+ const { index, jsonData } = userRow;
738
+ let { accountname, email, roles } = jsonData;
739
+
740
+ const responseCreateUser = await accountsData[accountname].fetch(auth?.pathCreateUser, {
741
+ method: 'POST',
742
+ data: { name: '', email }
743
+ }, false).catch(ex => {
744
+ const errReq = Util.getErrorData(ex);
745
+ Logger.error(errReq);
746
+ const { status, message } = errReq;
747
+ switch (status) {
748
+ case 400:
749
+ resolve({ index, error: `Invalid VTEX request for create user with email '${email}'` });
750
+ break;
751
+ case 403:
752
+ resolve({ index, error: `Denied access or invalid/inactived credentials for create user in account '${accountname}'` });
753
+ break;
754
+ default:
755
+ resolve({ index, error: message });
756
+ break;
757
+ }
758
+ });
759
+
760
+ if (responseCreateUser?.data?.id) {
761
+ const { id: userId } = responseCreateUser.data;
762
+
763
+ let rolesKeys = {};
764
+ for (let roleId of roles.split(',')) {
765
+ roleId = String(roleId);
766
+ if (roleId) {
767
+ rolesKeys[roleId] = true;
768
+ }
769
+ }
770
+
771
+ const relativeGetAddRoleByUser = auth?.pathGetAddRoleByUser.replace('{userId}', userId);
772
+ const responseGetRoles = await accountsData[accountname].fetch(`${relativeGetAddRoleByUser}`, { method: 'GET' }, false).catch(ex => {
773
+ const errReq = Util.getErrorData(ex);
774
+ Logger.error(errReq);
775
+ const { status, message } = errReq;
776
+ switch (status) {
777
+ case 400:
778
+ resolve({ index, error: `Invalid VTEX request for get user roles with email '${email}'. ${message}` });
779
+ break;
780
+ case 403:
781
+ resolve({ index, error: `Denied access or invalid/inactived credentials for get user roles in account '${accountname}'. ${message}` });
782
+ break;
783
+ default:
784
+ resolve({ index, error: message });
785
+ break;
786
+ }
787
+ });
788
+
789
+ if (responseGetRoles?.data) {
790
+ for (let roleItem of responseGetRoles.data) {
791
+ if (roleItem?.id) {
792
+ let userRoleId = String(roleItem.id);
793
+ if (rolesKeys.hasOwnProperty(userRoleId)) {
794
+ delete rolesKeys[userRoleId];
795
+ } else {
796
+ const relativeDeleteRoleByUser = auth?.pathRemoveRoleByUser.replace('{userId}', userId).replace('{roleId}', userRoleId);
797
+ const responseDelRoles = await accountsData[accountname].fetch(relativeDeleteRoleByUser, { method: 'DELETE' }, false).catch(ex => {
798
+ const errReq = Util.getErrorData(ex);
799
+ Logger.error(errReq);
800
+ });
801
+ if (responseDelRoles) {
802
+ Logger.log(`Role ${userRoleId} was deleted successfully for user ${userId}`);
803
+ }
804
+ }
805
+ }
806
+ }
807
+
808
+ const newRolesId = Object.keys(rolesKeys);
809
+ if (newRolesId.length) {
810
+ const responseSetUserRoles = await accountsData[accountname].fetch(relativeGetAddRoleByUser, {
811
+ method: 'PUT', data: newRolesId
812
+ }, false).catch(ex => {
813
+ const errReq = Util.getErrorData(ex);
814
+ Logger.error(errReq);
815
+ const { status, message } = errReq;
816
+ switch (status) {
817
+ case 400:
818
+ resolve({ index, error: `${message}. Roles list: '${newRolesId.join(',')}'. Email: '${email}'. Account: ${accountname}` });
819
+ break;
820
+ case 403:
821
+ resolve({ index, error: `Denied access or invalid/inactived credentials for put user roles '${newRolesId.join(',')}' in account '${accountname}'. ${message}` });
822
+ break;
823
+ default:
824
+ resolve({ index, error: message });
825
+ break;
826
+ }
827
+ });
828
+
829
+ if (responseSetUserRoles) {
830
+ resolve({ index, message: `User '${email}' created/updated successfully` });
831
+ }
832
+ } else {
833
+ resolve({ index, message: `User '${email}' not modified` });
834
+ }
835
+ } else {
836
+ resolve({ index, error: `Error white get user roles for email '${email}'` });
837
+ }
838
+ } else {
839
+ resolve({ index, error: `User '${email}' not created/updated` });
840
+ }
841
+ }
842
+
843
+ /**
844
+ * Realiza la eliminación del usuario de VTEX.
845
+ * @param {object} accountsData - Datos de cuentas
846
+ * @param {function} resolve - Función para resolver la promesa
847
+ * @param {object} userRow - Objeto de datos del usuario a eliminar
848
+ * @param {object} auth - Configuración de autenticación
849
+ * @param {object} checkUsers - Lista de usuarios con opción de ser eliminados
850
+ * @param {object} accountsIssue - Lista de accounts que haya presentado algún incidente
851
+ */
852
+ async function removeUser(accountsData, resolve, userRow, auth, checkUsers, accountsIssue) {
853
+ const { index, jsonData } = userRow;
854
+ let { accountname, email } = jsonData;
855
+
856
+ if (checkUsers?.[accountname]?.[email]) {
857
+ const deleteUserEndpoint = auth?.pathCreateUser + '/' + checkUsers[accountname][email];
858
+
859
+ const responseDeleteUser = await accountsData[accountname].fetch(deleteUserEndpoint, {
860
+ method: 'DELETE'
861
+ }, false).catch(ex => {
862
+ const errReq = Util.getErrorData(ex);
863
+ const { status, message } = errReq;
864
+ switch (status) {
865
+ case 400:
866
+ resolve({ index, error: `Invalid VTEX request for delete user with email '${email}'` });
867
+ break;
868
+ case 403:
869
+ resolve({ index, error: `Denied access or invalid/inactived credentials for delete user in account '${accountname}'` });
870
+ break;
871
+ default:
872
+ resolve({ index, error: message });
873
+ break;
874
+ }
875
+ });
876
+
877
+ if (responseDeleteUser) {
878
+ resolve({ index, message: `User '${email}' delete success in account '${accountname}'` });
879
+ }
880
+ } else if (accountsIssue.hasOwnProperty(accountname)) {
881
+ resolve({ index, error: accountsIssue[accountname] })
882
+ } else {
883
+ resolve({ index, message: `User '${email}' not found in account '${accountname}'` });
884
+ }
885
+ }
886
+
887
+ /**
888
+ * Consulta en cada account la lista de usuarios.
889
+ * @param {object} accountsData - Instancias de conexión a la API de VTEX por accounts.
890
+ * @param {object} checkUsers - Lista de usuarios a eliminar.
891
+ * @param {object} accountsIssue - Lista de accounts que haya presentado algún incidente.
892
+ */
893
+ async function findUsersInVtex(accountsData, checkUsers, accountsIssue) {
894
+ if (Object.keys(checkUsers).length) {
895
+ await Promise.allSettled(Object.entries(checkUsers).map(accountData => {
896
+ return new Promise(async (resolve, reject) => {
897
+ const [accountName, users] = accountData;
898
+
899
+ let pageNumber = 1, numItems = 100;
900
+ do {
901
+ const responseListUsers = await accountsData[accountName].fetch(Util.setQueryParams('/license-manager/site/pvt/logins/list/paged', {
902
+ pageNumber, numItems
903
+ }), {
904
+ method: 'GET'
905
+ }, false).catch(ex => {
906
+ const errReq = Util.getErrorData(ex);
907
+ Logger.error(errReq);
908
+
909
+ let message;
910
+ switch (errReq.status) {
911
+ case 400:
912
+ message = `Invalid VTEX request for find users in account '${accountName}'`;
913
+ break;
914
+ case 401:
915
+ message = `Credential key is inactived in account '${accountName}'`;
916
+ break;
917
+ case 403:
918
+ message = `Denied access or invalid/inactived credentials for find users in account '${accountName}'`;
919
+ break;
920
+ }
921
+
922
+ if (!accountsIssue.hasOwnProperty(accountName) && message) {
923
+ accountsIssue[accountName] = message;
924
+ }
925
+ });
926
+ if (responseListUsers?.data?.items?.length) {
927
+ const { items } = responseListUsers.data;
928
+ for (let userData of items) {
929
+ const { id, email } = userData;
930
+ if (id && email) {
931
+ if (users.hasOwnProperty(email)) {
932
+ users[email] = id;
933
+ }
934
+ }
935
+ }
936
+
937
+ pageNumber++;
938
+ } else {
939
+ break;
940
+ }
941
+ } while (true);
942
+ resolve(accountName);
943
+ });
944
+ })).catch(Logger.error);
945
+ }
946
+ }
947
+
948
+ module.exports = {
949
+ producer,
950
+ };