diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..97725b4 --- /dev/null +++ b/.gitignore @@ -0,0 +1,4 @@ +node_modules/ +data/ +.env +*.log diff --git a/Dockerfile b/Dockerfile index 37fadba..99dd94f 100644 --- a/Dockerfile +++ b/Dockerfile @@ -24,6 +24,7 @@ COPY --from=builder /app/node_modules ./node_modules # Copy application files COPY package*.json ./ COPY server.js ./ +COPY api/ ./api/ COPY *.html ./ COPY images/ ./images/ diff --git a/api/booking-sheet.js b/api/booking-sheet.js new file mode 100644 index 0000000..ff99496 --- /dev/null +++ b/api/booking-sheet.js @@ -0,0 +1,255 @@ +// Booking sheet integration for WORLDPLAY +// Manages bed assignments on a Google Sheets booking sheet +// Adapted from VotC booking-sheet.js for single-week event + +const fs = require('fs'); + +let sheetsClient = null; + +// Accommodation criteria mapping — maps accommodation_type to sheet matching rules +const ACCOMMODATION_CRITERIA = { + 'ch-shared': { + venue: 'Commons Hub', + bedTypes: ['Bunk up', 'Bunk down', 'Single'], + }, + 'ch-double': { + venue: 'Commons Hub', + bedTypes: ['Double'], + }, + 'hh-living': { + venue: 'Herrnhof Villa', + bedTypes: ['Living room', 'Sofa bed', 'Daybed'], + }, + 'hh-triple': { + venue: 'Herrnhof Villa', + bedTypes: ['Triple'], + }, + 'hh-twin': { + venue: 'Herrnhof Villa', + bedTypes: ['Twin', 'Double (separate)'], + }, + 'hh-single': { + venue: 'Herrnhof Villa', + bedTypes: ['Single'], + }, + 'hh-couple': { + venue: 'Herrnhof Villa', + bedTypes: ['Double (shared)', 'Double', 'Couple'], + }, +}; + +// Single week for WORLDPLAY (June 7-13) +const WEEK_COLUMNS = ['Week 1']; + +function getCredentials() { + const filePath = process.env.GOOGLE_SERVICE_ACCOUNT_FILE; + if (filePath) { + try { + return JSON.parse(fs.readFileSync(filePath, 'utf8')); + } catch (err) { + console.error('[Booking Sheet] Failed to read credentials file:', err.message); + return null; + } + } + + const raw = process.env.GOOGLE_SERVICE_ACCOUNT; + if (!raw) return null; + try { + return JSON.parse(raw.trim()); + } catch { + console.error('[Booking Sheet] Failed to parse service account JSON'); + return null; + } +} + +async function getSheetsClient() { + if (sheetsClient) return sheetsClient; + + const creds = getCredentials(); + if (!creds) return null; + + const { google } = require('googleapis'); + const auth = new google.auth.GoogleAuth({ + credentials: creds, + scopes: ['https://www.googleapis.com/auth/spreadsheets'], + }); + sheetsClient = google.sheets({ version: 'v4', auth }); + return sheetsClient; +} + +/** + * Read and parse the booking sheet. + * Expected structure per venue section: + * Row: "Commons Hub" or "Herrnhof Villa" (venue header) + * Row: Room | Bed Type | Week 1 (column headers) + * Row: 5 | Bunk up | (bed rows) + * ... + * Empty row (section separator) + * + * Returns array of bed objects: + * { venue, room, bedType, rowIndex, weekColumns: { 'Week 1': colIndex }, occupancy: { 'Week 1': 'Guest Name' | null } } + */ +async function parseBookingSheet() { + const sheets = await getSheetsClient(); + if (!sheets) { + console.log('[Booking Sheet] No credentials configured — skipping'); + return null; + } + + const sheetId = process.env.BOOKING_SHEET_ID; + if (!sheetId) { + console.log('[Booking Sheet] No BOOKING_SHEET_ID configured — skipping'); + return null; + } + const sheetName = process.env.BOOKING_SHEET_TAB || 'Booking Sheet'; + + const response = await sheets.spreadsheets.values.get({ + spreadsheetId: sheetId, + range: `${sheetName}!A:G`, + }); + + const rows = response.data.values || []; + const beds = []; + let currentVenue = null; + let weekColIndexes = {}; + + for (let i = 0; i < rows.length; i++) { + const row = rows[i]; + if (!row || row.length === 0 || row.every(cell => !cell || !cell.toString().trim())) { + currentVenue = null; + weekColIndexes = {}; + continue; + } + + const firstCell = (row[0] || '').toString().trim(); + + // Check if this is a venue header + if (firstCell === 'Commons Hub' || firstCell === 'Herrnhof Villa') { + currentVenue = firstCell; + weekColIndexes = {}; + continue; + } + + // Check if this is the column header row (contains "Room" and week columns) + if (firstCell.toLowerCase() === 'room' && currentVenue) { + for (let c = 0; c < row.length; c++) { + const header = (row[c] || '').toString().trim(); + if (WEEK_COLUMNS.includes(header)) { + weekColIndexes[header] = c; + } + } + continue; + } + + // If we have a venue and week columns, this is a bed row + if (currentVenue && Object.keys(weekColIndexes).length > 0 && firstCell) { + const room = firstCell; + const bedType = (row[1] || '').toString().trim(); + if (!bedType) continue; + + const occupancy = {}; + for (const [week, colIdx] of Object.entries(weekColIndexes)) { + const cellValue = (row[colIdx] || '').toString().trim(); + occupancy[week] = cellValue || null; + } + + beds.push({ + venue: currentVenue, + room, + bedType, + rowIndex: i, + weekColumns: { ...weekColIndexes }, + occupancy, + }); + } + } + + return beds; +} + +/** + * Find an available bed matching the accommodation criteria. + * A bed is "available" if the Week 1 column is empty. + */ +function findAvailableBed(beds, accommodationType) { + const criteria = ACCOMMODATION_CRITERIA[accommodationType]; + if (!criteria) { + console.error(`[Booking Sheet] Unknown accommodation type: ${accommodationType}`); + return null; + } + + for (const bed of beds) { + if (bed.venue !== criteria.venue) continue; + + const bedTypeLower = bed.bedType.toLowerCase(); + const matchesBedType = criteria.bedTypes.some(bt => bedTypeLower.includes(bt.toLowerCase())); + if (!matchesBedType) continue; + + // Check Week 1 is empty + if (!bed.occupancy['Week 1']) { + return bed; + } + } + + return null; +} + +/** + * Assign a guest to a bed on the booking sheet. + * Writes guest name to the Week 1 column for the matched bed row. + * + * @param {string} guestName - Full name of the guest + * @param {string} accommodationType - e.g. 'ch-shared', 'hh-single' + * @returns {object} Result with success status, venue, room, bedType + */ +async function assignBooking(guestName, accommodationType) { + if (!accommodationType) { + return { success: false, reason: 'Missing accommodation type' }; + } + + try { + const beds = await parseBookingSheet(); + if (!beds) { + return { success: false, reason: 'Booking sheet not configured' }; + } + + const bed = findAvailableBed(beds, accommodationType); + if (!bed) { + console.warn(`[Booking Sheet] No available bed for ${accommodationType}`); + return { success: false, reason: 'No available bed matching criteria' }; + } + + const sheets = await getSheetsClient(); + const sheetId = process.env.BOOKING_SHEET_ID; + const sheetName = process.env.BOOKING_SHEET_TAB || 'Booking Sheet'; + + const weekCol = bed.weekColumns['Week 1']; + if (weekCol === undefined) { + return { success: false, reason: 'Week 1 column not found' }; + } + + const colLetter = String.fromCharCode(65 + weekCol); + const rowNum = bed.rowIndex + 1; // Sheets is 1-indexed + + await sheets.spreadsheets.values.update({ + spreadsheetId: sheetId, + range: `${sheetName}!${colLetter}${rowNum}`, + valueInputOption: 'USER_ENTERED', + requestBody: { values: [[guestName]] }, + }); + + console.log(`[Booking Sheet] Assigned ${guestName} to ${bed.venue} Room ${bed.room} (${bed.bedType})`); + + return { + success: true, + venue: bed.venue, + room: bed.room, + bedType: bed.bedType, + }; + } catch (error) { + console.error('[Booking Sheet] Assignment failed:', error); + return { success: false, reason: error.message }; + } +} + +module.exports = { assignBooking, parseBookingSheet, findAvailableBed, ACCOMMODATION_CRITERIA }; diff --git a/docker-compose.yml b/docker-compose.yml index 921b5ca..c144633 100644 --- a/docker-compose.yml +++ b/docker-compose.yml @@ -23,6 +23,13 @@ services: - LISTMONK_DB_USER=listmonk - LISTMONK_DB_PASS=listmonk_secure_2025 - LISTMONK_LIST_ID=20 + # Mollie payment integration + - MOLLIE_API_KEY=${MOLLIE_API_KEY} + - BASE_URL=${BASE_URL:-https://worldplay.art} + # Booking sheet (occupancy/bed assignment) + - BOOKING_SHEET_ID=${BOOKING_SHEET_ID:-1LGEjsYpaHQn7em-dJPutL-C3t47UYSbqLEsGUVD8aIc} + - BOOKING_SHEET_TAB=${BOOKING_SHEET_TAB:-Booking Sheet} + - EMAIL_TEAM=${EMAIL_TEAM:-hello@worldplay.art} volumes: - worldplay-data:/app/data - ./google-service-account.json:/app/secrets/google-service-account.json:ro diff --git a/index.html b/index.html index 9306af7..9e680ed 100644 --- a/index.html +++ b/index.html @@ -943,6 +943,227 @@ color: var(--accent-magenta); } + /* Step indicator */ + .step-indicator { + display: flex; + justify-content: center; + gap: 0; + margin-bottom: 2rem; + } + + .step-indicator .step { + display: flex; + align-items: center; + gap: 0.5rem; + padding: 0.5rem 1.25rem; + font-size: 0.85rem; + color: var(--text-secondary); + opacity: 0.5; + transition: all 0.3s; + } + + .step-indicator .step.active { + opacity: 1; + color: var(--accent-cyan); + } + + .step-indicator .step.completed { + opacity: 0.8; + color: var(--accent-green); + } + + .step-indicator .step-number { + width: 28px; + height: 28px; + border-radius: 50%; + border: 2px solid currentColor; + display: flex; + align-items: center; + justify-content: center; + font-weight: 700; + font-size: 0.8rem; + } + + .step-indicator .step.completed .step-number { + background: var(--accent-green); + border-color: var(--accent-green); + color: #0a0a0f; + } + + .step-indicator .step-divider { + width: 40px; + height: 2px; + background: rgba(157, 78, 221, 0.3); + align-self: center; + } + + /* Form step visibility */ + .form-step { display: none; } + .form-step.active { display: block; } + + /* Accommodation toggle */ + .accom-toggle { + display: flex; + gap: 1rem; + margin-bottom: 1.5rem; + } + + .accom-toggle-btn { + flex: 1; + padding: 1rem; + background: var(--bg-input); + border: 2px solid rgba(157, 78, 221, 0.2); + border-radius: 10px; + color: var(--text-secondary); + font-family: 'Space Grotesk', sans-serif; + font-size: 0.95rem; + cursor: pointer; + transition: all 0.3s; + text-align: center; + } + + .accom-toggle-btn:hover { + border-color: var(--accent-purple); + } + + .accom-toggle-btn.selected { + border-color: var(--accent-cyan); + background: rgba(0, 217, 255, 0.08); + color: var(--text-primary); + } + + /* Accommodation cards */ + .accom-cards { + display: none; + flex-direction: column; + gap: 0.75rem; + margin-bottom: 1.5rem; + } + + .accom-cards.visible { display: flex; } + + .accom-venue-label { + font-size: 0.8rem; + color: var(--accent-purple); + text-transform: uppercase; + letter-spacing: 0.1em; + margin-top: 0.75rem; + margin-bottom: 0.25rem; + } + + .accom-card { + display: flex; + justify-content: space-between; + align-items: center; + padding: 1rem 1.25rem; + background: var(--bg-input); + border: 2px solid rgba(157, 78, 221, 0.15); + border-radius: 10px; + cursor: pointer; + transition: all 0.3s; + } + + .accom-card:hover { + border-color: var(--accent-purple); + background: rgba(157, 78, 221, 0.08); + } + + .accom-card.selected { + border-color: var(--accent-cyan); + background: rgba(0, 217, 255, 0.08); + } + + .accom-card .accom-name { + font-size: 0.95rem; + color: var(--text-primary); + } + + .accom-card .accom-price { + font-family: 'Space Mono', monospace; + font-size: 0.9rem; + color: var(--accent-cyan); + white-space: nowrap; + } + + .accom-card .accom-note { + font-size: 0.75rem; + color: var(--text-secondary); + margin-top: 2px; + } + + /* Price summary box */ + .price-summary { + background: rgba(157, 78, 221, 0.08); + border: 1px solid rgba(157, 78, 221, 0.3); + border-radius: 12px; + padding: 1.5rem; + margin-bottom: 1.5rem; + } + + .price-summary h4 { + font-size: 0.85rem; + color: var(--accent-purple); + text-transform: uppercase; + letter-spacing: 0.1em; + margin-bottom: 1rem; + } + + .price-line { + display: flex; + justify-content: space-between; + padding: 0.4rem 0; + font-size: 0.9rem; + color: var(--text-secondary); + } + + .price-line.total { + border-top: 1px solid rgba(157, 78, 221, 0.3); + margin-top: 0.5rem; + padding-top: 0.75rem; + font-weight: 700; + font-size: 1.1rem; + color: var(--text-primary); + } + + .price-line.total .price-amount { + color: var(--accent-green); + } + + .price-amount { + font-family: 'Space Mono', monospace; + } + + /* Food disclaimer info box */ + .info-box { + background: rgba(255, 149, 0, 0.08); + border: 1px solid rgba(255, 149, 0, 0.3); + border-radius: 10px; + padding: 1rem 1.25rem; + margin-bottom: 1.5rem; + font-size: 0.85rem; + color: var(--text-secondary); + line-height: 1.6; + } + + .info-box strong { + color: var(--accent-orange); + } + + /* Back button */ + .btn-back { + background: transparent; + color: var(--text-secondary); + border: 1px solid rgba(157, 78, 221, 0.3); + margin-bottom: 1rem; + font-size: 0.85rem; + padding: 0.6rem 1.25rem; + } + + .btn-back:hover { + color: var(--text-primary); + border-color: var(--accent-purple); + } + /* Call to Action */ .cta-section { text-align: center; @@ -1491,8 +1712,8 @@
-

Register Interest

-

This is the first edition of WORLDPLAY and spaces are limited to 60 participants. Express your interest early to secure your place.

+

Register & Pay

+

WORLDPLAY spaces are limited to 60 participants. Complete registration and payment to secure your place.

@@ -1523,132 +1744,252 @@
-
-

Express Interest

-

We'll be in touch with next steps

+
+
+ 1 + Your Info +
+
+
+ 2 + Accommodation & Payment +
-
+ +
+
+

Your Details

+

Tell us about yourself

+
+ +
+
+ + +
+
+ + +
+
+
- - + +
+
- - + +
-
-
- - -
+
+ + +
-
- - -
+ -
- - -
+
+ +
+ + + + + +
+
- +
+ +
+ + + + + + + + +
+
-
- -
- - - - -
-
- -
- - - - - - - - +
+
+ +
-
- - -
+ +
+
+

Accommodation & Payment

+

Choose your accommodation and complete payment

+
-
- -
+ -
- +
+ +
+ + +
+
+ +
+
Commons Hub
+
+
+
Shared Room
+
Bunk beds / shared room
+
+
€275
+
+
+
+
Double Room
+
Double bed, private or shared
+
+
€350
+
+ +
Herrnhof Villa
+
+
+
Living Room
+
Sofa bed / daybed in shared living area
+
+
€315
+
+
+
+
Triple Room
+
Shared with two others
+
+
€350
+
+
+
+
Twin Room
+
Two separate beds, shared with one other
+
+
€420
+
+
+
+
Single Room
+
Private room for one
+
+
€665
+
+
+
+
Couple Room
+
Double bed, private room for two
+
+
€700
+
+
+ +
+

Price Summary

+
+ Participation fee + €50.00 +
+ +
+ Processing fee (2%) + €1.00 +
+
+ Total + €51.00 +
+
+ +
+ About food: Food is not included in this payment. Expect approximately €15–20 per person per day. We'll be in touch about food choices and dietary preferences before the event. +
+ +
+ +
+ +
@@ -1662,7 +2003,7 @@

Ready to Hijack Reality?

Join fellow dreamers, makers, and reality-benders in prefiguring postcapitalist futures.

@@ -1897,16 +2238,52 @@ observer.observe(el); }); - // Form submission + // ===== Two-step registration + payment flow ===== const form = document.getElementById('registration-form'); - const formMessage = document.getElementById('form-message'); + const step1 = document.getElementById('step-1'); + const step2 = document.getElementById('step-2'); + const stepInd1 = document.getElementById('step-ind-1'); + const stepInd2 = document.getElementById('step-ind-2'); + const msgStep1 = document.getElementById('form-message-step1'); + const msgStep2 = document.getElementById('form-message-step2'); + const btnToStep2 = document.getElementById('btn-to-step-2'); + const btnBack = document.getElementById('btn-back-step1'); + const btnPay = document.getElementById('btn-pay'); - form.addEventListener('submit', async (e) => { - e.preventDefault(); + let registrationId = null; + let selectedAccom = null; // null = no accommodation + function showMsg(el, text, type) { + el.textContent = text; + el.className = 'form-message ' + type; + } + + function clearMsg(el) { + el.textContent = ''; + el.className = 'form-message'; + } + + // Step 1 → Step 2 transition + btnToStep2.addEventListener('click', async () => { + clearMsg(msgStep1); + + // Validate required fields + const firstName = form.querySelector('[name="firstName"]').value.trim(); + const lastName = form.querySelector('[name="lastName"]').value.trim(); + const email = form.querySelector('[name="email"]').value.trim(); + + if (!firstName || !lastName || !email) { + showMsg(msgStep1, 'Please fill in all required fields.', 'error'); + return; + } + if (!/^[^\s@]+@[^\s@]+\.[^\s@]+$/.test(email)) { + showMsg(msgStep1, 'Please enter a valid email address.', 'error'); + return; + } + + // Collect form data const formData = new FormData(form); const data = {}; - formData.forEach((value, key) => { if (key === 'interests' || key === 'contribute') { if (!data[key]) data[key] = []; @@ -1916,30 +2293,150 @@ } }); + btnToStep2.disabled = true; + btnToStep2.textContent = 'Saving...'; + try { const response = await fetch('/api/register', { method: 'POST', - headers: { - 'Content-Type': 'application/json', - }, + headers: { 'Content-Type': 'application/json' }, body: JSON.stringify(data), }); const result = await response.json(); - if (response.ok) { - formMessage.textContent = '✓ Thank you! We\'ll be in touch soon with next steps.'; - formMessage.className = 'form-message success'; - form.reset(); - } else { - throw new Error(result.error || 'Something went wrong'); + if (!response.ok) { + throw new Error(result.error || 'Registration failed'); } + + registrationId = result.id; + + // Transition to step 2 + step1.classList.remove('active'); + step2.classList.add('active'); + stepInd1.classList.remove('active'); + stepInd1.classList.add('completed'); + stepInd2.classList.add('active'); + + // Scroll to form top + form.scrollIntoView({ behavior: 'smooth', block: 'start' }); + } catch (error) { - formMessage.textContent = '✗ ' + error.message; - formMessage.className = 'form-message error'; + showMsg(msgStep1, error.message, 'error'); + } finally { + btnToStep2.disabled = false; + btnToStep2.textContent = 'Continue to Accommodation & Payment'; } }); + // Back button + btnBack.addEventListener('click', () => { + step2.classList.remove('active'); + step1.classList.add('active'); + stepInd2.classList.remove('active'); + stepInd1.classList.remove('completed'); + stepInd1.classList.add('active'); + }); + + // Accommodation toggle (yes/no) + const accomCards = document.getElementById('accom-cards'); + document.querySelectorAll('.accom-toggle-btn').forEach(btn => { + btn.addEventListener('click', () => { + document.querySelectorAll('.accom-toggle-btn').forEach(b => b.classList.remove('selected')); + btn.classList.add('selected'); + + if (btn.dataset.accom === 'yes') { + accomCards.classList.add('visible'); + } else { + accomCards.classList.remove('visible'); + selectedAccom = null; + document.querySelectorAll('.accom-card').forEach(c => c.classList.remove('selected')); + updatePriceSummary(); + } + }); + }); + + // Accommodation card selection + document.querySelectorAll('.accom-card').forEach(card => { + card.addEventListener('click', () => { + document.querySelectorAll('.accom-card').forEach(c => c.classList.remove('selected')); + card.classList.add('selected'); + selectedAccom = card.dataset.type; + updatePriceSummary(); + }); + }); + + // Live price calculator + function updatePriceSummary() { + const participation = 50; + const accomLine = document.getElementById('price-accom-line'); + const accomLabel = document.getElementById('price-accom-label'); + const accomAmount = document.getElementById('price-accom-amount'); + const processingEl = document.getElementById('price-processing'); + const totalEl = document.getElementById('price-total'); + + let accomPrice = 0; + if (selectedAccom) { + const card = document.querySelector(`.accom-card[data-type="${selectedAccom}"]`); + accomPrice = parseInt(card.dataset.price); + accomLabel.textContent = card.querySelector('.accom-name').textContent + ' (7 nights)'; + accomAmount.textContent = '\u20AC' + accomPrice.toFixed(2); + accomLine.style.display = 'flex'; + } else { + accomLine.style.display = 'none'; + } + + const subtotal = participation + accomPrice; + const processing = Math.round(subtotal * 0.02 * 100) / 100; + const total = subtotal + processing; + + processingEl.textContent = '\u20AC' + processing.toFixed(2); + totalEl.textContent = '\u20AC' + total.toFixed(2); + } + + // Pay button — create Mollie checkout + btnPay.addEventListener('click', async () => { + clearMsg(msgStep2); + + // If accommodation is toggled to "yes" but no card selected + const accomToggled = document.querySelector('.accom-toggle-btn[data-accom="yes"]').classList.contains('selected'); + if (accomToggled && !selectedAccom) { + showMsg(msgStep2, 'Please select an accommodation option.', 'error'); + return; + } + + btnPay.disabled = true; + btnPay.textContent = 'Creating payment...'; + + try { + const response = await fetch('/api/create-checkout-session', { + method: 'POST', + headers: { 'Content-Type': 'application/json' }, + body: JSON.stringify({ + registrationId, + accommodationType: selectedAccom || null, + }), + }); + + const result = await response.json(); + + if (!response.ok) { + throw new Error(result.error || 'Failed to create payment'); + } + + // Redirect to Mollie checkout + window.location.href = result.checkoutUrl; + + } catch (error) { + showMsg(msgStep2, error.message, 'error'); + btnPay.disabled = false; + btnPay.textContent = 'Pay via Mollie'; + } + }); + + // Prevent default form submit (we handle it via buttons) + form.addEventListener('submit', (e) => e.preventDefault()); + // Smooth scroll for anchor links document.querySelectorAll('a[href^="#"]').forEach(anchor => { anchor.addEventListener('click', function(e) { diff --git a/package-lock.json b/package-lock.json new file mode 100644 index 0000000..e72d826 --- /dev/null +++ b/package-lock.json @@ -0,0 +1,1405 @@ +{ + "name": "worldplay-website", + "version": "1.0.0", + "lockfileVersion": 3, + "requires": true, + "packages": { + "": { + "name": "worldplay-website", + "version": "1.0.0", + "license": "MIT", + "dependencies": { + "@mollie/api-client": "^4.5.0", + "express": "^4.18.2", + "googleapis": "^144.0.0", + "nodemailer": "^6.9.0", + "pg": "^8.11.3" + }, + "engines": { + "node": ">=18.0.0" + } + }, + "node_modules/@mollie/api-client": { + "version": "4.5.0", + "resolved": "https://registry.npmjs.org/@mollie/api-client/-/api-client-4.5.0.tgz", + "integrity": "sha512-Aj3Erv3EqY74fFK8gYegjzXZxPNq4T7cbsoPueRx5aF6o0kL8kR9eeaLZPaFkv2zHA4XQW6ky8yG7CMyu4oqnw==", + "license": "BSD-3-Clause", + "dependencies": { + "@types/node-fetch": "^2.6.13", + "node-fetch": "^2.7.0", + "ruply": "^1.0.1" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/@types/node": { + "version": "25.6.0", + "resolved": "https://registry.npmjs.org/@types/node/-/node-25.6.0.tgz", + "integrity": "sha512-+qIYRKdNYJwY3vRCZMdJbPLJAtGjQBudzZzdzwQYkEPQd+PJGixUL5QfvCLDaULoLv+RhT3LDkwEfKaAkgSmNQ==", + "license": "MIT", + "dependencies": { + "undici-types": "~7.19.0" + } + }, + "node_modules/@types/node-fetch": { + "version": "2.6.13", + "resolved": "https://registry.npmjs.org/@types/node-fetch/-/node-fetch-2.6.13.tgz", + "integrity": "sha512-QGpRVpzSaUs30JBSGPjOg4Uveu384erbHBoT1zeONvyCfwQxIkUshLAOqN/k9EjGviPRmWTTe6aH2qySWKTVSw==", + "license": "MIT", + "dependencies": { + "@types/node": "*", + "form-data": "^4.0.4" + } + }, + "node_modules/accepts": { + "version": "1.3.8", + "resolved": "https://registry.npmjs.org/accepts/-/accepts-1.3.8.tgz", + "integrity": "sha512-PYAthTa2m2VKxuvSD3DPC/Gy+U+sOA1LAuT8mkmRuvw+NACSaeXEQ+NHcVF7rONl6qcaxV3Uuemwawk+7+SJLw==", + "license": "MIT", + "dependencies": { + "mime-types": "~2.1.34", + "negotiator": "0.6.3" + }, + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/agent-base": { + "version": "7.1.4", + "resolved": "https://registry.npmjs.org/agent-base/-/agent-base-7.1.4.tgz", + "integrity": "sha512-MnA+YT8fwfJPgBx3m60MNqakm30XOkyIoH1y6huTQvC0PwZG7ki8NacLBcrPbNoo8vEZy7Jpuk7+jMO+CUovTQ==", + "license": "MIT", + "engines": { + "node": ">= 14" + } + }, + "node_modules/array-flatten": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/array-flatten/-/array-flatten-1.1.1.tgz", + "integrity": "sha512-PCVAQswWemu6UdxsDFFX/+gVeYqKAod3D3UVm91jHwynguOwAvYPhx8nNlM++NqRcK6CxxpUafjmhIdKiHibqg==", + "license": "MIT" + }, + "node_modules/asynckit": { + "version": "0.4.0", + "resolved": "https://registry.npmjs.org/asynckit/-/asynckit-0.4.0.tgz", + "integrity": "sha512-Oei9OH4tRh0YqU3GxhX79dM/mwVgvbZJaSNaRk+bshkj0S5cfHcgYakreBjrHwatXKbz+IoIdYLxrKim2MjW0Q==", + "license": "MIT" + }, + "node_modules/base64-js": { + "version": "1.5.1", + "resolved": "https://registry.npmjs.org/base64-js/-/base64-js-1.5.1.tgz", + "integrity": "sha512-AKpaYlHn8t4SVbOHCy+b5+KKgvR4vrsD8vbvrbiQJps7fKDTkjkDry6ji0rUJjC0kzbNePLwzxq8iypo41qeWA==", + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/feross" + }, + { + "type": "patreon", + "url": "https://www.patreon.com/feross" + }, + { + "type": "consulting", + "url": "https://feross.org/support" + } + ], + "license": "MIT" + }, + "node_modules/bignumber.js": { + "version": "9.3.1", + "resolved": "https://registry.npmjs.org/bignumber.js/-/bignumber.js-9.3.1.tgz", + "integrity": "sha512-Ko0uX15oIUS7wJ3Rb30Fs6SkVbLmPBAKdlm7q9+ak9bbIeFf0MwuBsQV6z7+X768/cHsfg+WlysDWJcmthjsjQ==", + "license": "MIT", + "engines": { + "node": "*" + } + }, + "node_modules/body-parser": { + "version": "1.20.4", + "resolved": "https://registry.npmjs.org/body-parser/-/body-parser-1.20.4.tgz", + "integrity": "sha512-ZTgYYLMOXY9qKU/57FAo8F+HA2dGX7bqGc71txDRC1rS4frdFI5R7NhluHxH6M0YItAP0sHB4uqAOcYKxO6uGA==", + "license": "MIT", + "dependencies": { + "bytes": "~3.1.2", + "content-type": "~1.0.5", + "debug": "2.6.9", + "depd": "2.0.0", + "destroy": "~1.2.0", + "http-errors": "~2.0.1", + "iconv-lite": "~0.4.24", + "on-finished": "~2.4.1", + "qs": "~6.14.0", + "raw-body": "~2.5.3", + "type-is": "~1.6.18", + "unpipe": "~1.0.0" + }, + "engines": { + "node": ">= 0.8", + "npm": "1.2.8000 || >= 1.4.16" + } + }, + "node_modules/buffer-equal-constant-time": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/buffer-equal-constant-time/-/buffer-equal-constant-time-1.0.1.tgz", + "integrity": "sha512-zRpUiDwd/xk6ADqPMATG8vc9VPrkck7T07OIx0gnjmJAnHnTVXNQG3vfvWNuiZIkwu9KrKdA1iJKfsfTVxE6NA==", + "license": "BSD-3-Clause" + }, + "node_modules/bytes": { + "version": "3.1.2", + "resolved": "https://registry.npmjs.org/bytes/-/bytes-3.1.2.tgz", + "integrity": "sha512-/Nf7TyzTx6S3yRJObOAV7956r8cr2+Oj8AC5dt8wSP3BQAoeX58NoHyCU8P8zGkNXStjTSi6fzO6F0pBdcYbEg==", + "license": "MIT", + "engines": { + "node": ">= 0.8" + } + }, + "node_modules/call-bind-apply-helpers": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/call-bind-apply-helpers/-/call-bind-apply-helpers-1.0.2.tgz", + "integrity": "sha512-Sp1ablJ0ivDkSzjcaJdxEunN5/XvksFJ2sMBFfq6x0ryhQV/2b/KwFe21cMpmHtPOSij8K99/wSfoEuTObmuMQ==", + "license": "MIT", + "dependencies": { + "es-errors": "^1.3.0", + "function-bind": "^1.1.2" + }, + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/call-bound": { + "version": "1.0.4", + "resolved": "https://registry.npmjs.org/call-bound/-/call-bound-1.0.4.tgz", + "integrity": "sha512-+ys997U96po4Kx/ABpBCqhA9EuxJaQWDQg7295H4hBphv3IZg0boBKuwYpt4YXp6MZ5AmZQnU/tyMTlRpaSejg==", + "license": "MIT", + "dependencies": { + "call-bind-apply-helpers": "^1.0.2", + "get-intrinsic": "^1.3.0" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/combined-stream": { + "version": "1.0.8", + "resolved": "https://registry.npmjs.org/combined-stream/-/combined-stream-1.0.8.tgz", + "integrity": "sha512-FQN4MRfuJeHf7cBbBMJFXhKSDq+2kAArBlmRBvcvFE5BB1HZKXtSFASDhdlz9zOYwxh8lDdnvmMOe/+5cdoEdg==", + "license": "MIT", + "dependencies": { + "delayed-stream": "~1.0.0" + }, + "engines": { + "node": ">= 0.8" + } + }, + "node_modules/content-disposition": { + "version": "0.5.4", + "resolved": "https://registry.npmjs.org/content-disposition/-/content-disposition-0.5.4.tgz", + "integrity": "sha512-FveZTNuGw04cxlAiWbzi6zTAL/lhehaWbTtgluJh4/E95DqMwTmha3KZN1aAWA8cFIhHzMZUvLevkw5Rqk+tSQ==", + "license": "MIT", + "dependencies": { + "safe-buffer": "5.2.1" + }, + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/content-type": { + "version": "1.0.5", + "resolved": "https://registry.npmjs.org/content-type/-/content-type-1.0.5.tgz", + "integrity": "sha512-nTjqfcBFEipKdXCv4YDQWCfmcLZKm81ldF0pAopTvyrFGVbcR6P/VAAd5G7N+0tTr8QqiU0tFadD6FK4NtJwOA==", + "license": "MIT", + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/cookie": { + "version": "0.7.2", + "resolved": "https://registry.npmjs.org/cookie/-/cookie-0.7.2.tgz", + "integrity": "sha512-yki5XnKuf750l50uGTllt6kKILY4nQ1eNIQatoXEByZ5dWgnKqbnqmTrBE5B4N7lrMJKQ2ytWMiTO2o0v6Ew/w==", + "license": "MIT", + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/cookie-signature": { + "version": "1.0.7", + "resolved": "https://registry.npmjs.org/cookie-signature/-/cookie-signature-1.0.7.tgz", + "integrity": "sha512-NXdYc3dLr47pBkpUCHtKSwIOQXLVn8dZEuywboCOJY/osA0wFSLlSawr3KN8qXJEyX66FcONTH8EIlVuK0yyFA==", + "license": "MIT" + }, + "node_modules/debug": { + "version": "2.6.9", + "resolved": "https://registry.npmjs.org/debug/-/debug-2.6.9.tgz", + "integrity": "sha512-bC7ElrdJaJnPbAP+1EotYvqZsb3ecl5wi6Bfi6BJTUcNowp6cvspg0jXznRTKDjm/E7AdgFBVeAPVMNcKGsHMA==", + "license": "MIT", + "dependencies": { + "ms": "2.0.0" + } + }, + "node_modules/delayed-stream": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/delayed-stream/-/delayed-stream-1.0.0.tgz", + "integrity": "sha512-ZySD7Nf91aLB0RxL4KGrKHBXl7Eds1DAmEdcoVawXnLD7SDhpNgtuII2aAkg7a7QS41jxPSZ17p4VdGnMHk3MQ==", + "license": "MIT", + "engines": { + "node": ">=0.4.0" + } + }, + "node_modules/depd": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/depd/-/depd-2.0.0.tgz", + "integrity": "sha512-g7nH6P6dyDioJogAAGprGpCtVImJhpPk/roCzdb3fIh61/s/nPsfR6onyMwkCAR/OlC3yBC0lESvUoQEAssIrw==", + "license": "MIT", + "engines": { + "node": ">= 0.8" + } + }, + "node_modules/destroy": { + "version": "1.2.0", + "resolved": "https://registry.npmjs.org/destroy/-/destroy-1.2.0.tgz", + "integrity": "sha512-2sJGJTaXIIaR1w4iJSNoN0hnMY7Gpc/n8D4qSCJw8QqFWXf7cuAgnEHxBpweaVcPevC2l3KpjYCx3NypQQgaJg==", + "license": "MIT", + "engines": { + "node": ">= 0.8", + "npm": "1.2.8000 || >= 1.4.16" + } + }, + "node_modules/dunder-proto": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/dunder-proto/-/dunder-proto-1.0.1.tgz", + "integrity": "sha512-KIN/nDJBQRcXw0MLVhZE9iQHmG68qAVIBg9CqmUYjmQIhgij9U5MFvrqkUL5FbtyyzZuOeOt0zdeRe4UY7ct+A==", + "license": "MIT", + "dependencies": { + "call-bind-apply-helpers": "^1.0.1", + "es-errors": "^1.3.0", + "gopd": "^1.2.0" + }, + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/ecdsa-sig-formatter": { + "version": "1.0.11", + "resolved": "https://registry.npmjs.org/ecdsa-sig-formatter/-/ecdsa-sig-formatter-1.0.11.tgz", + "integrity": "sha512-nagl3RYrbNv6kQkeJIpt6NJZy8twLB/2vtz6yN9Z4vRKHN4/QZJIEbqohALSgwKdnksuY3k5Addp5lg8sVoVcQ==", + "license": "Apache-2.0", + "dependencies": { + "safe-buffer": "^5.0.1" + } + }, + "node_modules/ee-first": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/ee-first/-/ee-first-1.1.1.tgz", + "integrity": "sha512-WMwm9LhRUo+WUaRN+vRuETqG89IgZphVSNkdFgeb6sS/E4OrDIN7t48CAewSHXc6C8lefD8KKfr5vY61brQlow==", + "license": "MIT" + }, + "node_modules/encodeurl": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/encodeurl/-/encodeurl-2.0.0.tgz", + "integrity": "sha512-Q0n9HRi4m6JuGIV1eFlmvJB7ZEVxu93IrMyiMsGC0lrMJMWzRgx6WGquyfQgZVb31vhGgXnfmPNNXmxnOkRBrg==", + "license": "MIT", + "engines": { + "node": ">= 0.8" + } + }, + "node_modules/es-define-property": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/es-define-property/-/es-define-property-1.0.1.tgz", + "integrity": "sha512-e3nRfgfUZ4rNGL232gUgX06QNyyez04KdjFrF+LTRoOXmrOgFKDg4BCdsjW8EnT69eqdYGmRpJwiPVYNrCaW3g==", + "license": "MIT", + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/es-errors": { + "version": "1.3.0", + "resolved": "https://registry.npmjs.org/es-errors/-/es-errors-1.3.0.tgz", + "integrity": "sha512-Zf5H2Kxt2xjTvbJvP2ZWLEICxA6j+hAmMzIlypy4xcBg1vKVnx89Wy0GbS+kf5cwCVFFzdCFh2XSCFNULS6csw==", + "license": "MIT", + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/es-object-atoms": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/es-object-atoms/-/es-object-atoms-1.1.1.tgz", + "integrity": "sha512-FGgH2h8zKNim9ljj7dankFPcICIK9Cp5bm+c2gQSYePhpaG5+esrLODihIorn+Pe6FGJzWhXQotPv73jTaldXA==", + "license": "MIT", + "dependencies": { + "es-errors": "^1.3.0" + }, + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/es-set-tostringtag": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/es-set-tostringtag/-/es-set-tostringtag-2.1.0.tgz", + "integrity": "sha512-j6vWzfrGVfyXxge+O0x5sh6cvxAog0a/4Rdd2K36zCMV5eJ+/+tOAngRO8cODMNWbVRdVlmGZQL2YS3yR8bIUA==", + "license": "MIT", + "dependencies": { + "es-errors": "^1.3.0", + "get-intrinsic": "^1.2.6", + "has-tostringtag": "^1.0.2", + "hasown": "^2.0.2" + }, + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/escape-html": { + "version": "1.0.3", + "resolved": "https://registry.npmjs.org/escape-html/-/escape-html-1.0.3.tgz", + "integrity": "sha512-NiSupZ4OeuGwr68lGIeym/ksIZMJodUGOSCZ/FSnTxcrekbvqrgdUxlJOMpijaKZVjAJrWrGs/6Jy8OMuyj9ow==", + "license": "MIT" + }, + "node_modules/etag": { + "version": "1.8.1", + "resolved": "https://registry.npmjs.org/etag/-/etag-1.8.1.tgz", + "integrity": "sha512-aIL5Fx7mawVa300al2BnEE4iNvo1qETxLrPI/o05L7z6go7fCw1J6EQmbK4FmJ2AS7kgVF/KEZWufBfdClMcPg==", + "license": "MIT", + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/express": { + "version": "4.22.1", + "resolved": "https://registry.npmjs.org/express/-/express-4.22.1.tgz", + "integrity": "sha512-F2X8g9P1X7uCPZMA3MVf9wcTqlyNp7IhH5qPCI0izhaOIYXaW9L535tGA3qmjRzpH+bZczqq7hVKxTR4NWnu+g==", + "license": "MIT", + "dependencies": { + "accepts": "~1.3.8", + "array-flatten": "1.1.1", + "body-parser": "~1.20.3", + "content-disposition": "~0.5.4", + "content-type": "~1.0.4", + "cookie": "~0.7.1", + "cookie-signature": "~1.0.6", + "debug": "2.6.9", + "depd": "2.0.0", + "encodeurl": "~2.0.0", + "escape-html": "~1.0.3", + "etag": "~1.8.1", + "finalhandler": "~1.3.1", + "fresh": "~0.5.2", + "http-errors": "~2.0.0", + "merge-descriptors": "1.0.3", + "methods": "~1.1.2", + "on-finished": "~2.4.1", + "parseurl": "~1.3.3", + "path-to-regexp": "~0.1.12", + "proxy-addr": "~2.0.7", + "qs": "~6.14.0", + "range-parser": "~1.2.1", + "safe-buffer": "5.2.1", + "send": "~0.19.0", + "serve-static": "~1.16.2", + "setprototypeof": "1.2.0", + "statuses": "~2.0.1", + "type-is": "~1.6.18", + "utils-merge": "1.0.1", + "vary": "~1.1.2" + }, + "engines": { + "node": ">= 0.10.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/express" + } + }, + "node_modules/extend": { + "version": "3.0.2", + "resolved": "https://registry.npmjs.org/extend/-/extend-3.0.2.tgz", + "integrity": "sha512-fjquC59cD7CyW6urNXK0FBufkZcoiGG80wTuPujX590cB5Ttln20E2UB4S/WARVqhXffZl2LNgS+gQdPIIim/g==", + "license": "MIT" + }, + "node_modules/finalhandler": { + "version": "1.3.2", + "resolved": "https://registry.npmjs.org/finalhandler/-/finalhandler-1.3.2.tgz", + "integrity": "sha512-aA4RyPcd3badbdABGDuTXCMTtOneUCAYH/gxoYRTZlIJdF0YPWuGqiAsIrhNnnqdXGswYk6dGujem4w80UJFhg==", + "license": "MIT", + "dependencies": { + "debug": "2.6.9", + "encodeurl": "~2.0.0", + "escape-html": "~1.0.3", + "on-finished": "~2.4.1", + "parseurl": "~1.3.3", + "statuses": "~2.0.2", + "unpipe": "~1.0.0" + }, + "engines": { + "node": ">= 0.8" + } + }, + "node_modules/form-data": { + "version": "4.0.5", + "resolved": "https://registry.npmjs.org/form-data/-/form-data-4.0.5.tgz", + "integrity": "sha512-8RipRLol37bNs2bhoV67fiTEvdTrbMUYcFTiy3+wuuOnUog2QBHCZWXDRijWQfAkhBj2Uf5UnVaiWwA5vdd82w==", + "license": "MIT", + "dependencies": { + "asynckit": "^0.4.0", + "combined-stream": "^1.0.8", + "es-set-tostringtag": "^2.1.0", + "hasown": "^2.0.2", + "mime-types": "^2.1.12" + }, + "engines": { + "node": ">= 6" + } + }, + "node_modules/forwarded": { + "version": "0.2.0", + "resolved": "https://registry.npmjs.org/forwarded/-/forwarded-0.2.0.tgz", + "integrity": "sha512-buRG0fpBtRHSTCOASe6hD258tEubFoRLb4ZNA6NxMVHNw2gOcwHo9wyablzMzOA5z9xA9L1KNjk/Nt6MT9aYow==", + "license": "MIT", + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/fresh": { + "version": "0.5.2", + "resolved": "https://registry.npmjs.org/fresh/-/fresh-0.5.2.tgz", + "integrity": "sha512-zJ2mQYM18rEFOudeV4GShTGIQ7RbzA7ozbU9I/XBpm7kqgMywgmylMwXHxZJmkVoYkna9d2pVXVXPdYTP9ej8Q==", + "license": "MIT", + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/function-bind": { + "version": "1.1.2", + "resolved": "https://registry.npmjs.org/function-bind/-/function-bind-1.1.2.tgz", + "integrity": "sha512-7XHNxH7qX9xG5mIwxkhumTox/MIRNcOgDrxWsMt2pAr23WHp6MrRlN7FBSFpCpr+oVO0F744iUgR82nJMfG2SA==", + "license": "MIT", + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/gaxios": { + "version": "6.7.1", + "resolved": "https://registry.npmjs.org/gaxios/-/gaxios-6.7.1.tgz", + "integrity": "sha512-LDODD4TMYx7XXdpwxAVRAIAuB0bzv0s+ywFonY46k126qzQHT9ygyoa9tncmOiQmmDrik65UYsEkv3lbfqQ3yQ==", + "license": "Apache-2.0", + "dependencies": { + "extend": "^3.0.2", + "https-proxy-agent": "^7.0.1", + "is-stream": "^2.0.0", + "node-fetch": "^2.6.9", + "uuid": "^9.0.1" + }, + "engines": { + "node": ">=14" + } + }, + "node_modules/gcp-metadata": { + "version": "6.1.1", + "resolved": "https://registry.npmjs.org/gcp-metadata/-/gcp-metadata-6.1.1.tgz", + "integrity": "sha512-a4tiq7E0/5fTjxPAaH4jpjkSv/uCaU2p5KC6HVGrvl0cDjA8iBZv4vv1gyzlmK0ZUKqwpOyQMKzZQe3lTit77A==", + "license": "Apache-2.0", + "dependencies": { + "gaxios": "^6.1.1", + "google-logging-utils": "^0.0.2", + "json-bigint": "^1.0.0" + }, + "engines": { + "node": ">=14" + } + }, + "node_modules/get-intrinsic": { + "version": "1.3.0", + "resolved": "https://registry.npmjs.org/get-intrinsic/-/get-intrinsic-1.3.0.tgz", + "integrity": "sha512-9fSjSaos/fRIVIp+xSJlE6lfwhES7LNtKaCBIamHsjr2na1BiABJPo0mOjjz8GJDURarmCPGqaiVg5mfjb98CQ==", + "license": "MIT", + "dependencies": { + "call-bind-apply-helpers": "^1.0.2", + "es-define-property": "^1.0.1", + "es-errors": "^1.3.0", + "es-object-atoms": "^1.1.1", + "function-bind": "^1.1.2", + "get-proto": "^1.0.1", + "gopd": "^1.2.0", + "has-symbols": "^1.1.0", + "hasown": "^2.0.2", + "math-intrinsics": "^1.1.0" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/get-proto": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/get-proto/-/get-proto-1.0.1.tgz", + "integrity": "sha512-sTSfBjoXBp89JvIKIefqw7U2CCebsc74kiY6awiGogKtoSGbgjYE/G/+l9sF3MWFPNc9IcoOC4ODfKHfxFmp0g==", + "license": "MIT", + "dependencies": { + "dunder-proto": "^1.0.1", + "es-object-atoms": "^1.0.0" + }, + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/google-auth-library": { + "version": "9.15.1", + "resolved": "https://registry.npmjs.org/google-auth-library/-/google-auth-library-9.15.1.tgz", + "integrity": "sha512-Jb6Z0+nvECVz+2lzSMt9u98UsoakXxA2HGHMCxh+so3n90XgYWkq5dur19JAJV7ONiJY22yBTyJB1TSkvPq9Ng==", + "license": "Apache-2.0", + "dependencies": { + "base64-js": "^1.3.0", + "ecdsa-sig-formatter": "^1.0.11", + "gaxios": "^6.1.1", + "gcp-metadata": "^6.1.0", + "gtoken": "^7.0.0", + "jws": "^4.0.0" + }, + "engines": { + "node": ">=14" + } + }, + "node_modules/google-logging-utils": { + "version": "0.0.2", + "resolved": "https://registry.npmjs.org/google-logging-utils/-/google-logging-utils-0.0.2.tgz", + "integrity": "sha512-NEgUnEcBiP5HrPzufUkBzJOD/Sxsco3rLNo1F1TNf7ieU8ryUzBhqba8r756CjLX7rn3fHl6iLEwPYuqpoKgQQ==", + "license": "Apache-2.0", + "engines": { + "node": ">=14" + } + }, + "node_modules/googleapis": { + "version": "144.0.0", + "resolved": "https://registry.npmjs.org/googleapis/-/googleapis-144.0.0.tgz", + "integrity": "sha512-ELcWOXtJxjPX4vsKMh+7V+jZvgPwYMlEhQFiu2sa9Qmt5veX8nwXPksOWGGN6Zk4xCiLygUyaz7xGtcMO+Onxw==", + "license": "Apache-2.0", + "dependencies": { + "google-auth-library": "^9.0.0", + "googleapis-common": "^7.0.0" + }, + "engines": { + "node": ">=14.0.0" + } + }, + "node_modules/googleapis-common": { + "version": "7.2.0", + "resolved": "https://registry.npmjs.org/googleapis-common/-/googleapis-common-7.2.0.tgz", + "integrity": "sha512-/fhDZEJZvOV3X5jmD+fKxMqma5q2Q9nZNSF3kn1F18tpxmA86BcTxAGBQdM0N89Z3bEaIs+HVznSmFJEAmMTjA==", + "license": "Apache-2.0", + "dependencies": { + "extend": "^3.0.2", + "gaxios": "^6.0.3", + "google-auth-library": "^9.7.0", + "qs": "^6.7.0", + "url-template": "^2.0.8", + "uuid": "^9.0.0" + }, + "engines": { + "node": ">=14.0.0" + } + }, + "node_modules/gopd": { + "version": "1.2.0", + "resolved": "https://registry.npmjs.org/gopd/-/gopd-1.2.0.tgz", + "integrity": "sha512-ZUKRh6/kUFoAiTAtTYPZJ3hw9wNxx+BIBOijnlG9PnrJsCcSjs1wyyD6vJpaYtgnzDrKYRSqf3OO6Rfa93xsRg==", + "license": "MIT", + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/gtoken": { + "version": "7.1.0", + "resolved": "https://registry.npmjs.org/gtoken/-/gtoken-7.1.0.tgz", + "integrity": "sha512-pCcEwRi+TKpMlxAQObHDQ56KawURgyAf6jtIY046fJ5tIv3zDe/LEIubckAO8fj6JnAxLdmWkUfNyulQ2iKdEw==", + "license": "MIT", + "dependencies": { + "gaxios": "^6.0.0", + "jws": "^4.0.0" + }, + "engines": { + "node": ">=14.0.0" + } + }, + "node_modules/has-symbols": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/has-symbols/-/has-symbols-1.1.0.tgz", + "integrity": "sha512-1cDNdwJ2Jaohmb3sg4OmKaMBwuC48sYni5HUw2DvsC8LjGTLK9h+eb1X6RyuOHe4hT0ULCW68iomhjUoKUqlPQ==", + "license": "MIT", + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/has-tostringtag": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/has-tostringtag/-/has-tostringtag-1.0.2.tgz", + "integrity": "sha512-NqADB8VjPFLM2V0VvHUewwwsw0ZWBaIdgo+ieHtK3hasLz4qeCRjYcqfB6AQrBggRKppKF8L52/VqdVsO47Dlw==", + "license": "MIT", + "dependencies": { + "has-symbols": "^1.0.3" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/hasown": { + "version": "2.0.2", + "resolved": "https://registry.npmjs.org/hasown/-/hasown-2.0.2.tgz", + "integrity": "sha512-0hJU9SCPvmMzIBdZFqNPXWa6dqh7WdH0cII9y+CyS8rG3nL48Bclra9HmKhVVUHyPWNH5Y7xDwAB7bfgSjkUMQ==", + "license": "MIT", + "dependencies": { + "function-bind": "^1.1.2" + }, + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/http-errors": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/http-errors/-/http-errors-2.0.1.tgz", + "integrity": "sha512-4FbRdAX+bSdmo4AUFuS0WNiPz8NgFt+r8ThgNWmlrjQjt1Q7ZR9+zTlce2859x4KSXrwIsaeTqDoKQmtP8pLmQ==", + "license": "MIT", + "dependencies": { + "depd": "~2.0.0", + "inherits": "~2.0.4", + "setprototypeof": "~1.2.0", + "statuses": "~2.0.2", + "toidentifier": "~1.0.1" + }, + "engines": { + "node": ">= 0.8" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/express" + } + }, + "node_modules/https-proxy-agent": { + "version": "7.0.6", + "resolved": "https://registry.npmjs.org/https-proxy-agent/-/https-proxy-agent-7.0.6.tgz", + "integrity": "sha512-vK9P5/iUfdl95AI+JVyUuIcVtd4ofvtrOr3HNtM2yxC9bnMbEdp3x01OhQNnjb8IJYi38VlTE3mBXwcfvywuSw==", + "license": "MIT", + "dependencies": { + "agent-base": "^7.1.2", + "debug": "4" + }, + "engines": { + "node": ">= 14" + } + }, + "node_modules/https-proxy-agent/node_modules/debug": { + "version": "4.4.3", + "resolved": "https://registry.npmjs.org/debug/-/debug-4.4.3.tgz", + "integrity": "sha512-RGwwWnwQvkVfavKVt22FGLw+xYSdzARwm0ru6DhTVA3umU5hZc28V3kO4stgYryrTlLpuvgI9GiijltAjNbcqA==", + "license": "MIT", + "dependencies": { + "ms": "^2.1.3" + }, + "engines": { + "node": ">=6.0" + }, + "peerDependenciesMeta": { + "supports-color": { + "optional": true + } + } + }, + "node_modules/https-proxy-agent/node_modules/ms": { + "version": "2.1.3", + "resolved": "https://registry.npmjs.org/ms/-/ms-2.1.3.tgz", + "integrity": "sha512-6FlzubTLZG3J2a/NVCAleEhjzq5oxgHyaCU9yYXvcLsvoVaHJq/s5xXI6/XXP6tz7R9xAOtHnSO/tXtF3WRTlA==", + "license": "MIT" + }, + "node_modules/iconv-lite": { + "version": "0.4.24", + "resolved": "https://registry.npmjs.org/iconv-lite/-/iconv-lite-0.4.24.tgz", + "integrity": "sha512-v3MXnZAcvnywkTUEZomIActle7RXXeedOR31wwl7VlyoXO4Qi9arvSenNQWne1TcRwhCL1HwLI21bEqdpj8/rA==", + "license": "MIT", + "dependencies": { + "safer-buffer": ">= 2.1.2 < 3" + }, + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/inherits": { + "version": "2.0.4", + "resolved": "https://registry.npmjs.org/inherits/-/inherits-2.0.4.tgz", + "integrity": "sha512-k/vGaX4/Yla3WzyMCvTQOXYeIHvqOKtnqBduzTHpzpQZzAskKMhZ2K+EnBiSM9zGSoIFeMpXKxa4dYeZIQqewQ==", + "license": "ISC" + }, + "node_modules/ipaddr.js": { + "version": "1.9.1", + "resolved": "https://registry.npmjs.org/ipaddr.js/-/ipaddr.js-1.9.1.tgz", + "integrity": "sha512-0KI/607xoxSToH7GjN1FfSbLoU0+btTicjsQSWQlh/hZykN8KpmMf7uYwPW3R+akZ6R/w18ZlXSHBYXiYUPO3g==", + "license": "MIT", + "engines": { + "node": ">= 0.10" + } + }, + "node_modules/is-stream": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/is-stream/-/is-stream-2.0.1.tgz", + "integrity": "sha512-hFoiJiTl63nn+kstHGBtewWSKnQLpyb155KHheA1l39uvtO9nWIop1p3udqPcUd/xbF1VLMO4n7OI6p7RbngDg==", + "license": "MIT", + "engines": { + "node": ">=8" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/json-bigint": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/json-bigint/-/json-bigint-1.0.0.tgz", + "integrity": "sha512-SiPv/8VpZuWbvLSMtTDU8hEfrZWg/mH/nV/b4o0CYbSxu1UIQPLdwKOCIyLQX+VIPO5vrLX3i8qtqFyhdPSUSQ==", + "license": "MIT", + "dependencies": { + "bignumber.js": "^9.0.0" + } + }, + "node_modules/jwa": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/jwa/-/jwa-2.0.1.tgz", + "integrity": "sha512-hRF04fqJIP8Abbkq5NKGN0Bbr3JxlQ+qhZufXVr0DvujKy93ZCbXZMHDL4EOtodSbCWxOqR8MS1tXA5hwqCXDg==", + "license": "MIT", + "dependencies": { + "buffer-equal-constant-time": "^1.0.1", + "ecdsa-sig-formatter": "1.0.11", + "safe-buffer": "^5.0.1" + } + }, + "node_modules/jws": { + "version": "4.0.1", + "resolved": "https://registry.npmjs.org/jws/-/jws-4.0.1.tgz", + "integrity": "sha512-EKI/M/yqPncGUUh44xz0PxSidXFr/+r0pA70+gIYhjv+et7yxM+s29Y+VGDkovRofQem0fs7Uvf4+YmAdyRduA==", + "license": "MIT", + "dependencies": { + "jwa": "^2.0.1", + "safe-buffer": "^5.0.1" + } + }, + "node_modules/math-intrinsics": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/math-intrinsics/-/math-intrinsics-1.1.0.tgz", + "integrity": "sha512-/IXtbwEk5HTPyEwyKX6hGkYXxM9nbj64B+ilVJnC/R6B0pH5G4V3b0pVbL7DBj4tkhBAppbQUlf6F6Xl9LHu1g==", + "license": "MIT", + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/media-typer": { + "version": "0.3.0", + "resolved": "https://registry.npmjs.org/media-typer/-/media-typer-0.3.0.tgz", + "integrity": "sha512-dq+qelQ9akHpcOl/gUVRTxVIOkAJ1wR3QAvb4RsVjS8oVoFjDGTc679wJYmUmknUF5HwMLOgb5O+a3KxfWapPQ==", + "license": "MIT", + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/merge-descriptors": { + "version": "1.0.3", + "resolved": "https://registry.npmjs.org/merge-descriptors/-/merge-descriptors-1.0.3.tgz", + "integrity": "sha512-gaNvAS7TZ897/rVaZ0nMtAyxNyi/pdbjbAwUpFQpN70GqnVfOiXpeUUMKRBmzXaSQ8DdTX4/0ms62r2K+hE6mQ==", + "license": "MIT", + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/methods": { + "version": "1.1.2", + "resolved": "https://registry.npmjs.org/methods/-/methods-1.1.2.tgz", + "integrity": "sha512-iclAHeNqNm68zFtnZ0e+1L2yUIdvzNoauKU4WBA3VvH/vPFieF7qfRlwUZU+DA9P9bPXIS90ulxoUoCH23sV2w==", + "license": "MIT", + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/mime": { + "version": "1.6.0", + "resolved": "https://registry.npmjs.org/mime/-/mime-1.6.0.tgz", + "integrity": "sha512-x0Vn8spI+wuJ1O6S7gnbaQg8Pxh4NNHb7KSINmEWKiPE4RKOplvijn+NkmYmmRgP68mc70j2EbeTFRsrswaQeg==", + "license": "MIT", + "bin": { + "mime": "cli.js" + }, + "engines": { + "node": ">=4" + } + }, + "node_modules/mime-db": { + "version": "1.52.0", + "resolved": "https://registry.npmjs.org/mime-db/-/mime-db-1.52.0.tgz", + "integrity": "sha512-sPU4uV7dYlvtWJxwwxHD0PuihVNiE7TyAbQ5SWxDCB9mUYvOgroQOwYQQOKPJ8CIbE+1ETVlOoK1UC2nU3gYvg==", + "license": "MIT", + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/mime-types": { + "version": "2.1.35", + "resolved": "https://registry.npmjs.org/mime-types/-/mime-types-2.1.35.tgz", + "integrity": "sha512-ZDY+bPm5zTTF+YpCrAU9nK0UgICYPT0QtT1NZWFv4s++TNkcgVaT0g6+4R2uI4MjQjzysHB1zxuWL50hzaeXiw==", + "license": "MIT", + "dependencies": { + "mime-db": "1.52.0" + }, + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/ms": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/ms/-/ms-2.0.0.tgz", + "integrity": "sha512-Tpp60P6IUJDTuOq/5Z8cdskzJujfwqfOTkrwIwj7IRISpnkJnT6SyJ4PCPnGMoFjC9ddhal5KVIYtAt97ix05A==", + "license": "MIT" + }, + "node_modules/negotiator": { + "version": "0.6.3", + "resolved": "https://registry.npmjs.org/negotiator/-/negotiator-0.6.3.tgz", + "integrity": "sha512-+EUsqGPLsM+j/zdChZjsnX51g4XrHFOIXwfnCVPGlQk/k5giakcKsuxCObBRu6DSm9opw/O6slWbJdghQM4bBg==", + "license": "MIT", + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/node-fetch": { + "version": "2.7.0", + "resolved": "https://registry.npmjs.org/node-fetch/-/node-fetch-2.7.0.tgz", + "integrity": "sha512-c4FRfUm/dbcWZ7U+1Wq0AwCyFL+3nt2bEw05wfxSz+DWpWsitgmSgYmy2dQdWyKC1694ELPqMs/YzUSNozLt8A==", + "license": "MIT", + "dependencies": { + "whatwg-url": "^5.0.0" + }, + "engines": { + "node": "4.x || >=6.0.0" + }, + "peerDependencies": { + "encoding": "^0.1.0" + }, + "peerDependenciesMeta": { + "encoding": { + "optional": true + } + } + }, + "node_modules/nodemailer": { + "version": "6.10.1", + "resolved": "https://registry.npmjs.org/nodemailer/-/nodemailer-6.10.1.tgz", + "integrity": "sha512-Z+iLaBGVaSjbIzQ4pX6XV41HrooLsQ10ZWPUehGmuantvzWoDVBnmsdUcOIDM1t+yPor5pDhVlDESgOMEGxhHA==", + "license": "MIT-0", + "engines": { + "node": ">=6.0.0" + } + }, + "node_modules/object-inspect": { + "version": "1.13.4", + "resolved": "https://registry.npmjs.org/object-inspect/-/object-inspect-1.13.4.tgz", + "integrity": "sha512-W67iLl4J2EXEGTbfeHCffrjDfitvLANg0UlX3wFUUSTx92KXRFegMHUVgSqE+wvhAbi4WqjGg9czysTV2Epbew==", + "license": "MIT", + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/on-finished": { + "version": "2.4.1", + "resolved": "https://registry.npmjs.org/on-finished/-/on-finished-2.4.1.tgz", + "integrity": "sha512-oVlzkg3ENAhCk2zdv7IJwd/QUD4z2RxRwpkcGY8psCVcCYZNq4wYnVWALHM+brtuJjePWiYF/ClmuDr8Ch5+kg==", + "license": "MIT", + "dependencies": { + "ee-first": "1.1.1" + }, + "engines": { + "node": ">= 0.8" + } + }, + "node_modules/parseurl": { + "version": "1.3.3", + "resolved": "https://registry.npmjs.org/parseurl/-/parseurl-1.3.3.tgz", + "integrity": "sha512-CiyeOxFT/JZyN5m0z9PfXw4SCBJ6Sygz1Dpl0wqjlhDEGGBP1GnsUVEL0p63hoG1fcj3fHynXi9NYO4nWOL+qQ==", + "license": "MIT", + "engines": { + "node": ">= 0.8" + } + }, + "node_modules/path-to-regexp": { + "version": "0.1.13", + "resolved": "https://registry.npmjs.org/path-to-regexp/-/path-to-regexp-0.1.13.tgz", + "integrity": "sha512-A/AGNMFN3c8bOlvV9RreMdrv7jsmF9XIfDeCd87+I8RNg6s78BhJxMu69NEMHBSJFxKidViTEdruRwEk/WIKqA==", + "license": "MIT" + }, + "node_modules/pg": { + "version": "8.20.0", + "resolved": "https://registry.npmjs.org/pg/-/pg-8.20.0.tgz", + "integrity": "sha512-ldhMxz2r8fl/6QkXnBD3CR9/xg694oT6DZQ2s6c/RI28OjtSOpxnPrUCGOBJ46RCUxcWdx3p6kw/xnDHjKvaRA==", + "license": "MIT", + "dependencies": { + "pg-connection-string": "^2.12.0", + "pg-pool": "^3.13.0", + "pg-protocol": "^1.13.0", + "pg-types": "2.2.0", + "pgpass": "1.0.5" + }, + "engines": { + "node": ">= 16.0.0" + }, + "optionalDependencies": { + "pg-cloudflare": "^1.3.0" + }, + "peerDependencies": { + "pg-native": ">=3.0.1" + }, + "peerDependenciesMeta": { + "pg-native": { + "optional": true + } + } + }, + "node_modules/pg-cloudflare": { + "version": "1.3.0", + "resolved": "https://registry.npmjs.org/pg-cloudflare/-/pg-cloudflare-1.3.0.tgz", + "integrity": "sha512-6lswVVSztmHiRtD6I8hw4qP/nDm1EJbKMRhf3HCYaqud7frGysPv7FYJ5noZQdhQtN2xJnimfMtvQq21pdbzyQ==", + "license": "MIT", + "optional": true + }, + "node_modules/pg-connection-string": { + "version": "2.12.0", + "resolved": "https://registry.npmjs.org/pg-connection-string/-/pg-connection-string-2.12.0.tgz", + "integrity": "sha512-U7qg+bpswf3Cs5xLzRqbXbQl85ng0mfSV/J0nnA31MCLgvEaAo7CIhmeyrmJpOr7o+zm0rXK+hNnT5l9RHkCkQ==", + "license": "MIT" + }, + "node_modules/pg-int8": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/pg-int8/-/pg-int8-1.0.1.tgz", + "integrity": "sha512-WCtabS6t3c8SkpDBUlb1kjOs7l66xsGdKpIPZsg4wR+B3+u9UAum2odSsF9tnvxg80h4ZxLWMy4pRjOsFIqQpw==", + "license": "ISC", + "engines": { + "node": ">=4.0.0" + } + }, + "node_modules/pg-pool": { + "version": "3.13.0", + "resolved": "https://registry.npmjs.org/pg-pool/-/pg-pool-3.13.0.tgz", + "integrity": "sha512-gB+R+Xud1gLFuRD/QgOIgGOBE2KCQPaPwkzBBGC9oG69pHTkhQeIuejVIk3/cnDyX39av2AxomQiyPT13WKHQA==", + "license": "MIT", + "peerDependencies": { + "pg": ">=8.0" + } + }, + "node_modules/pg-protocol": { + "version": "1.13.0", + "resolved": "https://registry.npmjs.org/pg-protocol/-/pg-protocol-1.13.0.tgz", + "integrity": "sha512-zzdvXfS6v89r6v7OcFCHfHlyG/wvry1ALxZo4LqgUoy7W9xhBDMaqOuMiF3qEV45VqsN6rdlcehHrfDtlCPc8w==", + "license": "MIT" + }, + "node_modules/pg-types": { + "version": "2.2.0", + "resolved": "https://registry.npmjs.org/pg-types/-/pg-types-2.2.0.tgz", + "integrity": "sha512-qTAAlrEsl8s4OiEQY69wDvcMIdQN6wdz5ojQiOy6YRMuynxenON0O5oCpJI6lshc6scgAY8qvJ2On/p+CXY0GA==", + "license": "MIT", + "dependencies": { + "pg-int8": "1.0.1", + "postgres-array": "~2.0.0", + "postgres-bytea": "~1.0.0", + "postgres-date": "~1.0.4", + "postgres-interval": "^1.1.0" + }, + "engines": { + "node": ">=4" + } + }, + "node_modules/pgpass": { + "version": "1.0.5", + "resolved": "https://registry.npmjs.org/pgpass/-/pgpass-1.0.5.tgz", + "integrity": "sha512-FdW9r/jQZhSeohs1Z3sI1yxFQNFvMcnmfuj4WBMUTxOrAyLMaTcE1aAMBiTlbMNaXvBCQuVi0R7hd8udDSP7ug==", + "license": "MIT", + "dependencies": { + "split2": "^4.1.0" + } + }, + "node_modules/postgres-array": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/postgres-array/-/postgres-array-2.0.0.tgz", + "integrity": "sha512-VpZrUqU5A69eQyW2c5CA1jtLecCsN2U/bD6VilrFDWq5+5UIEVO7nazS3TEcHf1zuPYO/sqGvUvW62g86RXZuA==", + "license": "MIT", + "engines": { + "node": ">=4" + } + }, + "node_modules/postgres-bytea": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/postgres-bytea/-/postgres-bytea-1.0.1.tgz", + "integrity": "sha512-5+5HqXnsZPE65IJZSMkZtURARZelel2oXUEO8rH83VS/hxH5vv1uHquPg5wZs8yMAfdv971IU+kcPUczi7NVBQ==", + "license": "MIT", + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/postgres-date": { + "version": "1.0.7", + "resolved": "https://registry.npmjs.org/postgres-date/-/postgres-date-1.0.7.tgz", + "integrity": "sha512-suDmjLVQg78nMK2UZ454hAG+OAW+HQPZ6n++TNDUX+L0+uUlLywnoxJKDou51Zm+zTCjrCl0Nq6J9C5hP9vK/Q==", + "license": "MIT", + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/postgres-interval": { + "version": "1.2.0", + "resolved": "https://registry.npmjs.org/postgres-interval/-/postgres-interval-1.2.0.tgz", + "integrity": "sha512-9ZhXKM/rw350N1ovuWHbGxnGh/SNJ4cnxHiM0rxE4VN41wsg8P8zWn9hv/buK00RP4WvlOyr/RBDiptyxVbkZQ==", + "license": "MIT", + "dependencies": { + "xtend": "^4.0.0" + }, + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/proxy-addr": { + "version": "2.0.7", + "resolved": "https://registry.npmjs.org/proxy-addr/-/proxy-addr-2.0.7.tgz", + "integrity": "sha512-llQsMLSUDUPT44jdrU/O37qlnifitDP+ZwrmmZcoSKyLKvtZxpyV0n2/bD/N4tBAAZ/gJEdZU7KMraoK1+XYAg==", + "license": "MIT", + "dependencies": { + "forwarded": "0.2.0", + "ipaddr.js": "1.9.1" + }, + "engines": { + "node": ">= 0.10" + } + }, + "node_modules/qs": { + "version": "6.14.2", + "resolved": "https://registry.npmjs.org/qs/-/qs-6.14.2.tgz", + "integrity": "sha512-V/yCWTTF7VJ9hIh18Ugr2zhJMP01MY7c5kh4J870L7imm6/DIzBsNLTXzMwUA3yZ5b/KBqLx8Kp3uRvd7xSe3Q==", + "license": "BSD-3-Clause", + "dependencies": { + "side-channel": "^1.1.0" + }, + "engines": { + "node": ">=0.6" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/range-parser": { + "version": "1.2.1", + "resolved": "https://registry.npmjs.org/range-parser/-/range-parser-1.2.1.tgz", + "integrity": "sha512-Hrgsx+orqoygnmhFbKaHE6c296J+HTAQXoxEF6gNupROmmGJRoyzfG3ccAveqCBrwr/2yxQ5BVd/GTl5agOwSg==", + "license": "MIT", + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/raw-body": { + "version": "2.5.3", + "resolved": "https://registry.npmjs.org/raw-body/-/raw-body-2.5.3.tgz", + "integrity": "sha512-s4VSOf6yN0rvbRZGxs8Om5CWj6seneMwK3oDb4lWDH0UPhWcxwOWw5+qk24bxq87szX1ydrwylIOp2uG1ojUpA==", + "license": "MIT", + "dependencies": { + "bytes": "~3.1.2", + "http-errors": "~2.0.1", + "iconv-lite": "~0.4.24", + "unpipe": "~1.0.0" + }, + "engines": { + "node": ">= 0.8" + } + }, + "node_modules/ruply": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/ruply/-/ruply-1.0.1.tgz", + "integrity": "sha512-p39LnaaJyuucPGlgaB0KiyifpcuOkn24+Hq5y0ejAD/LlH+mRAbkHn2tckCLgHir+S+nis1WYG+TYEC4zHX0WQ==", + "license": "MIT" + }, + "node_modules/safe-buffer": { + "version": "5.2.1", + "resolved": "https://registry.npmjs.org/safe-buffer/-/safe-buffer-5.2.1.tgz", + "integrity": "sha512-rp3So07KcdmmKbGvgaNxQSJr7bGVSVk5S9Eq1F+ppbRo70+YeaDxkw5Dd8NPN+GD6bjnYm2VuPuCXmpuYvmCXQ==", + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/feross" + }, + { + "type": "patreon", + "url": "https://www.patreon.com/feross" + }, + { + "type": "consulting", + "url": "https://feross.org/support" + } + ], + "license": "MIT" + }, + "node_modules/safer-buffer": { + "version": "2.1.2", + "resolved": "https://registry.npmjs.org/safer-buffer/-/safer-buffer-2.1.2.tgz", + "integrity": "sha512-YZo3K82SD7Riyi0E1EQPojLz7kpepnSQI9IyPbHHg1XXXevb5dJI7tpyN2ADxGcQbHG7vcyRHk0cbwqcQriUtg==", + "license": "MIT" + }, + "node_modules/send": { + "version": "0.19.2", + "resolved": "https://registry.npmjs.org/send/-/send-0.19.2.tgz", + "integrity": "sha512-VMbMxbDeehAxpOtWJXlcUS5E8iXh6QmN+BkRX1GARS3wRaXEEgzCcB10gTQazO42tpNIya8xIyNx8fll1OFPrg==", + "license": "MIT", + "dependencies": { + "debug": "2.6.9", + "depd": "2.0.0", + "destroy": "1.2.0", + "encodeurl": "~2.0.0", + "escape-html": "~1.0.3", + "etag": "~1.8.1", + "fresh": "~0.5.2", + "http-errors": "~2.0.1", + "mime": "1.6.0", + "ms": "2.1.3", + "on-finished": "~2.4.1", + "range-parser": "~1.2.1", + "statuses": "~2.0.2" + }, + "engines": { + "node": ">= 0.8.0" + } + }, + "node_modules/send/node_modules/ms": { + "version": "2.1.3", + "resolved": "https://registry.npmjs.org/ms/-/ms-2.1.3.tgz", + "integrity": "sha512-6FlzubTLZG3J2a/NVCAleEhjzq5oxgHyaCU9yYXvcLsvoVaHJq/s5xXI6/XXP6tz7R9xAOtHnSO/tXtF3WRTlA==", + "license": "MIT" + }, + "node_modules/serve-static": { + "version": "1.16.3", + "resolved": "https://registry.npmjs.org/serve-static/-/serve-static-1.16.3.tgz", + "integrity": "sha512-x0RTqQel6g5SY7Lg6ZreMmsOzncHFU7nhnRWkKgWuMTu5NN0DR5oruckMqRvacAN9d5w6ARnRBXl9xhDCgfMeA==", + "license": "MIT", + "dependencies": { + "encodeurl": "~2.0.0", + "escape-html": "~1.0.3", + "parseurl": "~1.3.3", + "send": "~0.19.1" + }, + "engines": { + "node": ">= 0.8.0" + } + }, + "node_modules/setprototypeof": { + "version": "1.2.0", + "resolved": "https://registry.npmjs.org/setprototypeof/-/setprototypeof-1.2.0.tgz", + "integrity": "sha512-E5LDX7Wrp85Kil5bhZv46j8jOeboKq5JMmYM3gVGdGH8xFpPWXUMsNrlODCrkoxMEeNi/XZIwuRvY4XNwYMJpw==", + "license": "ISC" + }, + "node_modules/side-channel": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/side-channel/-/side-channel-1.1.0.tgz", + "integrity": "sha512-ZX99e6tRweoUXqR+VBrslhda51Nh5MTQwou5tnUDgbtyM0dBgmhEDtWGP/xbKn6hqfPRHujUNwz5fy/wbbhnpw==", + "license": "MIT", + "dependencies": { + "es-errors": "^1.3.0", + "object-inspect": "^1.13.3", + "side-channel-list": "^1.0.0", + "side-channel-map": "^1.0.1", + "side-channel-weakmap": "^1.0.2" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/side-channel-list": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/side-channel-list/-/side-channel-list-1.0.1.tgz", + "integrity": "sha512-mjn/0bi/oUURjc5Xl7IaWi/OJJJumuoJFQJfDDyO46+hBWsfaVM65TBHq2eoZBhzl9EchxOijpkbRC8SVBQU0w==", + "license": "MIT", + "dependencies": { + "es-errors": "^1.3.0", + "object-inspect": "^1.13.4" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/side-channel-map": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/side-channel-map/-/side-channel-map-1.0.1.tgz", + "integrity": "sha512-VCjCNfgMsby3tTdo02nbjtM/ewra6jPHmpThenkTYh8pG9ucZ/1P8So4u4FGBek/BjpOVsDCMoLA/iuBKIFXRA==", + "license": "MIT", + "dependencies": { + "call-bound": "^1.0.2", + "es-errors": "^1.3.0", + "get-intrinsic": "^1.2.5", + "object-inspect": "^1.13.3" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/side-channel-weakmap": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/side-channel-weakmap/-/side-channel-weakmap-1.0.2.tgz", + "integrity": "sha512-WPS/HvHQTYnHisLo9McqBHOJk2FkHO/tlpvldyrnem4aeQp4hai3gythswg6p01oSoTl58rcpiFAjF2br2Ak2A==", + "license": "MIT", + "dependencies": { + "call-bound": "^1.0.2", + "es-errors": "^1.3.0", + "get-intrinsic": "^1.2.5", + "object-inspect": "^1.13.3", + "side-channel-map": "^1.0.1" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/split2": { + "version": "4.2.0", + "resolved": "https://registry.npmjs.org/split2/-/split2-4.2.0.tgz", + "integrity": "sha512-UcjcJOWknrNkF6PLX83qcHM6KHgVKNkV62Y8a5uYDVv9ydGQVwAHMKqHdJje1VTWpljG0WYpCDhrCdAOYH4TWg==", + "license": "ISC", + "engines": { + "node": ">= 10.x" + } + }, + "node_modules/statuses": { + "version": "2.0.2", + "resolved": "https://registry.npmjs.org/statuses/-/statuses-2.0.2.tgz", + "integrity": "sha512-DvEy55V3DB7uknRo+4iOGT5fP1slR8wQohVdknigZPMpMstaKJQWhwiYBACJE3Ul2pTnATihhBYnRhZQHGBiRw==", + "license": "MIT", + "engines": { + "node": ">= 0.8" + } + }, + "node_modules/toidentifier": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/toidentifier/-/toidentifier-1.0.1.tgz", + "integrity": "sha512-o5sSPKEkg/DIQNmH43V0/uerLrpzVedkUh8tGNvaeXpfpuwjKenlSox/2O/BTlZUtEe+JG7s5YhEz608PlAHRA==", + "license": "MIT", + "engines": { + "node": ">=0.6" + } + }, + "node_modules/tr46": { + "version": "0.0.3", + "resolved": "https://registry.npmjs.org/tr46/-/tr46-0.0.3.tgz", + "integrity": "sha512-N3WMsuqV66lT30CrXNbEjx4GEwlow3v6rr4mCcv6prnfwhS01rkgyFdjPNBYd9br7LpXV1+Emh01fHnq2Gdgrw==", + "license": "MIT" + }, + "node_modules/type-is": { + "version": "1.6.18", + "resolved": "https://registry.npmjs.org/type-is/-/type-is-1.6.18.tgz", + "integrity": "sha512-TkRKr9sUTxEH8MdfuCSP7VizJyzRNMjj2J2do2Jr3Kym598JVdEksuzPQCnlFPW4ky9Q+iA+ma9BGm06XQBy8g==", + "license": "MIT", + "dependencies": { + "media-typer": "0.3.0", + "mime-types": "~2.1.24" + }, + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/undici-types": { + "version": "7.19.2", + "resolved": "https://registry.npmjs.org/undici-types/-/undici-types-7.19.2.tgz", + "integrity": "sha512-qYVnV5OEm2AW8cJMCpdV20CDyaN3g0AjDlOGf1OW4iaDEx8MwdtChUp4zu4H0VP3nDRF/8RKWH+IPp9uW0YGZg==", + "license": "MIT" + }, + "node_modules/unpipe": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/unpipe/-/unpipe-1.0.0.tgz", + "integrity": "sha512-pjy2bYhSsufwWlKwPc+l3cN7+wuJlK6uz0YdJEOlQDbl6jo/YlPi4mb8agUkVC8BF7V8NuzeyPNqRksA3hztKQ==", + "license": "MIT", + "engines": { + "node": ">= 0.8" + } + }, + "node_modules/url-template": { + "version": "2.0.8", + "resolved": "https://registry.npmjs.org/url-template/-/url-template-2.0.8.tgz", + "integrity": "sha512-XdVKMF4SJ0nP/O7XIPB0JwAEuT9lDIYnNsK8yGVe43y0AWoKeJNdv3ZNWh7ksJ6KqQFjOO6ox/VEitLnaVNufw==", + "license": "BSD" + }, + "node_modules/utils-merge": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/utils-merge/-/utils-merge-1.0.1.tgz", + "integrity": "sha512-pMZTvIkT1d+TFGvDOqodOclx0QWkkgi6Tdoa8gC8ffGAAqz9pzPTZWAybbsHHoED/ztMtkv/VoYTYyShUn81hA==", + "license": "MIT", + "engines": { + "node": ">= 0.4.0" + } + }, + "node_modules/uuid": { + "version": "9.0.1", + "resolved": "https://registry.npmjs.org/uuid/-/uuid-9.0.1.tgz", + "integrity": "sha512-b+1eJOlsR9K8HJpow9Ok3fiWOWSIcIzXodvv0rQjVoOVNpWMpxf1wZNpt4y9h10odCNrqnYp1OBzRktckBe3sA==", + "funding": [ + "https://github.com/sponsors/broofa", + "https://github.com/sponsors/ctavan" + ], + "license": "MIT", + "bin": { + "uuid": "dist/bin/uuid" + } + }, + "node_modules/vary": { + "version": "1.1.2", + "resolved": "https://registry.npmjs.org/vary/-/vary-1.1.2.tgz", + "integrity": "sha512-BNGbWLfd0eUPabhkXUVm0j8uuvREyTh5ovRa/dyow/BqAbZJyC+5fU+IzQOzmAKzYqYRAISoRhdQr3eIZ/PXqg==", + "license": "MIT", + "engines": { + "node": ">= 0.8" + } + }, + "node_modules/webidl-conversions": { + "version": "3.0.1", + "resolved": "https://registry.npmjs.org/webidl-conversions/-/webidl-conversions-3.0.1.tgz", + "integrity": "sha512-2JAn3z8AR6rjK8Sm8orRC0h/bcl/DqL7tRPdGZ4I1CjdF+EaMLmYxBHyXuKL849eucPFhvBoxMsflfOb8kxaeQ==", + "license": "BSD-2-Clause" + }, + "node_modules/whatwg-url": { + "version": "5.0.0", + "resolved": "https://registry.npmjs.org/whatwg-url/-/whatwg-url-5.0.0.tgz", + "integrity": "sha512-saE57nupxk6v3HY35+jzBwYa0rKSy0XR8JSxZPwgLr7ys0IBzhGviA1/TUGJLmSVqs8pb9AnvICXEuOHLprYTw==", + "license": "MIT", + "dependencies": { + "tr46": "~0.0.3", + "webidl-conversions": "^3.0.0" + } + }, + "node_modules/xtend": { + "version": "4.0.2", + "resolved": "https://registry.npmjs.org/xtend/-/xtend-4.0.2.tgz", + "integrity": "sha512-LKYU1iAXJXUgAXn9URjiu+MWhyUXHsvfp7mcuYm9dSUKK0/CjtrUwFAxD82/mCWbtLsGjFIad0wIsod4zrTAEQ==", + "license": "MIT", + "engines": { + "node": ">=0.4" + } + } + } +} diff --git a/package.json b/package.json index c3fb804..496e5f1 100644 --- a/package.json +++ b/package.json @@ -17,6 +17,7 @@ "author": "WORLDPLAY Collective", "license": "MIT", "dependencies": { + "@mollie/api-client": "^4.5.0", "express": "^4.18.2", "googleapis": "^144.0.0", "nodemailer": "^6.9.0", diff --git a/payment-success.html b/payment-success.html new file mode 100644 index 0000000..e56ac81 --- /dev/null +++ b/payment-success.html @@ -0,0 +1,330 @@ + + + + + + Payment Confirmation — WORLDPLAY + + + + + + + +
+ + +
+ +
+
+ +
+

