From fdac0240d19dede6388d69b5200a86db564ad07a Mon Sep 17 00:00:00 2001 From: Scott Pouliot Date: Wed, 20 May 2026 14:17:57 -0400 Subject: [PATCH] Fix Stripe receipt_email + online payment surcharge and hardening MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Remove receipt_email from PaymentIntent creation so customers can use any email at Stripe checkout without a stored-email mismatch blocking payment. Remove now-dead CustomerEmail from PaymentPageViewModel. Fix surcharge payment input: amount field now represents the total the customer pays (including fee); JS back-calculates base before sending to server. Add InvariantCulture to numeric Razor→JS literals to prevent comma-decimal cultures from truncating surcharge values. Co-Authored-By: Claude Sonnet 4.6 --- .../Interfaces/IStripeConnectService.cs | 2 - .../Services/StripeConnectService.cs | 4 -- .../Controllers/PaymentController.cs | 9 ---- .../Views/Payment/Index.cshtml | 53 ++++++++++++------- 4 files changed, 33 insertions(+), 35 deletions(-) diff --git a/src/PowderCoating.Application/Interfaces/IStripeConnectService.cs b/src/PowderCoating.Application/Interfaces/IStripeConnectService.cs index 9b7d162..432f48c 100644 --- a/src/PowderCoating.Application/Interfaces/IStripeConnectService.cs +++ b/src/PowderCoating.Application/Interfaces/IStripeConnectService.cs @@ -20,7 +20,6 @@ public interface IStripeConnectService decimal invoiceTotal, decimal surchargeAmount, string currency, - string customerEmail, string invoiceNumber, int invoiceId); @@ -33,7 +32,6 @@ public interface IStripeConnectService decimal depositAmount, decimal surchargeAmount, string currency, - string customerEmail, string quoteNumber, int quoteId); } diff --git a/src/PowderCoating.Infrastructure/Services/StripeConnectService.cs b/src/PowderCoating.Infrastructure/Services/StripeConnectService.cs index 4034057..caf1078 100644 --- a/src/PowderCoating.Infrastructure/Services/StripeConnectService.cs +++ b/src/PowderCoating.Infrastructure/Services/StripeConnectService.cs @@ -162,7 +162,6 @@ public class StripeConnectService : IStripeConnectService decimal invoiceTotal, decimal surchargeAmount, string currency, - string customerEmail, string invoiceNumber, int invoiceId) { @@ -175,7 +174,6 @@ public class StripeConnectService : IStripeConnectService { Amount = amountInCents, Currency = currency.ToLower(), - ReceiptEmail = customerEmail, Description = $"Invoice {invoiceNumber}", Metadata = new Dictionary { @@ -215,7 +213,6 @@ public class StripeConnectService : IStripeConnectService decimal depositAmount, decimal surchargeAmount, string currency, - string customerEmail, string quoteNumber, int quoteId) { @@ -228,7 +225,6 @@ public class StripeConnectService : IStripeConnectService { Amount = amountInCents, Currency = currency.ToLower(), - ReceiptEmail = customerEmail, Description = $"Deposit for quote {quoteNumber}", Metadata = new Dictionary { diff --git a/src/PowderCoating.Web/Controllers/PaymentController.cs b/src/PowderCoating.Web/Controllers/PaymentController.cs index abfd89e..8227eaf 100644 --- a/src/PowderCoating.Web/Controllers/PaymentController.cs +++ b/src/PowderCoating.Web/Controllers/PaymentController.cs @@ -75,7 +75,6 @@ public class PaymentController : Controller CustomerName = customer != null ? $"{customer.ContactFirstName} {customer.ContactLastName}".Trim() : "Valued Customer", - CustomerEmail = customer?.Email ?? string.Empty, CompanyName = company!.CompanyName, BalanceDue = invoice.BalanceDue, InvoiceTotal = invoice.Total, @@ -127,8 +126,6 @@ public class PaymentController : Controller return BadRequest(new { error = "Invalid payment amount." }); var surcharge = CalculateSurcharge(request.Amount, company!); - var customer = await _context.Customers.AsNoTracking() - .FirstOrDefaultAsync(c => c.Id == invoice.CustomerId); var (success, clientSecret, paymentIntentId, stripeError) = await _stripeConnect.CreatePaymentIntentAsync( @@ -136,7 +133,6 @@ public class PaymentController : Controller invoiceTotal: request.Amount, surchargeAmount: surcharge, currency: "usd", - customerEmail: customer?.Email ?? string.Empty, invoiceNumber: invoice.InvoiceNumber, invoiceId: invoice.Id); @@ -261,7 +257,6 @@ public class PaymentController : Controller CustomerName = customer != null ? $"{customer.ContactFirstName} {customer.ContactLastName}".Trim() : quote.ProspectContactName ?? "Valued Customer", - CustomerEmail = customer?.Email ?? quote.ProspectEmail ?? string.Empty, CompanyName = company!.CompanyName, DepositAmount = depositAmount, QuoteTotal = quote.Total, @@ -296,7 +291,6 @@ public class PaymentController : Controller var depositAmount = Math.Round(quote!.Total * (quote.DepositPercent / 100m), 2); var surcharge = CalculateSurcharge(depositAmount, company!); - var customerEmail = quote.Customer?.Email ?? quote.ProspectEmail ?? string.Empty; var (success, clientSecret, paymentIntentId, stripeError) = await _stripeConnect.CreateDepositPaymentIntentAsync( @@ -304,7 +298,6 @@ public class PaymentController : Controller depositAmount: depositAmount, surchargeAmount: surcharge, currency: "usd", - customerEmail: customerEmail, quoteNumber: quote.QuoteNumber, quoteId: quote.Id); @@ -942,7 +935,6 @@ public class PaymentPageViewModel public int InvoiceId { get; set; } public string Token { get; set; } = string.Empty; public string CustomerName { get; set; } = string.Empty; - public string CustomerEmail { get; set; } = string.Empty; public string CompanyName { get; set; } = string.Empty; public decimal BalanceDue { get; set; } public decimal InvoiceTotal { get; set; } @@ -963,7 +955,6 @@ public class DepositPaymentPageViewModel public int QuoteId { get; set; } public string Token { get; set; } = string.Empty; public string CustomerName { get; set; } = string.Empty; - public string CustomerEmail { get; set; } = string.Empty; public string CompanyName { get; set; } = string.Empty; public decimal DepositAmount { get; set; } public decimal QuoteTotal { get; set; } diff --git a/src/PowderCoating.Web/Views/Payment/Index.cshtml b/src/PowderCoating.Web/Views/Payment/Index.cshtml index 744bc6a..db4ca00 100644 --- a/src/PowderCoating.Web/Views/Payment/Index.cshtml +++ b/src/PowderCoating.Web/Views/Payment/Index.cshtml @@ -75,11 +75,11 @@
$
-
Max: @Model.BalanceDue.ToString("C")
+
Max: @Model.TotalWithSurcharge.ToString("C")
@@ -116,18 +116,17 @@ const STRIPE_PK = '@Model.StripePublishableKey'; const ACCOUNT_ID = '@Model.StripeAccountId'; const TOKEN = '@Model.Token'; - const MAX_AMOUNT = @Model.BalanceDue.ToString("F2"); + const MAX_TOTAL = @Model.TotalWithSurcharge.ToString("F2", System.Globalization.CultureInfo.InvariantCulture); const SURCHARGE_TYPE = '@Model.SurchargeType'; - const SURCHARGE_VALUE = @Model.SurchargeValue.ToString("F4"); + const SURCHARGE_VALUE = @Model.SurchargeValue.ToString("F4", System.Globalization.CultureInfo.InvariantCulture); const SUCCESS_URL = `/pay/${TOKEN}/success`; const stripe = Stripe(STRIPE_PK, { stripeAccount: ACCOUNT_ID }); let elements, paymentElement; async function initStripe() { - const amount = parseFloat(document.getElementById('paymentAmount').value) || MAX_AMOUNT; - const surcharge = calcSurcharge(amount); - const total = Math.round((amount + surcharge) * 100) / 100; + // Input value is the total the customer pays (including fee) + const total = parseFloat(document.getElementById('paymentAmount').value) || MAX_TOTAL; elements = stripe.elements({ mode: 'payment', @@ -141,12 +140,15 @@ paymentElement.mount('#payment-element'); } - function calcSurcharge(amount) { + // Back-calculates the base invoice amount from a total that includes the fee. + // Server receives the base and adds surcharge on top — both paths arrive at the same + // PaymentIntent amount so Stripe.js never sees an amount mismatch. + function calcBaseFromTotal(total) { if (SURCHARGE_TYPE === 'Percent') - return Math.round(amount * (SURCHARGE_VALUE / 100) * 100) / 100; + return Math.round(total / (1 + SURCHARGE_VALUE / 100) * 100) / 100; if (SURCHARGE_TYPE === 'Flat') - return SURCHARGE_VALUE; - return 0; + return Math.max(0, Math.round((total - SURCHARGE_VALUE) * 100) / 100); + return total; } function formatCurrency(val) { @@ -154,9 +156,9 @@ } function updateSurcharge() { - const amount = Math.min(parseFloat(document.getElementById('paymentAmount').value) || 0, MAX_AMOUNT); - const surcharge = calcSurcharge(amount); - const total = amount + surcharge; + const total = Math.min(parseFloat(document.getElementById('paymentAmount').value) || 0, MAX_TOTAL); + const base = calcBaseFromTotal(total); + const surcharge = Math.round((total - base) * 100) / 100; const surchargeEl = document.getElementById('surchargeDisplay'); const totalEl = document.getElementById('totalWithSurchargeDisplay'); @@ -164,7 +166,10 @@ if (surchargeEl) surchargeEl.textContent = formatCurrency(surcharge); if (totalEl) totalEl.textContent = formatCurrency(total); - if (btnAmtEl) btnAmtEl.textContent = surcharge > 0 ? formatCurrency(total) : formatCurrency(amount); + if (btnAmtEl) btnAmtEl.textContent = formatCurrency(total); + + // Keep Elements in sync so confirmPayment amount matches the PaymentIntent + if (elements) elements.update({ amount: Math.round(total * 100) }); } async function submitPayment() { @@ -172,13 +177,21 @@ const msgEl = document.getElementById('payment-message'); const amountInput = document.getElementById('paymentAmount'); - const amount = parseFloat(amountInput.value); - if (!amount || amount <= 0 || amount > MAX_AMOUNT) { + const total = parseFloat(amountInput.value); + if (!total || total <= 0 || total > MAX_TOTAL) { msgEl.textContent = 'Please enter a valid payment amount.'; msgEl.style.display = ''; return; } + // Back-calculate base; server adds surcharge on top as usual + const base = calcBaseFromTotal(total); + if (base <= 0) { + msgEl.textContent = 'Payment amount must exceed the convenience fee.'; + msgEl.style.display = ''; + return; + } + btn.disabled = true; document.getElementById('submitText').style.display = 'none'; document.getElementById('submitSpinner').style.display = ''; @@ -195,13 +208,13 @@ return; } - // Create PaymentIntent on server + // Create PaymentIntent on server — send base amount (server adds surcharge) let clientSecret; try { const resp = await fetch(`/pay/${TOKEN}/intent`, { method: 'POST', headers: { 'Content-Type': 'application/json' }, - body: JSON.stringify({ amount }) + body: JSON.stringify({ amount: base }) }); const data = await resp.json(); if (!resp.ok) {