Commit 0e366893 by tdgiang

Ghi log request/response giữa hệ thống với Sổ Bán Lẻ và cổng epay

Thêm app/libs/gatewayLogger.js (bunyan, ghi file log/gateway.log,
xoay vòng theo ngày, giữ 14 ngày) phục vụ đối soát khi có tranh chấp
giao dịch. Ghi toàn bộ request/response (kể cả lỗi) cho: sobanle.login,
getProducts (response tóm tắt count/pagination, không log hết catalog
vì không phải dữ liệu của một giao dịch cụ thể), createSale,
changeSaleStatus, và epay.createTransaction/epayReturn/epayIPN.
Mật khẩu/token luôn được che (maskSecret) trước khi ghi log.

Log request của getProducts/createSale/changeSaleStatus được dời vào
đúng bên trong closure gửi request thật (sau khi có token), thay vì
đầu hàm, để thứ tự các dòng log phản ánh đúng thứ tự gọi HTTP thực tế
(trước đó dòng log getProducts request bị ghi trước cả login request
dù về mặt network login luôn xảy ra trước).
Co-Authored-By: 's avatarClaude Sonnet 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_017KPzWwuTEeX2vXGXvyGn4q
parent 4fc9bb04
...@@ -6,6 +6,7 @@ var AdminTransaction = require("../models/AdminTransaction"); ...@@ -6,6 +6,7 @@ var AdminTransaction = require("../models/AdminTransaction");
var AdminUser = require("../models/AdminUser"); var AdminUser = require("../models/AdminUser");
var epaySign = require("../libs/epaySign"); var epaySign = require("../libs/epaySign");
var SobanleClient = require("../libs/SobanleClient"); var SobanleClient = require("../libs/SobanleClient");
var gatewayLogger = require("../libs/gatewayLogger");
exports.newTransactionForm = function (req, res) { exports.newTransactionForm = function (req, res) {
res.render("admin/transactions-new", { res.render("admin/transactions-new", {
...@@ -93,6 +94,13 @@ exports.createTransaction = function (req, res) { ...@@ -93,6 +94,13 @@ exports.createTransaction = function (req, res) {
return res.status(500).json({ code: "99", data: "DB_ERROR" }); return res.status(500).json({ code: "99", data: "DB_ERROR" });
} }
var paymentUrl = config.epay.req_domain + "/admin/pay/" + tx.merTrxId; var paymentUrl = config.epay.req_domain + "/admin/pay/" + tx.merTrxId;
gatewayLogger.logRequest("epay", "createTransaction", {
merTrxId: tx.merTrxId,
transCode: tx.transCode,
amount: amount,
timeStamp: tx.timeStamp,
paymentUrl: paymentUrl,
});
return res.status(200).json({ return res.status(200).json({
code: "00", code: "00",
data: { merTrxId: tx.merTrxId, paymentUrl: paymentUrl }, data: { merTrxId: tx.merTrxId, paymentUrl: paymentUrl },
...@@ -129,6 +137,8 @@ exports.epayReturn = function (req, res) { ...@@ -129,6 +137,8 @@ exports.epayReturn = function (req, res) {
var resultCd = q.resultCd; var resultCd = q.resultCd;
var merTrxId = q.merTrxId; var merTrxId = q.merTrxId;
gatewayLogger.logResponse("epay", "epayReturn", q);
if (resultCd !== "00_000") { if (resultCd !== "00_000") {
return res.render("admin/pay-result", { return res.render("admin/pay-result", {
success: false, success: false,
...@@ -182,6 +192,8 @@ exports.epayIPN = function (req, res) { ...@@ -182,6 +192,8 @@ exports.epayIPN = function (req, res) {
var resultCd = b.resultCd; var resultCd = b.resultCd;
var merTrxId = b.merTrxId; var merTrxId = b.merTrxId;
gatewayLogger.logResponse("epay", "epayIPN", b);
AdminTransaction.findOne({ merTrxId: merTrxId }, function (err, tx) { AdminTransaction.findOne({ merTrxId: merTrxId }, function (err, tx) {
if (err) { if (err) {
console.error("epayIPN: DB error for merTrxId=" + merTrxId + ":", err.message); console.error("epayIPN: DB error for merTrxId=" + merTrxId + ":", err.message);
...@@ -259,6 +271,11 @@ exports.epayIPN = function (req, res) { ...@@ -259,6 +271,11 @@ exports.epayIPN = function (req, res) {
}); });
} }
gatewayLogger.logRequest("epay", "epayIPN", {
merTrxId: merTrxId,
newStatus: newStatus,
response: { code: "00", data: "Success" },
});
return res.status(200).json({ code: "00", data: "Success" }); return res.status(200).json({ code: "00", data: "Success" });
} }
); );
......
...@@ -2,21 +2,33 @@ ...@@ -2,21 +2,33 @@
var config = require(__config_path + "/config"); var config = require(__config_path + "/config");
var ApiRequest = require("./ApiRequest"); var ApiRequest = require("./ApiRequest");
var pickProductCombo = require("./productComboPicker").pickProductCombo; var pickProductCombo = require("./productComboPicker").pickProductCombo;
var gatewayLogger = require("./gatewayLogger");
var tokenCache = { token: null, expiresAt: 0 }; var tokenCache = { token: null, expiresAt: 0 };
function login(callback) { function login(callback) {
var url = config.sobanle.base_url + "/login"; var url = config.sobanle.base_url + "/login";
gatewayLogger.logRequest("sobanle", "login", {
url: url,
login: config.sobanle.username,
password: gatewayLogger.maskSecret(config.sobanle.password),
});
ApiRequest.postOtherUrl( ApiRequest.postOtherUrl(
url, url,
{ login: config.sobanle.username, password: config.sobanle.password }, { login: config.sobanle.username, password: config.sobanle.password },
function (err, body) { function (err, body) {
if (err) { if (err) {
gatewayLogger.logError("sobanle", "login", err);
return callback(err); return callback(err);
} }
if (!body || !body.access_token) { if (!body || !body.access_token) {
gatewayLogger.logResponse("sobanle", "login", body);
return callback(new Error("SOBANLE_LOGIN_FAILED")); return callback(new Error("SOBANLE_LOGIN_FAILED"));
} }
gatewayLogger.logResponse("sobanle", "login", {
access_token: gatewayLogger.maskSecret(body.access_token),
expires_in: body.expires_in,
});
tokenCache.token = body.access_token; tokenCache.token = body.access_token;
var expiresInMs = (body.expires_in || 0) * 1000; var expiresInMs = (body.expires_in || 0) * 1000;
// Refresh a bit early to avoid racing a near-expiry token. Headroom scales // Refresh a bit early to avoid racing a near-expiry token. Headroom scales
...@@ -82,11 +94,11 @@ function withAuthRetry(makeRequest, callback) { ...@@ -82,11 +94,11 @@ function withAuthRetry(makeRequest, callback) {
} }
function getProducts(callback) { function getProducts(callback) {
var url =
config.sobanle.base_url + "/products?page=1&is_active=true&per_page=1000&";
withAuthRetry( withAuthRetry(
function (token, cb) { function (token, cb) {
var url = gatewayLogger.logRequest("sobanle", "getProducts", { url: url });
config.sobanle.base_url +
"/products?page=1&is_active=true&per_page=1000&";
ApiRequest.getOtherUrlWithHeader( ApiRequest.getOtherUrlWithHeader(
url, url,
{}, {},
...@@ -96,11 +108,21 @@ function getProducts(callback) { ...@@ -96,11 +108,21 @@ function getProducts(callback) {
}, },
function (err, body) { function (err, body) {
if (err) { if (err) {
gatewayLogger.logError("sobanle", "getProducts", err);
return callback(err); return callback(err);
} }
if (!body || !Array.isArray(body.data)) { if (!body || !Array.isArray(body.data)) {
gatewayLogger.logResponse("sobanle", "getProducts", body);
return callback(new Error("SOBANLE_PRODUCTS_INVALID")); return callback(new Error("SOBANLE_PRODUCTS_INVALID"));
} }
// Ghi tóm tắt (không phải toàn bộ danh mục sản phẩm) - đây là catalog
// dùng để tính combo nội bộ, không phải nội dung của một giao dịch cụ
// thể, nên log đầy đủ hàng trăm sản phẩm mỗi lần gọi không có giá trị
// đối soát, chỉ làm phình file log vô ích.
gatewayLogger.logResponse("sobanle", "getProducts", {
count: body.data.length,
pagination: body.pagination,
});
callback(null, body.data); callback(null, body.data);
}, },
); );
...@@ -108,8 +130,6 @@ function getProducts(callback) { ...@@ -108,8 +130,6 @@ function getProducts(callback) {
exports.getProducts = getProducts; exports.getProducts = getProducts;
function createSale(lines, customerData, callback) { function createSale(lines, customerData, callback) {
withAuthRetry(
function (token, cb) {
var url = config.sobanle.base_url + "/sales"; var url = config.sobanle.base_url + "/sales";
var payload = { var payload = {
warehouse_id: config.sobanle.warehouse_id, warehouse_id: config.sobanle.warehouse_id,
...@@ -128,6 +148,9 @@ function createSale(lines, customerData, callback) { ...@@ -128,6 +148,9 @@ function createSale(lines, customerData, callback) {
payment_note: "", payment_note: "",
staff_note: "", staff_note: "",
}; };
withAuthRetry(
function (token, cb) {
gatewayLogger.logRequest("sobanle", "createSale", { url: url, payload: payload });
ApiRequest.postOtherUrlWithHeader( ApiRequest.postOtherUrlWithHeader(
url, url,
payload, payload,
...@@ -137,6 +160,7 @@ function createSale(lines, customerData, callback) { ...@@ -137,6 +160,7 @@ function createSale(lines, customerData, callback) {
}, },
function (err, body) { function (err, body) {
if (err) { if (err) {
gatewayLogger.logError("sobanle", "createSale", err);
return callback(err); return callback(err);
} }
if ( if (
...@@ -145,9 +169,11 @@ function createSale(lines, customerData, callback) { ...@@ -145,9 +169,11 @@ function createSale(lines, customerData, callback) {
typeof body.data.id === "undefined" || typeof body.data.id === "undefined" ||
typeof body.data.grand_total === "undefined" typeof body.data.grand_total === "undefined"
) { ) {
gatewayLogger.logResponse("sobanle", "createSale", body);
return callback(new Error("SOBANLE_CREATE_SALE_INVALID")); return callback(new Error("SOBANLE_CREATE_SALE_INVALID"));
} }
var total = Math.round(Number(body.data.grand_total)); var total = Math.round(Number(body.data.grand_total));
gatewayLogger.logResponse("sobanle", "createSale", body);
if (!isFinite(total) || total <= 0) { if (!isFinite(total) || total <= 0) {
return callback(new Error("SOBANLE_CREATE_SALE_INVALID")); return callback(new Error("SOBANLE_CREATE_SALE_INVALID"));
} }
...@@ -159,10 +185,14 @@ exports.createSale = createSale; ...@@ -159,10 +185,14 @@ exports.createSale = createSale;
// saleStatus: 1 = giao dịch thành công, 4 = giao dịch thất bại (theo Sổ Bán Lẻ). // saleStatus: 1 = giao dịch thành công, 4 = giao dịch thất bại (theo Sổ Bán Lẻ).
function changeSaleStatus(orderId, saleStatus, callback) { function changeSaleStatus(orderId, saleStatus, callback) {
var url = config.sobanle.base_url + "/sales/change-sale-status/" + orderId;
withAuthRetry( withAuthRetry(
function (token, cb) { function (token, cb) {
var url = gatewayLogger.logRequest("sobanle", "changeSaleStatus", {
config.sobanle.base_url + "/sales/change-sale-status/" + orderId; url: url,
orderId: orderId,
sale_status: saleStatus,
});
ApiRequest.patchOtherUrlWithHeader( ApiRequest.patchOtherUrlWithHeader(
url, url,
{ sale_status: saleStatus }, { sale_status: saleStatus },
...@@ -170,8 +200,13 @@ function changeSaleStatus(orderId, saleStatus, callback) { ...@@ -170,8 +200,13 @@ function changeSaleStatus(orderId, saleStatus, callback) {
cb, cb,
); );
}, },
function (err) { function (err, body) {
callback(err || null); if (err) {
gatewayLogger.logError("sobanle", "changeSaleStatus", err);
return callback(err);
}
gatewayLogger.logResponse("sobanle", "changeSaleStatus", body);
callback(null);
}, },
); );
} }
......
"use strict";
var bunyan = require("bunyan");
var config = require(__config_path + "/config");
// Ghi lại toàn bộ request/response trao đổi giữa hệ thống này với cổng thanh
// toán (Megapay/epay) và Sổ Bán Lẻ, phục vụ đối soát/tra cứu sau này. Tách
// file riêng (log/gateway.log) khỏi log lỗi chung (log/frontend-error.log)
// để dễ tra cứu theo provider mà không lẫn log framework/lỗi khác.
var logger = bunyan.createLogger({
name: "gateway",
streams: [
{
path: config.root + "/log/gateway.log",
type: "rotating-file",
period: "1d",
count: 14, // giữ 14 ngày gần nhất
},
],
});
// Token/mật khẩu không bao giờ ghi nguyên văn ra log - chỉ giữ vài ký tự đầu/cuối
// đủ để đối chiếu thủ công khi cần, không đủ để dùng lại nếu log bị lộ.
function maskSecret(value) {
if (!value || typeof value !== "string") {
return value;
}
if (value.length <= 12) {
return "***";
}
return value.slice(0, 6) + "..." + value.slice(-4);
}
function logRequest(provider, action, detail) {
logger.info({ provider: provider, action: action, direction: "request", detail: detail });
}
function logResponse(provider, action, detail) {
logger.info({ provider: provider, action: action, direction: "response", detail: detail });
}
function logError(provider, action, err, extra) {
logger.error({
provider: provider,
action: action,
direction: "error",
message: err && err.message,
statusCode: err && err.statusCode,
detail: extra,
});
}
module.exports = {
logRequest: logRequest,
logResponse: logResponse,
logError: logError,
maskSecret: maskSecret,
};
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