feat: add dynamic pricing tiers to CCG registration

Replace hardcoded €80 ticket price with date-based tiers:
- Early bird €80 (until Mar 31)
- Regular €120 (Apr 1 – Jun 30)
- Late €150 (Jul 1+)

Both the register page and checkout API now compute the current
tier at request time, so prices update automatically on cutoff dates.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
This commit is contained in:
Jeff Emmett 2026-03-23 12:45:39 -07:00
parent cdaf09e14d
commit a6c8f0a477
2 changed files with 29 additions and 7 deletions

View File

@ -11,8 +11,19 @@ function getMollie() {
return mollieClient return mollieClient
} }
// Dynamic pricing configuration (in EUR) // Dynamic pricing tiers (in EUR)
const TICKET_PRICE = 80 // €80 early bird const PRICING_TIERS = [
{ label: "Early bird", price: 80, cutoff: "2026-03-31" },
{ label: "Regular", price: 120, cutoff: "2026-07-01" },
{ label: "Late", price: 150, cutoff: "2099-12-31" },
]
function getCurrentTicketPrice(): number {
const now = new Date().toISOString().slice(0, 10)
const tier = PRICING_TIERS.find((t) => now < t.cutoff) ?? PRICING_TIERS[PRICING_TIERS.length - 1]
return tier.price
}
const PROCESSING_FEE_PERCENT = 0.02 // 2% to cover Mollie payment processing fees const PROCESSING_FEE_PERCENT = 0.02 // 2% to cover Mollie payment processing fees
// Accommodation prices per person for 7 nights // Accommodation prices per person for 7 nights
@ -39,8 +50,9 @@ export async function POST(request: NextRequest) {
const registrationData = registrationDataStr ? JSON.parse(registrationDataStr) : null const registrationData = registrationDataStr ? JSON.parse(registrationDataStr) : null
// Calculate subtotal // Calculate subtotal
let subtotal = TICKET_PRICE const ticketPrice = getCurrentTicketPrice()
const descriptionParts = ["CCG 2026 Ticket (€80)"] let subtotal = ticketPrice
const descriptionParts = [`CCG 2026 Ticket (€${ticketPrice})`]
if (includeAccommodation) { if (includeAccommodation) {
const accom = ACCOMMODATION_PRICES[accommodationType] const accom = ACCOMMODATION_PRICES[accommodationType]

View File

@ -30,7 +30,15 @@ export default function RegisterPage() {
dietaryOther: "", dietaryOther: "",
crewConsent: "", crewConsent: "",
}) })
const baseTicketPrice = 80 // Early bird price €80 // Dynamic pricing tiers
const pricingTiers = [
{ label: "Early bird", price: 80, cutoff: "2026-03-31" },
{ label: "Regular", price: 120, cutoff: "2026-07-01" },
{ label: "Late", price: 150, cutoff: "2099-12-31" },
]
const now = new Date().toISOString().slice(0, 10)
const currentTier = pricingTiers.find((t) => now < t.cutoff) ?? pricingTiers[pricingTiers.length - 1]
const baseTicketPrice = currentTier.price
const accommodationPrices: Record<string, { label: string; price: number }> = { const accommodationPrices: Record<string, { label: string; price: number }> = {
"ch-multi": { label: "Bed in shared room (Commons Hub)", price: 279.30 }, "ch-multi": { label: "Bed in shared room (Commons Hub)", price: 279.30 },
@ -133,7 +141,7 @@ export default function RegisterPage() {
<CardHeader> <CardHeader>
<CardTitle>Event Registration</CardTitle> <CardTitle>Event Registration</CardTitle>
<CardDescription> <CardDescription>
Early bird pricing available until March 31, 2026 {currentTier.label} pricing {currentTier.price}
</CardDescription> </CardDescription>
</CardHeader> </CardHeader>
<CardContent> <CardContent>
@ -143,7 +151,9 @@ export default function RegisterPage() {
<div> <div>
<div className="font-medium">CCG 2026 Ticket</div> <div className="font-medium">CCG 2026 Ticket</div>
<div className="text-sm text-muted-foreground"> <div className="text-sm text-muted-foreground">
80 Early bird (until Mar 31) · 120 Regular (Apr-Jun) · 150 Late (after Jul 1) {pricingTiers.map((t, i) => (
<span key={t.label}>{i > 0 ? " · " : ""}{t.price} {t.label}{t === currentTier ? " (current)" : ""}</span>
))}
</div> </div>
<div className="text-xs text-muted-foreground mt-1"> <div className="text-xs text-muted-foreground mt-1">
CCA members: Bring two newcomers, get a free ticket! CCA members: Bring two newcomers, get a free ticket!