Commit 9df1644b by tdgiang

Verify status and amount fields on epay IPN, not just resultCd

Per MGP_Merchant_Interface_V1.4.7VN.md section 5.3.2: resultCd "00_000"
alone does not mean "payment success" - MGP can send it for a refund
IPN too (status field: "0" = payment, "2" = refund). epayIPN previously
set status="success" purely off resultCd, which would have misclassified
a refund event on a still-pending transaction. Now requires resultCd
"00_000" AND status "0".

Also adds an explicit amount cross-check between the IPN body and the
stored transaction (spec's "Lưu ý đặc biệt 2") as a defense-in-depth,
clearly-diagnosable guard alongside the existing signature verification
(which already implicitly enforces this, but with a generic error).
Co-Authored-By: 's avatarClaude Sonnet 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_017KPzWwuTEeX2vXGXvyGn4q
parent a970a2ac
...@@ -204,13 +204,31 @@ exports.epayIPN = function (req, res) { ...@@ -204,13 +204,31 @@ exports.epayIPN = function (req, res) {
return res.status(400).json({ code: "99", data: "INVALID_SIGNATURE" }); return res.status(400).json({ code: "99", data: "INVALID_SIGNATURE" });
} }
// Defense-in-depth: the signature above is computed with tx.amount, so a mismatched
// b.amount would already fail verification - this makes that guarantee explicit and
// gives a clear, specific error instead of a generic signature failure if it ever fires.
if (String(parseInt(b.amount, 10)) !== String(tx.amount)) {
console.error(
"epayIPN: AMOUNT_MISMATCH for merTrxId=" + merTrxId + " expected=" + tx.amount + " got=" + b.amount
);
return res.status(400).json({ code: "99", data: "AMOUNT_MISMATCH" });
}
if (tx.status !== "pending") { if (tx.status !== "pending") {
// Already terminal - acknowledge without re-processing. Never let a later IPN // Already terminal - acknowledge without re-processing. Never let a later IPN
// (retry, or a duplicate payment attempt) downgrade an already-settled result. // (retry, or a duplicate payment attempt) downgrade an already-settled result.
return res.status(200).json({ code: "00", data: "Success" }); return res.status(200).json({ code: "00", data: "Success" });
} }
var newStatus = resultCd === "00_000" ? "success" : "failed"; // status: "0" = thanh toán, "2" = refund (theo tài liệu MGP mục 5.3.2). resultCd "00_000"
// alone is not enough - MGP can send an IPN with resultCd "00_000" for a refund event too,
// which must never flip a transaction to "success".
if (resultCd === "00_000" && b.status !== "0") {
console.error(
"epayIPN: resultCd 00_000 but status=" + b.status + " (not a payment event) for merTrxId=" + merTrxId
);
}
var newStatus = resultCd === "00_000" && b.status === "0" ? "success" : "failed";
AdminTransaction.updateOne( AdminTransaction.updateOne(
{ merTrxId: merTrxId, status: "pending" }, { merTrxId: merTrxId, status: "pending" },
{ {
......
Markdown is supported
0% or
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment