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 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

246 lines
9.8 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 = queryExecute("
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 queryExecute("
UPDATE Tasks
SET CompletedOn = NOW()
WHERE TaskID = ?
", [ { 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 queryExecute("
UPDATE Orders
SET StatusID = 5,
LastEditedOn = NOW()
WHERE OrderID = ?
", [ { 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 = queryExecute("
SELECT PayCents FROM Tasks WHERE TaskID = ?
", [ { value = TaskID, cfsqltype = "cf_sql_integer" } ], { datasource = "payfrit" })>
<cfset grossCents = val(qTaskPay.PayCents)>
<cfif grossCents GT 0>
<!--- Get worker's activation state --->
<cfset qActivation = queryExecute("
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 queryExecute("
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 queryExecute("
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 queryExecute("
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 queryExecute("
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>