agrs-sequelize-sdk 1.2.11 → 1.2.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.
@@ -0,0 +1,264 @@
1
+ const { CampaignCreationLogNew } = require("../db");
2
+ const {
3
+ getProcessHierarchy,
4
+ getProcessStats,
5
+ } = require("../utils/loggerUtils");
6
+ const { Op } = require("sequelize");
7
+
8
+ /**
9
+ * Get detailed logs for campaign creation processes
10
+ * @param {Object} req - Express request object
11
+ * @param {Object} res - Express response object
12
+ */
13
+ async function getCampaignCreationLogs(req, res) {
14
+ try {
15
+ const {
16
+ process_id,
17
+ country,
18
+ status,
19
+ stage,
20
+ entity_name,
21
+ facebook_campaign_id,
22
+ facebook_adset_id,
23
+ facebook_ad_id,
24
+ from_date,
25
+ to_date,
26
+ page = 0,
27
+ limit = 20,
28
+ } = req.query;
29
+
30
+ // Build where clause
31
+ const whereClause = {};
32
+ if (process_id) whereClause.process_id = process_id;
33
+ if (country) whereClause.country = country;
34
+ if (status) whereClause.status = status;
35
+ if (stage) whereClause.stage = stage;
36
+ if (entity_name)
37
+ whereClause.entity_name = { [Op.iLike]: `%${entity_name}%` };
38
+ if (facebook_campaign_id)
39
+ whereClause.facebook_campaign_id = facebook_campaign_id;
40
+ if (facebook_adset_id) whereClause.facebook_adset_id = facebook_adset_id;
41
+ if (facebook_ad_id) whereClause.facebook_ad_id = facebook_ad_id;
42
+
43
+ // Date range filtering
44
+ if (from_date || to_date) {
45
+ whereClause.createdAt = {};
46
+ if (from_date) whereClause.createdAt[Op.gte] = new Date(from_date);
47
+ if (to_date) whereClause.createdAt[Op.lte] = new Date(to_date);
48
+ }
49
+
50
+ // Pagination params
51
+ const pageInt = parseInt(page, 10);
52
+ const limitInt = parseInt(limit, 10);
53
+
54
+ // Execute query
55
+ const { count, rows } = await CampaignCreationLogNew.findAndCountAll({
56
+ where: whereClause,
57
+ limit: limitInt,
58
+ offset: pageInt * limitInt,
59
+ order: [["createdAt", "DESC"]],
60
+ });
61
+
62
+ return res.json({
63
+ success: true,
64
+ logs: rows,
65
+ pagination: {
66
+ total: count,
67
+ page: pageInt,
68
+ limit: limitInt,
69
+ pages: Math.ceil(count / limitInt),
70
+ },
71
+ });
72
+ } catch (error) {
73
+ console.error("Error in getCampaignCreationLogs:", error);
74
+ return res.status(500).json({
75
+ success: false,
76
+ error: error.message,
77
+ });
78
+ }
79
+ }
80
+
81
+ /**
82
+ * Get all processes with summary information
83
+ * @param {Object} req - Express request object
84
+ * @param {Object} res - Express response object
85
+ */
86
+ async function getCampaignCreationProcesses(req, res) {
87
+ try {
88
+ const {
89
+ status,
90
+ country,
91
+ from_date,
92
+ to_date,
93
+ page = 0,
94
+ limit = 10,
95
+ } = req.query;
96
+
97
+ // Get distinct process IDs with filter conditions
98
+ const whereClause = {};
99
+ if (status) whereClause.status = status;
100
+ if (country) whereClause.country = country;
101
+
102
+ // Date range filtering
103
+ if (from_date || to_date) {
104
+ whereClause.createdAt = {};
105
+ if (from_date) whereClause.createdAt[Op.gte] = new Date(from_date);
106
+ if (to_date) whereClause.createdAt[Op.lte] = new Date(to_date);
107
+ }
108
+
109
+ // Get distinct process IDs
110
+ const distinctProcessIds = await CampaignCreationLogNew.findAll({
111
+ attributes: [
112
+ "process_id",
113
+ [
114
+ CampaignCreationLogNew.sequelize.fn(
115
+ "MAX",
116
+ CampaignCreationLogNew.sequelize.col("createdAt")
117
+ ),
118
+ "latest_timestamp",
119
+ ],
120
+ ],
121
+ where: whereClause,
122
+ group: ["process_id"],
123
+ order: [
124
+ [CampaignCreationLogNew.sequelize.literal("latest_timestamp"), "DESC"],
125
+ ],
126
+ limit: parseInt(limit, 10),
127
+ offset: parseInt(page, 10) * parseInt(limit, 10),
128
+ });
129
+
130
+ // Get process IDs from result
131
+ const processIds = distinctProcessIds.map((item) => item.process_id);
132
+
133
+ // Get summary information for each process
134
+ const processes = [];
135
+ for (const processId of processIds) {
136
+ // Get the initial log for this process
137
+ const initialLog = await CampaignCreationLogNew.findOne({
138
+ where: {
139
+ process_id: processId,
140
+ stage: "PROCESS_STARTED",
141
+ },
142
+ order: [["createdAt", "ASC"]],
143
+ });
144
+
145
+ if (initialLog) {
146
+ // Get stats for this process
147
+ const stats = await getProcessStats(processId);
148
+
149
+ // Get the latest log to determine current status
150
+ const latestLog = await CampaignCreationLogNew.findOne({
151
+ where: { process_id: processId },
152
+ order: [["createdAt", "DESC"]],
153
+ });
154
+
155
+ processes.push({
156
+ process_id: processId,
157
+ created_at: initialLog.createdAt,
158
+ updated_at: latestLog.createdAt,
159
+ status: latestLog.status,
160
+ statistics: stats,
161
+ countries: await getCountriesByProcess(processId),
162
+ });
163
+ }
164
+ }
165
+
166
+ // Count total number of distinct processes
167
+ const totalProcessesCount = await CampaignCreationLogNew.count({
168
+ distinct: true,
169
+ col: "process_id",
170
+ where: whereClause,
171
+ });
172
+
173
+ return res.json({
174
+ success: true,
175
+ processes,
176
+ pagination: {
177
+ total: totalProcessesCount,
178
+ page: parseInt(page, 10),
179
+ limit: parseInt(limit, 10),
180
+ pages: Math.ceil(totalProcessesCount / parseInt(limit, 10)),
181
+ },
182
+ });
183
+ } catch (error) {
184
+ console.error("Error in getCampaignCreationProcesses:", error);
185
+ return res.status(500).json({
186
+ success: false,
187
+ error: error.message,
188
+ });
189
+ }
190
+ }
191
+
192
+ /**
193
+ * Get detailed hierarchical view of a specific process
194
+ * @param {Object} req - Express request object
195
+ * @param {Object} res - Express response object
196
+ */
197
+ async function getCampaignCreationProcessDetails(req, res) {
198
+ try {
199
+ const { process_id } = req.params;
200
+
201
+ if (!process_id) {
202
+ return res.status(400).json({
203
+ success: false,
204
+ error: "Process ID is required",
205
+ });
206
+ }
207
+
208
+ // Get hierarchical view
209
+ const processDetails = await getProcessHierarchy(process_id);
210
+
211
+ if (!processDetails) {
212
+ return res.status(404).json({
213
+ success: false,
214
+ error: "Process not found",
215
+ });
216
+ }
217
+
218
+ return res.json({
219
+ success: true,
220
+ process: processDetails,
221
+ });
222
+ } catch (error) {
223
+ console.error("Error in getCampaignCreationProcessDetails:", error);
224
+ return res.status(500).json({
225
+ success: false,
226
+ error: error.message,
227
+ });
228
+ }
229
+ }
230
+
231
+ /**
232
+ * Helper function to get countries involved in a process
233
+ * @param {String} processId - Process ID
234
+ * @returns {Promise<Array>} Array of country information
235
+ */
236
+ async function getCountriesByProcess(processId) {
237
+ const countries = await CampaignCreationLogNew.findAll({
238
+ attributes: [
239
+ "country",
240
+ [
241
+ CampaignCreationLogNew.sequelize.fn(
242
+ "MAX",
243
+ CampaignCreationLogNew.sequelize.col("status")
244
+ ),
245
+ "status",
246
+ ],
247
+ ],
248
+ where: {
249
+ process_id: processId,
250
+ country: { [Op.ne]: null },
251
+ stage: "CAMPAIGN_CREATION",
252
+ },
253
+ group: ["country"],
254
+ raw: true,
255
+ });
256
+
257
+ return countries;
258
+ }
259
+
260
+ module.exports = {
261
+ getCampaignCreationLogs,
262
+ getCampaignCreationProcesses,
263
+ getCampaignCreationProcessDetails,
264
+ };
@@ -0,0 +1,141 @@
1
+ module.exports = (sequelize, DataTypes) => {
2
+ const CampaignCreationLogNew = sequelize.define(
3
+ "CampaignCreationLogNew",
4
+ {
5
+ id: {
6
+ type: DataTypes.INTEGER,
7
+ primaryKey: true,
8
+ autoIncrement: true,
9
+ },
10
+ process_id: {
11
+ type: DataTypes.UUID,
12
+ allowNull: false,
13
+ comment: "Unique ID for the creation batch process",
14
+ },
15
+ stage: {
16
+ type: DataTypes.ENUM(
17
+ "PROCESS_STARTED",
18
+ "CAMPAIGN_CREATION",
19
+ "ADSET_CREATION",
20
+ "AD_CREATION"
21
+ ),
22
+ allowNull: false,
23
+ comment: "Current stage of the campaign creation process",
24
+ },
25
+ status: {
26
+ type: DataTypes.ENUM("STARTED", "IN_PROGRESS", "COMPLETED", "FAILED"),
27
+ allowNull: false,
28
+ defaultValue: "STARTED",
29
+ comment: "Status of the current stage",
30
+ },
31
+ country: {
32
+ type: DataTypes.STRING,
33
+ allowNull: true,
34
+ comment: "Country being processed",
35
+ },
36
+ feed_campaign_id: {
37
+ type: DataTypes.STRING,
38
+ allowNull: true,
39
+ comment: "AGRS_CID of the created feed campaign",
40
+ },
41
+ facebook_campaign_id: {
42
+ type: DataTypes.STRING,
43
+ allowNull: true,
44
+ comment: "Facebook campaign ID if created",
45
+ },
46
+ facebook_adset_id: {
47
+ type: DataTypes.STRING,
48
+ allowNull: true,
49
+ comment: "Facebook adset ID if created",
50
+ },
51
+ facebook_ad_id: {
52
+ type: DataTypes.STRING,
53
+ allowNull: true,
54
+ comment: "Facebook ad ID if created",
55
+ },
56
+ parent_id: {
57
+ type: DataTypes.STRING,
58
+ allowNull: true,
59
+ comment: "Parent entity ID (campaign ID for adsets, adset ID for ads)",
60
+ },
61
+ entity_name: {
62
+ type: DataTypes.STRING,
63
+ allowNull: true,
64
+ comment: "Name of the created entity (campaign, adset, or ad name)",
65
+ },
66
+ entity_index: {
67
+ type: DataTypes.INTEGER,
68
+ allowNull: true,
69
+ comment:
70
+ "Index of this entity in a collection (for multiple adsets/ads)",
71
+ },
72
+ details: {
73
+ type: DataTypes.JSONB,
74
+ allowNull: true,
75
+ comment:
76
+ "Structured details about the stage, including original request, campaign, adset, or ad data",
77
+ },
78
+ error_message: {
79
+ type: DataTypes.TEXT,
80
+ allowNull: true,
81
+ comment: "Error message if process failed",
82
+ },
83
+ },
84
+ {
85
+ tableName: "CampaignCreationLogsNew",
86
+ timestamps: true,
87
+ indexes: [
88
+ {
89
+ fields: ["process_id"],
90
+ },
91
+ {
92
+ fields: ["stage"],
93
+ },
94
+ {
95
+ fields: ["status"],
96
+ },
97
+ {
98
+ fields: ["country"],
99
+ },
100
+ {
101
+ fields: ["feed_campaign_id"],
102
+ },
103
+ {
104
+ fields: ["facebook_campaign_id"],
105
+ },
106
+ {
107
+ fields: ["facebook_adset_id"],
108
+ },
109
+ {
110
+ fields: ["facebook_ad_id"],
111
+ },
112
+ {
113
+ fields: ["parent_id"],
114
+ },
115
+ {
116
+ fields: ["entity_name"],
117
+ },
118
+ {
119
+ fields: ["entity_index"],
120
+ },
121
+ {
122
+ fields: ["createdAt"],
123
+ },
124
+ ],
125
+ }
126
+ );
127
+
128
+ CampaignCreationLogNew.associate = (models) => {
129
+ // Add associations if needed
130
+ if (models.CodefuelCampaign) {
131
+ CampaignCreationLogNew.belongsTo(models.CodefuelCampaign, {
132
+ foreignKey: "feed_campaign_id",
133
+ targetKey: "AGRSCID",
134
+ as: "FeedCampaign",
135
+ constraints: false,
136
+ });
137
+ }
138
+ };
139
+
140
+ return CampaignCreationLogNew;
141
+ };
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "agrs-sequelize-sdk",
3
- "version": "1.2.11",
3
+ "version": "1.2.12",
4
4
  "main": "index.js",
