"use client"; import React, { Suspense, useEffect, useMemo, useState } from "react"; import { useSearchParams } from "next/navigation"; import { useAuth } from "@/hooks/useAuth"; import { apiFetch } from "@/lib/api"; import { useDismissingState } from "@/hooks/useDismissingState"; function MakePaymentContent() { const searchParams = useSearchParams(); const registrationId = searchParams.get("registrationId"); const { token } = useAuth(); const [loading, setLoading] = useState(false); const [error, setError] = useDismissingState(null); const [info, setInfo] = useDismissingState(null); const [registration, setRegistration] = useState(null); const [payments, setPayments] = useState([]); const [amount, setAmount] = useState(""); const [priceUpdated, setPriceUpdated] = useState<{ newTotal: number; message: string } | null>(null); // Compute effective unit price for a registration option. // Uses priceSnapshot when available (authoritative backend price, variant-aware). // Falls back to deadline-only early-bird calculation for legacy rows without a snapshot. const optionUnitPrice = (opt: any, referenceTime: any, atTime: Date): number => { if (opt.priceSnapshot !== null && opt.priceSnapshot !== undefined) { return Number(opt.priceSnapshot); } const eo = opt.eventOption; const variantId: string | null = opt.variantId || null; const base = (opt.variant?.price !== null && opt.variant?.price !== undefined) ? Number(opt.variant.price) : Number(eo?.price || 0); const allTiers = Array.isArray(eo?.earlyBirdTiers) ? eo.earlyBirdTiers.slice() : []; const tiers = variantId ? allTiers.filter((t: any) => t.variantId === variantId) : allTiers.filter((t: any) => !t.variantId); if (tiers.length === 0) return base; const t = atTime ? new Date(atTime) : new Date(); const ref = referenceTime ? new Date(referenceTime) : t; const applicable = tiers .map((x: any) => ({ ...x, deadline: new Date(x.deadline) })) .filter((x: any) => (ref < x.deadline) && (t < x.deadline)) .sort((a: any, b: any) => a.deadline.getTime() - b.deadline.getTime() || (a.order||0) - (b.order||0) || a.price - b.price); if (applicable.length === 0) return base; const price = Number(applicable[0].price); return (price >= 0) ? price : base; }; const totals = useMemo(() => { if (!registration) return { totalDue: 0, totalPaid: 0, outstanding: 0 }; const now = new Date(); // totalDue uses priceSnapshot — not time-dependent, consistent with what the backend charges const totalDue = (registration.registrationOptions || []).reduce((s: number, opt: any) => s + optionUnitPrice(opt, null, now) * (opt.quantity || 0), 0); const totalPaid = payments.reduce((s: number, p: any) => s + (p.amount || 0), 0); const outstanding = Math.max(0, totalDue - totalPaid); return { totalDue, totalPaid, outstanding }; }, [registration, payments]); const minAllowed = useMemo(() => { if (totals.outstanding <= 0) return 0; return totals.outstanding >= 15 ? 15 : totals.outstanding; }, [totals.outstanding]); useEffect(() => { (async () => { if (!token || !registrationId) return; try { setError(null); setLoading(true); const reg = await apiFetch(`/api/registrations/${encodeURIComponent(registrationId)}`, { authToken: token }); setRegistration(reg); const pays = await apiFetch(`/api/payments/registration/${encodeURIComponent(registrationId)}`, { authToken: token }); setPayments(pays); } catch (e: any) { setError(e?.message || "Failed to load registration"); } finally { setLoading(false); } })(); }, [token, registrationId]); useEffect(() => { if (totals.outstanding <= 0) { setAmount(""); } }, [totals.outstanding]); const submit = async () => { if (!token || !registrationId) return; const amt = parseFloat(amount); if (!(amt > 0)) { setError("Enter a valid amount"); return; } if (amt > totals.outstanding) { setError(`Amount cannot exceed outstanding (R ${totals.outstanding.toFixed(2)})`); return; } if (amt < minAllowed) { if (totals.outstanding >= 15) { setError("Minimum payment is R15"); } else { setError(`Please pay the remaining outstanding amount (R ${totals.outstanding.toFixed(2)})`); } return; } try { setError(null); setInfo(null); setPriceUpdated(null); setLoading(true); const res = await apiFetch<{ redirectUrl?: string; priceUpdated?: boolean; newTotal?: number; message?: string }>("/api/payments/yoco-checkout", { method: "POST", body: { registrationId, amount: amt, successUrl: window.location.origin + "/payment/success", cancelUrl: window.location.origin + "/payment/cancel", failureUrl: window.location.origin + "/payment/failure", }, authToken: token, }); if (res.priceUpdated) { // Early-bird price changed — show warning and reload registration data setPriceUpdated({ newTotal: res.newTotal ?? 0, message: res.message ?? 'Prices have changed.' }); // Reload registration so totals reflect updated priceSnapshot try { const reg = await apiFetch(`/api/registrations/${encodeURIComponent(registrationId)}`, { authToken: token }); setRegistration(reg); const pays = await apiFetch(`/api/payments/registration/${encodeURIComponent(registrationId)}`, { authToken: token }); setPayments(pays); } catch {} setAmount(""); return; } setInfo("Redirecting to payment..."); window.location.href = res.redirectUrl!; } catch (e: any) { setError(e?.message || "Failed to create checkout"); } finally { setLoading(false); } }; if (!registrationId) return
Missing registrationId.
; return (

Make a payment

{error &&

{error}

} {info &&

{info}

} {priceUpdated && (
Pricing has changed. {priceUpdated.message}
New outstanding: R {priceUpdated.newTotal.toFixed(2)}
)} {registration ? (
{registration.event?.title || registration.eventId}
Registration #{registration.id.slice(0,8)}
Total: R {totals.totalDue.toFixed(2)}
Paid: R {totals.totalPaid.toFixed(2)}
Outstanding: 0?"text-red-600":"text-green-700"}>R {totals.outstanding.toFixed(2)}
{(() => { const tiers = (registration.registrationOptions || []).flatMap((ro: any) => Array.isArray(ro.eventOption?.earlyBirdTiers) ? ro.eventOption.earlyBirdTiers : []); const upcoming = tiers.map((t: any) => ({ ...t, deadline: new Date(t.deadline) })).filter((t: any) => new Date() < t.deadline).sort((a: any, b: any) => a.deadline.getTime() - b.deadline.getTime()); if (upcoming.length === 0) return null; const d = upcoming[0].deadline as Date; const dateStr = d.toLocaleDateString(undefined, { year: 'numeric', month: 'short', day: '2-digit' }); return
Early bird pricing applies if paid before {dateStr}.
; })()}
) : (

Loading registration...

)} 0 ? totals.outstanding.toFixed(2) : "0"} step="1" placeholder={totals.outstanding >= 15 ? "Enter amount (min R15)" : `Enter amount (max R ${totals.outstanding.toFixed(2)})`} value={amount} onChange={(e) => setAmount(e.target.value)} disabled={totals.outstanding <= 0} className="w-full border rounded px-3 py-2 mb-1 disabled:opacity-60" /> {totals.outstanding > 0 && (

Minimum payment {totals.outstanding >= 15 ? "is R15" : `is the remaining outstanding amount (R ${totals.outstanding.toFixed(2)})`}.

)}
); } export default function MakePaymentPage() { return ( Loading...}> ); }