Confirming Payment...

+

Waiting for payment confirmation from Mollie. This usually takes a few seconds.

+
+ + +
+
+

Payment Confirmed!

+

You're in, ! A confirmation email is on its way.

+ +
+ +
+ +
+ About food: Food is not included. Expect ~€15–20/person/day. We'll be in touch about food choices before the event. +
+
+ + +
+
+

Payment Not Completed

+

Your payment was not completed. You can return to the registration page to try again.

+
+
+ + ← Back to worldplay.art +
+ + + + diff --git a/server.js b/server.js index b153423..5c487ad 100644 --- a/server.js +++ b/server.js @@ -4,6 +4,8 @@ const path = require('path'); const { google } = require('googleapis'); const nodemailer = require('nodemailer'); const { Pool } = require('pg'); +const { createMollieClient } = require('@mollie/api-client'); +const { assignBooking } = require('./api/booking-sheet'); const app = express(); const PORT = process.env.PORT || 3000; @@ -32,6 +34,24 @@ const listmonkPool = process.env.LISTMONK_DB_HOST ? new Pool({ password: process.env.LISTMONK_DB_PASS || 'listmonk_secure_2025', }) : null; +// Initialize Mollie payment client +const mollieClient = process.env.MOLLIE_API_KEY ? createMollieClient({ apiKey: process.env.MOLLIE_API_KEY }) : null; +const BASE_URL = process.env.BASE_URL || 'https://worldplay.art'; +const EMAIL_TEAM = process.env.EMAIL_TEAM || 'hello@worldplay.art'; + +// Pricing configuration +const PARTICIPATION_FEE = 50; +const PROCESSING_FEE_RATE = 0.02; // 2% +const ACCOMMODATION_OPTIONS = { + 'ch-shared': { label: 'Commons Hub — Shared Room', price: 275 }, + 'ch-double': { label: 'Commons Hub — Double Room', price: 350 }, + 'hh-living': { label: 'Herrnhof Villa — Living Room', price: 315 }, + 'hh-triple': { label: 'Herrnhof Villa — Triple Room', price: 350 }, + 'hh-twin': { label: 'Herrnhof Villa — Twin Room', price: 420 }, + 'hh-single': { label: 'Herrnhof Villa — Single Room', price: 665 }, + 'hh-couple': { label: 'Herrnhof Villa — Couple Room', price: 700 }, +}; + // Google Sheets configuration const GOOGLE_SHEET_ID = process.env.GOOGLE_SHEET_ID; function loadGoogleCredentials() { @@ -117,12 +137,15 @@ async function appendToGoogleSheet(registration) { Array.isArray(registration.contribute) ? registration.contribute.join(', ') : registration.contribute, registration.message, registration.newsletter ? 'Yes' : 'No', - registration.id + registration.id, + registration.paymentStatus || '', + registration.molliePaymentId || '', + registration.accommodationType ? (ACCOMMODATION_OPTIONS[registration.accommodationType]?.label || registration.accommodationType) : '' ]]; await sheets.spreadsheets.values.append({ spreadsheetId: GOOGLE_SHEET_ID, - range: 'Registrations!A:K', + range: 'Registrations!A:N', valueInputOption: 'USER_ENTERED', insertDataOption: 'INSERT_ROWS', requestBody: { values }, @@ -360,7 +383,162 @@ async function addToListmonk(registration) { } } -// Registration endpoint +// Update payment status on Google Sheet (find row by registration ID in column K) +async function updateRegistrationPaymentStatus(registrationId, paymentStatus, molliePaymentId, accommodationType) { + if (!sheets || !GOOGLE_SHEET_ID) return; + + try { + // Read column K to find the row with matching registration ID + const response = await sheets.spreadsheets.values.get({ + spreadsheetId: GOOGLE_SHEET_ID, + range: 'Registrations!K:K', + }); + + const rows = response.data.values || []; + let rowIndex = -1; + for (let i = 0; i < rows.length; i++) { + if (rows[i][0] === registrationId) { + rowIndex = i + 1; // 1-indexed + break; + } + } + + if (rowIndex === -1) { + console.error(`Registration ${registrationId} not found in sheet`); + return; + } + + // Update columns L, M, N + await sheets.spreadsheets.values.batchUpdate({ + spreadsheetId: GOOGLE_SHEET_ID, + resource: { + valueInputOption: 'USER_ENTERED', + data: [ + { range: `Registrations!L${rowIndex}`, values: [[paymentStatus]] }, + { range: `Registrations!M${rowIndex}`, values: [[molliePaymentId || '']] }, + { range: `Registrations!N${rowIndex}`, values: [[accommodationType ? (ACCOMMODATION_OPTIONS[accommodationType]?.label || accommodationType) : '']] }, + ], + }, + }); + + console.log(`Updated sheet payment status for ${registrationId}: ${paymentStatus}`); + } catch (error) { + console.error('Error updating sheet payment status:', error.message); + } +} + +// Send payment confirmation email with booking details +async function sendPaymentConfirmationEmail(registration) { + if (!smtp) { + console.log('SMTP not configured, skipping payment confirmation email...'); + return; + } + + try { + const accomLabel = registration.accommodationType + ? (ACCOMMODATION_OPTIONS[registration.accommodationType]?.label || 'None') + : 'None (participation only)'; + const accomPrice = registration.accommodationType + ? (ACCOMMODATION_OPTIONS[registration.accommodationType]?.price || 0) + : 0; + const subtotal = PARTICIPATION_FEE + accomPrice; + const processingFee = Math.round(subtotal * PROCESSING_FEE_RATE * 100) / 100; + const total = subtotal + processingFee; + + const bookingInfo = registration.bookingResult?.success + ? `Room:${registration.bookingResult.venue} — Room ${registration.bookingResult.room} (${registration.bookingResult.bedType})` + : ''; + + await smtp.sendMail({ + from: process.env.EMAIL_FROM || 'WORLDPLAY ', + to: registration.email, + cc: EMAIL_TEAM, + subject: 'WORLDPLAY — Payment Confirmed & Booking Details', + html: ` + + + + +
+
+

