feat: update accommodation pricing with tiered rates and new room types
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 <noreply@anthropic.com>
This commit is contained in:
parent
2d0bdc7dd1
commit
618de35793
|
|
@ -16,25 +16,25 @@ const ACCOMMODATION_CRITERIA = {
|
||||||
venue: 'Commons Hub',
|
venue: 'Commons Hub',
|
||||||
bedTypes: ['Double'],
|
bedTypes: ['Double'],
|
||||||
},
|
},
|
||||||
'hh-single': {
|
'hh-living': {
|
||||||
venue: 'Herrnhof Villa',
|
venue: 'Herrnhof Villa',
|
||||||
bedTypes: ['Single'],
|
bedTypes: ['Living room', 'Sofa bed', 'Daybed'],
|
||||||
},
|
|
||||||
'hh-double-separate': {
|
|
||||||
venue: 'Herrnhof Villa',
|
|
||||||
bedTypes: ['Double (separate)', 'Twin'],
|
|
||||||
},
|
|
||||||
'hh-double-shared': {
|
|
||||||
venue: 'Herrnhof Villa',
|
|
||||||
bedTypes: ['Double (shared)', 'Double'],
|
|
||||||
},
|
},
|
||||||
'hh-triple': {
|
'hh-triple': {
|
||||||
venue: 'Herrnhof Villa',
|
venue: 'Herrnhof Villa',
|
||||||
bedTypes: ['Triple'],
|
bedTypes: ['Triple'],
|
||||||
},
|
},
|
||||||
'hh-daybed': {
|
'hh-twin': {
|
||||||
venue: 'Herrnhof Villa',
|
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'],
|
||||||
},
|
},
|
||||||
};
|
};
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -35,26 +35,46 @@ const PRICE_PER_WEEK = 300.00;
|
||||||
// Processing fee percentage (added on top of subtotal)
|
// Processing fee percentage (added on top of subtotal)
|
||||||
const PROCESSING_FEE_PERCENT = 0.02;
|
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 = {
|
const ACCOMMODATION_PRICES = {
|
||||||
'ch-multi': 279.30, // Commons Hub shared room (multi-bed)
|
'ch-multi': { '1week': { early: 395, standard: 475, lastMin: 515 }, '4week': { early: 1400, standard: 1600, lastMin: 1700 } },
|
||||||
'ch-double': 356.30, // Commons Hub double room
|
'ch-double': { '1week': { early: 470, standard: 550, lastMin: 590 }, '4week': { early: 1700, standard: 1900, lastMin: 2000 } },
|
||||||
'hh-single': 665.00, // Herrnhof single room
|
'hh-living': { '1week': { early: 435, standard: 515, lastMin: 555 }, '4week': { early: 1560, standard: 1760, lastMin: 1860 } },
|
||||||
'hh-double-separate': 420.00, // Herrnhof double room, separate beds
|
'hh-triple': { '1week': { early: 470, standard: 550, lastMin: 590 }, '4week': { early: 1700, standard: 1900, lastMin: 2000 } },
|
||||||
'hh-double-shared': 350.00, // Herrnhof double room, shared bed
|
'hh-twin': { '1week': { early: 540, standard: 620, lastMin: 660 }, '4week': { early: 1980, standard: 2180, lastMin: 2280 } },
|
||||||
'hh-triple': 350.00, // Herrnhof triple room
|
'hh-single': { '1week': { early: 785, standard: 865, lastMin: 905 }, '4week': { early: 2960, standard: 3160, lastMin: 3260 } },
|
||||||
'hh-daybed': 280.00, // Herrnhof daybed/extra bed
|
'hh-couple': { '1week': { early: 940, standard: 1100, lastMin: 1180 }, '4week': { early: 3400, standard: 3800, lastMin: 4000 } },
|
||||||
};
|
};
|
||||||
|
|
||||||
// Human-readable labels for accommodation types
|
// Human-readable labels for accommodation types
|
||||||
const ACCOMMODATION_LABELS = {
|
const ACCOMMODATION_LABELS = {
|
||||||
'ch-multi': 'Commons Hub — Shared Room',
|
'ch-multi': 'Commons Hub — Bed in Multi-Room',
|
||||||
'ch-double': 'Commons Hub — Double Room',
|
'ch-double': 'Commons Hub — Bed in Double Room',
|
||||||
'hh-single': 'Herrnhof Villa — Single Room',
|
'hh-living': 'Herrnhof Villa — Bed in Living Room',
|
||||||
'hh-double-separate': 'Herrnhof Villa — Double Room (separate beds)',
|
'hh-triple': 'Herrnhof Villa — Bed in Triple Room',
|
||||||
'hh-double-shared': 'Herrnhof Villa — Double Room (shared bed)',
|
'hh-twin': 'Herrnhof Villa — Bed in Twin Room',
|
||||||
'hh-triple': 'Herrnhof Villa — Triple Room',
|
'hh-single': 'Herrnhof Villa — Single Room',
|
||||||
'hh-daybed': 'Herrnhof Villa — Daybed / Extra Bed',
|
'hh-couple': 'Herrnhof Villa — Couple Room',
|
||||||
};
|
};
|
||||||
|
|
||||||
// Legacy ticket labels (kept for backward-compat with existing DB records)
|
// Legacy ticket labels (kept for backward-compat with existing DB records)
|
||||||
|
|
@ -71,10 +91,19 @@ const TICKET_LABELS = {
|
||||||
|
|
||||||
function calculateAmount(ticketType, weeksCount, accommodationType) {
|
function calculateAmount(ticketType, weeksCount, accommodationType) {
|
||||||
const weeks = weeksCount || 1;
|
const weeks = weeksCount || 1;
|
||||||
|
const tier = getPricingTier();
|
||||||
const registration = PRICE_PER_WEEK * weeks;
|
const registration = PRICE_PER_WEEK * weeks;
|
||||||
const accommodation = accommodationType && ACCOMMODATION_PRICES[accommodationType]
|
|
||||||
? ACCOMMODATION_PRICES[accommodationType] * weeks
|
let accommodation = 0;
|
||||||
: 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 subtotal = registration + accommodation;
|
||||||
const processingFee = subtotal * PROCESSING_FEE_PERCENT;
|
const processingFee = subtotal * PROCESSING_FEE_PERCENT;
|
||||||
const total = subtotal + processingFee;
|
const total = subtotal + processingFee;
|
||||||
|
|
@ -84,6 +113,7 @@ function calculateAmount(ticketType, weeksCount, accommodationType) {
|
||||||
subtotal: subtotal.toFixed(2),
|
subtotal: subtotal.toFixed(2),
|
||||||
processingFee: processingFee.toFixed(2),
|
processingFee: processingFee.toFixed(2),
|
||||||
total: total.toFixed(2),
|
total: total.toFixed(2),
|
||||||
|
tier,
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -411,5 +441,5 @@ module.exports = {
|
||||||
createPayment, handleWebhook, getPaymentStatus,
|
createPayment, handleWebhook, getPaymentStatus,
|
||||||
PRICE_PER_WEEK, PROCESSING_FEE_PERCENT,
|
PRICE_PER_WEEK, PROCESSING_FEE_PERCENT,
|
||||||
ACCOMMODATION_PRICES, ACCOMMODATION_LABELS,
|
ACCOMMODATION_PRICES, ACCOMMODATION_LABELS,
|
||||||
TICKET_LABELS, calculateAmount,
|
TICKET_LABELS, calculateAmount, getPricingTier,
|
||||||
};
|
};
|
||||||
|
|
|
||||||
100
apply.html
100
apply.html
|
|
@ -655,13 +655,13 @@
|
||||||
|
|
||||||
<label class="week-card" style="margin-bottom: 0.5rem; cursor: pointer;" onclick="selectRoom('ch-multi')">
|
<label class="week-card" style="margin-bottom: 0.5rem; cursor: pointer;" onclick="selectRoom('ch-multi')">
|
||||||
<input type="radio" name="room_type" value="ch-multi" style="display:none;">
|
<input type="radio" name="room_type" value="ch-multi" style="display:none;">
|
||||||
<h4>Shared Room <span style="float:right; color: var(--forest); font-weight: 600;">€279.30/wk</span></h4>
|
<h4>Bed in Multi-Room <span style="float:right; color: var(--forest); font-weight: 600;">€475/wk</span></h4>
|
||||||
<div class="desc">Multi-bed room (bunk beds). The most affordable option.</div>
|
<div class="desc">Bed in a shared multi-bed room. The most affordable option.</div>
|
||||||
</label>
|
</label>
|
||||||
|
|
||||||
<label class="week-card" style="cursor: pointer;" onclick="selectRoom('ch-double')">
|
<label class="week-card" style="cursor: pointer;" onclick="selectRoom('ch-double')">
|
||||||
<input type="radio" name="room_type" value="ch-double" style="display:none;">
|
<input type="radio" name="room_type" value="ch-double" style="display:none;">
|
||||||
<h4>Double Room <span style="float:right; color: var(--forest); font-weight: 600;">€356.30/wk</span></h4>
|
<h4>Bed in Double Room <span style="float:right; color: var(--forest); font-weight: 600;">€550/wk</span></h4>
|
||||||
<div class="desc">Shared with one other person.</div>
|
<div class="desc">Shared with one other person.</div>
|
||||||
</label>
|
</label>
|
||||||
</div>
|
</div>
|
||||||
|
|
@ -670,34 +670,34 @@
|
||||||
<div id="hh-rooms" style="display: none; padding: 0.75rem 0;">
|
<div id="hh-rooms" style="display: none; padding: 0.75rem 0;">
|
||||||
<label style="font-weight: 600; margin-bottom: 0.75rem; display: block;">Room type — Herrnhof Villa</label>
|
<label style="font-weight: 600; margin-bottom: 0.75rem; display: block;">Room type — Herrnhof Villa</label>
|
||||||
|
|
||||||
<label class="week-card" style="margin-bottom: 0.5rem; cursor: pointer;" onclick="selectRoom('hh-single')">
|
<label class="week-card" style="margin-bottom: 0.5rem; cursor: pointer;" onclick="selectRoom('hh-living')">
|
||||||
<input type="radio" name="room_type" value="hh-single" style="display:none;">
|
<input type="radio" name="room_type" value="hh-living" style="display:none;">
|
||||||
<h4>Single Room <span style="float:right; color: var(--forest); font-weight: 600;">€665.00/wk</span></h4>
|
<h4>Bed in Living Room <span style="float:right; color: var(--forest); font-weight: 600;">€515/wk</span></h4>
|
||||||
<div class="desc">Private room for one person.</div>
|
<div class="desc">Shared living space with flexible sleeping arrangement.</div>
|
||||||
</label>
|
|
||||||
|
|
||||||
<label class="week-card" style="margin-bottom: 0.5rem; cursor: pointer;" onclick="selectRoom('hh-double-separate')">
|
|
||||||
<input type="radio" name="room_type" value="hh-double-separate" style="display:none;">
|
|
||||||
<h4>Double Room (separate beds) <span style="float:right; color: var(--forest); font-weight: 600;">€420.00/wk</span></h4>
|
|
||||||
<div class="desc">Shared room with two separate beds.</div>
|
|
||||||
</label>
|
|
||||||
|
|
||||||
<label class="week-card" style="margin-bottom: 0.5rem; cursor: pointer;" onclick="selectRoom('hh-double-shared')">
|
|
||||||
<input type="radio" name="room_type" value="hh-double-shared" style="display:none;">
|
|
||||||
<h4>Double Room (shared bed) <span style="float:right; color: var(--forest); font-weight: 600;">€350.00/wk</span></h4>
|
|
||||||
<div class="desc">Shared double bed for couples or friends.</div>
|
|
||||||
</label>
|
</label>
|
||||||
|
|
||||||
<label class="week-card" style="margin-bottom: 0.5rem; cursor: pointer;" onclick="selectRoom('hh-triple')">
|
<label class="week-card" style="margin-bottom: 0.5rem; cursor: pointer;" onclick="selectRoom('hh-triple')">
|
||||||
<input type="radio" name="room_type" value="hh-triple" style="display:none;">
|
<input type="radio" name="room_type" value="hh-triple" style="display:none;">
|
||||||
<h4>Triple Room <span style="float:right; color: var(--forest); font-weight: 600;">€350.00/wk</span></h4>
|
<h4>Bed in Triple Room <span style="float:right; color: var(--forest); font-weight: 600;">€550/wk</span></h4>
|
||||||
<div class="desc">Room shared between three people.</div>
|
<div class="desc">Room shared between three people.</div>
|
||||||
</label>
|
</label>
|
||||||
|
|
||||||
<label class="week-card" style="cursor: pointer;" onclick="selectRoom('hh-daybed')">
|
<label class="week-card" style="margin-bottom: 0.5rem; cursor: pointer;" onclick="selectRoom('hh-twin')">
|
||||||
<input type="radio" name="room_type" value="hh-daybed" style="display:none;">
|
<input type="radio" name="room_type" value="hh-twin" style="display:none;">
|
||||||
<h4>Daybed / Extra Bed <span style="float:right; color: var(--forest); font-weight: 600;">€280.00/wk</span></h4>
|
<h4>Bed in Twin Room <span style="float:right; color: var(--forest); font-weight: 600;">€620/wk</span></h4>
|
||||||
<div class="desc">Flexible sleeping spot (daybed or extra bed).</div>
|
<div class="desc">Room shared with one other person, separate beds.</div>
|
||||||
|
</label>
|
||||||
|
|
||||||
|
<label class="week-card" style="margin-bottom: 0.5rem; cursor: pointer;" onclick="selectRoom('hh-single')">
|
||||||
|
<input type="radio" name="room_type" value="hh-single" style="display:none;">
|
||||||
|
<h4>Single Room <span style="float:right; color: var(--forest); font-weight: 600;">€865/wk</span></h4>
|
||||||
|
<div class="desc">Private room for one person.</div>
|
||||||
|
</label>
|
||||||
|
|
||||||
|
<label class="week-card" style="cursor: pointer;" onclick="selectRoom('hh-couple')">
|
||||||
|
<input type="radio" name="room_type" value="hh-couple" style="display:none;">
|
||||||
|
<h4>Couple Room <span style="float:right; color: var(--forest); font-weight: 600;">€1,100/wk</span></h4>
|
||||||
|
<div class="desc">Private room with double bed for couples.</div>
|
||||||
</label>
|
</label>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
@ -779,27 +779,38 @@
|
||||||
const PRICE_PER_WEEK = 300;
|
const PRICE_PER_WEEK = 300;
|
||||||
const PROCESSING_FEE_PERCENT = 0.02;
|
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 = {
|
const ACCOMMODATION_PRICES = {
|
||||||
'ch-multi': 279.30,
|
'ch-multi': { '1week': { early: 395, standard: 475, lastMin: 515 }, '4week': { early: 1400, standard: 1600, lastMin: 1700 } },
|
||||||
'ch-double': 356.30,
|
'ch-double': { '1week': { early: 470, standard: 550, lastMin: 590 }, '4week': { early: 1700, standard: 1900, lastMin: 2000 } },
|
||||||
'hh-single': 665.00,
|
'hh-living': { '1week': { early: 435, standard: 515, lastMin: 555 }, '4week': { early: 1560, standard: 1760, lastMin: 1860 } },
|
||||||
'hh-double-separate': 420.00,
|
'hh-triple': { '1week': { early: 470, standard: 550, lastMin: 590 }, '4week': { early: 1700, standard: 1900, lastMin: 2000 } },
|
||||||
'hh-double-shared': 350.00,
|
'hh-twin': { '1week': { early: 540, standard: 620, lastMin: 660 }, '4week': { early: 1980, standard: 2180, lastMin: 2280 } },
|
||||||
'hh-triple': 350.00,
|
'hh-single': { '1week': { early: 785, standard: 865, lastMin: 905 }, '4week': { early: 2960, standard: 3160, lastMin: 3260 } },
|
||||||
'hh-daybed': 280.00,
|
'hh-couple': { '1week': { early: 940, standard: 1100, lastMin: 1180 }, '4week': { early: 3400, standard: 3800, lastMin: 4000 } },
|
||||||
};
|
};
|
||||||
|
|
||||||
const ACCOMMODATION_LABELS = {
|
const ACCOMMODATION_LABELS = {
|
||||||
'ch-multi': 'Commons Hub — Shared Room',
|
'ch-multi': 'Commons Hub — Bed in Multi-Room',
|
||||||
'ch-double': 'Commons Hub — Double Room',
|
'ch-double': 'Commons Hub — Bed in Double Room',
|
||||||
'hh-single': 'Herrnhof Villa — Single Room',
|
'hh-living': 'Herrnhof Villa — Bed in Living Room',
|
||||||
'hh-double-separate': 'Herrnhof Villa — Double (separate beds)',
|
'hh-triple': 'Herrnhof Villa — Bed in Triple Room',
|
||||||
'hh-double-shared': 'Herrnhof Villa — Double (shared bed)',
|
'hh-twin': 'Herrnhof Villa — Bed in Twin Room',
|
||||||
'hh-triple': 'Herrnhof Villa — Triple Room',
|
'hh-single': 'Herrnhof Villa — Single Room',
|
||||||
'hh-daybed': 'Herrnhof Villa — Daybed / Extra Bed',
|
'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() {
|
function updateProgress() {
|
||||||
const percent = Math.round(((currentStep - 1) / totalSteps) * 100);
|
const percent = Math.round(((currentStep - 1) / totalSteps) * 100);
|
||||||
document.getElementById('progress-fill').style.width = percent + '%';
|
document.getElementById('progress-fill').style.width = percent + '%';
|
||||||
|
|
@ -957,17 +968,22 @@
|
||||||
|
|
||||||
const registration = PRICE_PER_WEEK * weeksCount;
|
const registration = PRICE_PER_WEEK * weeksCount;
|
||||||
const accomType = document.getElementById('accommodation_type').value;
|
const accomType = document.getElementById('accommodation_type').value;
|
||||||
const accomPerWeek = accomType ? (ACCOMMODATION_PRICES[accomType] || 0) : 0;
|
const accommodation = getAccommodationPrice(accomType, weeksCount);
|
||||||
const accommodation = accomPerWeek * weeksCount;
|
|
||||||
const subtotal = registration + accommodation;
|
const subtotal = registration + accommodation;
|
||||||
const fee = subtotal * PROCESSING_FEE_PERCENT;
|
const fee = subtotal * PROCESSING_FEE_PERCENT;
|
||||||
const total = subtotal + fee;
|
const total = subtotal + fee;
|
||||||
|
|
||||||
|
const tierLabel = { early: 'Early Bird', standard: 'Standard', lastMin: 'Last Minute' }[CURRENT_PRICING_TIER];
|
||||||
let html = `<strong>Registration:</strong> €${PRICE_PER_WEEK} × ${weeksCount} wk = €${registration.toFixed(2)}`;
|
let html = `<strong>Registration:</strong> €${PRICE_PER_WEEK} × ${weeksCount} wk = €${registration.toFixed(2)}`;
|
||||||
|
|
||||||
if (accommodation > 0) {
|
if (accommodation > 0) {
|
||||||
const label = ACCOMMODATION_LABELS[accomType] || accomType;
|
const label = ACCOMMODATION_LABELS[accomType] || accomType;
|
||||||
html += `<br><strong>Accommodation:</strong> ${label}<br> €${accomPerWeek.toFixed(2)} × ${weeksCount} wk = €${accommodation.toFixed(2)}`;
|
if (weeksCount === 4) {
|
||||||
|
html += `<br><strong>Accommodation:</strong> ${label}<br> 4-week ${tierLabel} rate = €${accommodation.toFixed(2)}`;
|
||||||
|
} else {
|
||||||
|
const perWeek = ACCOMMODATION_PRICES[accomType]['1week'][CURRENT_PRICING_TIER];
|
||||||
|
html += `<br><strong>Accommodation:</strong> ${label}<br> €${perWeek.toFixed(2)} × ${weeksCount} wk (${tierLabel}) = €${accommodation.toFixed(2)}`;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
html += `<br><strong>Processing fee (2%):</strong> €${fee.toFixed(2)}`;
|
html += `<br><strong>Processing fee (2%):</strong> €${fee.toFixed(2)}`;
|
||||||
|
|
|
||||||
Loading…
Reference in New Issue