This repository has been archived on 2026-03-21. You can view files and clone it, but cannot push or open issues or pull requests.
payfrit-biz/api/admin/scheduledTasks/save.cfm
John Mizerek 1210249f54 Normalize database column and table names across entire codebase
Update all SQL queries, query result references, and ColdFusion code to match
the renamed database schema. Tables use plural CamelCase, PKs are all `ID`,
column prefixes stripped (e.g. BusinessName→Name, UserFirstName→FirstName).

Key changes:
- Strip table-name prefixes from all column references (Businesses, Users,
  Addresses, Hours, Menus, Categories, Items, Stations, Orders,
  OrderLineItems, Tasks, TaskCategories, TaskRatings, QuickTaskTemplates,
  ScheduledTaskDefinitions, ChatMessages, Beacons, ServicePoints, Employees,
  VisitorTrackings, ApiPerfLogs, tt_States, tt_Days, tt_AddressTypes,
  tt_OrderTypes, tt_TaskTypes)
- Rename PK references from {TableName}ID to ID in all queries
- Rewrite 7 admin beacon files to use ServicePoints.BeaconID instead of
  dropped lt_Beacon_Businesses_ServicePoints link table
- Rewrite beacon assignment files (list, save, delete) for new schema
- Fix FK references incorrectly changed to ID (OrderLineItems.OrderID,
  Categories.MenuID, Tasks.CategoryID, ServicePoints.BeaconID)
- Update Addresses: AddressLat→Latitude, AddressLng→Longitude
- Update Users: UserPassword→Password, UserIsEmailVerified→IsEmailVerified,
  UserIsActive→IsActive, UserBalance→Balance, etc.

Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
2026-01-30 15:39:12 -08:00

289 lines
12 KiB
Text

<cfsetting showdebugoutput="false">
<cfsetting enablecfoutputonly="true">
<cfcontent type="application/json; charset=utf-8" reset="true">
<cfscript>
// Create or update a scheduled task definition
// Input: BusinessID (required), ScheduledTaskID (optional - for update),
// Name, Title, Details, CategoryID, TypeID, CronExpression, IsActive,
// ScheduleType ('cron' or 'interval'), IntervalMinutes (for interval type)
// 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", "");
cronExpression = structKeyExists(data, "CronExpression") ? trim(toString(data.CronExpression)) : "";
isActive = structKeyExists(data, "IsActive") ? (data.IsActive ? 1 : 0) : 1;
// New interval scheduling fields
scheduleType = structKeyExists(data, "ScheduleType") ? trim(toString(data.ScheduleType)) : "cron";
intervalMinutes = structKeyExists(data, "IntervalMinutes") && isNumeric(data.IntervalMinutes) ? int(data.IntervalMinutes) : javaCast("null", "");
// 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" });
}
// Validate based on schedule type
if (scheduleType == "interval" || scheduleType == "interval_after_completion") {
// Interval-based scheduling
if (isNull(intervalMinutes) || intervalMinutes < 1) {
apiAbort({ "OK": false, "ERROR": "missing_params", "MESSAGE": "IntervalMinutes is required for interval scheduling (minimum 1)" });
}
// Set a placeholder cron expression for interval type
if (!len(cronExpression)) {
cronExpression = "* * * * *";
}
// For NEW tasks: run immediately. For UPDATES: next run = NOW + interval
if (taskID == 0) {
nextRunOn = now(); // Run immediately on first creation
} else {
nextRunOn = dateAdd("n", intervalMinutes, now());
}
} else {
// Cron-based scheduling
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 from cron
nextRunOn = calculateNextRun(cronExpression);
}
if (taskID > 0) {
// UPDATE existing
qCheck = queryExecute("
SELECT ScheduledTaskID FROM ScheduledTaskDefinitions
WHERE ID = :id AND BusinessID = :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
Name = :name,
Title = :title,
Details = :details,
TaskCategoryID = :categoryID,
CronExpression = :cron,
ScheduleType = :scheduleType,
IntervalMinutes = :intervalMinutes,
IsActive = :isActive,
NextRunOn = :nextRun
WHERE ID = :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) },
cron: { value: cronExpression, cfsqltype: "cf_sql_varchar" },
scheduleType: { value: scheduleType, cfsqltype: "cf_sql_varchar" },
intervalMinutes: { value: intervalMinutes, cfsqltype: "cf_sql_integer", null: isNull(intervalMinutes) },
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 (
BusinessID, Name, Title,
Details, TaskCategoryID,
CronExpression, ScheduleType, IntervalMinutes,
IsActive, NextRunOn
) VALUES (
:businessID, :name, :title, :details, :categoryID, :cron, :scheduleType, :intervalMinutes, :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) },
cron: { value: cronExpression, cfsqltype: "cf_sql_varchar" },
scheduleType: { value: scheduleType, cfsqltype: "cf_sql_varchar" },
intervalMinutes: { value: intervalMinutes, cfsqltype: "cf_sql_integer", null: isNull(intervalMinutes) },
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" });
newScheduledTaskID = qNew.newID;
// Create the first task immediately
queryExecute("
INSERT INTO Tasks (
BusinessID, CategoryID, TaskTypeID,
Title, Details, CreatedOn, ClaimedByUserID
) VALUES (
:businessID, :categoryID, :typeID,
:title, :details, NOW(), 0
)
", {
businessID: { value: businessID, cfsqltype: "cf_sql_integer" },
categoryID: { value: categoryID, cfsqltype: "cf_sql_integer", null: isNull(categoryID) },
typeID: { value: 0, cfsqltype: "cf_sql_integer" },
title: { value: taskTitle, cfsqltype: "cf_sql_varchar" },
details: { value: taskDetails, cfsqltype: "cf_sql_longvarchar", null: !len(taskDetails) }
}, { datasource: "payfrit" });
qTask = queryExecute("SELECT LAST_INSERT_ID() as taskID", [], { datasource: "payfrit" });
// Now set the NEXT run time (not the immediate one we just created)
if (scheduleType == "interval" || scheduleType == "interval_after_completion") {
if (scheduleType == "interval_after_completion") {
// After-completion: don't schedule next until task is completed
actualNextRun = javaCast("null", "");
} else {
// Fixed interval: next run = NOW + interval
actualNextRun = dateAdd("n", intervalMinutes, now());
}
} else {
// Cron-based
actualNextRun = calculateNextRun(cronExpression);
}
queryExecute("
UPDATE ScheduledTaskDefinitions
SET LastRunOn = NOW(),
NextRunOn = :nextRun
WHERE ID = :id
", {
nextRun: { value: actualNextRun, cfsqltype: "cf_sql_timestamp", null: isNull(actualNextRun) },
id: { value: newScheduledTaskID, cfsqltype: "cf_sql_integer" }
}, { datasource: "payfrit" });
apiAbort({
"OK": true,
"SCHEDULED_TASK_ID": newScheduledTaskID,
"TASK_ID": qTask.taskID,
"NEXT_RUN": isNull(actualNextRun) ? "" : dateTimeFormat(actualNextRun, "yyyy-mm-dd HH:nn:ss"),
"MESSAGE": "Scheduled task created and first task added"
});
}
} catch (any e) {
apiAbort({
"OK": false,
"ERROR": "server_error",
"MESSAGE": e.message
});
}
</cfscript>