Files
PowderCoatingLogix/src/PowderCoating.Web/Views/Pay/Deposit.cshtml
T
spouliot a0bdd2b5b4 Sweep all .cshtml files for encoding corruption; add pre-commit guard
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>
2026-05-20 21:37:10 -04:00

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 &mdash; Quote @Model.QuoteNumber &mdash; @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&hellip;</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 &mdash; @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&hellip;</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&hellip;';
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>