Round balance amounts to cents before applying

Prevents sub-cent precision (e.g. $0.883125) from accumulating in
BalanceApplied and payment records. All balance math now rounds to
nearest cent first.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
This commit is contained in:
John Mizerek 2026-03-02 14:32:54 -08:00
parent c580e6ec78
commit c65cd8242b
2 changed files with 6 additions and 4 deletions

View file

@ -79,7 +79,7 @@
<cfset taxRate = (isNumeric(qOrder.TaxRate) AND val(qOrder.TaxRate) GT 0) ? val(qOrder.TaxRate) : 0> <cfset taxRate = (isNumeric(qOrder.TaxRate) AND val(qOrder.TaxRate) GT 0) ? val(qOrder.TaxRate) : 0>
<cfset taxAmount = subtotal * taxRate> <cfset taxAmount = subtotal * taxRate>
<cfset deliveryFee = (val(qOrder.OrderTypeID) EQ 3) ? val(qOrder.DeliveryFee) : 0> <cfset deliveryFee = (val(qOrder.OrderTypeID) EQ 3) ? val(qOrder.DeliveryFee) : 0>
<cfset orderTotal = subtotal + taxAmount + platformFee + Tip + deliveryFee> <cfset orderTotal = round((subtotal + taxAmount + platformFee + Tip + deliveryFee) * 100) / 100>
<!--- Auto-apply user balance (silently reduces cash owed) ---> <!--- Auto-apply user balance (silently reduces cash owed) --->
<cfset balanceApplied = 0> <cfset balanceApplied = 0>
@ -92,7 +92,8 @@
)> )>
<cfset userBalance = val(qBalance.Balance)> <cfset userBalance = val(qBalance.Balance)>
<cfif userBalance GT 0> <cfif userBalance GT 0>
<cfset balanceToApply = min(userBalance, orderTotal)> <!--- Round to cents to avoid sub-cent precision issues --->
<cfset balanceToApply = round(min(userBalance, orderTotal) * 100) / 100>
<!--- Atomic deduct: only succeeds if balance is still sufficient ---> <!--- Atomic deduct: only succeeds if balance is still sufficient --->
<cfset qDeduct = queryExecute( <cfset qDeduct = queryExecute(
"UPDATE Users SET Balance = Balance - ? WHERE ID = ? AND Balance >= ?", "UPDATE Users SET Balance = Balance - ? WHERE ID = ? AND Balance >= ?",

View file

@ -134,13 +134,14 @@ try {
userBalance = val(qOrder.Balance ?: 0); userBalance = val(qOrder.Balance ?: 0);
orderUserID = val(qOrder.UserID ?: 0); orderUserID = val(qOrder.UserID ?: 0);
if (userBalance > 0 && orderUserID > 0) { if (userBalance > 0 && orderUserID > 0) {
balanceApplied = min(userBalance, totalBeforeCardFee); // Round to cents to avoid sub-cent precision issues
balanceApplied = round(min(userBalance, totalBeforeCardFee) * 100) / 100;
// Ensure Stripe minimum: adjusted amount after card fee must be >= $0.50 // Ensure Stripe minimum: adjusted amount after card fee must be >= $0.50
adjustedTest = ((totalBeforeCardFee - balanceApplied) + cardFeeFixed) / (1 - cardFeePercent); adjustedTest = ((totalBeforeCardFee - balanceApplied) + cardFeeFixed) / (1 - cardFeePercent);
if (adjustedTest < 0.50) { if (adjustedTest < 0.50) {
// Cap balance so Stripe charge stays >= $0.50 // Cap balance so Stripe charge stays >= $0.50
maxBalance = totalBeforeCardFee - ((0.50 * (1 - cardFeePercent)) - cardFeeFixed); maxBalance = totalBeforeCardFee - ((0.50 * (1 - cardFeePercent)) - cardFeeFixed);
balanceApplied = max(0, min(userBalance, maxBalance)); balanceApplied = round(max(0, min(userBalance, maxBalance)) * 100) / 100;
} }
// Store intent on order (actual deduction happens in webhook after payment succeeds) // Store intent on order (actual deduction happens in webhook after payment succeeds)
if (balanceApplied > 0) { if (balanceApplied > 0) {