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,700 @@
1
+ const AdmZip = require('adm-zip');
2
+ const fs = require('node:fs');
3
+ const moment = require('moment');
4
+ const path = require('node:path');
5
+
6
+ const { connectToFtp, getFilesInFolder, getFileContent, moveProcessedFile, createFolder, downloadFile, uploadFile } = require("../../common/utils/sftp-utils");
7
+ const { getErrorData, getDateValues, setQueryParams } = require("../../common/utils/util");
8
+ const AccountData = require("../../entities/account");
9
+ const ValidFile = require('./processes/validateFileSchema');
10
+ const { sendMessageToSqs } = require('../../common/utils/aws-services');
11
+ const DynamoDBConnector = require('./utils/connect-dynamo');
12
+ const ServicesRedirect = require('./processes/redirectServices');
13
+ const VtexApi = require('../../vtex/clients/VtexApi');
14
+ const Logger = require('../../common/utils/logger');
15
+ const { delayStatusProcess } = require('../../common/utils/delay');
16
+
17
+ const {
18
+ SFTP_DATA_SCHEMA_TABLE,
19
+ SFTP_CREDENTIALS_TABLE,
20
+ SFTP_JOBS_QUEUE_URL,
21
+ SFTP_JOBS_DLQ_QUEUE_URL,
22
+ SQS_CRON_JOBS_QUEUE_URL,
23
+ SFTP_MAX_ATTEMPS = 3
24
+ } = process?.env ?? {};
25
+
26
+ /**
27
+ * Función que ejecuta el proceso de consultar los archivos en el servidor FTP.
28
+ * @param {Object} objectData Objeto proveniente de la cola SQS donde se ejecuta el Cron Job.
29
+ */
30
+ module.exports.callback = async (objectData) => {
31
+ const { AccountName, ActionProcess, attemps = 0 } = objectData;
32
+
33
+ const dateValues = getDateValues();
34
+ const { timestamp } = dateValues;
35
+
36
+ const logRecord = {
37
+ id: `cron_${AccountName}_${ActionProcess}`,
38
+ actionProcess: ActionProcess,
39
+ process: 'find_file',
40
+ timestamp,
41
+ success: true,
42
+ error: null,
43
+ notify: true
44
+ }
45
+
46
+ let clientFTP;
47
+ let vtexApiClient, entityName, schemaName;
48
+ try {
49
+ // Consulta los parámetros de la integración requeridos
50
+ const configAccount = await AccountData.getAccountDataByAccountName(AccountName);
51
+ const { ftpId, directoryProcess, ...integrationParameters } = await getIntegrationParameters(AccountName, ActionProcess, configAccount);
52
+
53
+ // Se obtiene la información
54
+ vtexApiClient = integrationParameters.apiClient;
55
+ entityName = integrationParameters.entityName;
56
+ schemaName = integrationParameters.schemaName;
57
+
58
+ // Conexión al servidor FTP
59
+ Logger.info(`Connecting to FTP ${ftpId}`);
60
+ const ftpCredentials = await getFtpProperties(ftpId);
61
+ clientFTP = await connectToFtp(ftpCredentials);
62
+
63
+ // Se obtiene la carpeta de pending
64
+ const { pendingPath, schemaId } = directoryProcess[ActionProcess];
65
+
66
+ // Se obtiene los datos del schema, para validar el nombre del archivo
67
+ const { PatternName } = await getSchemaProperties(schemaId);
68
+ // Se aplica el replace, debido a que el valor puede ser una expresión regular
69
+ // que contiene "\\"
70
+ logRecord.filename = PatternName.replaceAll('\\', '\\\\');
71
+ logRecord.folderPath = pendingPath;
72
+
73
+ // Se consulta los archivos existentes en la carpeta
74
+ const files = await getFilesInFolder(clientFTP, pendingPath), promises = [];
75
+ if (files?.length) {
76
+ for (let file of files) {
77
+ const { original } = file;
78
+ const { name, type } = original;
79
+ // Se valida que el elemento corresponda a un archivo, y el nombre concuerde con la RegExp definida
80
+ if (type == '-' && name.match(new RegExp(PatternName)) !== null) {
81
+ // Se agrega a la promise el path del archivo encontrado
82
+ Logger.info(`File found ${name}`);
83
+ promises.push(`${pendingPath}/${name}`);
84
+ }
85
+ }
86
+ }
87
+
88
+ // Se ejecutan las promises (si existen)
89
+ logRecord.countFiles = promises.length;
90
+ if (logRecord.countFiles) {
91
+ await Promise.allSettled(promises.map(filePath => {
92
+ return new Promise((resolve, reject) => {
93
+ // Se inserta en la cola SQS el accountName, nombre del proceso y ruta del archivo a procesar
94
+ sendMessageToSqs(SFTP_JOBS_QUEUE_URL, {
95
+ accountName: AccountName,
96
+ actionProcess: ActionProcess,
97
+ filePath,
98
+ timestamp
99
+ }).then(() => resolve(filePath)).catch(reject);
100
+ });
101
+ }));
102
+ } else {
103
+ // Si no se encontró archivos, se procede a registrar el log indicando que no hubo archivos (logRecord.countFiles = 0).
104
+ // Esto hara que se envíe la notificación indicando que se ejecutó el job, pero no encontró archivo
105
+ await saveInLogEntity(vtexApiClient, entityName, schemaName, logRecord);
106
+ }
107
+ } catch (ex) {
108
+ // Se valida que el número de intentos no supere el valor máximo configurado
109
+ if (attemps < SFTP_MAX_ATTEMPS) {
110
+ await delayStatusProcess(5);
111
+ Logger.warn('Error in sftp callback', { AccountName, ActionProcess, attemps });
112
+
113
+ // Se encola el message en la cola de jobs para procesar nuevamente, aumentando el n° de reintentos
114
+ objectData.attemps = attemps + 1;
115
+ await sendMessageToSqs(SQS_CRON_JOBS_QUEUE_URL, objectData).catch(Logger.error);
116
+ } else {
117
+ // Se registra en la entidad de logs la información de error en el proceso.
118
+ // Este registro hará que se envíe el mensaje de error por correo electrónico a través del trigger configurado en la entidad.
119
+ logRecord.success = false;
120
+ logRecord.error = ex.message
121
+ await saveInLogEntity(vtexApiClient, entityName, schemaName, logRecord);
122
+ throw ex;
123
+ }
124
+ } finally {
125
+ // Se cierra la conexión al servidor FTP (si esta existe)
126
+ if (clientFTP?.client) {
127
+ clientFTP.client.end();
128
+ }
129
+ }
130
+ };
131
+
132
+ /**
133
+ * Función que ejecuta el proceso de procesar nuevamente los archivos que presentaron errores de ejecución.
134
+ * @param {Object} objectData Objeto proveniente de la cola SQS donde se ejecuta el Cron Job.
135
+ */
136
+ module.exports.reprocessFailed = async (objectData) => {
137
+ const { AccountName, ActionProcess } = objectData;
138
+
139
+ try {
140
+ // Consulta los parámetros de la integración requeridos
141
+ const configAccount = await AccountData.getAccountDataByAccountName(AccountName);
142
+ const integrationParameters = await getIntegrationParameters(AccountName, ActionProcess, configAccount, false);
143
+
144
+ // Se obtiene la información para el consumo de la API de VTEX
145
+ const { apiClient, entityName, schemaName } = integrationParameters;
146
+
147
+ // Se consulta los registros cuyo archivo no fue procesado exitosamente
148
+ const urlSearch = setQueryParams(`/dataentities/${entityName}/search`, {
149
+ _schema: schemaName,
150
+ _fields: 'id,accountName,actionProcess,filename,folderPath,contentType,timestamp',
151
+ _where: '(success=false)'
152
+ });
153
+ const records = await apiClient.fetch(urlSearch, {
154
+ method: 'GET',
155
+ validateStatus: status => status >= 200 && status < 400
156
+ });
157
+ if (records?.status == 200) {
158
+ let { data } = records;
159
+ Logger.info(`Record for reprocess: ${data?.length}`);
160
+ if (data?.length) {
161
+ // Se encola nuevamente la información de los archivos cuyo proceso falló,
162
+ // para procesarlos nuevamente tal como se hizo originalmente.
163
+ do {
164
+ // Se extrae las primeras 'maxRows' filas de la lista
165
+ let rowsData = data.splice(0, 10);
166
+ await Promise.allSettled(rowsData.map(item => {
167
+ const { id, accountName, actionProcess, filename, folderPath, contentType, timestamp } = item;
168
+
169
+ // Se define la ruta del archivo concatenando la carpeta donde se procesó,
170
+ // el Id que corresponde al nombre final y la extensión del archivo original.
171
+ const filePath = `${folderPath}/${filename}`;
172
+ return new Promise((resolve, reject) => {
173
+ // Se envia a la cola el Id del registro
174
+ sendMessageToSqs(SFTP_JOBS_QUEUE_URL, {
175
+ id, accountName, actionProcess, filePath, contentType, timestamp
176
+ }).then(resolve).catch(reject);
177
+ });
178
+ })).catch(Logger.error);
179
+ } while (data.length);
180
+ }
181
+ }
182
+ } catch (ex) {
183
+ const { info } = getErrorData(ex);
184
+ Logger.error('Error in reprocessFailed', info);
185
+ }
186
+ }
187
+
188
+ /**
189
+ * Función que se invoca desde el trigger de la cola SQS definida en la propiedad 'sftpjobs-queue' del archivo 'constructs.yml'.
190
+ * @param {*} event Objeto que contiene información de una petición como: el body, path y método de una petición HTTP, o la lista de registros en una cola SQS.
191
+ * @param {*} context Objeto que contiene información sobre la invocación, la función y el entorno de ejecución.
192
+ */
193
+ module.exports.processFile = async (event, context) => {
194
+ // ID de la petición en AWS
195
+ const { awsRequestId } = context;
196
+
197
+ for (const record of event.Records) {
198
+ // Se obtiene el contenido del cuerpo del mensaje de la cola SQS
199
+ let { body } = record;
200
+ body = JSON.parse(body);
201
+
202
+ // Se ejecuta el proceso de validar el archivo, con los valores enviados a la cola SQS
203
+ await handleProcessFile(awsRequestId, body);
204
+ }
205
+ };
206
+
207
+ /**
208
+ * Extrae la información del archivo,
209
+ * @param {string} awsRequestId Id de la petición de AWS.
210
+ * @param {object} sqsMessage Mensaje SQS con la información del archivo a procesar.
211
+ * @param {string} sqsMessage.accountName Nombre del accountName.
212
+ * @param {string} sqsMessage.actionProcess Nombre del proceso.
213
+ * @param {string} sqsMessage.filePath Ruta del archivo a procesar.
214
+ * @param {string} sqsMessage.contentType Tipo de dato del archivo a procesar (si aplica).
215
+ * @param {string} sqsMessage.timestamp Marca de tiempo de cuando se ejecuta el proceso.
216
+ */
217
+ async function handleProcessFile(awsRequestId, sqsMessage) {
218
+ let { id, accountName, actionProcess, filePath, contentType = null, timestamp = null, attemps = 0 } = sqsMessage;
219
+
220
+ let clientFTP;
221
+
222
+ // Se establece el valor
223
+ if (!timestamp) {
224
+ const dateValues = getDateValues();
225
+ timestamp = dateValues.timestamp;
226
+ }
227
+
228
+ // Se extrae el nombre y extensión del archivo
229
+ const fileData = path.parse(filePath);
230
+ const { base, name, dir } = fileData;
231
+
232
+ // Si se definió el Id, este valor será el nombre del archivo a procesar
233
+ const fileName = id ?? (contentType ? name : `${name}_${timestamp}`);
234
+
235
+ // Variables asociadas al nombre de la entidad, conexión a la API de VTEX y objeto JSON a insertar
236
+ let vtexApiClient, entityName, schemaName;
237
+ const logRecord = {
238
+ id: fileName,
239
+ filename: base,
240
+ folderPath: dir,
241
+ contentType,
242
+ actionProcess,
243
+ timestamp,
244
+ error: null
245
+ };
246
+
247
+ // Si se está procesando un archivo dividido, se define notify=false para que no envíe notificación;
248
+ // salvo que ocurra un error en el proceso.
249
+ logRecord.notify = contentType ? false : true;
250
+
251
+ try {
252
+ // Se consulta los datos de integración asociados a la cuenta
253
+ const configAccount = await AccountData.getAccountDataByAccountName(accountName);
254
+ const { maxRows, ftpId, directoryProcess, ...integrationParameters } = await getIntegrationParameters(accountName, actionProcess, configAccount);
255
+ vtexApiClient = integrationParameters.apiClient;
256
+ entityName = integrationParameters.entityName;
257
+ schemaName = integrationParameters.schemaName;
258
+
259
+ // Se obtiene la carpeta de pending
260
+ const { process: processName } = directoryProcess[actionProcess];
261
+ logRecord.process = processName;
262
+ Logger.info(`Executing process ${processName} in account ${accountName}`);
263
+
264
+ // Conexión al servidor FTP
265
+ Logger.info(`Connecting to FTP ${ftpId}`);
266
+ const ftpCredentials = await getFtpProperties(ftpId);
267
+ clientFTP = await connectToFtp(ftpCredentials);
268
+
269
+ // Si el proceso es de descomprimir archivo .zip
270
+ if (processName == 'unzip_file') {
271
+ await descompressZipFile(clientFTP, logRecord, directoryProcess, ftpCredentials, {
272
+ accountName, actionProcess, filePath, timestamp, fileName, fileData
273
+ })
274
+ } else {
275
+ await callbackProcessFile(clientFTP, logRecord, maxRows, directoryProcess, configAccount,
276
+ { accountName, actionProcess, filePath, contentType, timestamp, fileName, fileData }
277
+ );
278
+ }
279
+
280
+ await saveInLogEntity(vtexApiClient, entityName, schemaName, logRecord);
281
+ } catch (ex) {
282
+ const { message } = getErrorData(ex);
283
+
284
+ // Se valida que el número de intentos no supere el valor máximo configurado
285
+ if (attemps < SFTP_MAX_ATTEMPS) {
286
+ await delayStatusProcess(5);
287
+ Logger.warn('Error in handleProcessFile', { accountName, actionProcess, filePath, attemps, message });
288
+
289
+ // Se encola el message para procesar nuevamente, aumentando el n° de reintentos
290
+ sqsMessage.attemps = attemps + 1;
291
+ await sendMessageToSqs(SFTP_JOBS_QUEUE_URL, sqsMessage).catch(Logger.error);
292
+ } else {
293
+ Logger.error('Error in handleProcessFile', message);
294
+
295
+ logRecord.success = false;
296
+ logRecord.error = message;
297
+
298
+ await saveInLogEntity(vtexApiClient, entityName, schemaName, logRecord);
299
+ await sendMessageToSqs(SFTP_JOBS_DLQ_QUEUE_URL, {
300
+ accountName, actionProcess, filePath,
301
+ reason: message,
302
+ awsRequestId
303
+ }).catch(ex => {
304
+ Logger.error('Error in handleProcessFile', ex);
305
+ });
306
+ }
307
+ } finally {
308
+ // Se cierra la conexión al servidor FTP (si esta existe)
309
+ if (clientFTP?.client) {
310
+ clientFTP.client.end();
311
+ }
312
+ }
313
+ }
314
+
315
+ async function descompressZipFile(clientFTP, logRecord, directoryProcess, ftpCredentials, handleParams) {
316
+ // Se obtiene los parámetros de la función de donde se invoca esta
317
+ const { accountName, actionProcess, filePath, timestamp, fileName, fileData } = handleParams;
318
+
319
+ // Se extrae el nombre y extensión del archivo
320
+ const { base, ext } = fileData;
321
+
322
+ if (ext !== '.zip') {
323
+ throw new Error(`File extension '${ext}' is not valid`);
324
+ }
325
+
326
+ // Se obtiene la información del proceso a ejecutar con el archivo, así como las carpetas requeridas
327
+ const { schemaId, pendingPath, processedPath } = directoryProcess[actionProcess];
328
+ Logger.info(`Getting schema file ${schemaId}`);
329
+ const schema = await getSchemaProperties(schemaId);
330
+ const { ContentType, ExtractOptions, PatternName } = schema;
331
+ if (ContentType !== 'zip') {
332
+ throw new Error(`Content type '${ContentType}' is not valid`);
333
+ }
334
+
335
+ // Se define la propiedad que contiene información de los archivos del .zip
336
+ logRecord.zipData = {
337
+ count: 0, // Archivos existentes
338
+ extracted: 0, // Archivos extraidos
339
+ errors: {} // Errores en procesamiento de los archivos
340
+ };
341
+
342
+ // Se descarga el archivo .zip al directorio temporal de la Lambda
343
+ const targetFolder = '/tmp';
344
+ const localZipFile = `${targetFolder}/${fileName}.zip`;
345
+ Logger.info(`Downloading file ${filePath} to ${targetFolder}`);
346
+ const downloadedFile = await downloadFile(clientFTP, filePath, localZipFile, ftpCredentials?.type);
347
+ if (downloadedFile) {
348
+ // Si existen valores por extraer
349
+ Logger.info(`Options for extract files: ${ExtractOptions?.length}`);
350
+ if (ExtractOptions?.length) {
351
+ const zipFile = new AdmZip(localZipFile);
352
+ const zipEntries = zipFile.getEntries();
353
+
354
+ // Se define el N° de archivos existentes en el .zip
355
+ logRecord.zipData.count = zipEntries.length;
356
+
357
+ for (let zipEntry of zipEntries) {
358
+ const { entryName } = zipEntry;
359
+ const filteredFiles = ExtractOptions.filter(option => {
360
+ const { PatternName } = option;
361
+ return entryName.match(new RegExp(PatternName));
362
+ });
363
+ for (let filteredFile of filteredFiles) {
364
+ const { ActionProcess, FolderPath = pendingPath } = filteredFile;
365
+ logRecord.zipData.extracted++;
366
+
367
+ // Se extrar el archivo del .zip
368
+ Logger.info(`Extracting file ${entryName}`);
369
+ const extractFile = zipFile.extractEntryTo(entryName, targetFolder, true, true);
370
+ if (extractFile) {
371
+ // Se referencia la ruta del archivo local extraido, y la ruta donde se subirá el archivo
372
+ const localPath = `${targetFolder}/${entryName}`, remotePath = `${FolderPath}/${entryName}`;
373
+ Logger.info(`Uploading file ${localPath} to ${remotePath}`);
374
+ const uploadedFile = await uploadFile(clientFTP, localPath, remotePath, ftpCredentials?.type).catch(ex => {
375
+ const { message, info } = getErrorData(ex);
376
+ Logger.error(message, info);
377
+ logRecord.zipData.errors[entryName] = message;
378
+ });
379
+ // Se valida si el archivo se subió al servidor FTP, y si se tiene definido una acción a ejecutar
380
+ Logger.info(`Uploaded file: ${uploadedFile}`);
381
+ if (uploadedFile && ActionProcess) {
382
+ Logger.info(`Set action process ${ActionProcess}`);
383
+ // Se envía el message a la cola SQS para procesar el archivo en cuestión
384
+ sendMessageToSqs(
385
+ SFTP_JOBS_QUEUE_URL,
386
+ { accountName, actionProcess: ActionProcess, filePath: remotePath, timestamp }
387
+ ).catch(ex => {
388
+ const { info } = getErrorData(ex);
389
+ Logger.error(filePath, info);
390
+ });
391
+ }
392
+ } else {
393
+ logRecord.zipData.errors[entryName] = 'File not extracted';
394
+ }
395
+ }
396
+ }
397
+ }
398
+
399
+ // Se obtiene el folder final a donde se moverán los archivos
400
+ const targetFolderRender = getRenderValue(PatternName, base, processedPath);
401
+
402
+ // Se mueve el archivo .zip original
403
+ Logger.info(`Moving file ${filePath} to ${targetFolderRender}`);
404
+ await moveProcessedFile(clientFTP, filePath, targetFolderRender, `${fileName}${ext}`).then(() => {
405
+ logRecord.success = true;
406
+ }).catch(ex => {
407
+ Logger.error('Error with moveProcessedFile in descompressZipFile', ex);
408
+ logRecord.success = false;
409
+ logRecord.error = ex.message
410
+ });
411
+ } else {
412
+ logRecord.success = false;
413
+ logRecord.error = `File ${filePath} cannot be downloaded`;
414
+ }
415
+ }
416
+
417
+ async function callbackProcessFile(clientFTP, logRecord, maxRows, directoryProcess, configAccount, handleParams) {
418
+ // Se obtiene los parámetros de la función de donde se invoca esta
419
+ const { accountName, actionProcess, filePath, contentType, timestamp, fileName, fileData } = handleParams;
420
+
421
+ // Se extrae el nombre y extensión del archivo
422
+ const { base, ext } = fileData;
423
+
424
+ // Se obtiene la información del proceso a ejecutar con el archivo, así como las carpetas requeridas
425
+ const { schemaId, process, processingPath, processedPath, logsPath } = directoryProcess[actionProcess];
426
+ const schema = await getSchemaProperties(schemaId);
427
+ const { PatternName } = schema;
428
+
429
+ const fileValidator = new ValidFile(schema);
430
+
431
+ // Se obtiene el contenido del archivo
432
+ Logger.info(`Getting content of file ${filePath}`);
433
+ const fileContent = await getFileContent(clientFTP, filePath);
434
+
435
+ // Se procede a extraer los datos del archivo a formato JSON
436
+ let { valid: validFile, errors: errorsFile, data: dataFile } = await fileValidator.extractContentFile({ content: fileContent }, contentType);
437
+ if (validFile && dataFile?.length) {
438
+ // Se define el número de filas
439
+ Logger.info(`Records count: ${dataFile.length}`);
440
+ logRecord.rows = dataFile.length;
441
+
442
+ // Se valida si el número de registros es menor al valor máximo configurado, para en caso contrario proceder a dividir el archivo grande
443
+ // en archivos mas pequeños en formato JSON.
444
+ if (dataFile.length <= maxRows) {
445
+ // Uso de clase para configAccount
446
+ const servicesRedirect = new ServicesRedirect(configAccount);
447
+
448
+ // Array donde se consolida los errores de validación presentados
449
+ const errors = [];
450
+
451
+ // Se ejecuta el proceso dentro de una promise
452
+ await Promise.allSettled(dataFile.map(data => {
453
+ return new Promise(resolve => {
454
+ // Se ejecuta la validación del registro con base a un Schema
455
+ const { valid: validRecord, errors: errorsRecord } = fileValidator.validateRecordSchema(data);
456
+ if (validRecord) {
457
+ // Se obtiene la data normalizada, para enviar a la respectiva cola SQS, segun el proceso a ejecutar
458
+ let newData = fileValidator.standardizeData(data);
459
+ servicesRedirect.initSendData({ typeProcess: process, data: newData }).then(serviceResponse => {
460
+ resolve(newData);
461
+ }).catch(ex => {
462
+ const { message } = getErrorData(ex);
463
+ Logger.error(message);
464
+ resolve(null);
465
+ });
466
+ } else {
467
+ errors.push(JSON.stringify({ data, errorsRecord }))
468
+ Logger.warn('Error in validation', { data, errorsRecord });
469
+ resolve(null);
470
+ }
471
+ });
472
+ }));
473
+
474
+ Logger.info(`Errors count: ${errors.length}`);
475
+ logRecord.errorRows = errors.length;
476
+
477
+ // Se genera el archivo de log con los datos de errores (si existieron)
478
+ if (errors.length) {
479
+ // Se crea la carpeta donde se moveran los archivos de log
480
+ await createFolder(clientFTP, logsPath);
481
+
482
+ // Se escribe el archivo de log
483
+ await createFile(clientFTP, `${fileName}.log`, errors.join('\n'), logsPath).catch(ex => { });
484
+ }
485
+ } else {
486
+ // Se obtiene el N° de archivos estimado, a partir de la división entre el N° de filas
487
+ // del archivo y el número de filas máximo configurado.
488
+ const estimatedFiles = Math.ceil(dataFile.length / maxRows);
489
+ const digits = String(estimatedFiles).length;
490
+ Logger.info(`Splitting file ${filePath} into ${estimatedFiles} files`);
491
+
492
+ // N° de archivos en que se divide el archivo original
493
+ logRecord.countFiles = estimatedFiles;
494
+
495
+ // Se concatena en la lista de promises los lotes de filas para generar
496
+ let promises = [], count = 1;
497
+ while (dataFile.length > 0) {
498
+ promises.push({ count, data: dataFile.splice(0, maxRows) });
499
+ count++;
500
+ }
501
+ if (promises.length) {
502
+ // Se crea la carpeta donde se moveran los archivos divididos (si aplica)
503
+ await createFolder(clientFTP, processingPath).catch(ex => {
504
+ Logger.error('Error with createFolder in callbackProcessFile', ex);
505
+ });
506
+
507
+ // Se generan los archivos JSON
508
+ Logger.info(`Generating ${promises.length} splitted files`);
509
+ await Promise.allSettled(promises.map(item => {
510
+ return new Promise((resolve, reject) => {
511
+ const { count, data } = item;
512
+ const jsonFileName = `${fileName}_${String(count).padStart(digits, '0')}.json`;
513
+
514
+ // Se escribe el archivo en un directorio temporal
515
+ createFile(clientFTP, jsonFileName, JSON.stringify(data), processingPath).then(filePath => {
516
+ // Se envía el message a la cola SQS para procesar el archivo en cuestión
517
+ Logger.info(`Sending message to SQS`);
518
+ sendMessageToSqs(
519
+ SFTP_JOBS_QUEUE_URL,
520
+ { accountName, actionProcess, filePath, contentType: 'json', timestamp }
521
+ ).then(() => {
522
+ Logger.info(`File processed ${filePath}`);
523
+ resolve(filePath)
524
+ }).catch(ex => {
525
+ const { info } = getErrorData(ex);
526
+ Logger.error(filePath, info);
527
+ reject(new Error('Error while send Data'));
528
+ });
529
+ }).catch(ex => {
530
+ Logger.info(`Error sending message`, ex);
531
+ reject(new Error('Error while upload FTP file'));
532
+ });
533
+ });
534
+ }));
535
+ }
536
+ }
537
+
538
+ // Se obtiene el folder final a donde se moverán los archivos
539
+ const targetFolderRender = getRenderValue(PatternName, base, processedPath);
540
+
541
+ // Se mueve el archivo a la carpeta de procesados
542
+ // Al nombre del archivo se le concatena una marca de tiempo "timestamp"
543
+ Logger.info(`Moving file ${filePath} to ${targetFolderRender}`);
544
+ await moveProcessedFile(clientFTP, filePath, targetFolderRender, `${fileName}${ext}`).then(() => {
545
+ logRecord.success = true;
546
+ }).catch(ex => {
547
+ Logger.error('Error with moveProcessedFile in callbackProcessFile', ex);
548
+ logRecord.success = false;
549
+ logRecord.error = ex.message
550
+ });
551
+ } else {
552
+ Logger.warn('Error validating file', errorsFile);
553
+ logRecord.success = false;
554
+ logRecord.error = errorsFile.join('|');
555
+ }
556
+ }
557
+
558
+ async function getIntegrationParameters(accountName, actionProcess, configAccount, includeDirectory = true) {
559
+ // Obtener datos de la integración SFTP
560
+ const { SftpIntegration, Credentials } = configAccount;
561
+ if (!SftpIntegration) {
562
+ throw new Error(`SftpIntegration not found for account ${accountName}`);
563
+ }
564
+
565
+ // Se define si se imprimen todos los mensajes de Logger.
566
+ Logger.setAccount(configAccount);
567
+
568
+ // Se establece una conexión a la API de VTEX
569
+ const { key, token } = Credentials;
570
+ const apiClient = new VtexApi(accountName, key, token);
571
+
572
+ // Se consulta las propiedades de la integración
573
+ let { isActive, maxRows, ftpId, directoryProcess, entityName, schemaName } = SftpIntegration;
574
+ if (!isActive) {
575
+ throw new Error(`SftpIntegration is not actived for account ${accountName}`);
576
+ }
577
+
578
+ const itemsData = { apiClient, maxRows, ftpId, entityName, schemaName };
579
+ if (includeDirectory) {
580
+ if (!directoryProcess?.[actionProcess]) {
581
+ throw new Error(`Directory options not found for actionProcess '${actionProcess}' in account ${accountName}`);
582
+ }
583
+
584
+ itemsData.directoryProcess = directoryProcess;
585
+
586
+ // Se establece el valor asociado al proceso (si este existe) dentro de las configuraciones
587
+ for (let prop of ['maxRows', 'ftpId', 'entityName', 'schemaName']) {
588
+ if (directoryProcess[actionProcess]?.[prop]) {
589
+ itemsData[prop] = directoryProcess[actionProcess][prop];
590
+ }
591
+ }
592
+ }
593
+
594
+ return itemsData;
595
+ }
596
+
597
+ /**
598
+ * Consulta los datos del schema
599
+ * @param {string} schemaId Id del schema
600
+ * @returns Datos del schema.
601
+ */
602
+ async function getSchemaProperties(schemaId) {
603
+ // Se obtiene los datos del schema, para validar el nombre del archivo
604
+ const dynamoConnector = new DynamoDBConnector(SFTP_DATA_SCHEMA_TABLE);
605
+ const schema = await dynamoConnector.getItem({ SchemaId: schemaId });
606
+ return schema;
607
+ }
608
+
609
+ /**
610
+ * Consulta en DynamoDB las credenciales de servidor SFTP asociadas a un Id.
611
+ * @param {string} ftpId Id de la credencial FTP.
612
+ * @returns Objeto con los datos de conexión al servidor FTP.
613
+ */
614
+ async function getFtpProperties(ftpId) {
615
+ // Se obtiene los datos del schema, para validar el nombre del archivo
616
+ const dynamoConnector = new DynamoDBConnector(SFTP_CREDENTIALS_TABLE);
617
+ const schema = await dynamoConnector.getItem({ FtpId: ftpId });
618
+ const { Type: type, Host: host, Port: port, User: user, Password: password } = schema;
619
+ return { type, host, port, user, password };
620
+ }
621
+
622
+ /**
623
+ * Crea un archivo en la carpeta "temp" y subirlo a un servidor FTP.
624
+ * @param {*} clientFTP Instancia de conexión al servidor FTP.
625
+ * @param {string} fileName Nombre del archivo.
626
+ * @param {string} data Contenido del archivo a crear.
627
+ * @param {string} targetFolder Carpeta del FTP donde se subirá el archivo.
628
+ * @return Path del archivo generado.
629
+ */
630
+ async function createFile(clientFTP, fileName, data, targetFolder) {
631
+ return new Promise((resolve, reject) => {
632
+ const tempPath = `/tmp/${fileName}`;
633
+ Logger.info(`Creating file ${tempPath}`);
634
+ fs.writeFile(tempPath, data, 'utf-8', err => {
635
+ if (err) {
636
+ Logger.error('Error in createFile', err);
637
+ reject(err);
638
+ } else {
639
+ // Path del archivo JSON generado
640
+ const filePath = `${targetFolder}/${fileName}`;
641
+ Logger.info(`Uploading file ${filePath}`);
642
+ clientFTP.put(fs.createReadStream(tempPath), filePath).then(() => {
643
+ Logger.info(`File uploaded ${filePath}`);
644
+ resolve(filePath)
645
+ }).catch(ex => {
646
+ const { info } = getErrorData(ex);
647
+ Logger.error('Error in createFile.put', filePath, info);
648
+ reject(new Error('Error while upload FTP file'));
649
+ });
650
+ }
651
+ });
652
+ });
653
+ }
654
+
655
+ /**
656
+ * Renderiza el valor de un texto a partir de una expresión regular, y/o valores de fecha del sistema.
657
+ * @param {*} patternName Expresión regular usada para la búsqueda del archivo.
658
+ * @param {*} fileName Nombre del archivo encontrado con la expresión regular.
659
+ * @param {*} processedPath Ruta del directorio a renderizar.
660
+ * @return Ruta del directorio con los comodines de RegExp sustituidos por los valores extraidos de la expresión regular,
661
+ * y/o valores de fecha sustituidos por los respectivos valores.
662
+ * @see {@link https://momentjs.com/|Moment.js Docs}
663
+ */
664
+ function getRenderValue(patternName, fileName, processedPath) {
665
+ // Expresión regular del filename
666
+ const regex = new RegExp(patternName);
667
+
668
+ // Se valida valores agrupados de la expresión regular, para sustituir en el nombre del folder
669
+ const match = fileName.match(regex);
670
+ if (match?.length > 1) {
671
+ processedPath = fileName.replace(regex, processedPath);
672
+ }
673
+
674
+ // Se formatea el texto con las variables de moment.js a partir de la fecha y hora actual del sistema
675
+ const currentDate = new Date();
676
+ processedPath = moment(currentDate).format(processedPath);
677
+ return processedPath;
678
+ }
679
+
680
+ /**
681
+ * Registra los datos de log en la entidad de Master Data.
682
+ * @param {*} vtexApiClient Instancia de conexión a la API de VTEX.
683
+ * @param {string} entityName Nombre de la entidad de Master Data.
684
+ * @param {string} schemaName NOmbre del schema de la entidad de Master Data.
685
+ * @param {object} logRecord Objeto de datos del log.
686
+ */
687
+ async function saveInLogEntity(vtexApiClient, entityName, schemaName, logRecord) {
688
+ if (vtexApiClient && entityName && schemaName) {
689
+ // Se inserta en la entidad de Master Data los datos de log
690
+ Logger.info(`Saving log data in entity ${entityName}:${schemaName}`);
691
+ await vtexApiClient.fetch(`/dataentities/${entityName}/documents?_schema=${schemaName}`, {
692
+ method: 'PATCH',
693
+ data: logRecord,
694
+ validateStatus: status => status >= 200 && status < 400
695
+ }).catch(ex => {
696
+ const { message } = getErrorData(ex);
697
+ Logger.error('Error with vtexApiClient.fetch', message);
698
+ });
699
+ }
700
+ }