5
5
  "scripts": {
6
6
  "start": "node index.js",
package/routes/api.js ADDED
@@ -0,0 +1,18 @@
1
+ // Add these new routes to the API router
2
+
3
+ // Import the controller
4
+ const campaignCreationLogsController = require("../controllers/campaignCreationLogsController");
5
+
6
+ // Add routes for campaign creation logs
7
+ router.get(
8
+ "/campaign-creation-logs",
9
+ campaignCreationLogsController.getCampaignCreationLogs
10
+ );
11
+ router.get(
12
+ "/campaign-creation-processes",
13
+ campaignCreationLogsController.getCampaignCreationProcesses
14
+ );
15
+ router.get(
16
+ "/campaign-creation-processes/:process_id",
17
+ campaignCreationLogsController.getCampaignCreationProcessDetails
18
+ );
@@ -0,0 +1,497 @@
1
+ /**
2
+ * Create logs in both old and new campaign creation log models
3
+ * @param {Object} oldLogData Data for the original CampaignCreationLog model
4
+ * @param {Object} newLogData Data for the new CampaignCreationLogNew model
5
+ * @returns {Promise<Object>} Object containing both created logs
6
+ */
7
+ async function createDualLogs(oldLogData, newLogData) {
8
+ const { CampaignCreationLog, CampaignCreationLogNew } = require("../db");
9
+
10
+ try {
11
+ // Create log in original model
12
+ const oldLog = await CampaignCreationLog.create(oldLogData);
13
+
14
+ // Create log in new model with extended data
15
+ const newLog = await CampaignCreationLogNew.create({
16
+ ...newLogData,
17
+ // Ensure we have the original data as well
18
+ process_id: oldLogData.process_id,
19
+ country: oldLogData.country,
20
+ feed_campaign_id: oldLogData.feed_campaign_id,
21
+ facebook_campaign_id: oldLogData.facebook_campaign_id,
22
+ error_message: oldLogData.error_message,
23
+ });
24
+
25
+ return { oldLog, newLog };
26
+ } catch (error) {
27
+ console.error("Error creating logs:", error);
28
+ // Always try to create the original log even if the new one fails
29
+ try {
30
+ const oldLog = await CampaignCreationLog.create(oldLogData);
31
+ return { oldLog, newLog: null };
32
+ } catch (oldLogError) {
33
+ console.error("Failed to create old log as well:", oldLogError);
34
+ throw error; // Re-throw original error
35
+ }
36
+ }
37
+ }
38
+
39
+ /**
40
+ * Log campaign creation stage - with support for hierarchy
41
+ * @param {Object} data Log data
42
+ * @returns {Promise<Object>} Created logs
43
+ */
44
+ async function logCampaignCreationStage(data) {
45
+ const {
46
+ process_id,
47
+ stage,
48
+ status,
49
+ country,
50
+ facebook_campaign_id,
51
+ feed_campaign_id,
52
+ facebook_adset_id,
53
+ facebook_ad_id,
54
+ parent_id,
55
+ entity_name,
56
+ entity_index,
57
+ details,
58
+ error_message,
59
+ } = data;
60
+
61
+ // Format data for old log model
62
+ const oldLogData = {
63
+ process_id,
64
+ status,
65
+ country,
66
+ feed_campaign_id,
67
+ facebook_campaign_id,
68
+ details: {
69
+ step: stage,
70
+ ...(facebook_adset_id && { adsetId: facebook_adset_id }),
71
+ ...(facebook_ad_id && { adId: facebook_ad_id }),
72
+ ...(entity_name && { name: entity_name }),
73
+ ...(details && { ...details }),
74
+ },
75
+ error_message,
76
+ };
77
+
78
+ // Format data for new log model
79
+ const newLogData = {
80
+ process_id,
81
+ stage,
82
+ status,
83
+ country,
84
+ feed_campaign_id,
85
+ facebook_campaign_id,
86
+ facebook_adset_id,
87
+ facebook_ad_id,
88
+ parent_id,
89
+ entity_name,
90
+ entity_index,
91
+ details,
92
+ error_message,
93
+ };
94
+
95
+ return createDualLogs(oldLogData, newLogData);
96
+ }
97
+
98
+ /**
99
+ * Log campaign creation with multiple adsets and ads
100
+ * @param {Object} data Campaign data
101
+ * @returns {Promise<Array>} Created logs
102
+ */
103
+ async function logCampaignWithChildEntities(data) {
104
+ const {
105
+ process_id,
106
+ country,
107
+ campaign_id,
108
+ campaign_name,
109
+ adsets = [],
110
+ campaign_details,
111
+ status = "COMPLETED",
112
+ error_message,
113
+ } = data;
114
+
115
+ const logs = [];
116
+
117
+ // Log the campaign creation
118
+ const campaignLog = await logCampaignCreationStage({
119
+ process_id,
120
+ stage: "CAMPAIGN_CREATION",
121
+ status,
122
+ country,
123
+ facebook_campaign_id: campaign_id,
124
+ entity_name: campaign_name,
125
+ details: {
126
+ campaign_data: campaign_details || { campaign_name },
127
+ planned_adsets: adsets.map((adset) => ({
128
+ adset_name: adset.name,
129
+ targeting: adset.targeting,
130
+ })),
131
+ },
132
+ error_message,
133
+ });
134
+
135
+ logs.push(campaignLog);
136
+
137
+ // If campaign was successful and we have adsets, log each adset
138
+ if (status === "COMPLETED" && adsets.length > 0) {
139
+ for (const [index, adset] of adsets.entries()) {
140
+ // Log adset creation
141
+ const adsetLog = await logCampaignCreationStage({
142
+ process_id,
143
+ stage: "ADSET_CREATION",
144
+ status: adset.status || "COMPLETED",
145
+ country,
146
+ facebook_campaign_id: campaign_id,
147
+ facebook_adset_id: adset.id,
148
+ parent_id: campaign_id,
149
+ entity_name: adset.name,
150
+ entity_index: index,
151
+ details: {
152
+ adset_data: {
153
+ adset_name: adset.name,
154
+ targeting: adset.targeting,
155
+ budget: adset.budget,
156
+ },
157
+ planned_ads: adset.ads?.map((ad) => ({
158
+ ad_name: ad.name,
159
+ creative: ad.creative,
160
+ })),
161
+ },
162
+ error_message: adset.error_message,
163
+ });
164
+
165
+ logs.push(adsetLog);
166
+
167
+ // If adset was successful and we have ads, log each ad
168
+ if (adset.status !== "FAILED" && adset.ads && adset.ads.length > 0) {
169
+ for (const [adIndex, ad] of adset.ads.entries()) {
170
+ // Log ad creation
171
+ const adLog = await logCampaignCreationStage({
172
+ process_id,
173
+ stage: "AD_CREATION",
174
+ status: ad.status || "COMPLETED",
175
+ country,
176
+ facebook_campaign_id: campaign_id,
177
+ facebook_adset_id: adset.id,
178
+ facebook_ad_id: ad.id,
179
+ parent_id: adset.id,
180
+ entity_name: ad.name,
181
+ entity_index: adIndex,
182
+ details: {
183
+ ad_data: {
184
+ ad_name: ad.name,
185
+ creative: ad.creative,
186
+ },
187
+ },
188
+ error_message: ad.error_message,
189
+ });
190
+
191
+ logs.push(adLog);
192
+ }
193
+ }
194
+ }
195
+ }
196
+
197
+ return logs;
198
+ }
199
+
200
+ /**
201
+ * Determine error stage based on error message or context
202
+ * @param {Error} error Error object
203
+ * @returns {String} Stage name where the error occurred
204
+ */
205
+ function determineErrorStage(error) {
206
+ if (!error) return "PROCESS_STARTED";
207
+
208
+ // Check if stage is explicitly set
209
+ if (error.stage) return error.stage;
210
+
211
+ // Try to infer from error message
212
+ const message = error.message ? error.message.toLowerCase() : "";
213
+
214
+ if (message.includes("campaign")) return "CAMPAIGN_CREATION";
215
+ if (message.includes("adset")) return "ADSET_CREATION";
216
+ if (message.includes("ad ") || message.includes("creative"))
217
+ return "AD_CREATION";
218
+
219
+ return "PROCESS_STARTED"; // Default
220
+ }
221
+
222
+ /**
223
+ * Get summary statistics for a campaign creation process
224
+ * @param {String} processId UUID of the process
225
+ * @returns {Promise<Object>} Object with stats about the process
226
+ */
227
+ async function getProcessStats(processId) {
228
+ const { CampaignCreationLogNew, sequelize } = require("../db");
229
+ const { Op } = require("sequelize");
230
+
231
+ // Count successful countries (completed campaign stage with adsets and ads)
232
+ const successfulCampaigns = await CampaignCreationLogNew.count({
233
+ where: {
234
+ process_id: processId,
235
+ stage: "CAMPAIGN_CREATION",
236
+ status: "COMPLETED",
237
+ },
238
+ });
239
+
240
+ // Count successful adsets
241
+ const successfulAdsets = await CampaignCreationLogNew.count({
242
+ where: {
243
+ process_id: processId,
244
+ stage: "ADSET_CREATION",
245
+ status: "COMPLETED",
246
+ },
247
+ });
248
+
249
+ // Count successful ads
250
+ const successfulAds = await CampaignCreationLogNew.count({
251
+ where: {
252
+ process_id: processId,
253
+ stage: "AD_CREATION",
254
+ status: "COMPLETED",
255
+ },
256
+ });
257
+
258
+ // Count failed entities at each level
259
+ const failedCampaigns = await CampaignCreationLogNew.count({
260
+ where: {
261
+ process_id: processId,
262
+ stage: "CAMPAIGN_CREATION",
263
+ status: "FAILED",
264
+ },
265
+ });
266
+
267
+ const failedAdsets = await CampaignCreationLogNew.count({
268
+ where: {
269
+ process_id: processId,
270
+ stage: "ADSET_CREATION",
271
+ status: "FAILED",
272
+ },
273
+ });
274
+
275
+ const failedAds = await CampaignCreationLogNew.count({
276
+ where: {
277
+ process_id: processId,
278
+ stage: "AD_CREATION",
279
+ status: "FAILED",
280
+ },
281
+ });
282
+
283
+ // Count countries
284
+ const successfulCountries = await CampaignCreationLogNew.count({
285
+ where: {
286
+ process_id: processId,
287
+ stage: "CAMPAIGN_CREATION",
288
+ status: "COMPLETED",
289
+ },
290
+ distinct: true,
291
+ col: "country",
292
+ });
293
+
294
+ const failedCountries = await CampaignCreationLogNew.count({
295
+ where: {
296
+ process_id: processId,
297
+ stage: "CAMPAIGN_CREATION",
298
+ status: "FAILED",
299
+ },
300
+ distinct: true,
301
+ col: "country",
302
+ });
303
+
304
+ // Get all countries in the process
305
+ const allCountries = await CampaignCreationLogNew.count({
306
+ where: {
307
+ process_id: processId,
308
+ country: { [Op.ne]: null },
309
+ },
310
+ distinct: true,
311
+ col: "country",
312
+ });
313
+
314
+ return {
315
+ total_countries: allCountries,
316
+ successful_countries: successfulCountries,
317
+ failed_countries: failedCountries,
318
+ in_progress_countries: allCountries - successfulCountries - failedCountries,
319
+ entity_stats: {
320
+ campaigns: {
321
+ successful: successfulCampaigns,
322
+ failed: failedCampaigns,
323
+ },
324
+ adsets: {
325
+ successful: successfulAdsets,
326
+ failed: failedAdsets,
327
+ },
328
+ ads: {
329
+ successful: successfulAds,
330
+ failed: failedAds,
331
+ },
332
+ },
333
+ };
334
+ }
335
+
336
+ /**
337
+ * Get hierarchical view of a campaign creation process
338
+ * @param {String} processId Process ID
339
+ * @returns {Promise<Object>} Hierarchical view of the process
340
+ */
341
+ async function getProcessHierarchy(processId) {
342
+ const { CampaignCreationLogNew } = require("../db");
343
+
344
+ // Get initial process log
345
+ const processLog = await CampaignCreationLogNew.findOne({
346
+ where: {
347
+ process_id: processId,
348
+ stage: "PROCESS_STARTED",
349
+ },
350
+ order: [["createdAt", "ASC"]],
351
+ });
352
+
353
+ if (!processLog) {
354
+ return null;
355
+ }
356
+
357
+ // Get all campaign logs
358
+ const campaignLogs = await CampaignCreationLogNew.findAll({
359
+ where: {
360
+ process_id: processId,
361
+ stage: "CAMPAIGN_CREATION",
362
+ },
363
+ order: [
364
+ ["country", "ASC"],
365
+ ["createdAt", "DESC"],
366
+ ],
367
+ });
368
+
369
+ // Group campaigns by country
370
+ const campaignsByCountry = {};
371
+
372
+ for (const log of campaignLogs) {
373
+ const country = log.country || "unknown";
374
+
375
+ if (!campaignsByCountry[country]) {
376
+ campaignsByCountry[country] = {
377
+ country,
378
+ status: log.status,
379
+ campaign_id: log.facebook_campaign_id,
380
+ campaign_name: log.entity_name,
381
+ details: log.details,
382
+ error: log.error_message,
383
+ adsets: [],
384
+ createdAt: log.createdAt,
385
+ };
386
+ }
387
+ }
388
+
389
+ // For each campaign, get its adsets
390
+ for (const country in campaignsByCountry) {
391
+ const campaign = campaignsByCountry[country];
392
+
393
+ if (campaign.campaign_id) {
394
+ // Get adsets for this campaign
395
+ const adsetLogs = await CampaignCreationLogNew.findAll({
396
+ where: {
397
+ process_id: processId,
398
+ stage: "ADSET_CREATION",
399
+ parent_id: campaign.campaign_id,
400
+ },
401
+ order: [
402
+ ["entity_index", "ASC"],
403
+ ["createdAt", "DESC"],
404
+ ],
405
+ });
406
+
407
+ // Process adsets
408
+ for (const adsetLog of adsetLogs) {
409
+ const adset = {
410
+ adset_id: adsetLog.facebook_adset_id,
411
+ adset_name: adsetLog.entity_name,
412
+ status: adsetLog.status,
413
+ details: adsetLog.details,
414
+ error: adsetLog.error_message,
415
+ ads: [],
416
+ createdAt: adsetLog.createdAt,
417
+ };
418
+
419
+ // Get ads for this adset
420
+ if (adsetLog.facebook_adset_id) {
421
+ const adLogs = await CampaignCreationLogNew.findAll({
422
+ where: {
423
+ process_id: processId,
424
+ stage: "AD_CREATION",
425
+ parent_id: adsetLog.facebook_adset_id,
426
+ },
427
+ order: [
428
+ ["entity_index", "ASC"],
429
+ ["createdAt", "DESC"],
430
+ ],
431
+ });
432
+
433
+ // Process ads
434
+ for (const adLog of adLogs) {
435
+ adset.ads.push({
436
+ ad_id: adLog.facebook_ad_id,
437
+ ad_name: adLog.entity_name,
438
+ status: adLog.status,
439
+ details: adLog.details,
440
+ error: adLog.error_message,
441
+ createdAt: adLog.createdAt,
442
+ });
443
+ }
444
+ }
445
+
446
+ campaign.adsets.push(adset);
447
+ }
448
+ }
449
+ }
450
+
451
+ // Return hierarchical view
452
+ return {
453
+ process_id: processId,
454
+ status: processLog.status,
455
+ original_request: processLog.details?.original_request,
456
+ created_at: processLog.createdAt,
457
+ campaigns_by_country: campaignsByCountry,
458
+ statistics: await getProcessStats(processId),
459
+ };
460
+ }
461
+
462
+ /**
463
+ * Determine the final status of a process based on its logs
464
+ * @param {String} processId UUID of the process
465
+ * @returns {Promise<String>} Final status (COMPLETED, FAILED, etc.)
466
+ */
467
+ async function determineFinalStatus(processId) {
468
+ const stats = await getProcessStats(processId);
469
+
470
+ // If all countries failed
471
+ if (stats.failed_countries > 0 && stats.successful_countries === 0) {
472
+ return "FAILED";
473
+ }
474
+
475
+ // If some countries succeeded and some failed
476
+ if (stats.failed_countries > 0 && stats.successful_countries > 0) {
477
+ return "COMPLETED"; // Could also be "PARTIALLY_COMPLETED" if you want to add this status
478
+ }
479
+
480
+ // If all countries succeeded
481
+ if (stats.successful_countries > 0 && stats.failed_countries === 0) {
482
+ return "COMPLETED";
483
+ }
484
+
485
+ // If no countries completed or failed yet
486
+ return "IN_PROGRESS";
487
+ }
488
+
489
+ module.exports = {
490
+ createDualLogs,
491
+ logCampaignCreationStage,
492
+ logCampaignWithChildEntities,
493
+ determineErrorStage,
494
+ getProcessStats,
495
+ getProcessHierarchy,
496
+ determineFinalStatus,
497
+ };