// Create or update a scheduled task definition // Input: BusinessID (required), ScheduledTaskID (optional - for update), // Name, Title, Details, CategoryID, TypeID, CronExpression, IsActive // Output: { OK: true, SCHEDULED_TASK_ID: int } function apiAbort(required struct payload) { writeOutput(serializeJSON(payload)); abort; } function readJsonBody() { var raw = getHttpRequestData().content; if (isNull(raw)) raw = ""; if (!len(trim(raw))) return {}; try { var data = deserializeJSON(raw); if (isStruct(data)) return data; } catch (any e) {} return {}; } // Parse cron expression and calculate next run time // Supports: minute hour day month weekday // Examples: "0 9 * * *" (daily 9am), "0 9 * * 1-5" (weekdays 9am), "30 14 * * *" (daily 2:30pm) function calculateNextRun(required string cronExpression) { var parts = listToArray(cronExpression, " "); if (arrayLen(parts) != 5) { return dateAdd("d", 1, now()); // Fallback: tomorrow } var cronMinute = parts[1]; var cronHour = parts[2]; var cronDay = parts[3]; var cronMonth = parts[4]; var cronWeekday = parts[5]; // Start from next minute var checkDate = dateAdd("n", 1, now()); checkDate = createDateTime(year(checkDate), month(checkDate), day(checkDate), hour(checkDate), minute(checkDate), 0); // Check up to 400 days ahead (handles yearly schedules) var maxIterations = 400 * 24 * 60; // 400 days in minutes var iterations = 0; while (iterations < maxIterations) { var matchMinute = (cronMinute == "*" || cronMinute == minute(checkDate) || (isNumeric(cronMinute) && minute(checkDate) == int(cronMinute))); var matchHour = (cronHour == "*" || cronHour == hour(checkDate) || (isNumeric(cronHour) && hour(checkDate) == int(cronHour))); var matchDay = (cronDay == "*" || cronDay == day(checkDate) || (isNumeric(cronDay) && day(checkDate) == int(cronDay))); var matchMonth = (cronMonth == "*" || cronMonth == month(checkDate) || (isNumeric(cronMonth) && month(checkDate) == int(cronMonth))); // Weekday: 0=Sunday, 1=Monday, ... 6=Saturday (Lucee dayOfWeek: 1=Sunday) var dow = dayOfWeek(checkDate) - 1; // Convert to 0-based var matchWeekday = (cronWeekday == "*"); if (!matchWeekday) { // Handle ranges like 1-5 (Mon-Fri) if (find("-", cronWeekday)) { var range = listToArray(cronWeekday, "-"); if (arrayLen(range) == 2 && isNumeric(range[1]) && isNumeric(range[2])) { matchWeekday = (dow >= int(range[1]) && dow <= int(range[2])); } } else if (isNumeric(cronWeekday)) { matchWeekday = (dow == int(cronWeekday)); } } if (matchMinute && matchHour && matchDay && matchMonth && matchWeekday) { return checkDate; } // Move to next minute checkDate = dateAdd("n", 1, checkDate); iterations++; } // Fallback if no match found return dateAdd("d", 1, now()); } try { data = readJsonBody(); // Get BusinessID businessID = 0; httpHeaders = getHttpRequestData().headers; if (structKeyExists(data, "BusinessID") && isNumeric(data.BusinessID)) { businessID = int(data.BusinessID); } else if (structKeyExists(httpHeaders, "X-Business-ID") && isNumeric(httpHeaders["X-Business-ID"])) { businessID = int(httpHeaders["X-Business-ID"]); } if (businessID == 0) { apiAbort({ "OK": false, "ERROR": "missing_params", "MESSAGE": "BusinessID is required" }); } // Get fields taskID = structKeyExists(data, "ScheduledTaskID") && isNumeric(data.ScheduledTaskID) ? int(data.ScheduledTaskID) : 0; taskName = structKeyExists(data, "Name") ? trim(toString(data.Name)) : ""; taskTitle = structKeyExists(data, "Title") ? trim(toString(data.Title)) : ""; taskDetails = structKeyExists(data, "Details") ? trim(toString(data.Details)) : ""; categoryID = structKeyExists(data, "CategoryID") && isNumeric(data.CategoryID) && data.CategoryID > 0 ? int(data.CategoryID) : javaCast("null", ""); typeID = structKeyExists(data, "TypeID") && isNumeric(data.TypeID) && data.TypeID > 0 ? int(data.TypeID) : javaCast("null", ""); cronExpression = structKeyExists(data, "CronExpression") ? trim(toString(data.CronExpression)) : ""; isActive = structKeyExists(data, "IsActive") ? (data.IsActive ? 1 : 0) : 1; // Validate required fields if (!len(taskName)) { apiAbort({ "OK": false, "ERROR": "missing_params", "MESSAGE": "Name is required" }); } if (!len(taskTitle)) { apiAbort({ "OK": false, "ERROR": "missing_params", "MESSAGE": "Title is required" }); } if (!len(cronExpression)) { apiAbort({ "OK": false, "ERROR": "missing_params", "MESSAGE": "CronExpression is required" }); } // Validate cron format (5 parts) cronParts = listToArray(cronExpression, " "); if (arrayLen(cronParts) != 5) { apiAbort({ "OK": false, "ERROR": "invalid_cron", "MESSAGE": "Cron expression must have 5 parts: minute hour day month weekday" }); } // Calculate next run time nextRunOn = calculateNextRun(cronExpression); if (taskID > 0) { // UPDATE existing qCheck = queryExecute(" SELECT ScheduledTaskID FROM ScheduledTaskDefinitions WHERE ScheduledTaskID = :id AND ScheduledTaskBusinessID = :businessID ", { id: { value: taskID, cfsqltype: "cf_sql_integer" }, businessID: { value: businessID, cfsqltype: "cf_sql_integer" } }, { datasource: "payfrit" }); if (qCheck.recordCount == 0) { apiAbort({ "OK": false, "ERROR": "not_found", "MESSAGE": "Scheduled task not found" }); } queryExecute(" UPDATE ScheduledTaskDefinitions SET ScheduledTaskName = :name, ScheduledTaskTitle = :title, ScheduledTaskDetails = :details, ScheduledTaskCategoryID = :categoryID, ScheduledTaskTypeID = :typeID, ScheduledTaskCronExpression = :cron, ScheduledTaskIsActive = :isActive, ScheduledTaskNextRunOn = :nextRun WHERE ScheduledTaskID = :id ", { name: { value: taskName, cfsqltype: "cf_sql_varchar" }, title: { value: taskTitle, cfsqltype: "cf_sql_varchar" }, details: { value: taskDetails, cfsqltype: "cf_sql_longvarchar", null: !len(taskDetails) }, categoryID: { value: categoryID, cfsqltype: "cf_sql_integer", null: isNull(categoryID) }, typeID: { value: typeID, cfsqltype: "cf_sql_integer", null: isNull(typeID) }, cron: { value: cronExpression, cfsqltype: "cf_sql_varchar" }, isActive: { value: isActive, cfsqltype: "cf_sql_bit" }, nextRun: { value: nextRunOn, cfsqltype: "cf_sql_timestamp" }, id: { value: taskID, cfsqltype: "cf_sql_integer" } }, { datasource: "payfrit" }); apiAbort({ "OK": true, "SCHEDULED_TASK_ID": taskID, "NEXT_RUN": dateTimeFormat(nextRunOn, "yyyy-mm-dd HH:nn:ss"), "MESSAGE": "Scheduled task updated" }); } else { // INSERT new queryExecute(" INSERT INTO ScheduledTaskDefinitions ( ScheduledTaskBusinessID, ScheduledTaskName, ScheduledTaskTitle, ScheduledTaskDetails, ScheduledTaskCategoryID, ScheduledTaskTypeID, ScheduledTaskCronExpression, ScheduledTaskIsActive, ScheduledTaskNextRunOn ) VALUES ( :businessID, :name, :title, :details, :categoryID, :typeID, :cron, :isActive, :nextRun ) ", { businessID: { value: businessID, cfsqltype: "cf_sql_integer" }, name: { value: taskName, cfsqltype: "cf_sql_varchar" }, title: { value: taskTitle, cfsqltype: "cf_sql_varchar" }, details: { value: taskDetails, cfsqltype: "cf_sql_longvarchar", null: !len(taskDetails) }, categoryID: { value: categoryID, cfsqltype: "cf_sql_integer", null: isNull(categoryID) }, typeID: { value: typeID, cfsqltype: "cf_sql_integer", null: isNull(typeID) }, cron: { value: cronExpression, cfsqltype: "cf_sql_varchar" }, isActive: { value: isActive, cfsqltype: "cf_sql_bit" }, nextRun: { value: nextRunOn, cfsqltype: "cf_sql_timestamp" } }, { datasource: "payfrit" }); qNew = queryExecute("SELECT LAST_INSERT_ID() as newID", [], { datasource: "payfrit" }); apiAbort({ "OK": true, "SCHEDULED_TASK_ID": qNew.newID, "NEXT_RUN": dateTimeFormat(nextRunOn, "yyyy-mm-dd HH:nn:ss"), "MESSAGE": "Scheduled task created" }); } } catch (any e) { apiAbort({ "OK": false, "ERROR": "server_error", "MESSAGE": e.message }); }