function readJsonBody() { var raw = getHttpRequestData().content; if (isNull(raw) || len(trim(raw)) == 0) return {}; try { var data = deserializeJSON(raw); return isStruct(data) ? data : {}; } catch (any e) { return {}; } } try { data = readJsonBody(); // Token can come from URL param or JSON body token = ""; if (structKeyExists(url, "token")) token = trim(url.token); if (len(token) == 0 && structKeyExists(data, "token")) token = trim(data.token); if (len(token) == 0) { writeOutput(serializeJSON({ "OK": false, "ERROR": "missing_token", "MESSAGE": "Rating token is required." })); abort; } // Look up the rating by token qRating = queryTimed(" SELECT r.*, t.Title, u_for.FirstName AS ForFirstName, u_for.LastName AS ForLastName, u_by.FirstName AS ByFirstName, u_by.LastName AS ByLastName FROM TaskRatings r JOIN Tasks t ON t.ID = r.TaskID LEFT JOIN Users u_for ON u_for.ID = r.ForUserID LEFT JOIN Users u_by ON u_by.ID = r.ByUserID WHERE r.AccessToken = ? LIMIT 1 ", [{ value = token, cfsqltype = "cf_sql_varchar" }]); if (qRating.recordCount == 0) { writeOutput(serializeJSON({ "OK": false, "ERROR": "invalid_token", "MESSAGE": "Rating not found or link is invalid." })); abort; } // Check if expired if (qRating.ExpiresOn < now()) { writeOutput(serializeJSON({ "OK": false, "ERROR": "expired", "MESSAGE": "This rating link has expired." })); abort; } // Check if already completed if (len(trim(qRating.CompletedOn)) > 0) { writeOutput(serializeJSON({ "OK": false, "ERROR": "already_submitted", "MESSAGE": "This rating has already been submitted." })); abort; } // If GET request (or no rating data), return rating info for display isSubmission = structKeyExists(data, "onTime") || structKeyExists(data, "completedScope") || structKeyExists(data, "requiredFollowup") || structKeyExists(data, "continueAllow") || structKeyExists(data, "prepared") || structKeyExists(data, "respectful") || structKeyExists(data, "wouldAutoAssign"); if (!isSubmission) { // Return rating details for UI to display result = { "OK": true, "RatingID": qRating.ID, "Direction": qRating.Direction, "Title": qRating.Title, "ForUserName": trim(qRating.ForFirstName & " " & qRating.ForLastName), "ExpiresOn": dateTimeFormat(qRating.ExpiresOn, "yyyy-mm-dd'T'HH:nn:ss") & "Z" }; // Include appropriate questions based on direction if (qRating.Direction == "customer_rates_worker" || qRating.Direction == "admin_rates_worker") { result["Questions"] = { "onTime": "Was the worker on time?", "completedScope": "Was the scope completed?", "requiredFollowup": "Was follow-up required?", "continueAllow": "Continue to allow these tasks?" }; } else if (qRating.Direction == "worker_rates_customer") { result["Questions"] = { "prepared": "Was the customer prepared?", "completedScope": "Was the scope clear?", "respectful": "Was the customer respectful?", "wouldAutoAssign": "Would you serve this customer again?" }; } writeOutput(serializeJSON(result)); abort; } // Process submission based on direction if (qRating.Direction == "customer_rates_worker" || qRating.Direction == "admin_rates_worker") { queryTimed(" UPDATE TaskRatings SET OnTime = ?, CompletedScope = ?, RequiredFollowup = ?, ContinueAllow = ?, CompletedOn = NOW() WHERE ID = ? ", [ { value = structKeyExists(data,"onTime") ? (data.onTime ? 1 : 0) : javaCast("null",""), cfsqltype = "cf_sql_tinyint", null = !structKeyExists(data,"onTime") }, { value = structKeyExists(data,"completedScope") ? (data.completedScope ? 1 : 0) : javaCast("null",""), cfsqltype = "cf_sql_tinyint", null = !structKeyExists(data,"completedScope") }, { value = structKeyExists(data,"requiredFollowup") ? (data.requiredFollowup ? 1 : 0) : javaCast("null",""), cfsqltype = "cf_sql_tinyint", null = !structKeyExists(data,"requiredFollowup") }, { value = structKeyExists(data,"continueAllow") ? (data.continueAllow ? 1 : 0) : javaCast("null",""), cfsqltype = "cf_sql_tinyint", null = !structKeyExists(data,"continueAllow") }, { value = qRating.ID, cfsqltype = "cf_sql_integer" } ]); } else if (qRating.Direction == "worker_rates_customer") { queryTimed(" UPDATE TaskRatings SET Prepared = ?, CompletedScope = ?, Respectful = ?, WouldAutoAssign = ?, CompletedOn = NOW() WHERE ID = ? ", [ { value = structKeyExists(data,"prepared") ? (data.prepared ? 1 : 0) : javaCast("null",""), cfsqltype = "cf_sql_tinyint", null = !structKeyExists(data,"prepared") }, { value = structKeyExists(data,"completedScope") ? (data.completedScope ? 1 : 0) : javaCast("null",""), cfsqltype = "cf_sql_tinyint", null = !structKeyExists(data,"completedScope") }, { value = structKeyExists(data,"respectful") ? (data.respectful ? 1 : 0) : javaCast("null",""), cfsqltype = "cf_sql_tinyint", null = !structKeyExists(data,"respectful") }, { value = structKeyExists(data,"wouldAutoAssign") ? (data.wouldAutoAssign ? 1 : 0) : javaCast("null",""), cfsqltype = "cf_sql_tinyint", null = !structKeyExists(data,"wouldAutoAssign") }, { value = qRating.ID, cfsqltype = "cf_sql_integer" } ]); } writeOutput(serializeJSON({ "OK": true, "MESSAGE": "Rating submitted successfully. Thank you for your feedback!" })); } catch (any e) { writeOutput(serializeJSON({ "OK": false, "ERROR": "server_error", "MESSAGE": "Error submitting rating", "DETAIL": e.message })); }