// Mollie payment integration // Handles payment creation and webhook callbacks const { createMollieClient } = require('@mollie/api-client'); const { Pool } = require('pg'); const nodemailer = require('nodemailer'); const { assignBooking } = require('./booking-sheet'); // Initialize PostgreSQL connection pool const pool = new Pool({ connectionString: process.env.DATABASE_URL, ssl: process.env.DATABASE_SSL === 'true' ? { rejectUnauthorized: false } : false }); // Initialize Mollie client const mollieClient = createMollieClient({ apiKey: process.env.MOLLIE_API_KEY, }); // Initialize SMTP transport (Mailcow) const smtp = nodemailer.createTransport({ host: process.env.SMTP_HOST || 'mail.rmail.online', port: parseInt(process.env.SMTP_PORT || '587'), secure: false, auth: { user: process.env.SMTP_USER || 'contact@valleyofthecommons.com', pass: process.env.SMTP_PASS || '', }, tls: { rejectUnauthorized: false }, }); // Base registration price per week (EUR) const PRICE_PER_WEEK = 300.00; // Processing fee percentage (added on top of subtotal) const PROCESSING_FEE_PERCENT = 0.02; // 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': { '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 — 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) const TICKET_LABELS = { 'full-dorm': 'Full Resident - Dorm (4-6 people)', 'full-shared': 'Full Resident - Shared Double', 'full-single': 'Full Resident - Single (deluxe apartment)', 'week-dorm': '1-Week Visitor - Dorm (4-6 people)', 'week-shared': '1-Week Visitor - Shared Double', 'week-single': '1-Week Visitor - Single (deluxe apartment)', 'no-accom': 'Non-Accommodation Pass', 'registration': 'Event Registration', }; function calculateAmount(ticketType, weeksCount, accommodationType) { const weeks = weeksCount || 1; const tier = getPricingTier(); const registration = PRICE_PER_WEEK * weeks; 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; return { registration: registration.toFixed(2), accommodation: accommodation.toFixed(2), subtotal: subtotal.toFixed(2), processingFee: processingFee.toFixed(2), total: total.toFixed(2), tier, }; } // Create a Mollie payment for an application async function createPayment(applicationId, ticketType, weeksCount, email, firstName, lastName, accommodationType, selectedWeeks) { const pricing = calculateAmount(ticketType, weeksCount, accommodationType); const baseUrl = process.env.BASE_URL || 'https://valleyofthecommons.com'; // Build itemized description const parts = [`Registration (${weeksCount} week${weeksCount > 1 ? 's' : ''})`]; if (accommodationType && ACCOMMODATION_PRICES[accommodationType]) { const label = ACCOMMODATION_LABELS[accommodationType] || accommodationType; parts.push(`Accommodation: ${label} (${weeksCount} week${weeksCount > 1 ? 's' : ''})`); } parts.push('incl. 2% processing fee'); const description = `Valley of the Commons - ${parts.join(' + ')}`; const payment = await mollieClient.payments.create({ amount: { currency: 'EUR', value: pricing.total, }, description, redirectUrl: `${baseUrl}/payment-return.html?id=${applicationId}`, webhookUrl: `${baseUrl}/api/mollie/webhook`, metadata: { applicationId, ticketType, weeksCount, accommodationType: accommodationType || null, selectedWeeks: selectedWeeks || [], breakdown: pricing, }, }); // Store Mollie payment ID in database await pool.query( `UPDATE applications SET mollie_payment_id = $1, payment_amount = $2, payment_status = 'pending' WHERE id = $3`, [payment.id, pricing.total, applicationId] ); return { paymentId: payment.id, checkoutUrl: payment.getCheckoutUrl(), amount: pricing.total, pricing, }; } // Payment confirmation email const paymentConfirmationEmail = (application, bookingResult) => { const accomLabel = application.accommodation_type ? (ACCOMMODATION_LABELS[application.accommodation_type] || application.accommodation_type) : null; // Build accommodation row if applicable const accomRow = accomLabel ? `
You have been assigned to ${bookingResult.venue} — Room ${bookingResult.room}, ${bookingResult.bedType}.
Your accommodation request has been noted. Our team will follow up with your room assignment shortly.
Dear ${application.first_name},
Your payment of €${application.payment_amount} for Valley of the Commons has been received.
| Type: | Event Registration${accomLabel ? ' + Accommodation' : ''} |
| Amount: | €${application.payment_amount} |
| Mollie Reference: | ${application.mollie_payment_id} |
Your application is now complete. Our team will review it and get back to you within 2-3 weeks.
If you have any questions, reply to this email and we'll get back to you.
With warmth,
The Valley of the Commons Team
Application ID: ${application.id}
| Guest: | ${application.first_name} ${application.last_name} |
| Email: | ${application.email} |
| Accommodation: | ${accomLabel} |
| Weeks: | ${selectedWeeks.join(', ') || 'N/A'} |
| Status: | ${bookingStatus} |
| Payment: | €${application.payment_amount} |