fix: show trucking order prices and payment options
This commit is contained in:
parent
d7de818f39
commit
ee07064b0f
28 changed files with 199 additions and 93 deletions
69
site/src/components/OrderPriceBanner.astro
Normal file
69
site/src/components/OrderPriceBanner.astro
Normal 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>
|
||||
|
|
@ -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}`);
|
||||
|
|
|
|||
|
|
@ -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>
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue