From c580e6ec788a459e65328119bc0f9f4263d79329 Mon Sep 17 00:00:00 2001 From: John Mizerek Date: Mon, 2 Mar 2026 14:16:21 -0800 Subject: [PATCH] Auto-apply user balance on cash and card orders Balance from cash change now silently reduces the amount owed on the next order. For cash: deducted immediately in submitCash, reduces cash the worker needs to collect (or skips cash task entirely if fully covered). For card: reduces the Stripe PaymentIntent amount, deducted in webhook on successful payment. Receipt shows "Balance applied" line. Co-Authored-By: Claude Opus 4.6 --- api/auth/profile.cfm | 6 ++- api/orders/submitCash.cfm | 71 ++++++++++++++++++++++++++---- api/stripe/createPaymentIntent.cfm | 43 ++++++++++++++++-- api/stripe/webhook.cfm | 11 +++++ api/tasks/complete.cfm | 20 +++++++-- receipt/index.cfm | 11 ++++- 6 files changed, 143 insertions(+), 19 deletions(-) diff --git a/api/auth/profile.cfm b/api/auth/profile.cfm index 1734684..d560f25 100644 --- a/api/auth/profile.cfm +++ b/api/auth/profile.cfm @@ -64,7 +64,8 @@ if (cgi.REQUEST_METHOD == "GET") { LastName, EmailAddress, ContactNumber, - ImageExtension + ImageExtension, + Balance FROM Users WHERE ID = :userId LIMIT 1 @@ -88,7 +89,8 @@ if (cgi.REQUEST_METHOD == "GET") { "LastName": qUser.LastName ?: "", "Email": qUser.EmailAddress ?: "", "Phone": qUser.ContactNumber ?: "", - "AvatarUrl": avatarUrl + "AvatarUrl": avatarUrl, + "Balance": val(qUser.Balance) } })); abort; diff --git a/api/orders/submitCash.cfm b/api/orders/submitCash.cfm index 29ce89a..e99b021 100644 --- a/api/orders/submitCash.cfm +++ b/api/orders/submitCash.cfm @@ -43,7 +43,8 @@ - - + + + + + + + + + + + + + + + + = ?", + [ + { value = balanceToApply, cfsqltype = "cf_sql_decimal" }, + { value = qOrder.UserID, cfsqltype = "cf_sql_integer" }, + { value = balanceToApply, cfsqltype = "cf_sql_decimal" } + ], + { datasource = "payfrit", result = "deductResult" } + )> + + + + + + + + + + - + + + + - + + + + + + + 0 && orderUserID > 0) { + balanceApplied = min(userBalance, totalBeforeCardFee); + // Ensure Stripe minimum: adjusted amount after card fee must be >= $0.50 + adjustedTest = ((totalBeforeCardFee - balanceApplied) + cardFeeFixed) / (1 - cardFeePercent); + if (adjustedTest < 0.50) { + // Cap balance so Stripe charge stays >= $0.50 + maxBalance = totalBeforeCardFee - ((0.50 * (1 - cardFeePercent)) - cardFeeFixed); + balanceApplied = max(0, min(userBalance, maxBalance)); + } + // Store intent on order (actual deduction happens in webhook after payment succeeds) + if (balanceApplied > 0) { + queryExecute("UPDATE Orders SET BalanceApplied = :bal WHERE ID = :orderID", + { bal: { value: balanceApplied, cfsqltype: "cf_sql_decimal" }, orderID: orderID }, + { datasource: "payfrit" }); + } + } + + // Apply balance: reduce the pre-card-fee amount, recalculate card fee on smaller amount + adjustedBeforeCardFee = totalBeforeCardFee - balanceApplied; + // Stripe charges 2.9% + $0.30 on the TOTAL charged, not the pre-fee amount. // To fully pass through Stripe fees: charge = (net + fixed) / (1 - percent) - totalCustomerPays = (totalBeforeCardFee + cardFeeFixed) / (1 - cardFeePercent); - cardFee = totalCustomerPays - totalBeforeCardFee; + totalCustomerPays = (adjustedBeforeCardFee + cardFeeFixed) / (1 - cardFeePercent); + cardFee = totalCustomerPays - adjustedBeforeCardFee; // Convert to cents for Stripe totalAmountCents = round(totalCustomerPays * 100); @@ -171,6 +198,14 @@ try { response["PAYMENT_INTENT_ID"] = existingPi.id; response["PUBLISHABLE_KEY"] = application.stripePublishableKey ?: "pk_test_sPBNzSyJ9HcEPJGC7dSo8NqN"; response["REUSED"] = true; + response["FEE_BREAKDOWN"] = { + "SUBTOTAL": subtotal, "TAX": tax, "TIP": tip, + "DELIVERY_FEE": deliveryFee, "PAYFRIT_FEE": payfritCustomerFee, + "CARD_FEE": cardFee, "BALANCE_APPLIED": balanceApplied, + "TOTAL": totalCustomerPays, + "TOTAL_BEFORE_BALANCE": (balanceApplied > 0) ? totalCustomerPays + balanceApplied : 0, + "GRANT_OWNER_FEE_CENTS": grantOwnerFeeCents + }; writeOutput(serializeJSON(response)); abort; } else if (piStatus == "succeeded") { @@ -360,7 +395,9 @@ try { "DELIVERY_FEE": deliveryFee, "PAYFRIT_FEE": payfritCustomerFee, "CARD_FEE": cardFee, + "BALANCE_APPLIED": balanceApplied, "TOTAL": totalCustomerPays, + "TOTAL_BEFORE_BALANCE": (balanceApplied > 0) ? totalCustomerPays + balanceApplied : 0, "GRANT_OWNER_FEE_CENTS": grantOwnerFeeCents }; response["STRIPE_CONNECT_ENABLED"] = hasStripeConnect; diff --git a/api/stripe/webhook.cfm b/api/stripe/webhook.cfm index e38be3c..a9fa775 100644 --- a/api/stripe/webhook.cfm +++ b/api/stripe/webhook.cfm @@ -123,6 +123,17 @@ try { ", { orderID: orderID }); writeLog(file="stripe_webhooks", text="Order #orderID# marked as paid"); + + // === DEDUCT USER BALANCE (if balance was applied at checkout) === + qBalOrder = queryTimed(" + SELECT BalanceApplied, UserID FROM Orders WHERE ID = :orderID + ", { orderID: orderID }); + if (qBalOrder.recordCount > 0 && val(qBalOrder.BalanceApplied) > 0) { + queryTimed(" + UPDATE Users SET Balance = GREATEST(Balance - :amount, 0) WHERE ID = :userID + ", { amount: val(qBalOrder.BalanceApplied), userID: val(qBalOrder.UserID) }); + writeLog(file="stripe_webhooks", text="Order #orderID# balance deducted: $#qBalOrder.BalanceApplied#"); + } } // === WORKER PAYOUT TRANSFER === diff --git a/api/tasks/complete.cfm b/api/tasks/complete.cfm index e5c0700..bef643e 100644 --- a/api/tasks/complete.cfm +++ b/api/tasks/complete.cfm @@ -144,7 +144,8 @@ - - + + + + + + + - + + + + + diff --git a/receipt/index.cfm b/receipt/index.cfm index c830792..443ce99 100644 --- a/receipt/index.cfm +++ b/receipt/index.cfm @@ -16,7 +16,7 @@ - SELECT O.OrderTypeID, O.BusinessID, O.Remarks, O.ID, + SELECT O.OrderTypeID, O.BusinessID, O.Remarks, O.ID, O.BalanceApplied, B.Name, B.TaxRate, B.PayfritFee FROM Orders O JOIN Businesses B ON B.ID = O.BusinessID @@ -347,6 +347,15 @@ #dollarFormat(cardFee)# + + +
+ Balance applied + -#dollarFormat(receiptBalanceApplied)# +
+ +
+
Total #dollarFormat(order_grand_total)#