Spaces:
Sleeping
Sleeping
File size: 3,095 Bytes
4c6b1e8 | 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 | const Order = require("../models/Order");
const payoneerCheckout = require("../utils/payoneerCheckout");
const { confirmOrderAndGrantAccess } = require("../utils/orderConfirmation");
const logger = require("../utils/logger");
// PAYONEER-DOCS-NEEDED: confirm the actual header name Payoneer sends
// the webhook signature in — this is a placeholder guess following
// common convention (e.g. Stripe uses "Stripe-Signature").
const SIGNATURE_HEADER = "x-payoneer-signature";
/**
* Receives Payoneer Checkout's payment-status webhook and, on a
* successful payment, automatically confirms the matching Order and
* grants the buyer access — no admin involved, unlike the manual
* bank_wire/payoneer-reference flow.
*
* Registered in app.js with a raw body parser (see
* routes/webhook.routes.js) so `req.body` here is the exact raw
* bytes Payoneer signed, not the parsed JSON the rest of the app uses.
*
* Always responds 200 once the webhook is understood (even for a
* "not our order" or "already confirmed" case) so Payoneer doesn't
* keep retrying a webhook we've already handled. Only a genuinely bad
* signature or unreadable payload gets a 4xx.
*/
async function handlePayoneerWebhook(req, res) {
let parsed;
try {
parsed = payoneerCheckout.verifyAndParseWebhook({
rawBody: req.body,
signatureHeader: req.headers[SIGNATURE_HEADER],
});
} catch (err) {
logger.error("Payoneer webhook rejected", { error: err.message });
return res.status(err.status || 400).json({ error: "Invalid webhook." });
}
try {
if (parsed.status !== "paid") {
// Payoneer also sends non-success events (failed, cancelled,
// pending, etc.) — nothing to do for those except acknowledge.
logger.info("Payoneer webhook received (non-paid status, no action)", { status: parsed.status });
return res.status(200).json({ received: true });
}
const order = await Order.findOne({ providerSessionId: parsed.sessionId, paymentMethod: "payoneer_checkout" });
if (!order) {
logger.error("Payoneer webhook: no matching order for session", { sessionId: parsed.sessionId });
// Still 200 — this is Payoneer's event, not a request we want retried forever.
return res.status(200).json({ received: true, matched: false });
}
const { alreadyConfirmed } = await confirmOrderAndGrantAccess({ orderId: order._id, confirmedBy: null });
logger.info("Payoneer webhook: order auto-confirmed", {
orderId: order._id.toString(),
alreadyConfirmed,
});
res.status(200).json({ received: true, matched: true });
} catch (err) {
logger.error("Payoneer webhook processing failed", { error: err.message });
// 200 anyway: a bug on our side confirming the order shouldn't
// cause Payoneer to hammer us with retries for an event we did
// successfully authenticate and understand. This case should
// instead be caught by monitoring your server logs.
res.status(200).json({ received: true, error: "processing_failed" });
}
}
module.exports = { handlePayoneerWebhook };
|