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:
@@ -158,7 +158,9 @@ export default function AdminRegistrationsPage() {
|
||||
return "text-gray-700 bg-gray-50";
|
||||
};
|
||||
|
||||
const totalDueFor = (r: any) => (r.registrationOptions || []).reduce((sum: number, opt: any) => {
|
||||
// Backend attaches a tranche-aware totalDueComputed (exact even when a line spans multiple
|
||||
// early-bird prices) — fall back to the old client-side estimate only for stale payloads.
|
||||
const totalDueFor = (r: any) => r.totalDueComputed ?? (r.registrationOptions || []).reduce((sum: number, opt: any) => {
|
||||
const unit = (opt.priceSnapshot !== null && opt.priceSnapshot !== undefined)
|
||||
? Number(opt.priceSnapshot)
|
||||
: (opt.eventOption?.price || 0);
|
||||
@@ -334,25 +336,40 @@ export default function AdminRegistrationsPage() {
|
||||
<div className="mb-3">
|
||||
<div className="text-xs font-semibold text-gray-600 mb-1 uppercase tracking-wide">Ticket options</div>
|
||||
<div className="grid sm:grid-cols-2 gap-2">
|
||||
{r.registrationOptions.map((opt: any) => (
|
||||
<div key={opt.id} className="bg-white border rounded p-2 text-xs">
|
||||
<div className="font-medium">
|
||||
{opt.eventOption?.name || opt.eventOptionId}
|
||||
{opt.variant?.name && <span className="text-gray-500"> ({opt.variant.name})</span>}
|
||||
</div>
|
||||
<div className="text-gray-500">
|
||||
{(() => {
|
||||
const unit = (opt.priceSnapshot !== null && opt.priceSnapshot !== undefined)
|
||||
{r.registrationOptions.map((opt: any) => {
|
||||
// A line can span multiple price tranches (e.g. tickets bought
|
||||
// before and after an early-bird tier expired) — show one row per
|
||||
// tranche so its own price/tier status is accurate, not blended.
|
||||
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);
|
||||
return `Qty: ${opt.quantity} × R ${unit.toFixed(2)} = R ${(unit * (opt.quantity || 0)).toFixed(2)}`;
|
||||
})()}
|
||||
: (opt.variant?.price ?? opt.eventOption?.price ?? 0),
|
||||
appliedTierId: opt.appliedTierId,
|
||||
}];
|
||||
return (
|
||||
<div key={opt.id} className="bg-white border rounded p-2 text-xs">
|
||||
<div className="font-medium">
|
||||
{opt.eventOption?.name || opt.eventOptionId}
|
||||
{opt.variant?.name && <span className="text-gray-500"> ({opt.variant.name})</span>}
|
||||
</div>
|
||||
{tranches.map((t: any, idx: number) => {
|
||||
const unit = Number(t.priceSnapshot || 0);
|
||||
return (
|
||||
<div key={t.id || idx} className="text-gray-500">
|
||||
{`Qty: ${t.quantity} × R ${unit.toFixed(2)} = R ${(unit * (t.quantity || 0)).toFixed(2)}`}
|
||||
{t.appliedTierId && (
|
||||
<span className="text-green-700 text-[10px] ml-1">(early bird)</span>
|
||||
)}
|
||||
</div>
|
||||
);
|
||||
})}
|
||||
</div>
|
||||
{opt.appliedTierId && (
|
||||
<div className="text-green-700 text-[10px] mt-0.5">Early-bird price applied</div>
|
||||
)}
|
||||
</div>
|
||||
))}
|
||||
);
|
||||
})}
|
||||
</div>
|
||||
<div className="text-xs text-gray-700 mt-1 font-medium">Total: R {totalDue.toFixed(2)}</div>
|
||||
</div>
|
||||
|
||||
Reference in New Issue
Block a user