Fix Stripe receipt_email + online payment surcharge and hardening
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 <noreply@anthropic.com>
This commit is contained in:
@@ -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);
|
||||
}
|
||||
|
||||
@@ -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<string, string>
|
||||
{
|
||||
@@ -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<string, string>
|
||||
{
|
||||
|
||||
@@ -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; }
|
||||
|
||||
@@ -75,11 +75,11 @@
|
||||
<div class="input-group mb-1">
|
||||
<span class="input-group-text">$</span>
|
||||
<input type="number" id="paymentAmount" class="form-control"
|
||||
value="@Model.BalanceDue.ToString("F2")"
|
||||
min="0.01" max="@Model.BalanceDue.ToString("F2")" step="0.01"
|
||||
value="@Model.TotalWithSurcharge.ToString("F2")"
|
||||
min="0.01" max="@Model.TotalWithSurcharge.ToString("F2")" step="0.01"
|
||||
oninput="updateSurcharge()" />
|
||||
</div>
|
||||
<div class="form-text">Max: @Model.BalanceDue.ToString("C")</div>
|
||||
<div class="form-text">Max: @Model.TotalWithSurcharge.ToString("C")</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
@@ -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) {
|
||||
|
||||
Reference in New Issue
Block a user