// 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 }, }); // Tiered registration pricing (EUR) // Early Bird: before May 15 | Standard: before July 15 | Last Minute: after July 15 const REGISTRATION_PRICING = { early: { perWeek: 120, perMonth: 300 }, standard: { perWeek: 200, perMonth: 500 }, lastMin: { perWeek: 240, perMonth: 600 }, }; // Processing fee percentage (added on top of subtotal) const PROCESSING_FEE_PERCENT = 0.02; // Date-based tier cutoffs const PRICING_TIER_DATES = { earlyEnd: '2026-05-15', // before this date → 'early' standardEnd: '2026-07-15', // before this date → 'standard'; after → 'lastMin' }; function getPricingTier() { const now = new Date(); if (now < new Date(PRICING_TIER_DATES.earlyEnd)) return 'early'; if (now < new Date(PRICING_TIER_DATES.standardEnd)) return 'standard'; return 'lastMin'; } // Accommodation prices (EUR) — flat rates (per week and per month/4-week) const ACCOMMODATION_PRICES = { 'ch-multi': { perWeek: 275, perMonth: 1100 }, 'ch-double': { perWeek: 350, perMonth: 1400 }, 'hh-living': { perWeek: 315, perMonth: 1260 }, 'hh-triple': { perWeek: 350, perMonth: 1400 }, 'hh-twin': { perWeek: 420, perMonth: 1680 }, 'hh-single': { perWeek: 665, perMonth: 2660 }, 'hh-couple': { perWeek: 700, perMonth: 2800 }, }; // 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 — Single Bed in Double 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 regPricing = REGISTRATION_PRICING[tier]; // Full month (4 weeks) gets the month rate; otherwise per-week const registration = weeks === 4 ? regPricing.perMonth : regPricing.perWeek * weeks; let accommodation = 0; if (accommodationType && ACCOMMODATION_PRICES[accommodationType]) { const prices = ACCOMMODATION_PRICES[accommodationType]; accommodation = weeks === 4 ? prices.perMonth : prices.perWeek * 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} |