WORLDPLAY

+

To be Defined

+
+ +
+

Payment Confirmed!

+

+ Thank you, ${registration.firstName}! Your registration and payment for WORLDPLAY have been confirmed. You're officially in. +

+
+ +
+

Payment Breakdown

+ + + + + + ${accomPrice > 0 ? ` + + + + + ` : ''} + + + + + + + + +
Participation fee€${PARTICIPATION_FEE.toFixed(2)}
${accomLabel} (7 nights)€${accomPrice.toFixed(2)}
Processing fee (2%)€${processingFee.toFixed(2)}
Total paid€${total.toFixed(2)}
+
+ + ${bookingInfo ? ` +
+

Accommodation

+ + ${bookingInfo} +
+
+ ` : ''} + +
+

About Food

+

+ Food is not included in this payment. Expect approximately €15–20 per person per day. We'll be in touch about food choices and dietary preferences before the event. +

+
+ +
+

Event Details

+

+ 📅 June 7–13, 2026
+ 📍 Commons Hub, Hirschwang an der Rax, Austria
+ 🏔️ Austrian Alps, ~1.5 hours from Vienna by train +

+
+ +
+

+ Questions? Reply to this email or contact ${EMAIL_TEAM} +

+
+
+ + + `, + }); + + console.log(`Payment confirmation email sent to: ${registration.email}`); + } catch (error) { + console.error('Error sending payment confirmation email:', error.message); + } +} + +// Registration endpoint (Step 1 — saves registration with awaiting_payment status) app.post('/api/register', async (req, res) => { try { const { firstName, lastName, email, location, role, otherRole, interests, contribute, message, newsletter } = req.body; @@ -382,7 +560,7 @@ app.post('/api/register', async (req, res) => { return res.status(400).json({ error: 'This email is already registered' }); } - // Create new registration + // Create new registration with awaiting_payment status const registration = { id: Date.now().toString(36) + Math.random().toString(36).substr(2), firstName: firstName.trim(), @@ -394,6 +572,7 @@ app.post('/api/register', async (req, res) => { contribute: contribute || [], message: message?.trim() || '', newsletter: newsletter === 'yes', + paymentStatus: 'awaiting_payment', registeredAt: new Date().toISOString(), ipAddress: req.ip || req.connection.remoteAddress }; @@ -402,20 +581,16 @@ app.post('/api/register', async (req, res) => { registrations.push(registration); await saveRegistrations(registrations); - console.log(`New registration: ${registration.firstName} ${registration.lastName} <${registration.email}>`); + console.log(`New registration (awaiting payment): ${registration.firstName} ${registration.lastName} <${registration.email}>`); // Add to Google Sheet (async, don't block response) appendToGoogleSheet(registration).catch(err => console.error('Sheet error:', err)); - // Send confirmation email (async, don't block response) - sendConfirmationEmail(registration).catch(err => console.error('Email error:', err)); - - // Add to Listmonk for newsletter management (async, don't block response) - addToListmonk(registration).catch(err => console.error('Listmonk error:', err)); + // NOTE: Confirmation email and Listmonk subscription happen AFTER payment res.json({ success: true, - message: 'Registration successful', + message: 'Registration saved', id: registration.id }); @@ -425,6 +600,200 @@ app.post('/api/register', async (req, res) => { } }); +// Create Mollie checkout session (Step 2) +app.post('/api/create-checkout-session', async (req, res) => { + if (!mollieClient) { + return res.status(503).json({ error: 'Payment system not configured' }); + } + + try { + const { registrationId, accommodationType } = req.body; + + if (!registrationId) { + return res.status(400).json({ error: 'Registration ID is required' }); + } + + // Find registration + const registrations = await loadRegistrations(); + const registration = registrations.find(r => r.id === registrationId); + if (!registration) { + return res.status(404).json({ error: 'Registration not found' }); + } + + if (registration.paymentStatus === 'paid') { + return res.status(400).json({ error: 'This registration has already been paid' }); + } + + // Calculate price + let accomPrice = 0; + let accomLabel = 'Participation only'; + if (accommodationType && ACCOMMODATION_OPTIONS[accommodationType]) { + accomPrice = ACCOMMODATION_OPTIONS[accommodationType].price; + accomLabel = ACCOMMODATION_OPTIONS[accommodationType].label; + } + + const subtotal = PARTICIPATION_FEE + accomPrice; + const processingFee = Math.round(subtotal * PROCESSING_FEE_RATE * 100) / 100; + const total = subtotal + processingFee; + + // Save accommodation choice to registration + registration.accommodationType = accommodationType || null; + registration.paymentAmount = total; + await saveRegistrations(registrations); + + // Create Mollie payment + const payment = await mollieClient.payments.create({ + amount: { + currency: 'EUR', + value: total.toFixed(2), + }, + description: `WORLDPLAY 2026 — ${registration.firstName} ${registration.lastName}${accommodationType ? ` + ${accomLabel}` : ''}`, + redirectUrl: `${BASE_URL}/payment-success.html?id=${registrationId}`, + webhookUrl: `${BASE_URL}/api/mollie/webhook`, + metadata: { + registrationId, + accommodationType: accommodationType || 'none', + }, + }); + + // Save Mollie payment ID + registration.molliePaymentId = payment.id; + await saveRegistrations(registrations); + + // Update sheet with pending payment info + updateRegistrationPaymentStatus(registrationId, 'awaiting_payment', payment.id, accommodationType).catch(err => console.error('Sheet update error:', err)); + + console.log(`Mollie checkout created for ${registration.email}: ${payment.id} (€${total.toFixed(2)})`); + + res.json({ + success: true, + checkoutUrl: payment.getCheckoutUrl(), + }); + + } catch (error) { + console.error('Checkout session error:', error); + res.status(500).json({ error: 'Failed to create payment session' }); + } +}); + +// Mollie webhook — called when payment status changes +app.post('/api/mollie/webhook', async (req, res) => { + if (!mollieClient) { + return res.status(503).send('Payment system not configured'); + } + + try { + const paymentId = req.body.id; + if (!paymentId) { + return res.status(400).send('Missing payment ID'); + } + + const payment = await mollieClient.payments.get(paymentId); + const registrationId = payment.metadata?.registrationId; + + if (!registrationId) { + console.error(`Mollie webhook: no registrationId in metadata for ${paymentId}`); + return res.status(200).send('OK'); + } + + const registrations = await loadRegistrations(); + const registration = registrations.find(r => r.id === registrationId); + + if (!registration) { + console.error(`Mollie webhook: registration ${registrationId} not found`); + return res.status(200).send('OK'); + } + + console.log(`Mollie webhook: payment ${paymentId} status=${payment.status} for ${registration.email}`); + + if (payment.status === 'paid' && registration.paymentStatus !== 'paid') { + // Update registration + registration.paymentStatus = 'paid'; + registration.molliePaymentId = paymentId; + registration.paidAt = new Date().toISOString(); + await saveRegistrations(registrations); + + // Update Google Sheet + updateRegistrationPaymentStatus( + registrationId, 'paid', paymentId, registration.accommodationType + ).catch(err => console.error('Sheet update error:', err)); + + // Assign bed on booking sheet if accommodation was selected + if (registration.accommodationType) { + const guestName = `${registration.firstName} ${registration.lastName}`; + const bookingResult = await assignBooking(guestName, registration.accommodationType); + registration.bookingResult = bookingResult; + await saveRegistrations(registrations); + + if (bookingResult.success) { + console.log(`Bed assigned for ${registration.email}: ${bookingResult.venue} Room ${bookingResult.room}`); + } else { + console.warn(`Bed assignment failed for ${registration.email}: ${bookingResult.reason}`); + } + } + + // Send payment confirmation email + sendPaymentConfirmationEmail(registration).catch(err => console.error('Payment email error:', err)); + + // Add to Listmonk (now that payment is confirmed) + addToListmonk(registration).catch(err => console.error('Listmonk error:', err)); + + } else if (payment.status === 'failed' || payment.status === 'canceled' || payment.status === 'expired') { + registration.paymentStatus = payment.status; + await saveRegistrations(registrations); + + updateRegistrationPaymentStatus( + registrationId, payment.status, paymentId, registration.accommodationType + ).catch(err => console.error('Sheet update error:', err)); + } + + res.status(200).send('OK'); + } catch (error) { + console.error('Mollie webhook error:', error); + res.status(200).send('OK'); // Always return 200 to Mollie + } +}); + +// Payment status polling endpoint (for frontend) +app.get('/api/payment-status', async (req, res) => { + try { + const { id } = req.query; + if (!id) { + return res.status(400).json({ error: 'Registration ID required' }); + } + + const registrations = await loadRegistrations(); + const registration = registrations.find(r => r.id === id); + + if (!registration) { + return res.status(404).json({ error: 'Registration not found' }); + } + + const accomType = registration.accommodationType; + const accomPrice = accomType ? (ACCOMMODATION_OPTIONS[accomType]?.price || 0) : 0; + const subtotal = PARTICIPATION_FEE + accomPrice; + const processingFee = Math.round(subtotal * PROCESSING_FEE_RATE * 100) / 100; + const total = subtotal + processingFee; + + res.json({ + paymentStatus: registration.paymentStatus, + firstName: registration.firstName, + lastName: registration.lastName, + email: registration.email, + accommodationType: accomType || null, + accommodationLabel: accomType ? (ACCOMMODATION_OPTIONS[accomType]?.label || null) : null, + participationFee: PARTICIPATION_FEE, + accommodationPrice: accomPrice, + processingFee, + total, + bookingResult: registration.bookingResult || null, + }); + } catch (error) { + console.error('Payment status error:', error); + res.status(500).json({ error: 'Failed to check payment status' }); + } +}); + // Admin endpoint to view registrations (protected by simple token) app.get('/api/registrations', async (req, res) => { const token = req.headers['x-admin-token'] || req.query.token; @@ -547,5 +916,6 @@ ensureDataDir().then(() => { console.log(`Google Sheets: ${sheets ? 'enabled' : 'disabled'}`); console.log(`Email notifications: ${smtp ? 'enabled (Mailcow SMTP)' : 'disabled (no SMTP_PASS)'}`); console.log(`Listmonk newsletter sync: ${listmonkPool ? 'enabled' : 'disabled'} (list ID: ${LISTMONK_LIST_ID})`); + console.log(`Mollie payments: ${mollieClient ? 'enabled' : 'disabled (no MOLLIE_API_KEY)'}`); }); });