From 618de35793a0e4749dcc8f6dfff17735afb3248c Mon Sep 17 00:00:00 2001 From: Jeff Emmett Date: Thu, 12 Mar 2026 22:20:16 -0700 Subject: [PATCH] feat: update accommodation pricing with tiered rates and new room types MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Replace flat per-week accommodation prices with tiered pricing (early/standard/last-min) and duration-based rates (1-week vs 4-week). Rename room types: double-separate → twin, double-shared → couple. Add living room option, remove daybed. Co-Authored-By: Claude Opus 4.6 --- api/booking-sheet.js | 24 +++++------ api/mollie.js | 68 +++++++++++++++++++++-------- apply.html | 100 +++++++++++++++++++++++++------------------ 3 files changed, 119 insertions(+), 73 deletions(-) diff --git a/api/booking-sheet.js b/api/booking-sheet.js index f4ffed5..7657d09 100644 --- a/api/booking-sheet.js +++ b/api/booking-sheet.js @@ -16,25 +16,25 @@ const ACCOMMODATION_CRITERIA = { venue: 'Commons Hub', bedTypes: ['Double'], }, - 'hh-single': { + 'hh-living': { venue: 'Herrnhof Villa', - bedTypes: ['Single'], - }, - 'hh-double-separate': { - venue: 'Herrnhof Villa', - bedTypes: ['Double (separate)', 'Twin'], - }, - 'hh-double-shared': { - venue: 'Herrnhof Villa', - bedTypes: ['Double (shared)', 'Double'], + bedTypes: ['Living room', 'Sofa bed', 'Daybed'], }, 'hh-triple': { venue: 'Herrnhof Villa', bedTypes: ['Triple'], }, - 'hh-daybed': { + 'hh-twin': { venue: 'Herrnhof Villa', - bedTypes: ['Daybed', 'Extra bed', 'Sofa bed'], + bedTypes: ['Twin', 'Double (separate)'], + }, + 'hh-single': { + venue: 'Herrnhof Villa', + bedTypes: ['Single'], + }, + 'hh-couple': { + venue: 'Herrnhof Villa', + bedTypes: ['Double (shared)', 'Double', 'Couple'], }, }; diff --git a/api/mollie.js b/api/mollie.js index 160e3e0..ba35ded 100644 --- a/api/mollie.js +++ b/api/mollie.js @@ -35,26 +35,46 @@ const PRICE_PER_WEEK = 300.00; // Processing fee percentage (added on top of subtotal) const PROCESSING_FEE_PERCENT = 0.02; -// Accommodation prices per week (EUR) — same as CCG rates +// Pricing tier — override with a fixed tier, or set to null to use date-based logic +const CURRENT_PRICING_TIER = 'standard'; + +// Date-based tier cutoffs (used when CURRENT_PRICING_TIER is null) +// Set ISO date strings, e.g. '2026-05-01' +const PRICING_TIER_DATES = { + earlyEnd: null, // before this date → 'early' + standardEnd: null, // before this date → 'standard'; after → 'lastMin' +}; + +function getPricingTier() { + if (CURRENT_PRICING_TIER) return CURRENT_PRICING_TIER; + const now = new Date(); + if (PRICING_TIER_DATES.earlyEnd && now < new Date(PRICING_TIER_DATES.earlyEnd)) return 'early'; + if (PRICING_TIER_DATES.standardEnd && now < new Date(PRICING_TIER_DATES.standardEnd)) return 'standard'; + return 'lastMin'; +} + +// Accommodation prices (EUR) — tiered by duration and booking window +// 1week = per-week rate (multiply by weeks for 1–3 week stays) +// 4week = flat total for a full 4-week stay const ACCOMMODATION_PRICES = { - 'ch-multi': 279.30, // Commons Hub shared room (multi-bed) - 'ch-double': 356.30, // Commons Hub double room - 'hh-single': 665.00, // Herrnhof single room - 'hh-double-separate': 420.00, // Herrnhof double room, separate beds - 'hh-double-shared': 350.00, // Herrnhof double room, shared bed - 'hh-triple': 350.00, // Herrnhof triple room - 'hh-daybed': 280.00, // Herrnhof daybed/extra bed + 'ch-multi': { '1week': { early: 395, standard: 475, lastMin: 515 }, '4week': { early: 1400, standard: 1600, lastMin: 1700 } }, + 'ch-double': { '1week': { early: 470, standard: 550, lastMin: 590 }, '4week': { early: 1700, standard: 1900, lastMin: 2000 } }, + 'hh-living': { '1week': { early: 435, standard: 515, lastMin: 555 }, '4week': { early: 1560, standard: 1760, lastMin: 1860 } }, + 'hh-triple': { '1week': { early: 470, standard: 550, lastMin: 590 }, '4week': { early: 1700, standard: 1900, lastMin: 2000 } }, + 'hh-twin': { '1week': { early: 540, standard: 620, lastMin: 660 }, '4week': { early: 1980, standard: 2180, lastMin: 2280 } }, + 'hh-single': { '1week': { early: 785, standard: 865, lastMin: 905 }, '4week': { early: 2960, standard: 3160, lastMin: 3260 } }, + 'hh-couple': { '1week': { early: 940, standard: 1100, lastMin: 1180 }, '4week': { early: 3400, standard: 3800, lastMin: 4000 } }, }; // Human-readable labels for accommodation types const ACCOMMODATION_LABELS = { - 'ch-multi': 'Commons Hub — Shared Room', - 'ch-double': 'Commons Hub — Double Room', - 'hh-single': 'Herrnhof Villa — Single Room', - 'hh-double-separate': 'Herrnhof Villa — Double Room (separate beds)', - 'hh-double-shared': 'Herrnhof Villa — Double Room (shared bed)', - 'hh-triple': 'Herrnhof Villa — Triple Room', - 'hh-daybed': 'Herrnhof Villa — Daybed / Extra Bed', + 'ch-multi': 'Commons Hub — Bed in Multi-Room', + 'ch-double': 'Commons Hub — Bed in Double Room', + 'hh-living': 'Herrnhof Villa — Bed in Living Room', + 'hh-triple': 'Herrnhof Villa — Bed in Triple Room', + 'hh-twin': 'Herrnhof Villa — Bed in Twin Room', + 'hh-single': 'Herrnhof Villa — Single Room', + 'hh-couple': 'Herrnhof Villa — Couple Room', }; // Legacy ticket labels (kept for backward-compat with existing DB records) @@ -71,10 +91,19 @@ const TICKET_LABELS = { function calculateAmount(ticketType, weeksCount, accommodationType) { const weeks = weeksCount || 1; + const tier = getPricingTier(); const registration = PRICE_PER_WEEK * weeks; - const accommodation = accommodationType && ACCOMMODATION_PRICES[accommodationType] - ? ACCOMMODATION_PRICES[accommodationType] * weeks - : 0; + + let accommodation = 0; + if (accommodationType && ACCOMMODATION_PRICES[accommodationType]) { + const prices = ACCOMMODATION_PRICES[accommodationType]; + if (weeks === 4) { + accommodation = prices['4week'][tier]; + } else { + accommodation = prices['1week'][tier] * weeks; + } + } + const subtotal = registration + accommodation; const processingFee = subtotal * PROCESSING_FEE_PERCENT; const total = subtotal + processingFee; @@ -84,6 +113,7 @@ function calculateAmount(ticketType, weeksCount, accommodationType) { subtotal: subtotal.toFixed(2), processingFee: processingFee.toFixed(2), total: total.toFixed(2), + tier, }; } @@ -411,5 +441,5 @@ module.exports = { createPayment, handleWebhook, getPaymentStatus, PRICE_PER_WEEK, PROCESSING_FEE_PERCENT, ACCOMMODATION_PRICES, ACCOMMODATION_LABELS, - TICKET_LABELS, calculateAmount, + TICKET_LABELS, calculateAmount, getPricingTier, }; diff --git a/apply.html b/apply.html index fb4785e..38b290b 100644 --- a/apply.html +++ b/apply.html @@ -655,13 +655,13 @@ @@ -670,34 +670,34 @@ @@ -779,27 +779,38 @@ const PRICE_PER_WEEK = 300; const PROCESSING_FEE_PERCENT = 0.02; - // Accommodation prices per week (must match api/mollie.js) + // Current pricing tier (must match api/mollie.js) + const CURRENT_PRICING_TIER = 'standard'; + + // Accommodation prices — tiered by duration and booking window (must match api/mollie.js) const ACCOMMODATION_PRICES = { - 'ch-multi': 279.30, - 'ch-double': 356.30, - 'hh-single': 665.00, - 'hh-double-separate': 420.00, - 'hh-double-shared': 350.00, - 'hh-triple': 350.00, - 'hh-daybed': 280.00, + 'ch-multi': { '1week': { early: 395, standard: 475, lastMin: 515 }, '4week': { early: 1400, standard: 1600, lastMin: 1700 } }, + 'ch-double': { '1week': { early: 470, standard: 550, lastMin: 590 }, '4week': { early: 1700, standard: 1900, lastMin: 2000 } }, + 'hh-living': { '1week': { early: 435, standard: 515, lastMin: 555 }, '4week': { early: 1560, standard: 1760, lastMin: 1860 } }, + 'hh-triple': { '1week': { early: 470, standard: 550, lastMin: 590 }, '4week': { early: 1700, standard: 1900, lastMin: 2000 } }, + 'hh-twin': { '1week': { early: 540, standard: 620, lastMin: 660 }, '4week': { early: 1980, standard: 2180, lastMin: 2280 } }, + 'hh-single': { '1week': { early: 785, standard: 865, lastMin: 905 }, '4week': { early: 2960, standard: 3160, lastMin: 3260 } }, + 'hh-couple': { '1week': { early: 940, standard: 1100, lastMin: 1180 }, '4week': { early: 3400, standard: 3800, lastMin: 4000 } }, }; const ACCOMMODATION_LABELS = { - 'ch-multi': 'Commons Hub — Shared Room', - 'ch-double': 'Commons Hub — Double Room', - 'hh-single': 'Herrnhof Villa — Single Room', - 'hh-double-separate': 'Herrnhof Villa — Double (separate beds)', - 'hh-double-shared': 'Herrnhof Villa — Double (shared bed)', - 'hh-triple': 'Herrnhof Villa — Triple Room', - 'hh-daybed': 'Herrnhof Villa — Daybed / Extra Bed', + 'ch-multi': 'Commons Hub — Bed in Multi-Room', + 'ch-double': 'Commons Hub — Bed in Double Room', + 'hh-living': 'Herrnhof Villa — Bed in Living Room', + 'hh-triple': 'Herrnhof Villa — Bed in Triple Room', + 'hh-twin': 'Herrnhof Villa — Bed in Twin Room', + 'hh-single': 'Herrnhof Villa — Single Room', + 'hh-couple': 'Herrnhof Villa — Couple Room', }; + function getAccommodationPrice(accomType, weeksCount) { + if (!accomType || !ACCOMMODATION_PRICES[accomType]) return 0; + const prices = ACCOMMODATION_PRICES[accomType]; + const tier = CURRENT_PRICING_TIER; + if (weeksCount === 4) return prices['4week'][tier]; + return prices['1week'][tier] * weeksCount; + } + function updateProgress() { const percent = Math.round(((currentStep - 1) / totalSteps) * 100); document.getElementById('progress-fill').style.width = percent + '%'; @@ -957,17 +968,22 @@ const registration = PRICE_PER_WEEK * weeksCount; const accomType = document.getElementById('accommodation_type').value; - const accomPerWeek = accomType ? (ACCOMMODATION_PRICES[accomType] || 0) : 0; - const accommodation = accomPerWeek * weeksCount; + const accommodation = getAccommodationPrice(accomType, weeksCount); const subtotal = registration + accommodation; const fee = subtotal * PROCESSING_FEE_PERCENT; const total = subtotal + fee; + const tierLabel = { early: 'Early Bird', standard: 'Standard', lastMin: 'Last Minute' }[CURRENT_PRICING_TIER]; let html = `Registration: €${PRICE_PER_WEEK} × ${weeksCount} wk = €${registration.toFixed(2)}`; if (accommodation > 0) { const label = ACCOMMODATION_LABELS[accomType] || accomType; - html += `
Accommodation: ${label}
  €${accomPerWeek.toFixed(2)} × ${weeksCount} wk = €${accommodation.toFixed(2)}`; + if (weeksCount === 4) { + html += `
Accommodation: ${label}
  4-week ${tierLabel} rate = €${accommodation.toFixed(2)}`; + } else { + const perWeek = ACCOMMODATION_PRICES[accomType]['1week'][CURRENT_PRICING_TIER]; + html += `
Accommodation: ${label}
  €${perWeek.toFixed(2)} × ${weeksCount} wk (${tierLabel}) = €${accommodation.toFixed(2)}`; + } } html += `
Processing fee (2%): €${fee.toFixed(2)}`;