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/tasks/complete.cfm
John 16a3b7c9a3 Replace queryExecute with queryTimed across all endpoints for perf tracking
Converts 200+ endpoint files to use queryTimed() wrapper which tracks
DB query count and execution time. Restores perf dashboard files that
were accidentally moved to _scripts/. Includes portal UI updates.
2026-02-02 00:28:37 -08:00

246 lines
9.7 KiB
Text

<cfsetting showdebugoutput="false">
<cfsetting enablecfoutputonly="true">
<cffunction name="apiAbort" access="public" returntype="void" output="true">
<cfargument name="payload" type="struct" required="true">
<cfcontent type="application/json; charset=utf-8">
<cfoutput>#serializeJSON(arguments.payload)#</cfoutput>
<cfabort>
</cffunction>
<cffunction name="readJsonBody" access="public" returntype="struct" output="false">
<cfset var raw = getHttpRequestData().content>
<cfif isNull(raw) OR len(trim(raw)) EQ 0>
<cfreturn {}>
</cfif>
<cftry>
<cfset var data = deserializeJSON(raw)>
<cfif isStruct(data)>
<cfreturn data>
<cfelse>
<cfreturn {}>
</cfif>
<cfcatch>
<cfreturn {}>
</cfcatch>
</cftry>
</cffunction>
<cffunction name="generateToken" access="public" returntype="string" output="false">
<cfreturn lcase(replace(createUUID(), "-", "", "all"))>
</cffunction>
<cfset data = readJsonBody()>
<cfset TaskID = val( structKeyExists(data,"TaskID") ? data.TaskID : 0 )>
<!--- Get UserID from request (auth header) or from JSON body as fallback --->
<cfset UserID = val( structKeyExists(request,"UserID") ? request.UserID : (structKeyExists(data,"UserID") ? data.UserID : 0) )>
<!--- Optional: Worker rating of customer (when required or voluntary) --->
<cfset workerRating = structKeyExists(data,"workerRating") ? data.workerRating : {}>
<cfif TaskID LTE 0>
<cfset apiAbort({ "OK": false, "ERROR": "missing_params", "MESSAGE": "TaskID is required." })>
</cfif>
<cftry>
<!--- Verify task exists --->
<cfset qTask = queryTimed("
SELECT t.ID, t.ClaimedByUserID, t.CompletedOn, t.OrderID, t.TaskTypeID, t.BusinessID,
o.UserID, o.ServicePointID
FROM Tasks t
LEFT JOIN Orders o ON o.ID = t.OrderID
WHERE t.ID = ?
", [ { value = TaskID, cfsqltype = "cf_sql_integer" } ], { datasource = "payfrit" })>
<cfif qTask.recordCount EQ 0>
<cfset apiAbort({ "OK": false, "ERROR": "not_found", "MESSAGE": "Task not found." })>
</cfif>
<!--- Chat tasks (TaskTypeID = 2) can be closed without being claimed first --->
<cfset isChatTask = (qTask.TaskTypeID EQ 2)>
<cfif (NOT isChatTask) AND (qTask.ClaimedByUserID EQ 0)>
<cfset apiAbort({ "OK": false, "ERROR": "not_claimed", "MESSAGE": "Task has not been claimed yet." })>
</cfif>
<cfif (NOT isChatTask) AND (UserID GT 0) AND (qTask.ClaimedByUserID NEQ UserID)>
<cfset apiAbort({ "OK": false, "ERROR": "not_yours", "MESSAGE": "This task was claimed by someone else." })>
</cfif>
<!--- Check if already completed - use len() to handle both NULL and empty string --->
<cfif len(trim(qTask.CompletedOn)) GT 0>
<cfset apiAbort({ "OK": false, "ERROR": "already_completed", "MESSAGE": "Task has already been completed." })>
</cfif>
<!--- Check if this is a service point task (customer-facing) --->
<cfset hasServicePoint = (val(qTask.ServicePointID) GT 0)>
<cfset customerUserID = val(qTask.UserID)>
<cfset workerUserID = val(qTask.ClaimedByUserID)>
<cfset ratingRequired = false>
<cfset ratingsCreated = []>
<!--- For service point tasks, check if worker rating of customer is required (10% based on TaskID) --->
<!--- Use TaskID modulo for deterministic selection - same task always has same requirement --->
<cfif hasServicePoint AND customerUserID GT 0 AND workerUserID GT 0>
<cfset ratingRequired = (TaskID MOD 10 EQ 0)>
<!--- If rating required but not provided, return error --->
<cfif ratingRequired AND structIsEmpty(workerRating)>
<cfset apiAbort({
"OK": false,
"ERROR": "rating_required",
"MESSAGE": "Please rate the customer before completing this task.",
"CustomerUserID": customerUserID,
"Questions": {
"prepared": "Was the customer prepared?",
"completedScope": "Was the scope clear?",
"respectful": "Was the customer respectful?",
"wouldAutoAssign": "Would you serve this customer again?"
}
})>
</cfif>
</cfif>
<!--- Mark task as completed --->
<cfset queryTimed("
UPDATE Tasks
SET CompletedOn = NOW()
WHERE ID = ?
", [ { value = TaskID, cfsqltype = "cf_sql_integer" } ], { datasource = "payfrit" })>
<!--- If task has an associated order, mark it as Delivered/Complete (status 5) --->
<cfset orderUpdated = false>
<cfif qTask.OrderID GT 0>
<cfset queryTimed("
UPDATE Orders
SET StatusID = 5,
LastEditedOn = NOW()
WHERE ID = ?
", [ { value = qTask.OrderID, cfsqltype = "cf_sql_integer" } ], { datasource = "payfrit" })>
<cfset orderUpdated = true>
</cfif>
<!--- === PAYOUT LEDGER + ACTIVATION WITHHOLDING === --->
<cfset ledgerCreated = false>
<cfset workerUserID_for_payout = val(qTask.ClaimedByUserID)>
<cfif workerUserID_for_payout GT 0>
<!--- Get PayCents from the task --->
<cfset qTaskPay = queryTimed("
SELECT PayCents FROM Tasks WHERE ID = ?
", [ { value = TaskID, cfsqltype = "cf_sql_integer" } ], { datasource = "payfrit" })>
<cfset grossCents = val(qTaskPay.PayCents)>
<cfif grossCents GT 0>
<!--- Get worker's activation state --->
<cfset qActivation = queryTimed("
SELECT ActivationBalanceCents, ActivationCapCents
FROM Users WHERE ID = ?
", [ { value = workerUserID_for_payout, cfsqltype = "cf_sql_integer" } ], { datasource = "payfrit" })>
<cfset activationBalance = val(qActivation.ActivationBalanceCents)>
<cfset activationCap = val(qActivation.ActivationCapCents)>
<cfset remainingActivation = activationCap - activationBalance>
<!--- Withholding: min($1, remaining, gross). Never negative. --->
<cfset activationWithhold = 0>
<cfif remainingActivation GT 0>
<cfset activationWithhold = min(100, remainingActivation, grossCents)>
</cfif>
<cfset netCents = grossCents - activationWithhold>
<!--- Insert ledger row --->
<cfset queryTimed("
INSERT INTO WorkPayoutLedgers
(UserID, TaskID, GrossEarningsCents, ActivationWithheldCents, NetTransferCents, Status)
VALUES
(:userID, :taskID, :gross, :withheld, :net, 'pending_charge')
", {
userID: workerUserID_for_payout,
taskID: TaskID,
gross: grossCents,
withheld: activationWithhold,
net: netCents
}, { datasource = "payfrit" })>
<!--- Increment activation balance --->
<cfif activationWithhold GT 0>
<cfset queryTimed("
UPDATE Users
SET ActivationBalanceCents = ActivationBalanceCents + :withhold
WHERE ID = :userID
", {
withhold: activationWithhold,
userID: workerUserID_for_payout
}, { datasource = "payfrit" })>
</cfif>
<cfset ledgerCreated = true>
</cfif>
</cfif>
<!--- Create rating records for service point tasks --->
<cfif hasServicePoint AND customerUserID GT 0 AND workerUserID GT 0>
<!--- 1. Customer rates Worker (always created, submitted via receipt link) --->
<cfset customerToken = generateToken()>
<cfset queryTimed("
INSERT INTO TaskRatings (
TaskID, ByUserID, ForUserID, Direction,
AccessToken, ExpiresOn
) VALUES (
?, ?, ?, 'customer_rates_worker',
?, DATE_ADD(NOW(), INTERVAL 24 HOUR)
)
", [
{ value = TaskID, cfsqltype = "cf_sql_integer" },
{ value = customerUserID, cfsqltype = "cf_sql_integer" },
{ value = workerUserID, cfsqltype = "cf_sql_integer" },
{ value = customerToken, cfsqltype = "cf_sql_varchar" }
], { datasource = "payfrit" })>
<cfset arrayAppend(ratingsCreated, { "direction": "customer_rates_worker", "token": customerToken })>
<!--- 2. Worker rates Customer (if provided or required) --->
<cfif NOT structIsEmpty(workerRating)>
<cfset workerToken = generateToken()>
<cfset queryTimed("
INSERT INTO TaskRatings (
TaskID, ByUserID, ForUserID, Direction,
Prepared, CompletedScope, Respectful, WouldAutoAssign,
AccessToken, ExpiresOn, CompletedOn
) VALUES (
?, ?, ?, 'worker_rates_customer',
?, ?, ?, ?,
?, DATE_ADD(NOW(), INTERVAL 24 HOUR), NOW()
)
", [
{ value = TaskID, cfsqltype = "cf_sql_integer" },
{ value = workerUserID, cfsqltype = "cf_sql_integer" },
{ value = customerUserID, cfsqltype = "cf_sql_integer" },
{ value = structKeyExists(workerRating,"prepared") ? (workerRating.prepared ? 1 : 0) : javaCast("null",""), cfsqltype = "cf_sql_tinyint", null = !structKeyExists(workerRating,"prepared") },
{ value = structKeyExists(workerRating,"completedScope") ? (workerRating.completedScope ? 1 : 0) : javaCast("null",""), cfsqltype = "cf_sql_tinyint", null = !structKeyExists(workerRating,"completedScope") },
{ value = structKeyExists(workerRating,"respectful") ? (workerRating.respectful ? 1 : 0) : javaCast("null",""), cfsqltype = "cf_sql_tinyint", null = !structKeyExists(workerRating,"respectful") },
{ value = structKeyExists(workerRating,"wouldAutoAssign") ? (workerRating.wouldAutoAssign ? 1 : 0) : javaCast("null",""), cfsqltype = "cf_sql_tinyint", null = !structKeyExists(workerRating,"wouldAutoAssign") },
{ value = workerToken, cfsqltype = "cf_sql_varchar" }
], { datasource = "payfrit" })>
<cfset arrayAppend(ratingsCreated, { "direction": "worker_rates_customer", "submitted": true })>
</cfif>
</cfif>
<cfset apiAbort({
"OK": true,
"ERROR": "",
"MESSAGE": "Task completed successfully.",
"TaskID": TaskID,
"OrderUpdated": orderUpdated,
"RatingsCreated": ratingsCreated,
"LedgerCreated": ledgerCreated
})>
<cfcatch>
<cfset apiAbort({
"OK": false,
"ERROR": "server_error",
"MESSAGE": "Error completing task",
"DETAIL": cfcatch.message
})>
</cfcatch>
</cftry>