dev testing
This commit is contained in:
124
functions/index.js
Normal file
124
functions/index.js
Normal file
@@ -0,0 +1,124 @@
|
||||
const { onRequest } = require("firebase-functions/v2/https");
|
||||
const { initializeApp } = require("firebase-admin/app");
|
||||
const { getFirestore, FieldValue } = require("firebase-admin/firestore");
|
||||
const crypto = require("crypto");
|
||||
|
||||
initializeApp();
|
||||
const db = getFirestore();
|
||||
|
||||
/**
|
||||
* Shopdm Pay Webhook Handler
|
||||
*
|
||||
* Receives payment.successful events from Shopdm Pay.
|
||||
* Matches the invoice_id to a pending donation and creates
|
||||
* a confirmed donor record.
|
||||
*/
|
||||
exports.shopdmPayWebhook = onRequest(
|
||||
{ secrets: ["SHOPDM_MERCHANT_SECRET_DEV"] },
|
||||
async (req, res) => {
|
||||
// Only accept POST requests
|
||||
if (req.method !== "POST") {
|
||||
res.status(405).send("Method not allowed");
|
||||
return;
|
||||
}
|
||||
|
||||
const payload = req.body;
|
||||
|
||||
// Verify webhook signature
|
||||
// Supports both dev and prod secrets
|
||||
const secrets = [
|
||||
process.env.SHOPDM_MERCHANT_SECRET,
|
||||
process.env.SHOPDM_MERCHANT_SECRET_DEV,
|
||||
].filter(Boolean);
|
||||
|
||||
if (secrets.length > 0) {
|
||||
const signature = req.headers["x-shopdm-signature"];
|
||||
if (!signature) {
|
||||
console.error("Missing X-Shopdm-Signature header");
|
||||
res.status(401).send("Unauthorized");
|
||||
return;
|
||||
}
|
||||
|
||||
const sortedPayload = JSON.stringify(
|
||||
payload,
|
||||
Object.keys(payload).sort()
|
||||
);
|
||||
|
||||
const isValid = secrets.some((secret) => {
|
||||
const expected = crypto
|
||||
.createHmac("sha256", secret)
|
||||
.update(sortedPayload, "utf8")
|
||||
.digest("hex");
|
||||
return signature === expected;
|
||||
});
|
||||
|
||||
if (!isValid) {
|
||||
console.error("Invalid webhook signature");
|
||||
res.status(401).send("Unauthorized");
|
||||
return;
|
||||
}
|
||||
}
|
||||
|
||||
// Only process successful payments
|
||||
if (payload.event !== "payment.successful") {
|
||||
res.status(200).send("Event ignored");
|
||||
return;
|
||||
}
|
||||
|
||||
const invoiceId = payload.metadata?.invoice_id;
|
||||
if (!invoiceId) {
|
||||
console.log("No invoice_id in webhook payload, skipping donor creation");
|
||||
res.status(200).send("OK - no invoice_id");
|
||||
return;
|
||||
}
|
||||
|
||||
try {
|
||||
// Look up the pending donation
|
||||
const pendingRef = db.collection("pendingDonations").doc(invoiceId);
|
||||
const pendingDoc = await pendingRef.get();
|
||||
|
||||
if (!pendingDoc.exists) {
|
||||
console.log(`No pending donation found for invoice_id: ${invoiceId}`);
|
||||
res.status(200).send("OK - no matching pending donation");
|
||||
return;
|
||||
}
|
||||
|
||||
const pending = pendingDoc.data();
|
||||
|
||||
// Check if already processed (idempotency)
|
||||
if (pending.status === "confirmed") {
|
||||
console.log(`Donation ${invoiceId} already confirmed, skipping`);
|
||||
res.status(200).send("OK - already processed");
|
||||
return;
|
||||
}
|
||||
|
||||
// Get the actual payment amount from the webhook
|
||||
const paidAmount = payload.data?.amount_xcd || pending.amount;
|
||||
|
||||
// Create the confirmed donor record
|
||||
await db.collection("donors").add({
|
||||
name: pending.name,
|
||||
amount: Number(paidAmount),
|
||||
classYear: pending.classYear || "",
|
||||
message: pending.message || "",
|
||||
anonymous: !!pending.anonymous,
|
||||
status: "confirmed",
|
||||
pendingDonationId: invoiceId,
|
||||
paymentId: payload.data?.object_id || "",
|
||||
date: FieldValue.serverTimestamp(),
|
||||
});
|
||||
|
||||
// Mark the pending donation as confirmed
|
||||
await pendingRef.update({
|
||||
status: "confirmed",
|
||||
confirmedAt: FieldValue.serverTimestamp(),
|
||||
paymentId: payload.data?.object_id || "",
|
||||
});
|
||||
|
||||
console.log(`Donation confirmed for invoice_id: ${invoiceId}, amount: ${paidAmount}`);
|
||||
res.status(200).send("OK");
|
||||
} catch (error) {
|
||||
console.error("Error processing webhook:", error);
|
||||
res.status(500).send("Internal server error");
|
||||
}
|
||||
});
|
||||
Reference in New Issue
Block a user