fix: show trucking order prices and payment options

This commit is contained in:
justin 2026-06-03 13:24:40 -05:00
parent d7de818f39
commit ee07064b0f
28 changed files with 199 additions and 93 deletions

View file

@ -0,0 +1,69 @@
---
import { formatUSD } from "../lib/intake_manifest";
export interface Props {
priceCents?: number;
govFeeLabel?: string;
note?: string;
}
const { priceCents, govFeeLabel, note } = Astro.props;
const hasPrice = typeof priceCents === "number";
---
{hasPrice && (
<aside class="pw-price-banner" aria-label="Service price">
<div>
<p class="pw-price-label">Service fee</p>
<p class="pw-price-value">{formatUSD(priceCents!)}</p>
</div>
<div class="pw-price-copy">
{govFeeLabel ? (
<p><strong>Pass-through fees:</strong>{" "}{govFeeLabel}. Billed separately at cost.</p>
) : (
<p>No hidden Performance West service fee. You will review the total before payment.</p>
)}
{note && <p>{note}</p>}
</div>
</aside>
)}
<style>
.pw-price-banner {
display: flex;
gap: 1.25rem;
align-items: center;
justify-content: space-between;
padding: 1rem 1.15rem;
margin: 1rem 0 1.5rem;
border: 2px solid #fed7aa;
border-radius: 14px;
background: linear-gradient(135deg, #fff7ed, #ffffff);
box-shadow: 0 10px 28px rgba(249, 115, 22, 0.10);
}
.pw-price-label {
margin: 0 0 0.15rem;
color: #9a3412;
font-size: 0.78rem;
font-weight: 800;
letter-spacing: 0.08em;
text-transform: uppercase;
}
.pw-price-value {
margin: 0;
color: #ea580c;
font-size: clamp(2rem, 5vw, 3rem);
line-height: 1;
font-weight: 900;
}
.pw-price-copy {
max-width: 30rem;
color: #475569;
font-size: 0.95rem;
line-height: 1.45;
}
.pw-price-copy p { margin: 0.2rem 0; }
@media (max-width: 640px) {
.pw-price-banner { align-items: flex-start; flex-direction: column; }
}
</style>

View file

@ -621,14 +621,19 @@ const STEP_LABELS: Record<string, string> = {
} else if (!valResp.ok) {
throw new Error(`Validation HTTP ${valResp.status}`);
}
// Kick off Stripe Checkout for the new order.
const methodSel = document.getElementById("pw-pay-method") as HTMLSelectElement | null;
const paymentMethod = methodSel?.value || "card";
// Kick off checkout for the new order using the method selected on the
// Payment step. PayPal and crypto return provider-specific checkout URLs;
// card/ACH/Klarna use Stripe Checkout.
const checkoutResp = await fetch(`${API}/api/v1/checkout/create-session`, {
method: "POST",
headers: { "Content-Type": "application/json" },
body: JSON.stringify({
order_id: newOrderNumber,
order_type: "compliance",
payment_method: "card",
payment_method: paymentMethod,
}),
});
if (!checkoutResp.ok) throw new Error(`Checkout HTTP ${checkoutResp.status}`);

View file

@ -1,5 +1,6 @@
---
// PaymentStep — hand off to the existing Stripe Checkout flow.
// PaymentStep — choose a payment method. The Wizard's Finish button creates the
// order, validates intake, and starts the selected checkout flow.
export interface Props { service_slug: string; }
const { service_slug } = Astro.props;
import { SERVICE_META, formatUSD } from "../../../lib/intake_manifest";
@ -9,7 +10,8 @@ const meta = SERVICE_META[service_slug];
<div class="pw-step" data-slug={service_slug}>
<h2>Payment</h2>
<p class="pw-help">
You'll be redirected to our secure payment processor to complete the order.
Choose how you want to pay. You'll be redirected to the matching secure
checkout after you click Finish.
After payment, the filing handler runs automatically. If admin review is
enabled on your account, you'll see the packet in your portal first.
</p>
@ -32,8 +34,7 @@ const meta = SERVICE_META[service_slug];
<option value="crypto">Cryptocurrency</option>
</select>
<button type="button" id="pw-pay-go" class="pw-btn">Continue to payment →</button>
<div id="pw-pay-err" class="pw-err" hidden></div>
<p class="pw-method-note">Click <strong>Finish</strong> below to continue with the selected method.</p>
</div>
<style>
@ -43,46 +44,5 @@ const meta = SERVICE_META[service_slug];
.pw-total-line { display: flex; justify-content: space-between; padding: 0.25rem 0; font-size: 1rem; }
.pw-field { display: block; font-weight: 600; margin: 0.6rem 0 0.2rem; font-size: 0.88rem; }
.pw-input { width: 100%; padding: 0.5rem 0.7rem; border: 1px solid #cbd5e1; border-radius: 6px; font-size: 0.93rem; }
.pw-btn { padding: 0.75rem 2rem; border: 0; border-radius: 6px; background: #059669; color: #fff; font-weight: 600; cursor: pointer; font-size: 1rem; margin-top: 1.5rem; }
.pw-err { color: #b91c1c; margin-top: 0.75rem; font-size: 0.9rem; }
.pw-method-note { margin: 0.75rem 0 0; color: #64748b; font-size: 0.9rem; }
</style>
<script>
const slugEl = document.querySelector(".pw-step[data-slug]") as HTMLElement | null;
const methodSel = document.getElementById("pw-pay-method") as HTMLSelectElement | null;
const goBtn = document.getElementById("pw-pay-go") as HTMLButtonElement | null;
const err = document.getElementById("pw-pay-err") as HTMLDivElement | null;
if (slugEl && methodSel && goBtn && err) {
const slug = slugEl.getAttribute("data-slug")!;
goBtn.addEventListener("click", async () => {
err.hidden = true;
goBtn.disabled = true; goBtn.textContent = "Creating checkout session…";
try {
const state = (window as any).PWIntake.get();
if (!state.order_number) throw new Error("Order was not created; go back to Review.");
const resp = await fetch("/api/v1/checkout/create-session", {
method: "POST",
headers: { "Content-Type": "application/json" },
body: JSON.stringify({
order_id: state.order_number,
order_type: "compliance",
payment_method: methodSel.value,
}),
});
if (!resp.ok) throw new Error(`HTTP ${resp.status}`);
const { checkout_url } = await resp.json();
if (!checkout_url) throw new Error("No checkout URL returned.");
// Clear the wizard state, success page handles the rest
sessionStorage.removeItem(`pw-intake-${slug}`);
window.location.href = checkout_url;
} catch (e: any) {
err.hidden = false;
err.textContent = "Could not start checkout: " + e.message;
goBtn.disabled = false;
goBtn.textContent = "Continue to payment →";
}
});
}
</script>