From e0aec64bda5ba8a533ac59dec5c32adf664b6f43 Mon Sep 17 00:00:00 2001 From: Geoffrey Wu Date: Sat, 24 Oct 2026 17:55:52 -0500 Subject: [PATCH 1/4] Record the amount a user paid on geoword payments Co-Authored-By: Claude Sonnet 5.5 --- database/geoword/admin/get-payment-amount.js | 4 ++++ database/geoword/record-payment.js | 11 ++++++++--- routes/api/geoword/admin/record-payment.js | 2 +- routes/api/webhook.js | 2 +- 4 files changed, 14 insertions(+), 5 deletions(-) diff --git a/database/geoword/admin/get-payment-amount.js b/database/geoword/admin/get-payment-amount.js index 7e17bc471..54f698cde 100644 --- a/database/geoword/admin/get-payment-amount.js +++ b/database/geoword/admin/get-payment-amount.js @@ -6,6 +6,10 @@ import getSet from '../get-set.js'; * @returns {Promise} the amount of the payment, in cents (USD) */ async function getPaymentAmount (payment) { + if (payment.amountInCents !== null && payment.amountInCents !== undefined) { + return payment.amountInCents; + } + if (payment.packet) { return await getCost(payment.packet.name); } diff --git a/database/geoword/record-payment.js b/database/geoword/record-payment.js index 6bdf506dc..3d4922bd0 100644 --- a/database/geoword/record-payment.js +++ b/database/geoword/record-payment.js @@ -2,12 +2,17 @@ import { packets, payments } from './collections.js'; import getSetPayment from './get-set-payment.js'; -async function recordPayment (packetName, userId, manual = false) { +/** + * @param {String} packetName + * @param {ObjectId} userId + * @param {{ manual?: boolean, amountInCents?: number }} [options] + */ +async function recordPayment (packetName, userId, { manual = false, amountInCents = null } = {}) { const setPayment = await getSetPayment(packetName); if (setPayment) { return await payments.replaceOne( { user_id: userId, 'set._id': setPayment.set._id, packet: { $exists: false } }, - { user_id: userId, set: setPayment.set, createdAt: new Date(), manual }, + { user_id: userId, set: setPayment.set, createdAt: new Date(), manual, amountInCents }, { upsert: true } ); } @@ -15,7 +20,7 @@ async function recordPayment (packetName, userId, manual = false) { const packet = await packets.findOne({ name: packetName }); return await payments.replaceOne( { user_id: userId, 'packet.name': packetName }, - { user_id: userId, packet: { _id: packet._id, name: packetName }, set: packet.set, createdAt: new Date(), manual }, + { user_id: userId, packet: { _id: packet._id, name: packetName }, set: packet.set, createdAt: new Date(), manual, amountInCents }, { upsert: true } ); } diff --git a/routes/api/geoword/admin/record-payment.js b/routes/api/geoword/admin/record-payment.js index 9a529f457..09e6b1cfc 100644 --- a/routes/api/geoword/admin/record-payment.js +++ b/routes/api/geoword/admin/record-payment.js @@ -11,7 +11,7 @@ router.put('/', async (req, res) => { const userId = await getUserId(username); if (!userId) { return res.status(400).send('User not found'); } - const result = await recordPayment(packetName, userId, true); + const result = await recordPayment(packetName, userId, { manual: true }); console.log(userId, result); diff --git a/routes/api/webhook.js b/routes/api/webhook.js index cc645a0b1..a85c6e0ee 100644 --- a/routes/api/webhook.js +++ b/routes/api/webhook.js @@ -33,7 +33,7 @@ router.post('/', (req, res) => { // Then define and call a function to handle the event payment_intent.succeeded let { user_id: userId, packetName } = paymentIntentSucceeded.metadata; try { userId = new ObjectId(userId); } catch (e) { return res.status(400).send('Invalid user ID'); } - recordPayment(packetName, userId); + recordPayment(packetName, userId, { amountInCents: paymentIntentSucceeded.amount_received }); break; } From fa3d106d72fdaea8f7f2bb9913968f927cfccaea Mon Sep 17 00:00:00 2001 From: Geoffrey Wu Date: Tue, 27 Oct 2026 00:01:30 -0400 Subject: [PATCH 2/4] Record the cost at the time of a manual geoword payment Co-Authored-By: Claude Sonnet 5.5 --- routes/api/geoword/admin/record-payment.js | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/routes/api/geoword/admin/record-payment.js b/routes/api/geoword/admin/record-payment.js index 09e6b1cfc..206479c8e 100644 --- a/routes/api/geoword/admin/record-payment.js +++ b/routes/api/geoword/admin/record-payment.js @@ -1,3 +1,4 @@ +import getCost from '../../../../database/geoword/get-cost.js'; import recordPayment from '../../../../database/geoword/record-payment.js'; import getUserId from '../../../../database/account-info/get-user-id.js'; @@ -11,7 +12,8 @@ router.put('/', async (req, res) => { const userId = await getUserId(username); if (!userId) { return res.status(400).send('User not found'); } - const result = await recordPayment(packetName, userId, { manual: true }); + const amountInCents = await getCost(packetName); + const result = await recordPayment(packetName, userId, { manual: true, amountInCents }); console.log(userId, result); From 73f15901827c0725d6fd4e4e5ede06222c99a323 Mon Sep 17 00:00:00 2001 From: Geoffrey Wu Date: Sat, 24 Oct 2026 17:56:30 -0500 Subject: [PATCH 3/4] Log who manually recorded a geoword payment Co-Authored-By: Claude Sonnet 5.5 --- client/play/geoword/admin/invoice.html | 1 + client/play/geoword/admin/invoice.js | 6 +++++- database/geoword/admin/get-invoice.js | 3 +++ database/geoword/record-payment.js | 8 ++++---- routes/api/geoword/admin/record-payment.js | 2 +- 5 files changed, 14 insertions(+), 6 deletions(-) diff --git a/client/play/geoword/admin/invoice.html b/client/play/geoword/admin/invoice.html index 9eeb84812..afe0512ed 100644 --- a/client/play/geoword/admin/invoice.html +++ b/client/play/geoword/admin/invoice.html @@ -25,6 +25,7 @@

Invoice

Packet id (or set id):

Username:

User id:

+

Manually recorded by:

Amount: $

diff --git a/client/play/geoword/admin/invoice.js b/client/play/geoword/admin/invoice.js index 5a75d1206..03e2d3b6b 100644 --- a/client/play/geoword/admin/invoice.js +++ b/client/play/geoword/admin/invoice.js @@ -6,11 +6,15 @@ fetch('/api/geoword/admin/invoice?' + new URLSearchParams({ _id, packetName })) .then(response => response.json()) .then(data => { const { amount, payment } = data; - const { packet, set, user_id: userId, username, createdAt } = payment; + const { packet, set, user_id: userId, username, createdAt, recordedBy, recordedByUsername } = payment; document.getElementById('date').textContent = new Date(createdAt).toLocaleString(); document.getElementById('packet-name').textContent = packet?.name ?? `All packets in ${set.name}`; document.getElementById('packet-id').textContent = packet?._id ?? set._id; document.getElementById('user-id').textContent = userId; document.getElementById('username').textContent = username; + if (recordedBy) { + document.getElementById('recorded-by').textContent = `${recordedByUsername} (${recordedBy})`; + document.getElementById('recorded-by-row').classList.remove('d-none'); + } document.getElementById('amount').textContent = (amount / 100).toFixed(2); }); diff --git a/database/geoword/admin/get-invoice.js b/database/geoword/admin/get-invoice.js index 55ea2170c..0d5398426 100644 --- a/database/geoword/admin/get-invoice.js +++ b/database/geoword/admin/get-invoice.js @@ -6,6 +6,9 @@ export default async function getInvoice ({ _id }) { if (payment) { payment.username = await getUsername(payment.user_id); + if (payment.recordedBy) { + payment.recordedByUsername = await getUsername(payment.recordedBy); + } } return payment; diff --git a/database/geoword/record-payment.js b/database/geoword/record-payment.js index 3d4922bd0..88c768310 100644 --- a/database/geoword/record-payment.js +++ b/database/geoword/record-payment.js @@ -5,14 +5,14 @@ import getSetPayment from './get-set-payment.js'; /** * @param {String} packetName * @param {ObjectId} userId - * @param {{ manual?: boolean, amountInCents?: number }} [options] + * @param {{ manual?: boolean, amountInCents?: number, recordedBy?: ObjectId }} [options] */ -async function recordPayment (packetName, userId, { manual = false, amountInCents = null } = {}) { +async function recordPayment (packetName, userId, { manual = false, amountInCents = null, recordedBy = null } = {}) { const setPayment = await getSetPayment(packetName); if (setPayment) { return await payments.replaceOne( { user_id: userId, 'set._id': setPayment.set._id, packet: { $exists: false } }, - { user_id: userId, set: setPayment.set, createdAt: new Date(), manual, amountInCents }, + { user_id: userId, set: setPayment.set, createdAt: new Date(), manual, amountInCents, recordedBy }, { upsert: true } ); } @@ -20,7 +20,7 @@ async function recordPayment (packetName, userId, { manual = false, amountInCent const packet = await packets.findOne({ name: packetName }); return await payments.replaceOne( { user_id: userId, 'packet.name': packetName }, - { user_id: userId, packet: { _id: packet._id, name: packetName }, set: packet.set, createdAt: new Date(), manual, amountInCents }, + { user_id: userId, packet: { _id: packet._id, name: packetName }, set: packet.set, createdAt: new Date(), manual, amountInCents, recordedBy }, { upsert: true } ); } diff --git a/routes/api/geoword/admin/record-payment.js b/routes/api/geoword/admin/record-payment.js index 206479c8e..ada74dd61 100644 --- a/routes/api/geoword/admin/record-payment.js +++ b/routes/api/geoword/admin/record-payment.js @@ -13,7 +13,7 @@ router.put('/', async (req, res) => { if (!userId) { return res.status(400).send('User not found'); } const amountInCents = await getCost(packetName); - const result = await recordPayment(packetName, userId, { manual: true, amountInCents }); + const result = await recordPayment(packetName, userId, { manual: true, amountInCents, recordedBy: res.locals.userId }); console.log(userId, result); From 158ab7a5a7c429dbe21fd231cec82049b8c557a9 Mon Sep 17 00:00:00 2001 From: Geoffrey Wu Date: Sat, 24 Oct 2026 17:56:30 -0500 Subject: [PATCH 4/4] Log who manually recorded a geoword payment Co-Authored-By: Claude Sonnet 5.5 --- client/play/geoword/admin/invoice.html | 1 + client/play/geoword/admin/invoice.js | 6 +++++- database/geoword/admin/get-invoice.js | 3 +++ database/geoword/record-payment.js | 8 ++++---- routes/api/geoword/admin/record-payment.js | 2 +- 5 files changed, 14 insertions(+), 6 deletions(-) diff --git a/client/play/geoword/admin/invoice.html b/client/play/geoword/admin/invoice.html index 9eeb84812..afe0512ed 100644 --- a/client/play/geoword/admin/invoice.html +++ b/client/play/geoword/admin/invoice.html @@ -25,6 +25,7 @@

Invoice

Packet id (or set id):

Username:

User id:

+

Manually recorded by:

Amount: $

diff --git a/client/play/geoword/admin/invoice.js b/client/play/geoword/admin/invoice.js index 5a75d1206..03e2d3b6b 100644 --- a/client/play/geoword/admin/invoice.js +++ b/client/play/geoword/admin/invoice.js @@ -6,11 +6,15 @@ fetch('/api/geoword/admin/invoice?' + new URLSearchParams({ _id, packetName })) .then(response => response.json()) .then(data => { const { amount, payment } = data; - const { packet, set, user_id: userId, username, createdAt } = payment; + const { packet, set, user_id: userId, username, createdAt, recordedBy, recordedByUsername } = payment; document.getElementById('date').textContent = new Date(createdAt).toLocaleString(); document.getElementById('packet-name').textContent = packet?.name ?? `All packets in ${set.name}`; document.getElementById('packet-id').textContent = packet?._id ?? set._id; document.getElementById('user-id').textContent = userId; document.getElementById('username').textContent = username; + if (recordedBy) { + document.getElementById('recorded-by').textContent = `${recordedByUsername} (${recordedBy})`; + document.getElementById('recorded-by-row').classList.remove('d-none'); + } document.getElementById('amount').textContent = (amount / 100).toFixed(2); }); diff --git a/database/geoword/admin/get-invoice.js b/database/geoword/admin/get-invoice.js index 55ea2170c..0d5398426 100644 --- a/database/geoword/admin/get-invoice.js +++ b/database/geoword/admin/get-invoice.js @@ -6,6 +6,9 @@ export default async function getInvoice ({ _id }) { if (payment) { payment.username = await getUsername(payment.user_id); + if (payment.recordedBy) { + payment.recordedByUsername = await getUsername(payment.recordedBy); + } } return payment; diff --git a/database/geoword/record-payment.js b/database/geoword/record-payment.js index 3d4922bd0..88c768310 100644 --- a/database/geoword/record-payment.js +++ b/database/geoword/record-payment.js @@ -5,14 +5,14 @@ import getSetPayment from './get-set-payment.js'; /** * @param {String} packetName * @param {ObjectId} userId - * @param {{ manual?: boolean, amountInCents?: number }} [options] + * @param {{ manual?: boolean, amountInCents?: number, recordedBy?: ObjectId }} [options] */ -async function recordPayment (packetName, userId, { manual = false, amountInCents = null } = {}) { +async function recordPayment (packetName, userId, { manual = false, amountInCents = null, recordedBy = null } = {}) { const setPayment = await getSetPayment(packetName); if (setPayment) { return await payments.replaceOne( { user_id: userId, 'set._id': setPayment.set._id, packet: { $exists: false } }, - { user_id: userId, set: setPayment.set, createdAt: new Date(), manual, amountInCents }, + { user_id: userId, set: setPayment.set, createdAt: new Date(), manual, amountInCents, recordedBy }, { upsert: true } ); } @@ -20,7 +20,7 @@ async function recordPayment (packetName, userId, { manual = false, amountInCent const packet = await packets.findOne({ name: packetName }); return await payments.replaceOne( { user_id: userId, 'packet.name': packetName }, - { user_id: userId, packet: { _id: packet._id, name: packetName }, set: packet.set, createdAt: new Date(), manual, amountInCents }, + { user_id: userId, packet: { _id: packet._id, name: packetName }, set: packet.set, createdAt: new Date(), manual, amountInCents, recordedBy }, { upsert: true } ); } diff --git a/routes/api/geoword/admin/record-payment.js b/routes/api/geoword/admin/record-payment.js index 206479c8e..ada74dd61 100644 --- a/routes/api/geoword/admin/record-payment.js +++ b/routes/api/geoword/admin/record-payment.js @@ -13,7 +13,7 @@ router.put('/', async (req, res) => { if (!userId) { return res.status(400).send('User not found'); } const amountInCents = await getCost(packetName); - const result = await recordPayment(packetName, userId, { manual: true, amountInCents }); + const result = await recordPayment(packetName, userId, { manual: true, amountInCents, recordedBy: res.locals.userId }); console.log(userId, result);