diff --git a/api/application.js b/api/application.js index d4d60ea..449c492 100644 --- a/api/application.js +++ b/api/application.js @@ -3,11 +3,13 @@ const { Pool } = require('pg'); const nodemailer = require('nodemailer'); +const { syncApplication } = require('./google-sheets'); +const { createPayment } = require('./mollie'); // Initialize PostgreSQL connection pool const pool = new Pool({ connectionString: process.env.DATABASE_URL, - ssl: process.env.NODE_ENV === 'production' ? { rejectUnauthorized: false } : false + ssl: process.env.DATABASE_SSL === 'true' ? { rejectUnauthorized: false } : false }); // Initialize SMTP transport (Mailcow) @@ -239,13 +241,23 @@ module.exports = async function handler(req, res) { first_name: data.first_name, last_name: data.last_name, email: data.email, + phone: data.phone, city: data.city, country: data.country, attendance_type: data.attendance_type, scholarship_needed: data.scholarship_needed, - motivation: data.motivation + scholarship_reason: data.scholarship_reason, + motivation: data.motivation, + contribution: data.contribution, + how_heard: data.how_heard, + referral_name: data.referral_name, + arrival_date: data.arrival_date, + departure_date: data.departure_date, }; + // Sync to Google Sheets (fire-and-forget backup) + syncApplication(application); + // Send confirmation email to applicant if (process.env.SMTP_PASS) { try { @@ -279,10 +291,32 @@ module.exports = async function handler(req, res) { } } + // Create Mollie payment if ticket was selected and Mollie is configured + let checkoutUrl = null; + if (data.contribution_amount && process.env.MOLLIE_API_KEY) { + try { + const weeksCount = Array.isArray(data.weeks) ? data.weeks.length : 1; + const paymentResult = await createPayment( + application.id, + data.contribution_amount, + weeksCount, + application.email, + application.first_name, + application.last_name + ); + checkoutUrl = paymentResult.checkoutUrl; + console.log(`Mollie payment created: ${paymentResult.paymentId} (${paymentResult.amount} EUR)`); + } catch (paymentError) { + console.error('Failed to create Mollie payment:', paymentError); + // Don't fail the application - payment can be retried + } + } + return res.status(201).json({ success: true, message: 'Application submitted successfully', - applicationId: application.id + applicationId: application.id, + checkoutUrl, }); } catch (error) { diff --git a/api/mollie.js b/api/mollie.js new file mode 100644 index 0000000..9ed4730 --- /dev/null +++ b/api/mollie.js @@ -0,0 +1,284 @@ +// Mollie payment integration +// Handles payment creation and webhook callbacks + +const { createMollieClient } = require('@mollie/api-client'); +const { Pool } = require('pg'); +const nodemailer = require('nodemailer'); + +// 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 || 'noreply@jeffemmett.com', + pass: process.env.SMTP_PASS || '', + }, + tls: { rejectUnauthorized: false }, +}); + +// Ticket price mapping (in EUR) +const TICKET_PRICES = { + 'full-dorm': 1500.00, + 'full-shared': 1800.00, + 'full-single': 3200.00, + 'week-dorm': 425.00, + 'week-shared': 500.00, + 'week-single': 850.00, + 'no-accom': 300.00, // per week +}; + +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', +}; + +function calculateAmount(ticketType, weeksCount) { + const basePrice = TICKET_PRICES[ticketType]; + if (!basePrice) return null; + + // no-accom is priced per week + if (ticketType === 'no-accom') { + return (basePrice * (weeksCount || 1)).toFixed(2); + } + + return basePrice.toFixed(2); +} + +// Create a Mollie payment for an application +async function createPayment(applicationId, ticketType, weeksCount, email, firstName, lastName) { + const amount = calculateAmount(ticketType, weeksCount); + if (!amount) { + throw new Error(`Invalid ticket type: ${ticketType}`); + } + + const baseUrl = process.env.BASE_URL || 'https://votc.jeffemmett.com'; + const description = `Valley of the Commons - ${TICKET_LABELS[ticketType] || ticketType}`; + + const payment = await mollieClient.payments.create({ + amount: { + currency: 'EUR', + value: amount, + }, + description, + redirectUrl: `${baseUrl}/payment-return.html?id=${applicationId}`, + webhookUrl: `${baseUrl}/api/mollie/webhook`, + metadata: { + applicationId, + ticketType, + weeksCount, + }, + }); + + // 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, amount, applicationId] + ); + + return { + paymentId: payment.id, + checkoutUrl: payment.getCheckoutUrl(), + amount, + }; +} + +// Payment confirmation email +const paymentConfirmationEmail = (application) => ({ + subject: 'Payment Confirmed - Valley of the Commons', + html: ` +
Dear ${application.first_name},
+ +Your payment of €${application.payment_amount} for Valley of the Commons has been received.
+ +| Ticket: | +${TICKET_LABELS[application.contribution_amount] || application.contribution_amount} | +
| 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} +
+