diff --git a/.gitignore b/.gitignore index 3372c3b8..a80850a3 100644 --- a/.gitignore +++ b/.gitignore @@ -1,3 +1,5 @@ *.swp _site .DS_Store +.env +node_modules diff --git a/_pages/pay.md b/_pages/pay.md index 027c1d2a..4bc674b9 100644 --- a/_pages/pay.md +++ b/_pages/pay.md @@ -3,14 +3,9 @@ title: Pay MITOC --- -
-

Please be advised that we have been having issues processing payments from PayPal.

-

We recommend that you use a credit or debit card for online payment (we always accept personal checks in the office).

-
- -This page can be used to pay money to MITOC using PayPal or a credit/debit card. Payments can also be made by check at the [MITOC office](/where-is-mitoc) during [office hours](/calendar). **All payments are final and non-refundable.** +This page can be used to pay MITOC by credit or debit card. Payments can also be made by check at the [MITOC office](/where-is-mitoc) during [office hours](/calendar). **All payments are final and non-refundable.** -If you are having trouble with the payment system, contact [mitoc-bursar@mit.edu](mailto:mitoc-bursar@mit.edu). The most common issue is that the billing address must exactly match what your bank has on file, so if your card is declined, double-check the address you entered. +If you are having trouble with the payment system, contact [mitoc-bursar@mit.edu](mailto:mitoc-bursar@mit.edu). The most common issue is that the billing address must exactly match what your bank has on file. ### Financial Assistance We can provide need-blind financial assistance to any MIT undergrad, upon request. @@ -25,20 +20,10 @@ See the signup page [here](/join). ### Gear Rentals and Purchases
-
- - +
- - @@ -48,17 +33,12 @@ See the signup page [here](/join).
- - + +
- - + +
@@ -67,37 +47,21 @@ See the signup page [here](/join). ### Trip and Event Fees
-
- - +
- - + {% for fee in site.data.trip_fees %} - + {% endfor %}
- +
- - + +
- - + +
-
@@ -136,68 +88,42 @@ Be sure to fill out the visitor log ([Camelot](https://docs.google.com/spreadshe Days spent on-site but not in the cabin (e.g. Yurt / Lean-to / camping) cost 0.5 person-days (i.e. half price of using the cabin.) Enter the total number of fractional days spent outside and full days spent inside in the form below.
-
- +
- -
- +
- - + +
- - + +
diff --git a/_pages/pay/success.md b/_pages/pay/success.md new file mode 100644 index 00000000..99a44a26 --- /dev/null +++ b/_pages/pay/success.md @@ -0,0 +1,10 @@ +--- +permalink: /pay/success +title: Payment Received +--- + +Your payment was successful. A receipt has been sent to your email. + +Questions? Contact [mitoc-bursar@mit.edu](mailto:mitoc-bursar@mit.edu). + +Back to payments diff --git a/docker-compose.yml b/docker-compose.yml index 971c7a10..aeec6360 100644 --- a/docker-compose.yml +++ b/docker-compose.yml @@ -7,3 +7,22 @@ services: - .:/site ports: - "4000:4000" + + functions: + image: node:18-alpine + working_dir: /app + volumes: + - .:/app + command: node docker/functions-server.js + env_file: + - .env + + proxy: + image: nginx:alpine + volumes: + - ./docker/nginx.conf:/etc/nginx/nginx.conf:ro + ports: + - "8888:8888" + depends_on: + - jekyll + - functions diff --git a/docker/functions-server.js b/docker/functions-server.js new file mode 100644 index 00000000..14badd43 --- /dev/null +++ b/docker/functions-server.js @@ -0,0 +1,26 @@ +const http = require('http'); +const handler = require('../netlify/functions/create-checkout-session'); + +const server = http.createServer(async (req, res) => { + if (req.method !== 'POST') { + res.writeHead(405); + res.end('Method Not Allowed'); + return; + } + + let body = ''; + req.on('data', chunk => body += chunk); + req.on('end', async () => { + const event = { + httpMethod: 'POST', + body, + headers: { origin: req.headers.origin || 'http://localhost:8888' }, + }; + + const result = await handler.handler(event); + res.writeHead(result.statusCode, result.headers || {}); + res.end(result.body); + }); +}); + +server.listen(9999, () => console.log('Functions server on port 9999')); diff --git a/docker/nginx.conf b/docker/nginx.conf new file mode 100644 index 00000000..5c10568e --- /dev/null +++ b/docker/nginx.conf @@ -0,0 +1,16 @@ +events {} + +http { + server { + listen 8888; + + location /.netlify/functions/ { + proxy_pass http://functions:9999/; + proxy_set_header Origin http://localhost:8888; + } + + location / { + proxy_pass http://jekyll:4000; + } + } +} diff --git a/js/mitoc.js b/js/mitoc.js index 21c9712b..765b4254 100644 --- a/js/mitoc.js +++ b/js/mitoc.js @@ -1,30 +1,86 @@ ---- ---- - -//Load trip fee schedule from Google Sheets on the payment page -function load_trip_fees() { - //Populate dropdown menu and list of prices - var form = document.forms['trip_form']; - var options = form.merchantDefinedData2.options; - var prices = { - {% for fee in site.data.trip_fees %} - "{{fee.name}}": { "price": "{{ fee.price }}", "category": "{{ fee.category }}" }, - {% endfor %} - }; - //Add handler to calculate price when quantity changes - var calculate_amount = function() { - if(form.merchantDefinedData2.value) { - form.amount.value = form.merchantDefinedData4.value * prices[form.merchantDefinedData2.value].price; - } - }; - $('#merchantDefinedData4trip').keyup(calculate_amount); - $('#merchantDefinedData4trip').change(calculate_amount); - - //Add handler to calculate price and populate category when trip selection changes - $('#merchantDefinedData2trip').change(function() { - if(form.merchantDefinedData2.value) { - calculate_amount(); - form.merchantDefinedData1.value = prices[form.merchantDefinedData2.value].category; - } - }); -} +--- +--- + +function submit_payment(amount_dollars, description, metadata) { + var amount_cents = Math.round(parseFloat(amount_dollars) * 100); + if (!amount_cents || amount_cents < 50) { + alert('Please enter a valid amount (minimum $0.50).'); + return; + } + + fetch('/.netlify/functions/create-checkout-session', { + method: 'POST', + headers: { 'Content-Type': 'application/json' }, + body: JSON.stringify({ amount_cents: amount_cents, description: description, metadata: metadata }), + }) + .then(function(res) { return res.json(); }) + .then(function(data) { + if (data.url) { + window.location = data.url; + } else { + alert('Payment error. Please try again or contact mitoc-bursar@mit.edu.'); + } + }) + .catch(function() { + alert('Payment error. Please try again or contact mitoc-bursar@mit.edu.'); + }); +} + +function init_payment_forms() { + var gear_form = document.getElementById('gear_form'); + if (gear_form) { + gear_form.addEventListener('submit', function(e) { + e.preventDefault(); + var type = gear_form.querySelector('[name="payment_type"]').value; + var renter = gear_form.querySelector('[name="renter_name"]').value; + var amount = gear_form.querySelector('[name="amount"]').value; + submit_payment(amount, 'Gear: ' + type, { payment_type: type, renter_name: renter }); + }); + } + + var trip_form = document.getElementById('trip_form'); + if (trip_form) { + var prices = { + {% for fee in site.data.trip_fees %} + "{{ fee.name }}": {{ fee.price }}, + {% endfor %} + }; + + var calc_trip_amount = function() { + var trip = trip_form.querySelector('[name="trip_name"]').value; + var qty = parseInt(trip_form.querySelector('[name="quantity"]').value, 10) || 1; + trip_form.querySelector('[name="amount"]').value = + trip && prices[trip] ? (prices[trip] * qty).toFixed(2) : '0.00'; + }; + + trip_form.querySelector('[name="trip_name"]').addEventListener('change', calc_trip_amount); + trip_form.querySelector('[name="quantity"]').addEventListener('change', calc_trip_amount); + trip_form.querySelector('[name="quantity"]').addEventListener('keyup', calc_trip_amount); + + trip_form.addEventListener('submit', function(e) { + e.preventDefault(); + var trip = trip_form.querySelector('[name="trip_name"]').value; + var qty = trip_form.querySelector('[name="quantity"]').value; + var comments = trip_form.querySelector('[name="comments"]').value; + var amount = trip_form.querySelector('[name="amount"]').value; + if (!trip) { alert('Please select a trip.'); return; } + submit_payment(amount, trip, { trip: trip, quantity: qty, comments: comments }); + }); + } + + var cabin_form = document.getElementById('cabin_form'); + if (cabin_form) { + cabin_form.addEventListener('submit', function(e) { + e.preventDefault(); + var cabin = cabin_form.querySelector('[name="cabin"]').value; + var nights = cabin_form.querySelector('[name="person_nights"]').value; + var keyholder = cabin_form.querySelector('[name="keyholder_name"]').value; + var amount = cabin_form.querySelector('[name="amount"]').value; + submit_payment( + amount, + 'Cabin: ' + cabin + ' x ' + nights + ' person-nights', + { cabin: cabin, person_nights: nights, keyholder_name: keyholder } + ); + }); + } +} diff --git a/netlify.toml b/netlify.toml index 6621d15f..5388575e 100644 --- a/netlify.toml +++ b/netlify.toml @@ -1,3 +1,6 @@ [build] publish = "_site" command = "jekyll build" + +[functions] + directory = "netlify/functions" diff --git a/netlify/functions/create-checkout-session.js b/netlify/functions/create-checkout-session.js new file mode 100644 index 00000000..764eb735 --- /dev/null +++ b/netlify/functions/create-checkout-session.js @@ -0,0 +1,50 @@ +const Stripe = require('stripe'); + +exports.handler = async function (event) { + if (event.httpMethod !== 'POST') { + return { statusCode: 405, body: 'Method Not Allowed' }; + } + + let body; + try { + body = JSON.parse(event.body); + } catch { + return { statusCode: 400, body: 'Invalid request body' }; + } + + const { amount_cents, description, metadata } = body; + + if (!Number.isInteger(amount_cents) || amount_cents < 50) { + return { statusCode: 400, body: 'amount_cents must be an integer >= 50' }; + } + + const stripe = Stripe(process.env.STRIPE_SECRET_KEY); + const origin = event.headers.origin || 'https://mitoc.mit.edu'; + + try { + const session = await stripe.checkout.sessions.create({ + mode: 'payment', + line_items: [ + { + price_data: { + currency: 'usd', + unit_amount: amount_cents, + product_data: { name: description }, + }, + quantity: 1, + }, + ], + metadata: metadata || {}, + success_url: `${origin}/pay/success`, + cancel_url: `${origin}/pay`, + }); + + return { + statusCode: 200, + headers: { 'Content-Type': 'application/json' }, + body: JSON.stringify({ url: session.url }), + }; + } catch (err) { + return { statusCode: 500, body: err.message }; + } +}; diff --git a/package-lock.json b/package-lock.json new file mode 100644 index 00000000..ca5b3552 --- /dev/null +++ b/package-lock.json @@ -0,0 +1,304 @@ +{ + "name": "mitoc-website", + "lockfileVersion": 3, + "requires": true, + "packages": { + "": { + "name": "mitoc-website", + "dependencies": { + "stripe": "^16.0.0" + } + }, + "node_modules/@types/node": { + "version": "25.6.2", + "resolved": "https://registry.npmjs.org/@types/node/-/node-25.6.2.tgz", + "integrity": "sha512-sokuT28dxf9JT5Kady1fsXOvI4HVpjZa95NKT5y9PNTIrs2AsobR4GFAA90ZG8M+nxVRLysCXsVj6eGC7Vbrlw==", + "license": "MIT", + "dependencies": { + "undici-types": "~7.19.0" + } + }, + "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/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/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/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/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/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/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/hasown": { + "version": "2.0.3", + "resolved": "https://registry.npmjs.org/hasown/-/hasown-2.0.3.tgz", + "integrity": "sha512-ej4AhfhfL2Q2zpMmLo7U1Uv9+PyhIZpgQLGT1F9miIGmiCJIoCgSmczFdrc97mWT4kVY72KA+WnnhJ5pghSvSg==", + "license": "MIT", + "dependencies": { + "function-bind": "^1.1.2" + }, + "engines": { + "node": ">= 0.4" + } + }, + "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/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/qs": { + "version": "6.15.1", + "resolved": "https://registry.npmjs.org/qs/-/qs-6.15.1.tgz", + "integrity": "sha512-6YHEFRL9mfgcAvql/XhwTvf5jKcOiiupt2FiJxHkiX1z4j7WL8J/jRHYLluORvc1XxB5rV20KoeK00gVJamspg==", + "license": "BSD-3-Clause", + "dependencies": { + "side-channel": "^1.1.0" + }, + "engines": { + "node": ">=0.6" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "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/stripe": { + "version": "16.12.0", + "resolved": "https://registry.npmjs.org/stripe/-/stripe-16.12.0.tgz", + "integrity": "sha512-H7eFVLDxeTNNSn4JTRfL2//LzCbDrMSZ+2q1c7CanVWgK2qIW5TwS+0V7N9KcKZZNpYh/uCqK0PyZh/2UsaAtQ==", + "license": "MIT", + "dependencies": { + "@types/node": ">=8.1.0", + "qs": "^6.11.0" + }, + "engines": { + "node": ">=12.*" + } + }, + "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" + } + } +} diff --git a/package.json b/package.json new file mode 100644 index 00000000..4247bbc6 --- /dev/null +++ b/package.json @@ -0,0 +1,7 @@ +{ + "name": "mitoc-website", + "private": true, + "dependencies": { + "stripe": "^16.0.0" + } +}