@devx-commerce/plugin-gati 0.0.11 → 0.0.12
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/.medusa/server/src/api/erp/webhook/config.js +7 -1
- package/.medusa/server/src/api/erp/webhook/route.js +90 -6
- package/.medusa/server/src/commands/bulk-jobs/erp-webhook-batch.js +560 -0
- package/.medusa/server/src/commands/utils/command-args.js +85 -0
- package/.medusa/server/src/modules/erp-event/migrations/{Migration20250608221239.js → Migration20250908040911.js} +5 -5
- package/.medusa/server/src/modules/erp-event/models/erp-event.js +6 -1
- package/.medusa/server/src/modules/promocode-master/index.js +13 -0
- package/.medusa/server/src/modules/promocode-master/migrations/Migration20250819123821.js +27 -0
- package/.medusa/server/src/modules/promocode-master/models/promocode-master.js +66 -0
- package/.medusa/server/src/modules/promocode-master/service.js +13 -0
- package/.medusa/server/src/workflows/bulk-jobs-task-trigger/index.js +31 -0
- package/.medusa/server/src/workflows/bulk-jobs-task-trigger/steps/create-job-tracking.js +15 -0
- package/.medusa/server/src/workflows/bulk-jobs-task-trigger/steps/trigger-ecs-task.js +95 -0
- package/.medusa/server/src/workflows/create-extended-product-from-product/index.js +2 -2
- package/.medusa/server/src/workflows/create-extended-variant-from-variant/index.js +3 -3
- package/.medusa/server/src/workflows/erp-event/index.js +17 -8
- package/.medusa/server/src/workflows/erp-event/steps/create-erp-event.js +3 -1
- package/.medusa/server/src/workflows/erp-event/steps/index.js +19 -0
- package/.medusa/server/src/workflows/erp-event/steps/update-erp-event.js +22 -0
- package/.medusa/server/src/workflows/erp-event/workflows/create-erp-event.js +10 -0
- package/.medusa/server/src/workflows/erp-event/workflows/index.js +18 -0
- package/.medusa/server/src/workflows/erp-event/workflows/update-erp-event.js +10 -0
- package/.medusa/server/src/workflows/hooks/product-created.js +7 -9
- package/.medusa/server/src/workflows/hooks/product-updated.js +7 -9
- package/.medusa/server/src/workflows/hooks/variant-created.js +7 -9
- package/.medusa/server/src/workflows/hooks/variant-updated.js +7 -9
- package/.medusa/server/src/workflows/notification/index.js +10 -0
- package/.medusa/server/src/workflows/notification/steps/create-notification.js +15 -0
- package/.medusa/server/src/workflows/orders/steps/sync-order-to-erp.js +23 -2
- package/.medusa/server/src/workflows/orders/utils/order-helper.js +6 -6
- package/.medusa/server/src/workflows/party-style-master/workflows/create-or-update-party-style-master.js +7 -1
- package/.medusa/server/src/workflows/party-style-master/workflows/delete-party-style-master.js +3 -3
- package/.medusa/server/src/workflows/promocode-master/index.js +58 -0
- package/.medusa/server/src/workflows/promocode-master/steps/create-promocode.js +72 -0
- package/.medusa/server/src/workflows/promocode-master/steps/delete-promocode.js +75 -0
- package/.medusa/server/src/workflows/promocode-master/steps/fetch-collection-master.js +70 -0
- package/.medusa/server/src/workflows/promocode-master/steps/update-promocode.js +132 -0
- package/.medusa/server/src/workflows/style-master/workflows/create-or-update-style-master.js +4 -3
- package/.medusa/server/src/workflows/types/bulk-export.js +10 -0
- package/.medusa/server/src/workflows/update-extended-product-from-product/index.js +2 -2
- package/.medusa/server/src/workflows/update-extended-variant-from-variant/workflows/update-extended-variant-from-variant.js +2 -2
- package/.medusa/server/src/workflows/update-extended-variant-from-variant/workflows/update-extended-variant-status-from-variant.js +24 -13
- package/package.json +2 -1
|
@@ -0,0 +1,560 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
"use strict";
|
|
3
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
4
|
+
exports.default = erpWebhookBatchProcessingScript;
|
|
5
|
+
const client_s3_1 = require("@aws-sdk/client-s3");
|
|
6
|
+
const s3_request_presigner_1 = require("@aws-sdk/s3-request-presigner");
|
|
7
|
+
const utils_1 = require("@medusajs/framework/utils");
|
|
8
|
+
const node_http_handler_1 = require("@smithy/node-http-handler");
|
|
9
|
+
const config_1 = require("../../api/erp/webhook/config");
|
|
10
|
+
const update_erp_event_1 = require("../../workflows/erp-event/workflows/update-erp-event");
|
|
11
|
+
const notification_1 = require("../../workflows/notification");
|
|
12
|
+
const command_args_1 = require("../utils/command-args");
|
|
13
|
+
// Utility function to wait for a specified duration
|
|
14
|
+
async function delay(ms) {
|
|
15
|
+
return new Promise((resolve) => setTimeout(resolve, ms));
|
|
16
|
+
}
|
|
17
|
+
class S3MultipartUploader {
|
|
18
|
+
constructor(bucket, fileKey, s3Client, logger) {
|
|
19
|
+
this.uploadId = null;
|
|
20
|
+
this.parts = [];
|
|
21
|
+
this.currentPart = 1;
|
|
22
|
+
this.currentBuffer = Buffer.alloc(0);
|
|
23
|
+
this.minPartSize = 5 * 1024 * 1024; // 5MB minimum for multipart
|
|
24
|
+
this.totalSize = 0;
|
|
25
|
+
this.s3Client = s3Client;
|
|
26
|
+
this.bucket = bucket;
|
|
27
|
+
this.fileKey = fileKey;
|
|
28
|
+
this.logger = logger;
|
|
29
|
+
}
|
|
30
|
+
async initializeUpload() {
|
|
31
|
+
try {
|
|
32
|
+
const command = new client_s3_1.CreateMultipartUploadCommand({
|
|
33
|
+
Bucket: this.bucket,
|
|
34
|
+
Key: this.fileKey,
|
|
35
|
+
ContentType: "text/csv",
|
|
36
|
+
ContentDisposition: `attachment; filename="${this.fileKey
|
|
37
|
+
.split("/")
|
|
38
|
+
.pop()}"`,
|
|
39
|
+
Metadata: {
|
|
40
|
+
"generated-by": "medusa-erp-batch-processor",
|
|
41
|
+
"created-at": new Date().toISOString(),
|
|
42
|
+
"export-type": "error-report",
|
|
43
|
+
},
|
|
44
|
+
});
|
|
45
|
+
const response = await this.s3Client.send(command);
|
|
46
|
+
this.uploadId = response.UploadId;
|
|
47
|
+
this.logger.info(`S3 multipart upload initialized. UploadId: ${this.uploadId}`);
|
|
48
|
+
}
|
|
49
|
+
catch (error) {
|
|
50
|
+
this.logger.error("Failed to initialize S3 multipart upload", error);
|
|
51
|
+
throw error;
|
|
52
|
+
}
|
|
53
|
+
}
|
|
54
|
+
async writeChunk(csvChunk) {
|
|
55
|
+
if (!this.uploadId) {
|
|
56
|
+
await this.initializeUpload();
|
|
57
|
+
}
|
|
58
|
+
const chunkBuffer = Buffer.from(csvChunk, "utf-8");
|
|
59
|
+
this.currentBuffer = Buffer.concat([this.currentBuffer, chunkBuffer]);
|
|
60
|
+
this.totalSize += chunkBuffer.length;
|
|
61
|
+
// Upload when buffer reaches minimum part size
|
|
62
|
+
if (this.currentBuffer.length >= this.minPartSize) {
|
|
63
|
+
await this.uploadPart();
|
|
64
|
+
}
|
|
65
|
+
}
|
|
66
|
+
async uploadPart() {
|
|
67
|
+
if (this.currentBuffer.length === 0 || !this.uploadId)
|
|
68
|
+
return;
|
|
69
|
+
try {
|
|
70
|
+
this.logger.info(`Uploading part ${this.currentPart}, size: ${(this.currentBuffer.length /
|
|
71
|
+
1024 /
|
|
72
|
+
1024).toFixed(2)}MB`);
|
|
73
|
+
const command = new client_s3_1.UploadPartCommand({
|
|
74
|
+
Bucket: this.bucket,
|
|
75
|
+
Key: this.fileKey,
|
|
76
|
+
PartNumber: this.currentPart,
|
|
77
|
+
UploadId: this.uploadId,
|
|
78
|
+
Body: this.currentBuffer,
|
|
79
|
+
});
|
|
80
|
+
const response = await this.s3Client.send(command);
|
|
81
|
+
if (!response.ETag) {
|
|
82
|
+
throw new Error(`Part ${this.currentPart} upload failed: Missing ETag in response`);
|
|
83
|
+
}
|
|
84
|
+
this.parts.push({
|
|
85
|
+
ETag: response.ETag,
|
|
86
|
+
PartNumber: this.currentPart,
|
|
87
|
+
});
|
|
88
|
+
this.logger.info(`Part ${this.currentPart} uploaded successfully, ETag: ${response.ETag}`);
|
|
89
|
+
// Clear buffer and increment part number
|
|
90
|
+
this.currentBuffer = Buffer.alloc(0);
|
|
91
|
+
this.currentPart++;
|
|
92
|
+
}
|
|
93
|
+
catch (error) {
|
|
94
|
+
this.logger.error(`Failed to upload part ${this.currentPart}`, error);
|
|
95
|
+
throw error;
|
|
96
|
+
}
|
|
97
|
+
}
|
|
98
|
+
async finalize() {
|
|
99
|
+
if (!this.uploadId) {
|
|
100
|
+
throw new Error("Upload not initialized");
|
|
101
|
+
}
|
|
102
|
+
try {
|
|
103
|
+
// Upload any remaining data in buffer as final part
|
|
104
|
+
if (this.currentBuffer.length > 0) {
|
|
105
|
+
await this.uploadPart();
|
|
106
|
+
}
|
|
107
|
+
if (this.parts.length === 0) {
|
|
108
|
+
throw new Error("No parts uploaded");
|
|
109
|
+
}
|
|
110
|
+
// Complete multipart upload
|
|
111
|
+
const command = new client_s3_1.CompleteMultipartUploadCommand({
|
|
112
|
+
Bucket: this.bucket,
|
|
113
|
+
Key: this.fileKey,
|
|
114
|
+
UploadId: this.uploadId,
|
|
115
|
+
MultipartUpload: {
|
|
116
|
+
Parts: this.parts.sort((a, b) => a.PartNumber - b.PartNumber),
|
|
117
|
+
},
|
|
118
|
+
});
|
|
119
|
+
const response = await this.s3Client.send(command);
|
|
120
|
+
this.logger.info(`S3 multipart upload completed successfully`);
|
|
121
|
+
this.logger.info(`Location: ${response.Location}`);
|
|
122
|
+
this.logger.info(`Total parts: ${this.parts.length}`);
|
|
123
|
+
this.logger.info(`Total size: ${(this.totalSize / 1024 / 1024).toFixed(2)}MB`);
|
|
124
|
+
return {
|
|
125
|
+
fileId: this.fileKey,
|
|
126
|
+
fileKey: this.fileKey,
|
|
127
|
+
fileSize: this.totalSize,
|
|
128
|
+
};
|
|
129
|
+
}
|
|
130
|
+
catch (error) {
|
|
131
|
+
this.logger.error("Failed to complete S3 multipart upload", error);
|
|
132
|
+
await this.abort();
|
|
133
|
+
throw error;
|
|
134
|
+
}
|
|
135
|
+
}
|
|
136
|
+
async abort() {
|
|
137
|
+
if (!this.uploadId)
|
|
138
|
+
return;
|
|
139
|
+
try {
|
|
140
|
+
const command = new client_s3_1.AbortMultipartUploadCommand({
|
|
141
|
+
Bucket: this.bucket,
|
|
142
|
+
Key: this.fileKey,
|
|
143
|
+
UploadId: this.uploadId,
|
|
144
|
+
});
|
|
145
|
+
await this.s3Client.send(command);
|
|
146
|
+
this.logger.info(`S3 multipart upload aborted: ${this.uploadId}`);
|
|
147
|
+
}
|
|
148
|
+
catch (error) {
|
|
149
|
+
this.logger.error("Failed to abort S3 multipart upload", error);
|
|
150
|
+
}
|
|
151
|
+
}
|
|
152
|
+
}
|
|
153
|
+
async function generateErrorCSV(errors) {
|
|
154
|
+
const csvHeaders = [
|
|
155
|
+
"Batch Index",
|
|
156
|
+
"Batch Size",
|
|
157
|
+
"Failed Items",
|
|
158
|
+
"Error Message",
|
|
159
|
+
"Timestamp",
|
|
160
|
+
];
|
|
161
|
+
const csvRows = errors.map((error) => [
|
|
162
|
+
error.batchIndex.toString(),
|
|
163
|
+
error.batchSize.toString(),
|
|
164
|
+
`"${error.items.join(", ")}"`, // Wrap in quotes and join items
|
|
165
|
+
`"${error.error.replace(/"/g, '""')}"`, // Escape quotes in error message
|
|
166
|
+
error.timestamp,
|
|
167
|
+
]);
|
|
168
|
+
const csvContent = [
|
|
169
|
+
csvHeaders.join(","),
|
|
170
|
+
...csvRows.map((row) => row.join(",")),
|
|
171
|
+
].join("\n");
|
|
172
|
+
return csvContent;
|
|
173
|
+
}
|
|
174
|
+
async function uploadErrorCSVToS3(container, csvContent, jobId, datafor, operation) {
|
|
175
|
+
const logger = container.resolve(utils_1.ContainerRegistrationKeys.LOGGER);
|
|
176
|
+
let uploader = null;
|
|
177
|
+
try {
|
|
178
|
+
// Get S3 configuration from environment
|
|
179
|
+
const bucket = process.env.AWS_MEDIA_BUCKET;
|
|
180
|
+
if (!bucket) {
|
|
181
|
+
throw new Error("AWS_MEDIA_BUCKET not configured");
|
|
182
|
+
}
|
|
183
|
+
// Create optimized S3 client
|
|
184
|
+
const s3Client = new client_s3_1.S3Client({
|
|
185
|
+
region: process.env.AWS_REGION || "ap-south-1",
|
|
186
|
+
credentials: {
|
|
187
|
+
accessKeyId: process.env.AWS_ACCESS_KEY_ID,
|
|
188
|
+
secretAccessKey: process.env.AWS_SECRET_ACCESS_KEY,
|
|
189
|
+
},
|
|
190
|
+
...(process.env.S3_ENDPOINT && { endpoint: process.env.S3_ENDPOINT }),
|
|
191
|
+
requestHandler: new node_http_handler_1.NodeHttpHandler({
|
|
192
|
+
connectionTimeout: 30000, // 30s connection timeout
|
|
193
|
+
socketTimeout: 120000, // 2min socket timeout for large uploads
|
|
194
|
+
httpsAgent: {
|
|
195
|
+
keepAlive: true,
|
|
196
|
+
maxSockets: 50, // Connection pooling
|
|
197
|
+
maxFreeSockets: 10,
|
|
198
|
+
keepAliveMsecs: 30000, // Keep connections alive for 30s
|
|
199
|
+
},
|
|
200
|
+
}),
|
|
201
|
+
maxAttempts: 3, // Retry failed requests
|
|
202
|
+
retryMode: "adaptive", // Intelligent retry strategy
|
|
203
|
+
});
|
|
204
|
+
const fileName = `erp-batch-errors-${datafor}-${operation}-${jobId}-${Date.now()}.csv`;
|
|
205
|
+
const fileKey = `bulk-jobs/errors/${fileName}`;
|
|
206
|
+
// Initialize S3 multipart uploader
|
|
207
|
+
uploader = new S3MultipartUploader(bucket, fileKey, s3Client, logger);
|
|
208
|
+
// Upload CSV content in chunks
|
|
209
|
+
logger.info(`Uploading error CSV to S3: ${fileKey}`);
|
|
210
|
+
await uploader.writeChunk(csvContent);
|
|
211
|
+
// Finalize the upload
|
|
212
|
+
const uploadResult = await uploader.finalize();
|
|
213
|
+
logger.info(`Error CSV uploaded successfully: ${uploadResult.fileKey}`);
|
|
214
|
+
logger.info(`File size: ${(uploadResult.fileSize / 1024).toFixed(2)} KB`);
|
|
215
|
+
// Generate presigned download URL
|
|
216
|
+
let downloadUrl;
|
|
217
|
+
try {
|
|
218
|
+
// Generate presigned URL using AWS SDK
|
|
219
|
+
const getObjectCommand = new client_s3_1.GetObjectCommand({
|
|
220
|
+
Bucket: bucket,
|
|
221
|
+
Key: uploadResult.fileKey,
|
|
222
|
+
});
|
|
223
|
+
downloadUrl = await (0, s3_request_presigner_1.getSignedUrl)(s3Client, getObjectCommand, {
|
|
224
|
+
expiresIn: 24 * 60 * 60, // 24 hours expiration
|
|
225
|
+
});
|
|
226
|
+
logger.info(`Generated presigned download URL for: ${uploadResult.fileKey}`);
|
|
227
|
+
logger.info(`URL expires in 24 hours`);
|
|
228
|
+
}
|
|
229
|
+
catch (presignError) {
|
|
230
|
+
// Fallback to direct S3 URL (may not work if bucket is private)
|
|
231
|
+
logger.warn(`Presigned URL generation failed, using direct S3 URL: ${presignError.message}`);
|
|
232
|
+
downloadUrl = `https://${bucket}.s3.${process.env.AWS_REGION || "ap-south-1"}.amazonaws.com/${uploadResult.fileKey}`;
|
|
233
|
+
logger.warn("Using direct S3 URL - this may not work if the bucket is private");
|
|
234
|
+
}
|
|
235
|
+
return { fileKey: uploadResult.fileKey, downloadUrl };
|
|
236
|
+
}
|
|
237
|
+
catch (error) {
|
|
238
|
+
// Abort multipart upload if it was started
|
|
239
|
+
if (uploader) {
|
|
240
|
+
await uploader.abort();
|
|
241
|
+
}
|
|
242
|
+
logger.error("Failed to upload error CSV to S3:", error);
|
|
243
|
+
throw error;
|
|
244
|
+
}
|
|
245
|
+
}
|
|
246
|
+
async function sendErrorNotificationEmail(container, jobId, datafor, operation, totalItems, failedBatches, totalBatches, csvDownloadUrl, csvFileName) {
|
|
247
|
+
const logger = container.resolve(utils_1.ContainerRegistrationKeys.LOGGER);
|
|
248
|
+
try {
|
|
249
|
+
await (0, notification_1.createNotificationWorkflow)(container).run({
|
|
250
|
+
input: {
|
|
251
|
+
to: "vasu.chapadia@devxconsultancy.com", // Will be filled by notification service
|
|
252
|
+
channel: "email",
|
|
253
|
+
template: "erp-batch-report",
|
|
254
|
+
data: {
|
|
255
|
+
jobId,
|
|
256
|
+
datafor: JSON.stringify(datafor),
|
|
257
|
+
operation,
|
|
258
|
+
totalItems,
|
|
259
|
+
failedBatches,
|
|
260
|
+
totalBatches,
|
|
261
|
+
errorRate: ((failedBatches / totalBatches) * 100).toFixed(2),
|
|
262
|
+
csvFileName,
|
|
263
|
+
csvDownloadUrl,
|
|
264
|
+
},
|
|
265
|
+
},
|
|
266
|
+
});
|
|
267
|
+
logger.info("Error notification email sent successfully");
|
|
268
|
+
}
|
|
269
|
+
catch (error) {
|
|
270
|
+
logger.error("Failed to send error notification email:", error);
|
|
271
|
+
// Don't throw here - we don't want email failure to fail the entire job
|
|
272
|
+
}
|
|
273
|
+
}
|
|
274
|
+
async function logJobStatus(container, jobId, status, errorMessage, result) {
|
|
275
|
+
const logger = container.resolve(utils_1.ContainerRegistrationKeys.LOGGER);
|
|
276
|
+
logger.info(`Job ${jobId} status: ${status}`);
|
|
277
|
+
if (errorMessage) {
|
|
278
|
+
logger.error(`Job ${jobId} error: ${errorMessage}`);
|
|
279
|
+
}
|
|
280
|
+
if (result) {
|
|
281
|
+
logger.info(`Job ${jobId} result: ${JSON.stringify(result)}`);
|
|
282
|
+
}
|
|
283
|
+
}
|
|
284
|
+
async function processBatch(container, datafor, operation, batch, batchIndex, totalBatches, logger) {
|
|
285
|
+
const errors = [];
|
|
286
|
+
let processed = 0;
|
|
287
|
+
try {
|
|
288
|
+
logger.info(`Processing batch ${batchIndex + 1}/${totalBatches} - ${batch.length} items for ${datafor} ${operation}`);
|
|
289
|
+
const workflowConfig = config_1.MASTER_WORKFLOW_CONFIG[datafor];
|
|
290
|
+
if (!workflowConfig) {
|
|
291
|
+
throw new Error(`Unsupported datafor type: ${datafor}`);
|
|
292
|
+
}
|
|
293
|
+
const workflowHandler = workflowConfig[operation];
|
|
294
|
+
if (!workflowHandler) {
|
|
295
|
+
throw new Error(`Unsupported operation: ${operation} for ${datafor}`);
|
|
296
|
+
}
|
|
297
|
+
const input = (0, config_1.getWorkflowInput)(datafor, operation, batch);
|
|
298
|
+
// Execute the workflow for this batch
|
|
299
|
+
await workflowHandler(container, input);
|
|
300
|
+
processed = batch.length;
|
|
301
|
+
logger.info(`✅ Successfully processed batch ${batchIndex + 1}/${totalBatches} - ${processed} items`);
|
|
302
|
+
return { success: true, processed, errors };
|
|
303
|
+
}
|
|
304
|
+
catch (error) {
|
|
305
|
+
const errorMessage = error instanceof Error ? error.message : String(error);
|
|
306
|
+
errors.push(`Batch ${batchIndex + 1}: ${errorMessage}`);
|
|
307
|
+
logger.error(`❌ Failed to process batch ${batchIndex + 1}/${totalBatches}: ${errorMessage}`);
|
|
308
|
+
const batchError = {
|
|
309
|
+
batchIndex: batchIndex + 1,
|
|
310
|
+
batchSize: batch.length,
|
|
311
|
+
items: batch,
|
|
312
|
+
error: errorMessage,
|
|
313
|
+
timestamp: new Date().toISOString(),
|
|
314
|
+
};
|
|
315
|
+
return { success: false, processed, errors, batchError };
|
|
316
|
+
}
|
|
317
|
+
}
|
|
318
|
+
async function erpWebhookBatchProcessingScript({ container, }) {
|
|
319
|
+
const jobId = (0, command_args_1.getArgString)("job-id", `erp_webhook_batch_job_${Date.now()}`);
|
|
320
|
+
const batchSize = (0, command_args_1.getArgNumber)("batch-size", 200);
|
|
321
|
+
const erpEventId = (0, command_args_1.getArgString)("event-id");
|
|
322
|
+
const logger = container.resolve(utils_1.ContainerRegistrationKeys.LOGGER);
|
|
323
|
+
if (!erpEventId) {
|
|
324
|
+
logger.error("event-id parameter is required");
|
|
325
|
+
throw new Error("event-id parameter is required");
|
|
326
|
+
}
|
|
327
|
+
try {
|
|
328
|
+
const query = container.resolve(utils_1.ContainerRegistrationKeys.QUERY);
|
|
329
|
+
logger.info(`ERP webhook batch processing job started: ${jobId}`);
|
|
330
|
+
logger.info(`Event ID: ${erpEventId}`);
|
|
331
|
+
logger.info(`Batch size: ${batchSize} items per batch`);
|
|
332
|
+
logger.info(`Initial memory usage: ${Math.round(process.memoryUsage().heapUsed / 1024 / 1024)}MB`);
|
|
333
|
+
// Get the ERP event data
|
|
334
|
+
logger.info("Fetching ERP event data...");
|
|
335
|
+
const startTime = Date.now();
|
|
336
|
+
const { data: erpEventData } = await query.graph({
|
|
337
|
+
entity: "erp_event",
|
|
338
|
+
fields: ["id", "datafor", "operation", "data", "status"],
|
|
339
|
+
filters: {
|
|
340
|
+
id: erpEventId,
|
|
341
|
+
},
|
|
342
|
+
pagination: {
|
|
343
|
+
skip: 0,
|
|
344
|
+
take: 1,
|
|
345
|
+
},
|
|
346
|
+
});
|
|
347
|
+
logger.info(`erpEvent ${JSON.stringify(erpEventData)}`);
|
|
348
|
+
const erpEvent = erpEventData[0];
|
|
349
|
+
if (!erpEvent) {
|
|
350
|
+
logger.warn(`No ERP event found with ID: ${erpEventId}`);
|
|
351
|
+
await logJobStatus(container, jobId, "completed", undefined, {
|
|
352
|
+
message: "No ERP event found",
|
|
353
|
+
});
|
|
354
|
+
throw new Error("No ERP event found");
|
|
355
|
+
}
|
|
356
|
+
if (erpEvent.status !== "pending") {
|
|
357
|
+
logger.info(`ERP event ${erpEventId} is already completed`);
|
|
358
|
+
await logJobStatus(container, jobId, "completed", undefined, {
|
|
359
|
+
message: "ERP event is already completed",
|
|
360
|
+
});
|
|
361
|
+
throw new Error("ERP event is already completed");
|
|
362
|
+
}
|
|
363
|
+
await logJobStatus(container, jobId, "processing");
|
|
364
|
+
const { datafor, operation, data } = erpEvent;
|
|
365
|
+
const totalItems = data.length;
|
|
366
|
+
await (0, notification_1.createNotificationWorkflow)(container).run({
|
|
367
|
+
input: {
|
|
368
|
+
to: "",
|
|
369
|
+
channel: "feed",
|
|
370
|
+
template: "admin-ui",
|
|
371
|
+
data: {
|
|
372
|
+
title: "ERP Batch Processing Started",
|
|
373
|
+
description: `ERP batch processing job ${jobId} and event ${erpEventId} started. Total items to process: ${totalItems}`,
|
|
374
|
+
},
|
|
375
|
+
},
|
|
376
|
+
});
|
|
377
|
+
if (totalItems === 0) {
|
|
378
|
+
logger.warn("No data items to process in the ERP event");
|
|
379
|
+
await logJobStatus(container, jobId, "completed", undefined, {
|
|
380
|
+
message: "No data items to process",
|
|
381
|
+
});
|
|
382
|
+
return;
|
|
383
|
+
}
|
|
384
|
+
logger.info(`Processing ${totalItems} items for ${datafor} ${operation}`);
|
|
385
|
+
// Split data into batches
|
|
386
|
+
const batches = [];
|
|
387
|
+
for (let i = 0; i < totalItems; i += batchSize) {
|
|
388
|
+
batches.push(data.slice(i, i + batchSize));
|
|
389
|
+
}
|
|
390
|
+
const totalBatches = batches.length;
|
|
391
|
+
logger.info(`Split into ${totalBatches} batches of up to ${batchSize} items each`);
|
|
392
|
+
// Process batches
|
|
393
|
+
let totalProcessed = 0;
|
|
394
|
+
let totalErrors = 0;
|
|
395
|
+
const allErrors = [];
|
|
396
|
+
const batchErrors = [];
|
|
397
|
+
for (let i = 0; i < batches.length; i++) {
|
|
398
|
+
const batch = batches[i];
|
|
399
|
+
try {
|
|
400
|
+
const result = await processBatch(container, datafor, operation, batch, i, totalBatches, logger);
|
|
401
|
+
totalProcessed += result.processed;
|
|
402
|
+
if (!result.success) {
|
|
403
|
+
totalErrors++;
|
|
404
|
+
allErrors.push(...result.errors);
|
|
405
|
+
if (result.batchError) {
|
|
406
|
+
batchErrors.push(result.batchError);
|
|
407
|
+
}
|
|
408
|
+
}
|
|
409
|
+
// Log progress every 5 batches
|
|
410
|
+
if ((i + 1) % 5 === 0 || i === totalBatches - 1) {
|
|
411
|
+
const memUsage = Math.round(process.memoryUsage().heapUsed / 1024 / 1024);
|
|
412
|
+
logger.info(`Progress: ${i + 1}/${totalBatches} batches, ${totalProcessed}/${totalItems} items processed, Memory: ${memUsage}MB`);
|
|
413
|
+
// Force garbage collection for large jobs (if enabled)
|
|
414
|
+
if (memUsage > 1500 && global.gc) {
|
|
415
|
+
logger.info("High memory usage detected, triggering garbage collection");
|
|
416
|
+
global.gc();
|
|
417
|
+
}
|
|
418
|
+
}
|
|
419
|
+
}
|
|
420
|
+
catch (error) {
|
|
421
|
+
totalErrors++;
|
|
422
|
+
const errorMessage = error instanceof Error ? error.message : String(error);
|
|
423
|
+
allErrors.push(`Batch ${i + 1}: ${errorMessage}`);
|
|
424
|
+
logger.error(`Failed to process batch ${i + 1}: ${errorMessage}`);
|
|
425
|
+
// Add to batch errors for CSV generation
|
|
426
|
+
const batchError = {
|
|
427
|
+
batchIndex: i + 1,
|
|
428
|
+
batchSize: batch.length,
|
|
429
|
+
items: batch,
|
|
430
|
+
error: errorMessage,
|
|
431
|
+
timestamp: new Date().toISOString(),
|
|
432
|
+
};
|
|
433
|
+
batchErrors.push(batchError);
|
|
434
|
+
// Continue processing other batches even if one fails
|
|
435
|
+
continue;
|
|
436
|
+
}
|
|
437
|
+
}
|
|
438
|
+
const processingTime = Date.now() - startTime;
|
|
439
|
+
const throughput = Math.round(totalProcessed / (processingTime / 1000));
|
|
440
|
+
const successRate = totalItems > 0 ? (totalProcessed / totalItems) * 100 : 0;
|
|
441
|
+
logger.info(`Batch processing completed!`);
|
|
442
|
+
logger.info(`Total items: ${totalItems}`);
|
|
443
|
+
logger.info(`Successfully processed: ${totalProcessed}`);
|
|
444
|
+
logger.info(`Failed batches: ${totalErrors}/${totalBatches}`);
|
|
445
|
+
logger.info(`Success rate: ${successRate.toFixed(2)}%`);
|
|
446
|
+
logger.info(`Processing time: ${processingTime}ms`);
|
|
447
|
+
logger.info(`Throughput: ${throughput} items/second`);
|
|
448
|
+
// Generate CSV and send email if there were errors
|
|
449
|
+
let csvDownloadUrl = "";
|
|
450
|
+
let csvFileName = "";
|
|
451
|
+
if (totalErrors > 0 && batchErrors.length > 0) {
|
|
452
|
+
try {
|
|
453
|
+
logger.info(`Generating error CSV for ${batchErrors.length} failed batches...`);
|
|
454
|
+
const csvContent = await generateErrorCSV(batchErrors);
|
|
455
|
+
const uploadResult = await uploadErrorCSVToS3(container, csvContent, jobId, datafor, operation);
|
|
456
|
+
csvDownloadUrl = uploadResult.downloadUrl;
|
|
457
|
+
csvFileName =
|
|
458
|
+
uploadResult.fileKey.split("/").pop() || "error-report.csv";
|
|
459
|
+
logger.info(`Error CSV generated and uploaded: ${csvFileName}`);
|
|
460
|
+
// Send error notification email
|
|
461
|
+
await sendErrorNotificationEmail(container, jobId, datafor, operation, totalItems, totalErrors, totalBatches, csvDownloadUrl, csvFileName);
|
|
462
|
+
logger.info("Error notification email sent with CSV attachment");
|
|
463
|
+
}
|
|
464
|
+
catch (csvError) {
|
|
465
|
+
logger.error("Failed to generate/send error CSV:", csvError);
|
|
466
|
+
// Don't throw here - the main job processing was completed
|
|
467
|
+
}
|
|
468
|
+
}
|
|
469
|
+
// Send completion notification
|
|
470
|
+
const notificationTitle = totalErrors === 0
|
|
471
|
+
? `✅ ERP Batch Processing Completed Successfully`
|
|
472
|
+
: `⚠️ ERP Batch Processing Completed with Errors`;
|
|
473
|
+
const notificationDescription = totalErrors === 0
|
|
474
|
+
? `Successfully processed all ${totalProcessed} items for ${datafor} ${operation}`
|
|
475
|
+
: `Processed ${totalProcessed}/${totalItems} items for ${datafor} ${operation}. ${totalErrors} batches failed.${csvFileName ? ` Error report: ${csvFileName}` : ""}`;
|
|
476
|
+
await (0, notification_1.createNotificationWorkflow)(container).run({
|
|
477
|
+
input: {
|
|
478
|
+
to: "",
|
|
479
|
+
channel: "feed",
|
|
480
|
+
template: "admin-ui",
|
|
481
|
+
data: {
|
|
482
|
+
title: notificationTitle,
|
|
483
|
+
description: notificationDescription,
|
|
484
|
+
},
|
|
485
|
+
},
|
|
486
|
+
});
|
|
487
|
+
await logJobStatus(container, jobId, totalErrors === 0 ? "completed" : "completed_with_errors", totalErrors > 0 ? `${totalErrors} batches failed` : undefined, {
|
|
488
|
+
datafor,
|
|
489
|
+
operation,
|
|
490
|
+
totalItems,
|
|
491
|
+
processedItems: totalProcessed,
|
|
492
|
+
failedBatches: totalErrors,
|
|
493
|
+
successRate: successRate.toFixed(2),
|
|
494
|
+
processingTimeMs: processingTime,
|
|
495
|
+
throughputPerSecond: throughput,
|
|
496
|
+
erpEventId,
|
|
497
|
+
errors: allErrors.slice(0, 10), // Only include first 10 errors to avoid log overflow
|
|
498
|
+
});
|
|
499
|
+
await (0, update_erp_event_1.updateErpEventWorkflow)(container).run({
|
|
500
|
+
input: {
|
|
501
|
+
id: erpEventId,
|
|
502
|
+
status: totalErrors > 0 ? "failed" : "completed",
|
|
503
|
+
sync_completed_at: new Date(),
|
|
504
|
+
},
|
|
505
|
+
});
|
|
506
|
+
logger.info(`ERP webhook batch processing job completed: ${jobId}`);
|
|
507
|
+
logger.info(`Final memory usage: ${Math.round(process.memoryUsage().heapUsed / 1024 / 1024)}MB`);
|
|
508
|
+
// Handle completion based on whether there were errors
|
|
509
|
+
if (totalErrors > 0) {
|
|
510
|
+
// For partial failures, update status and exit with error code
|
|
511
|
+
logger.warn(`Job completed with errors: ${totalErrors} out of ${totalBatches} batches failed`);
|
|
512
|
+
// Wait briefly for error notifications to complete
|
|
513
|
+
logger.info("Waiting 30 seconds for error processing to complete...");
|
|
514
|
+
await delay(30 * 1000); // 30 seconds
|
|
515
|
+
logger.info("Error processing completed, exiting with error code");
|
|
516
|
+
// Force exit for ECS task completion with error code to indicate partial failure
|
|
517
|
+
process.exit(1);
|
|
518
|
+
}
|
|
519
|
+
else {
|
|
520
|
+
// For successful completion, wait longer for sync processes
|
|
521
|
+
logger.info("All batches processed successfully");
|
|
522
|
+
// Wait for 2 minutes to allow all sync processes to complete
|
|
523
|
+
logger.info("Waiting 2 minutes for all sync processes to complete...");
|
|
524
|
+
await delay(2 * 60 * 1000); // 2 minutes in milliseconds
|
|
525
|
+
logger.info("Wait period completed, exiting process");
|
|
526
|
+
// Force exit for ECS task completion - prevents hanging connections from keeping task alive
|
|
527
|
+
process.exit(0);
|
|
528
|
+
}
|
|
529
|
+
}
|
|
530
|
+
catch (error) {
|
|
531
|
+
logger.error("Error in ERP webhook batch processing:", error);
|
|
532
|
+
await logJobStatus(container, jobId, "failed", error.message);
|
|
533
|
+
await (0, update_erp_event_1.updateErpEventWorkflow)(container).run({
|
|
534
|
+
input: {
|
|
535
|
+
id: erpEventId,
|
|
536
|
+
status: "failed",
|
|
537
|
+
sync_completed_at: new Date(),
|
|
538
|
+
},
|
|
539
|
+
});
|
|
540
|
+
// Send failure notification
|
|
541
|
+
await (0, notification_1.createNotificationWorkflow)(container).run({
|
|
542
|
+
input: {
|
|
543
|
+
to: "",
|
|
544
|
+
channel: "feed",
|
|
545
|
+
template: "admin-ui",
|
|
546
|
+
data: {
|
|
547
|
+
title: "❌ ERP Batch Processing Failed",
|
|
548
|
+
description: `ERP batch processing job ${jobId} and event ${erpEventId} failed. Cause: ${error.message}`,
|
|
549
|
+
},
|
|
550
|
+
},
|
|
551
|
+
});
|
|
552
|
+
// Wait briefly for error notifications to be sent
|
|
553
|
+
logger.info("Waiting 30 seconds for error notifications to complete...");
|
|
554
|
+
await delay(30 * 1000); // 30 seconds
|
|
555
|
+
logger.info("Error handling completed, exiting process");
|
|
556
|
+
// Force exit for ECS task completion with error code
|
|
557
|
+
process.exit(1);
|
|
558
|
+
}
|
|
559
|
+
}
|
|
560
|
+
//# sourceMappingURL=data:application/json;base64,eyJ2ZXJzaW9uIjozLCJmaWxlIjoiZXJwLXdlYmhvb2stYmF0Y2guanMiLCJzb3VyY2VSb290IjoiIiwic291cmNlcyI6WyIuLi8uLi8uLi8uLi8uLi9zcmMvY29tbWFuZHMvYnVsay1qb2JzL2VycC13ZWJob29rLWJhdGNoLnRzIl0sIm5hbWVzIjpbXSwibWFwcGluZ3MiOiI7OztBQTZkQSxrREF5VkM7QUFwekJELGtEQU80QjtBQUM1Qix3RUFBNkQ7QUFHN0QscURBQXNFO0FBQ3RFLGlFQUE0RDtBQUU1RCx5REFLc0M7QUFDdEMsMkZBQThGO0FBQzlGLCtEQUEwRTtBQUMxRSx3REFBbUU7QUFFbkUsb0RBQW9EO0FBQ3BELEtBQUssVUFBVSxLQUFLLENBQUMsRUFBVTtJQUM3QixPQUFPLElBQUksT0FBTyxDQUFDLENBQUMsT0FBTyxFQUFFLEVBQUUsQ0FBQyxVQUFVLENBQUMsT0FBTyxFQUFFLEVBQUUsQ0FBQyxDQUFDLENBQUM7QUFDM0QsQ0FBQztBQWdCRCxNQUFNLG1CQUFtQjtJQVl2QixZQUNFLE1BQWMsRUFDZCxPQUFlLEVBQ2YsUUFBa0IsRUFDbEIsTUFBVztRQVpMLGFBQVEsR0FBa0IsSUFBSSxDQUFDO1FBQy9CLFVBQUssR0FBZ0QsRUFBRSxDQUFDO1FBQ3hELGdCQUFXLEdBQUcsQ0FBQyxDQUFDO1FBQ2hCLGtCQUFhLEdBQVcsTUFBTSxDQUFDLEtBQUssQ0FBQyxDQUFDLENBQUMsQ0FBQztRQUN4QyxnQkFBVyxHQUFXLENBQUMsR0FBRyxJQUFJLEdBQUcsSUFBSSxDQUFDLENBQUMsNEJBQTRCO1FBQ25FLGNBQVMsR0FBRyxDQUFDLENBQUM7UUFTcEIsSUFBSSxDQUFDLFFBQVEsR0FBRyxRQUFRLENBQUM7UUFDekIsSUFBSSxDQUFDLE1BQU0sR0FBRyxNQUFNLENBQUM7UUFDckIsSUFBSSxDQUFDLE9BQU8sR0FBRyxPQUFPLENBQUM7UUFDdkIsSUFBSSxDQUFDLE1BQU0sR0FBRyxNQUFNLENBQUM7SUFDdkIsQ0FBQztJQUVELEtBQUssQ0FBQyxnQkFBZ0I7UUFDcEIsSUFBSSxDQUFDO1lB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
|