a0bdd2b5b4
Replace all corruption variants with HTML entities across 226 view files: - 3-char UTF-8-as-Win1252 sequences (ae-corruption) - Standalone smart/curly quotes that break C# Razor expressions - Partially re-corrupted variants where the 3rd byte was normalised to ASCII tools/Fix-Encoding.ps1: re-runnable sweep; uses [char] code points so the script itself never contains a literal non-ASCII character; supports -DryRun .githooks/pre-commit: blocks commits containing the ae-corruption byte signature (xc3xa2xe2x82xac); git core.hooksPath = .githooks so the hook is repo-committed and active for all future work on this machine. Build clean; 225 unit tests pass. Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
195 lines
8.8 KiB
Plaintext
195 lines
8.8 KiB
Plaintext
@using PowderCoating.Web.Controllers
|
|
@model DepositPaymentPageViewModel
|
|
@{
|
|
Layout = null;
|
|
var hasSurcharge = Model.SurchargeType != PowderCoating.Core.Enums.OnlinePaymentSurchargeType.None && Model.SurchargeValue > 0;
|
|
}
|
|
<!DOCTYPE html>
|
|
<html lang="en">
|
|
<head>
|
|
<meta charset="utf-8" />
|
|
<meta name="viewport" content="width=device-width, initial-scale=1" />
|
|
<title>Pay Deposit — Quote @Model.QuoteNumber — @Model.CompanyName</title>
|
|
<link rel="stylesheet" href="~/lib/bootstrap/css/bootstrap.min.css" />
|
|
<style>
|
|
body { background: #f0f2f5; font-family: 'Segoe UI', sans-serif; }
|
|
.pay-card { max-width: 520px; margin: 60px auto; background: #fff; border-radius: 12px; box-shadow: 0 4px 24px rgba(0,0,0,.10); padding: 36px 40px; }
|
|
.company-name { font-size: 1.1rem; color: #6c757d; margin-bottom: 4px; }
|
|
.quote-number { font-size: 1.5rem; font-weight: 700; color: #1a1a2e; }
|
|
.deposit-badge { display: inline-block; background: #fff3cd; color: #856404; border-radius: 6px; font-size: .8rem; font-weight: 600; padding: 3px 10px; margin-bottom: 12px; }
|
|
.amount-display { background: #f8f9fa; border-radius: 8px; padding: 16px 20px; margin: 20px 0; }
|
|
.amount-display .label { font-size: .85rem; color: #6c757d; }
|
|
.amount-display .value { font-size: 1.1rem; font-weight: 600; }
|
|
.total-line { border-top: 2px solid #dee2e6; margin-top: 8px; padding-top: 10px; }
|
|
.total-line .value { font-size: 1.4rem; color: #f59e0b; font-weight: 700; }
|
|
#payment-element { margin: 20px 0; }
|
|
#pay-btn { font-size: 1.05rem; padding: 12px; }
|
|
#error-msg { font-size: .9rem; }
|
|
.surcharge-notice { font-size: .78rem; color: #6c757d; margin-top: 6px; }
|
|
.expires-note { font-size: .8rem; color: #6c757d; text-align: center; margin-top: 16px; }
|
|
#processing-overlay { display: none; position: fixed; inset: 0; background: rgba(255,255,255,.7); z-index: 9999; align-items: center; justify-content: center; }
|
|
</style>
|
|
</head>
|
|
<body>
|
|
<div id="processing-overlay" aria-live="polite">
|
|
<div class="text-center">
|
|
<div class="spinner-border text-warning" role="status"></div>
|
|
<p class="mt-2 fw-semibold">Processing deposit…</p>
|
|
</div>
|
|
</div>
|
|
|
|
<div class="pay-card">
|
|
<p class="company-name">@Model.CompanyName</p>
|
|
<p class="quote-number">Quote #@Model.QuoteNumber</p>
|
|
<span class="deposit-badge">Deposit Required — @Model.DepositPercent.ToString("0.##")% of quote total</span>
|
|
<p class="text-muted mb-0">Hello, @Model.CustomerName</p>
|
|
|
|
<div class="amount-display">
|
|
<div class="d-flex justify-content-between mb-1">
|
|
<span class="label">Quote Total</span>
|
|
<span class="value text-muted">@Model.QuoteTotal.ToString("C")</span>
|
|
</div>
|
|
<div class="d-flex justify-content-between mb-1 total-line">
|
|
<span class="label fw-semibold">Deposit Due</span>
|
|
<span class="value">@Model.DepositAmount.ToString("C")</span>
|
|
</div>
|
|
</div>
|
|
|
|
<form id="payment-form">
|
|
@if (hasSurcharge)
|
|
{
|
|
<div id="surchargeRow" class="amount-display py-2 d-none">
|
|
<div class="d-flex justify-content-between mb-1">
|
|
<span class="label">Deposit</span>
|
|
<span id="subtotalDisplay" class="value"></span>
|
|
</div>
|
|
<div class="d-flex justify-content-between mb-1">
|
|
<span class="label">
|
|
@if (Model.SurchargeType == PowderCoating.Core.Enums.OnlinePaymentSurchargeType.Percent)
|
|
{
|
|
@($"Online payment fee ({Model.SurchargeValue:0.##}%)")
|
|
}
|
|
else
|
|
{
|
|
<text>Online payment fee</text>
|
|
}
|
|
</span>
|
|
<span id="surchargeDisplay" class="value"></span>
|
|
</div>
|
|
<div class="d-flex justify-content-between total-line">
|
|
<span class="label fw-semibold">Total Charged</span>
|
|
<span id="totalDisplay" class="value"></span>
|
|
</div>
|
|
</div>
|
|
<p class="surcharge-notice">
|
|
A convenience fee applies to online payments. You may pay in person to avoid this fee.
|
|
Surcharges do not apply to debit card transactions in states where prohibited.
|
|
</p>
|
|
}
|
|
|
|
<div id="payment-element"></div>
|
|
|
|
<div id="error-msg" class="alert alert-danger d-none mt-3" role="alert"></div>
|
|
|
|
<button id="pay-btn" type="submit" class="btn btn-warning w-100 mt-3" disabled>
|
|
<span id="btn-text">Loading payment form…</span>
|
|
</button>
|
|
</form>
|
|
|
|
<p class="expires-note">
|
|
This payment link expires on @Model.ExpiresAt.Tz(ViewBag.CompanyTimeZone as string).ToString("MMMM d, yyyy").
|
|
</p>
|
|
</div>
|
|
|
|
<script src="https://js.stripe.com/v3/"></script>
|
|
<script>
|
|
const STRIPE_KEY = '@Model.StripePublishableKey';
|
|
const ACCOUNT_ID = '@Model.StripeAccountId';
|
|
const TOKEN = '@Model.Token';
|
|
const DEPOSIT_AMOUNT = @Model.DepositAmount.ToString("F2", System.Globalization.CultureInfo.InvariantCulture);
|
|
const HAS_SURCHARGE = @(hasSurcharge ? "true" : "false");
|
|
|
|
const stripe = Stripe(STRIPE_KEY, { stripeAccount: ACCOUNT_ID });
|
|
let elements = null;
|
|
let paymentElement = null;
|
|
|
|
const form = document.getElementById('payment-form');
|
|
const payBtn = document.getElementById('pay-btn');
|
|
const btnText = document.getElementById('btn-text');
|
|
const errorMsg = document.getElementById('error-msg');
|
|
const surchargeRow = document.getElementById('surchargeRow');
|
|
const subtotalDisplay = document.getElementById('subtotalDisplay');
|
|
const surchargeDisplay = document.getElementById('surchargeDisplay');
|
|
const totalDisplay = document.getElementById('totalDisplay');
|
|
const overlay = document.getElementById('processing-overlay');
|
|
|
|
function formatCurrency(val) {
|
|
return '$' + parseFloat(val).toFixed(2);
|
|
}
|
|
|
|
function showError(msg) {
|
|
errorMsg.textContent = msg;
|
|
errorMsg.classList.remove('d-none');
|
|
overlay.style.display = 'none';
|
|
payBtn.disabled = false;
|
|
btnText.textContent = 'Try Again';
|
|
}
|
|
|
|
async function initPaymentElement() {
|
|
errorMsg.classList.add('d-none');
|
|
payBtn.disabled = true;
|
|
btnText.textContent = 'Preparing…';
|
|
|
|
if (paymentElement) { paymentElement.destroy(); paymentElement = null; }
|
|
document.getElementById('payment-element').innerHTML = '';
|
|
|
|
const resp = await fetch('/pay/deposit/' + TOKEN + '/intent', {
|
|
method: 'POST',
|
|
headers: { 'Content-Type': 'application/json' },
|
|
body: JSON.stringify({ amount: DEPOSIT_AMOUNT })
|
|
});
|
|
|
|
const data = await resp.json();
|
|
if (!resp.ok) { showError(data.error || 'Could not initialize payment.'); return; }
|
|
|
|
const surcharge = data.surchargeAmount || 0;
|
|
|
|
if (HAS_SURCHARGE && surcharge > 0) {
|
|
subtotalDisplay.textContent = formatCurrency(DEPOSIT_AMOUNT);
|
|
surchargeDisplay.textContent = formatCurrency(surcharge);
|
|
totalDisplay.textContent = formatCurrency(DEPOSIT_AMOUNT + surcharge);
|
|
surchargeRow.classList.remove('d-none');
|
|
}
|
|
|
|
elements = stripe.elements({ clientSecret: data.clientSecret });
|
|
paymentElement = elements.create('payment');
|
|
paymentElement.mount('#payment-element');
|
|
paymentElement.on('ready', () => {
|
|
payBtn.disabled = false;
|
|
btnText.textContent = 'Pay Deposit ' + formatCurrency(DEPOSIT_AMOUNT + surcharge);
|
|
});
|
|
}
|
|
|
|
form.addEventListener('submit', async (e) => {
|
|
e.preventDefault();
|
|
errorMsg.classList.add('d-none');
|
|
payBtn.disabled = true;
|
|
overlay.style.display = 'flex';
|
|
|
|
const { error } = await stripe.confirmPayment({
|
|
elements,
|
|
confirmParams: {
|
|
return_url: window.location.origin + '/pay/deposit/' + TOKEN + '/success'
|
|
}
|
|
});
|
|
|
|
if (error) {
|
|
showError(error.message || 'Payment failed. Please try again.');
|
|
}
|
|
});
|
|
|
|
initPaymentElement();
|
|
</script>
|
|
</body>
|
|
</html>
|