Fix early-bird price blending and mislabeling; add contact-only events
- Early-bird pricing: RegistrationOption now tracks each purchase as a separate price tranche instead of overwriting a single price/quantity on repeat purchases, so buying more tickets after a tier expires no longer re-prices tickets already bought at the old price. Stock-limit checks, total-due calculation, and the Finance report's revenue-by- option are all tranche-aware; pages that showed one blended price per line now render/total each tranche. Viewing a pending/partially-paid registration (dashboard, detail page, or an event's registration list) now refreshes stale pricing on the spot instead of only at payment time. - Fixed the "(early bird)" dashboard label incorrectly firing on any line priced below the base option price (e.g. a plain cheaper variant) — it now checks the real applied-tier flag. - Added contact-only events (e.g. baptism): no registration/payment flow, shown on the public site with a "Contact us" popup instead of a Register button. Configurable via the admin event wizard. Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
This commit is contained in:
@@ -140,12 +140,11 @@ export default function UserDashboardPage() {
|
||||
setRegistrations(myRegs);
|
||||
setTickets(myTicks);
|
||||
|
||||
// Compute totalDue from priceSnapshot (authoritative backend price, variant-aware).
|
||||
// priceSnapshot is set at registration time and refreshed before each payment.
|
||||
const now = new Date();
|
||||
// Use the backend's tranche-aware totalDueComputed (exact even when a line spans
|
||||
// multiple early-bird prices) rather than re-deriving from priceSnapshot client-side.
|
||||
const totals: Record<string, { totalDue: number; totalPaid: number; outstanding: number; payments: any[] }> = {};
|
||||
for (const r of myRegs) {
|
||||
const totalDue = (r.registrationOptions || []).reduce((sum: number, opt: any) => sum + optionUnitPrice(opt, null, now) * (opt.quantity || 0), 0);
|
||||
const totalDue = r.totalDueComputed ?? (r.registrationOptions || []).reduce((sum: number, opt: any) => sum + optionUnitPrice(opt, null, new Date()) * (opt.quantity || 0), 0);
|
||||
totals[r.id] = { totalDue, totalPaid: 0, outstanding: totalDue, payments: [] };
|
||||
}
|
||||
setBilling(totals);
|
||||
@@ -156,8 +155,8 @@ export default function UserDashboardPage() {
|
||||
try {
|
||||
const pays = await apiFetch<any[]>(`/api/payments/registration/${encodeURIComponent(r.id)}`, { authToken: token });
|
||||
const totalPaid = pays.reduce((s, p) => s + (p.amount || 0), 0);
|
||||
// totalDue uses priceSnapshot — not time-dependent, no need to recompute per payment time
|
||||
const totalDue = (r.registrationOptions || []).reduce((sum: number, opt: any) => sum + optionUnitPrice(opt, null, now) * (opt.quantity || 0), 0);
|
||||
// totalDueComputed is not time-dependent, no need to recompute per payment time
|
||||
const totalDue = r.totalDueComputed ?? (r.registrationOptions || []).reduce((sum: number, opt: any) => sum + optionUnitPrice(opt, null, new Date()) * (opt.quantity || 0), 0);
|
||||
const outstanding = Math.max(0, totalDue - totalPaid);
|
||||
setBilling(prev => ({
|
||||
...prev,
|
||||
@@ -638,11 +637,10 @@ export default function UserDashboardPage() {
|
||||
const myTicks = await apiFetch<any[]>("/api/tickets/mytickets", { authToken: token });
|
||||
setTickets(myTicks);
|
||||
} catch {}
|
||||
// Recompute billing totals using priceSnapshot
|
||||
const now2 = new Date();
|
||||
// Recompute billing totals using the backend's tranche-aware totalDueComputed
|
||||
const totals: Record<string, { totalDue: number; totalPaid: number; outstanding: number; payments: any[] }> = {};
|
||||
for (const r of myRegs) {
|
||||
const totalDue = (r.registrationOptions || []).reduce((sum: number, opt: any) => sum + optionUnitPrice(opt, null, now2) * (opt.quantity || 0), 0);
|
||||
const totalDue = r.totalDueComputed ?? (r.registrationOptions || []).reduce((sum: number, opt: any) => sum + optionUnitPrice(opt, null, new Date()) * (opt.quantity || 0), 0);
|
||||
totals[r.id] = { totalDue, totalPaid: 0, outstanding: totalDue, payments: [] };
|
||||
}
|
||||
setBilling(totals);
|
||||
@@ -971,19 +969,32 @@ export default function UserDashboardPage() {
|
||||
</div>
|
||||
{!editMode ? (
|
||||
<ul className="text-sm list-disc pl-5 space-y-1">
|
||||
{(activeReg.registrationOptions || []).map((opt: any) => {
|
||||
{(activeReg.registrationOptions || []).flatMap((opt: any) => {
|
||||
const variantLabel = opt.variant?.name ? ` (${opt.variant.name})` : '';
|
||||
const unitPrice = (opt.priceSnapshot !== null && opt.priceSnapshot !== undefined)
|
||||
? Number(opt.priceSnapshot)
|
||||
: (opt.variant?.price ?? opt.eventOption?.price ?? 0);
|
||||
return (
|
||||
<li key={opt.id}>
|
||||
{opt.eventOption?.name}{variantLabel} x {opt.quantity} — {formatRand(unitPrice * (opt.quantity || 0))}
|
||||
{unitPrice < (opt.eventOption?.price || 0) && (
|
||||
<span className="ml-1 text-xs text-green-700">(early bird)</span>
|
||||
)}
|
||||
</li>
|
||||
);
|
||||
// A line can span multiple price tranches (e.g. tickets bought before
|
||||
// and after an early-bird tier expired) — render one row per tranche so
|
||||
// each shows its own price, rather than one blended row for the line.
|
||||
const tranches = Array.isArray(opt.tranches) && opt.tranches.length > 0
|
||||
? opt.tranches
|
||||
: [{
|
||||
id: opt.id,
|
||||
quantity: opt.quantity,
|
||||
priceSnapshot: (opt.priceSnapshot !== null && opt.priceSnapshot !== undefined)
|
||||
? Number(opt.priceSnapshot)
|
||||
: (opt.variant?.price ?? opt.eventOption?.price ?? 0),
|
||||
appliedTierId: opt.appliedTierId,
|
||||
}];
|
||||
return tranches.map((t: any, idx: number) => {
|
||||
const unitPrice = Number(t.priceSnapshot || 0);
|
||||
return (
|
||||
<li key={`${opt.id}-${t.id || idx}`}>
|
||||
{opt.eventOption?.name}{variantLabel} x {t.quantity} — {formatRand(unitPrice * (t.quantity || 0))}
|
||||
{t.appliedTierId && (
|
||||
<span className="ml-1 text-xs text-green-700">(early bird)</span>
|
||||
)}
|
||||
</li>
|
||||
);
|
||||
});
|
||||
})}
|
||||
</ul>
|
||||
) : (
|
||||
|
||||
Reference in New Issue
Block a user