Add role-aware cash routing and backend improvements
Staff cash goes to worker payout ledger, admin/manager cash deletes pending payout and reverses withholding. Add RoleID to myBusinesses response. Various order and webhook improvements. Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
This commit is contained in:
parent
9c09e18833
commit
b9755a1e72
10 changed files with 197 additions and 79 deletions
|
|
@ -136,8 +136,8 @@ try {
|
|||
// Send via SMS
|
||||
try {
|
||||
if (structKeyExists(application, "twilioObj")) {
|
||||
smsMessage = "Your Payfrit code is: " & code;
|
||||
application.twilioObj.sendSMS(to=qUser.ContactNumber, message=smsMessage);
|
||||
smsMessage = "Your Payfrit login code is: " & code & ". It expires in 10 minutes.";
|
||||
application.twilioObj.sendSMS(recipientNumber="+1" & qUser.ContactNumber, messageBody=smsMessage);
|
||||
}
|
||||
} catch (any smsErr) {
|
||||
writeLog(file="otp_errors", text="SMS send failed for user #userId#: #smsErr.message#");
|
||||
|
|
|
|||
|
|
@ -37,24 +37,26 @@
|
|||
<cfset qOrder = queryTimed(
|
||||
"
|
||||
SELECT
|
||||
ID,
|
||||
UUID,
|
||||
UserID,
|
||||
BusinessID,
|
||||
DeliveryMultiplier,
|
||||
OrderTypeID,
|
||||
DeliveryFee,
|
||||
StatusID,
|
||||
AddressID,
|
||||
PaymentID,
|
||||
PaymentStatus,
|
||||
Remarks,
|
||||
AddedOn,
|
||||
LastEditedOn,
|
||||
SubmittedOn,
|
||||
ServicePointID
|
||||
FROM Orders
|
||||
WHERE ID = ?
|
||||
o.ID,
|
||||
o.UUID,
|
||||
o.UserID,
|
||||
o.BusinessID,
|
||||
o.DeliveryMultiplier,
|
||||
o.OrderTypeID,
|
||||
o.DeliveryFee,
|
||||
o.StatusID,
|
||||
o.AddressID,
|
||||
o.PaymentID,
|
||||
o.PaymentStatus,
|
||||
o.Remarks,
|
||||
o.AddedOn,
|
||||
o.LastEditedOn,
|
||||
o.SubmittedOn,
|
||||
o.ServicePointID,
|
||||
sp.Name AS ServicePointName
|
||||
FROM Orders o
|
||||
LEFT JOIN ServicePoints sp ON sp.ID = o.ServicePointID
|
||||
WHERE o.ID = ?
|
||||
LIMIT 1
|
||||
",
|
||||
[ { value = OrderID, cfsqltype = "cf_sql_integer" } ],
|
||||
|
|
@ -161,7 +163,8 @@
|
|||
"AddedOn": qOrder.AddedOn,
|
||||
"LastEditedOn": qOrder.LastEditedOn,
|
||||
"SubmittedOn": qOrder.SubmittedOn,
|
||||
"ServicePointID": val(qOrder.ServicePointID)
|
||||
"ServicePointID": val(qOrder.ServicePointID),
|
||||
"ServicePointName": qOrder.ServicePointName ?: ""
|
||||
},
|
||||
"ORDERLINEITEMS": rows
|
||||
})>
|
||||
|
|
|
|||
|
|
@ -33,28 +33,30 @@
|
|||
<cfset var qOrder = queryTimed(
|
||||
"
|
||||
SELECT
|
||||
ID,
|
||||
UUID,
|
||||
UserID,
|
||||
BusinessID,
|
||||
DeliveryMultiplier,
|
||||
OrderTypeID,
|
||||
DeliveryFee,
|
||||
StatusID,
|
||||
AddressID,
|
||||
PaymentID,
|
||||
Remarks,
|
||||
AddedOn,
|
||||
LastEditedOn,
|
||||
SubmittedOn,
|
||||
ServicePointID,
|
||||
GrantID,
|
||||
GrantOwnerBusinessID,
|
||||
GrantEconomicsType,
|
||||
GrantEconomicsValue,
|
||||
TabID
|
||||
FROM Orders
|
||||
WHERE ID = ?
|
||||
o.ID,
|
||||
o.UUID,
|
||||
o.UserID,
|
||||
o.BusinessID,
|
||||
o.DeliveryMultiplier,
|
||||
o.OrderTypeID,
|
||||
o.DeliveryFee,
|
||||
o.StatusID,
|
||||
o.AddressID,
|
||||
o.PaymentID,
|
||||
o.Remarks,
|
||||
o.AddedOn,
|
||||
o.LastEditedOn,
|
||||
o.SubmittedOn,
|
||||
o.ServicePointID,
|
||||
sp.Name AS ServicePointName,
|
||||
o.GrantID,
|
||||
o.GrantOwnerBusinessID,
|
||||
o.GrantEconomicsType,
|
||||
o.GrantEconomicsValue,
|
||||
o.TabID
|
||||
FROM Orders o
|
||||
LEFT JOIN ServicePoints sp ON sp.ID = o.ServicePointID
|
||||
WHERE o.ID = ?
|
||||
LIMIT 1
|
||||
",
|
||||
[ { value = arguments.OrderID, cfsqltype = "cf_sql_integer" } ],
|
||||
|
|
@ -94,6 +96,7 @@
|
|||
"LastEditedOn": qOrder.LastEditedOn,
|
||||
"SubmittedOn": qOrder.SubmittedOn,
|
||||
"ServicePointID": val(qOrder.ServicePointID),
|
||||
"ServicePointName": qOrder.ServicePointName ?: "",
|
||||
"GrantID": val(qOrder.GrantID),
|
||||
"GrantOwnerBusinessID": val(qOrder.GrantOwnerBusinessID),
|
||||
"GrantEconomicsType": qOrder.GrantEconomicsType ?: "",
|
||||
|
|
@ -172,6 +175,16 @@
|
|||
})>
|
||||
</cfif>
|
||||
|
||||
<!--- Require ServicePointID for now (delivery/takeaway not yet supported) --->
|
||||
<cfif ServicePointID LTE 0>
|
||||
<cfset apiAbort({
|
||||
"OK": false,
|
||||
"ERROR": "missing_service_point",
|
||||
"MESSAGE": "ServicePointID is required. Please scan a table beacon.",
|
||||
"DETAIL": ""
|
||||
})>
|
||||
</cfif>
|
||||
|
||||
<!--- OrderTypeID can be 0 (undecided) for delivery/takeaway flow, or 1 for dine-in --->
|
||||
<cfif OrderTypeID LT 0 OR OrderTypeID GT 3>
|
||||
<cfset apiAbort({
|
||||
|
|
|
|||
|
|
@ -191,9 +191,11 @@
|
|||
o.LastEditedOn,
|
||||
o.SubmittedOn,
|
||||
o.ServicePointID,
|
||||
sp.Name AS ServicePointName,
|
||||
COALESCE(b.DeliveryFlatFee, 0) AS BusinessDeliveryFee
|
||||
FROM Orders o
|
||||
LEFT JOIN Businesses b ON b.ID = o.BusinessID
|
||||
LEFT JOIN ServicePoints sp ON sp.ID = o.ServicePointID
|
||||
WHERE o.ID = ?
|
||||
LIMIT 1
|
||||
",
|
||||
|
|
@ -221,6 +223,7 @@
|
|||
"LastEditedOn": qOrder.LastEditedOn,
|
||||
"SubmittedOn": qOrder.SubmittedOn,
|
||||
"ServicePointID": val(qOrder.ServicePointID),
|
||||
"ServicePointName": qOrder.ServicePointName ?: "",
|
||||
"BusinessDeliveryFee": val(qOrder.BusinessDeliveryFee)
|
||||
}>
|
||||
|
||||
|
|
|
|||
|
|
@ -33,23 +33,25 @@
|
|||
<cfset var qOrder = queryTimed(
|
||||
"
|
||||
SELECT
|
||||
ID,
|
||||
UUID,
|
||||
UserID,
|
||||
BusinessID,
|
||||
DeliveryMultiplier,
|
||||
OrderTypeID,
|
||||
DeliveryFee,
|
||||
StatusID,
|
||||
AddressID,
|
||||
PaymentID,
|
||||
Remarks,
|
||||
AddedOn,
|
||||
LastEditedOn,
|
||||
SubmittedOn,
|
||||
ServicePointID
|
||||
FROM Orders
|
||||
WHERE ID = ?
|
||||
o.ID,
|
||||
o.UUID,
|
||||
o.UserID,
|
||||
o.BusinessID,
|
||||
o.DeliveryMultiplier,
|
||||
o.OrderTypeID,
|
||||
o.DeliveryFee,
|
||||
o.StatusID,
|
||||
o.AddressID,
|
||||
o.PaymentID,
|
||||
o.Remarks,
|
||||
o.AddedOn,
|
||||
o.LastEditedOn,
|
||||
o.SubmittedOn,
|
||||
o.ServicePointID,
|
||||
sp.Name AS ServicePointName
|
||||
FROM Orders o
|
||||
LEFT JOIN ServicePoints sp ON sp.ID = o.ServicePointID
|
||||
WHERE o.ID = ?
|
||||
LIMIT 1
|
||||
",
|
||||
[ { value = arguments.OrderID, cfsqltype = "cf_sql_integer" } ],
|
||||
|
|
@ -84,7 +86,8 @@
|
|||
"AddedOn": qOrder.AddedOn,
|
||||
"LastEditedOn": qOrder.LastEditedOn,
|
||||
"SubmittedOn": qOrder.SubmittedOn,
|
||||
"ServicePointID": val(qOrder.ServicePointID)
|
||||
"ServicePointID": val(qOrder.ServicePointID),
|
||||
"ServicePointName": qOrder.ServicePointName ?: ""
|
||||
}>
|
||||
|
||||
<cfset var qLI = queryTimed(
|
||||
|
|
|
|||
|
|
@ -112,7 +112,6 @@ try {
|
|||
|
||||
if (orderID > 0) {
|
||||
// Update order status to paid/submitted (status 1)
|
||||
// Note: Task is created later when order status changes to Ready (3) in updateStatus.cfm
|
||||
queryTimed("
|
||||
UPDATE Orders
|
||||
SET PaymentStatus = 'paid',
|
||||
|
|
@ -134,6 +133,34 @@ try {
|
|||
", { amount: val(qBalOrder.BalanceApplied), userID: val(qBalOrder.UserID) });
|
||||
writeLog(file="stripe_webhooks", text="Order #orderID# balance deducted: $#qBalOrder.BalanceApplied#");
|
||||
}
|
||||
|
||||
// Create kitchen task for the order
|
||||
qOrder = queryTimed("
|
||||
SELECT o.BusinessID, o.ServicePointID, sp.Name AS ServicePointName
|
||||
FROM Orders o
|
||||
LEFT JOIN ServicePoints sp ON sp.ID = o.ServicePointID
|
||||
WHERE o.ID = :orderID
|
||||
", { orderID: orderID });
|
||||
|
||||
if (qOrder.recordCount > 0) {
|
||||
tableName = len(trim(qOrder.ServicePointName)) ? qOrder.ServicePointName : "Table";
|
||||
taskTitle = "Prepare Order ##" & orderID & " for " & tableName;
|
||||
|
||||
queryTimed("
|
||||
INSERT INTO Tasks (
|
||||
BusinessID, OrderID, ServicePointID, Title, CreatedOn, ClaimedByUserID
|
||||
) VALUES (
|
||||
:businessID, :orderID, :servicePointID, :title, NOW(), 0
|
||||
)
|
||||
", {
|
||||
businessID: qOrder.BusinessID,
|
||||
orderID: orderID,
|
||||
servicePointID: val(qOrder.ServicePointID),
|
||||
title: taskTitle
|
||||
});
|
||||
|
||||
writeLog(file="stripe_webhooks", text="Kitchen task created for order #orderID#");
|
||||
}
|
||||
}
|
||||
|
||||
// === WORKER PAYOUT TRANSFER ===
|
||||
|
|
|
|||
|
|
@ -36,6 +36,16 @@ try {
|
|||
apiAbort({ "OK": false, "ERROR": "missing_params", "MESSAGE": "BusinessID is required" });
|
||||
}
|
||||
|
||||
// If servicePointID not provided but orderID is, look it up from the order
|
||||
if (servicePointID == 0 && orderID > 0) {
|
||||
qOrderSP = queryExecute("
|
||||
SELECT ServicePointID FROM Orders WHERE ID = :orderID
|
||||
", { orderID: { value: orderID, cfsqltype: "cf_sql_integer" } }, { datasource: "payfrit" });
|
||||
if (qOrderSP.recordCount && val(qOrderSP.ServicePointID) > 0) {
|
||||
servicePointID = val(qOrderSP.ServicePointID);
|
||||
}
|
||||
}
|
||||
|
||||
if (servicePointID == 0) {
|
||||
apiAbort({ "OK": false, "ERROR": "missing_params", "MESSAGE": "ServicePointID is required" });
|
||||
}
|
||||
|
|
|
|||
|
|
@ -297,6 +297,26 @@
|
|||
<!--- === CASH TRANSACTION PROCESSING === --->
|
||||
<cfset cashProcessed = false>
|
||||
<cfif isCashTask AND CashReceivedCents GT 0 AND qTask.OrderID GT 0 AND NOT CancelOrder>
|
||||
<!--- Look up worker's role for this business (1=Staff, 2=Manager, 3=Admin) --->
|
||||
<cfset workerRoleID = 1>
|
||||
<cfif workerUserID_for_payout GT 0>
|
||||
<cfset qWorkerRole = queryTimed("
|
||||
SELECT COALESCE(RoleID, 1) AS RoleID
|
||||
FROM Employees
|
||||
WHERE UserID = ? AND BusinessID = ? AND IsActive = b'1'
|
||||
ORDER BY RoleID DESC
|
||||
LIMIT 1
|
||||
", [
|
||||
{ value = workerUserID_for_payout, cfsqltype = "cf_sql_integer" },
|
||||
{ value = qTask.BusinessID, cfsqltype = "cf_sql_integer" }
|
||||
], { datasource = "payfrit" })>
|
||||
<cfif qWorkerRole.recordCount GT 0>
|
||||
<cfset workerRoleID = val(qWorkerRole.RoleID)>
|
||||
</cfif>
|
||||
</cfif>
|
||||
|
||||
<cfset isAdmin = (workerRoleID GTE 2)>
|
||||
|
||||
<!--- Credit customer change to their balance --->
|
||||
<cfif changeCents GT 0 AND customerUserID GT 0>
|
||||
<cfset queryTimed("
|
||||
|
|
@ -318,21 +338,47 @@
|
|||
|
||||
<!--- Debit for physical cash received --->
|
||||
<!--- Staff: debit the worker (they pocketed the cash) --->
|
||||
<!--- Admin/Manager: debit the business owner (restaurant has the cash) --->
|
||||
<cfset cashDebitUserID = isAdminRole ? businessOwnerUserID : workerUserID_for_payout>
|
||||
<cfif cashDebitUserID GT 0>
|
||||
<!--- Admin/Manager: cash goes to the business, delete worker's pending payout --->
|
||||
<cfif isAdmin>
|
||||
<!--- Admin/Manager: cash goes to the business, delete worker's pending payout --->
|
||||
<!--- Remove the task earning ledger entry (admin hands cash to register) --->
|
||||
<cfif workerUserID_for_payout GT 0>
|
||||
<cfset queryTimed("
|
||||
DELETE FROM WorkPayoutLedgers
|
||||
WHERE TaskID = :taskID AND UserID = :userID AND Status = 'pending_charge'
|
||||
", {
|
||||
taskID: TaskID,
|
||||
userID: workerUserID_for_payout
|
||||
}, { datasource = "payfrit" })>
|
||||
|
||||
<!--- Reverse activation withholding if any was applied --->
|
||||
<cfif ledgerCreated AND activationWithhold GT 0>
|
||||
<cfset queryTimed("
|
||||
UPDATE Users
|
||||
SET ActivationBalanceCents = GREATEST(0, ActivationBalanceCents - :withhold)
|
||||
WHERE ID = :userID
|
||||
", {
|
||||
withhold: activationWithhold,
|
||||
userID: workerUserID_for_payout
|
||||
}, { datasource = "payfrit" })>
|
||||
</cfif>
|
||||
</cfif>
|
||||
<cfelse>
|
||||
<!--- Staff: cash stays in their pocket, debit their payout balance --->
|
||||
<cfif workerUserID_for_payout GT 0>
|
||||
<cfset queryTimed("
|
||||
INSERT INTO WorkPayoutLedgers
|
||||
(UserID, TaskID, GrossEarningsCents, ActivationWithheldCents, NetTransferCents, Status)
|
||||
VALUES
|
||||
(:userID, :taskID, :gross, 0, :net, 'cash_debit')
|
||||
", {
|
||||
userID: cashDebitUserID,
|
||||
userID: workerUserID_for_payout,
|
||||
taskID: TaskID,
|
||||
gross: -CashReceivedCents,
|
||||
net: -CashReceivedCents
|
||||
}, { datasource = "payfrit" })>
|
||||
</cfif>
|
||||
</cfif>
|
||||
|
||||
<!--- Log transaction in Payments table --->
|
||||
<cfset queryTimed("
|
||||
|
|
@ -345,7 +391,7 @@
|
|||
:sentBy, :receivedBy, :orderID,
|
||||
0, 0, :cashAmount,
|
||||
:payfritCut, 0, :bizFee,
|
||||
'Cash payment', NOW()
|
||||
:remark, NOW()
|
||||
)
|
||||
", {
|
||||
sentBy: customerUserID,
|
||||
|
|
@ -353,7 +399,8 @@
|
|||
orderID: qTask.OrderID,
|
||||
cashAmount: CashReceivedCents / 100,
|
||||
payfritCut: payfritRevenueCents / 100,
|
||||
bizFee: round(businessFeeDollars * 100) / 100
|
||||
bizFee: round(businessFeeDollars * 100) / 100,
|
||||
remark: isAdmin ? "Cash payment (collected by manager)" : "Cash payment"
|
||||
}, { datasource = "payfrit" })>
|
||||
|
||||
<cfset cashProcessed = true>
|
||||
|
|
@ -430,6 +477,8 @@
|
|||
<cfset response["BalanceApplied"] = numberFormat(balanceAppliedCents / 100, "0.00")>
|
||||
<cfset response["CashOwed"] = numberFormat(cashOwedCents / 100, "0.00")>
|
||||
</cfif>
|
||||
<cfset response["CashRoutedTo"] = isAdmin ? "business" : "worker">
|
||||
<cfset response["WorkerRoleID"] = workerRoleID>
|
||||
</cfif>
|
||||
|
||||
<cfset apiAbort(response)>
|
||||
|
|
|
|||
|
|
@ -35,6 +35,16 @@ try {
|
|||
apiAbort({ "OK": false, "ERROR": "missing_params", "MESSAGE": "BusinessID is required" });
|
||||
}
|
||||
|
||||
// If servicePointID not provided but orderID is, look it up from the order
|
||||
if (servicePointID == 0 && orderID > 0) {
|
||||
qOrderSP = queryTimed("
|
||||
SELECT ServicePointID FROM Orders WHERE ID = :orderID
|
||||
", { orderID: { value: orderID, cfsqltype: "cf_sql_integer" } }, { datasource: "payfrit" });
|
||||
if (qOrderSP.recordCount && val(qOrderSP.ServicePointID) > 0) {
|
||||
servicePointID = val(qOrderSP.ServicePointID);
|
||||
}
|
||||
}
|
||||
|
||||
// Look up "Chat With Staff" task type for this business
|
||||
ttQuery = queryTimed("
|
||||
SELECT ID FROM tt_TaskTypes
|
||||
|
|
|
|||
|
|
@ -40,7 +40,7 @@
|
|||
e.BusinessID,
|
||||
MIN(e.StatusID) AS StatusID,
|
||||
MAX(e.IsActive) AS IsActive,
|
||||
MAX(e.RoleID) AS RoleID,
|
||||
MAX(COALESCE(e.RoleID, 1)) AS RoleID,
|
||||
b.Name AS Name,
|
||||
(SELECT COUNT(*) FROM Tasks t WHERE t.BusinessID = e.BusinessID AND t.ClaimedByUserID = 0 AND t.CompletedOn IS NULL) AS PendingTaskCount,
|
||||
(SELECT COUNT(*) FROM Tasks t WHERE t.BusinessID = e.BusinessID AND t.ClaimedByUserID = ? AND t.CompletedOn IS NULL) AS ActiveTaskCount
|
||||
|
|
|
|||
Reference in a new issue