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");
var AdminUser = require("../models/AdminUser");
var epaySign = require("../libs/epaySign");
var SobanleClient = require("../libs/SobanleClient");
var gatewayLogger = require("../libs/gatewayLogger");
exports.newTransactionForm = function (req, res) {
res.render("admin/transactions-new", {
......@@ -93,6 +94,13 @@ exports.createTransaction = function (req, res) {
return res.status(500).json({ code: "99", data: "DB_ERROR" });
}
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({
code: "00",
data: { merTrxId: tx.merTrxId, paymentUrl: paymentUrl },
......@@ -129,6 +137,8 @@ exports.epayReturn = function (req, res) {
var resultCd = q.resultCd;
var merTrxId = q.merTrxId;
gatewayLogger.logResponse("epay", "epayReturn", q);
if (resultCd !== "00_000") {
return res.render("admin/pay-result", {
success: false,
......@@ -182,6 +192,8 @@ exports.epayIPN = function (req, res) {
var resultCd = b.resultCd;
var merTrxId = b.merTrxId;
gatewayLogger.logResponse("epay", "epayIPN", b);
AdminTransaction.findOne({ merTrxId: merTrxId }, function (err, tx) {
if (err) {
console.error("epayIPN: DB error for merTrxId=" + merTrxId + ":", err.message);
......@@ -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" });
}
);
......
......@@ -2,21 +2,33 @@
var config = require(__config_path + "/config");
var ApiRequest = require("./ApiRequest");
var pickProductCombo = require("./productComboPicker").pickProductCombo;
var gatewayLogger = require("./gatewayLogger");
var tokenCache = { token: null, expiresAt: 0 };
function login(callback) {
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(
url,
{ login: config.sobanle.username, password: config.sobanle.password },
function (err, body) {
if (err) {
gatewayLogger.logError("sobanle", "login", err);
return callback(err);
}
if (!body || !body.access_token) {
gatewayLogger.logResponse("sobanle", "login", body);
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;
var expiresInMs = (body.expires_in || 0) * 1000;
// Refresh a bit early to avoid racing a near-expiry token. Headroom scales
......@@ -82,11 +94,11 @@ function withAuthRetry(makeRequest, callback) {
}
function getProducts(callback) {
var url =
config.sobanle.base_url + "/products?page=1&is_active=true&per_page=1000&";
withAuthRetry(
function (token, cb) {
var url =
config.sobanle.base_url +
"/products?page=1&is_active=true&per_page=1000&";
gatewayLogger.logRequest("sobanle", "getProducts", { url: url });
ApiRequest.getOtherUrlWithHeader(
url,
{},
......@@ -96,11 +108,21 @@ function getProducts(callback) {
},
function (err, body) {
if (err) {
gatewayLogger.logError("sobanle", "getProducts", err);
return callback(err);
}
if (!body || !Array.isArray(body.data)) {
gatewayLogger.logResponse("sobanle", "getProducts", body);
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);
},
);
......@@ -108,8 +130,6 @@ function getProducts(callback) {
exports.getProducts = getProducts;
function createSale(lines, customerData, callback) {
withAuthRetry(
function (token, cb) {
var url = config.sobanle.base_url + "/sales";
var payload = {
warehouse_id: config.sobanle.warehouse_id,
......@@ -128,6 +148,9 @@ function createSale(lines, customerData, callback) {
payment_note: "",
staff_note: "",
};
withAuthRetry(
function (token, cb) {
gatewayLogger.logRequest("sobanle", "createSale", { url: url, payload: payload });
ApiRequest.postOtherUrlWithHeader(
url,
payload,
......@@ -137,6 +160,7 @@ function createSale(lines, customerData, callback) {
},
function (err, body) {
if (err) {
gatewayLogger.logError("sobanle", "createSale", err);
return callback(err);
}
if (
......@@ -145,9 +169,11 @@ function createSale(lines, customerData, callback) {
typeof body.data.id === "undefined" ||
typeof body.data.grand_total === "undefined"
) {
gatewayLogger.logResponse("sobanle", "createSale", body);
return callback(new Error("SOBANLE_CREATE_SALE_INVALID"));
}
var total = Math.round(Number(body.data.grand_total));
gatewayLogger.logResponse("sobanle", "createSale", body);
if (!isFinite(total) || total <= 0) {
return callback(new Error("SOBANLE_CREATE_SALE_INVALID"));
}
......@@ -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ẻ).
function changeSaleStatus(orderId, saleStatus, callback) {
var url = config.sobanle.base_url + "/sales/change-sale-status/" + orderId;
withAuthRetry(
function (token, cb) {
var url =
config.sobanle.base_url + "/sales/change-sale-status/" + orderId;
gatewayLogger.logRequest("sobanle", "changeSaleStatus", {
url: url,
orderId: orderId,
sale_status: saleStatus,
});
ApiRequest.patchOtherUrlWithHeader(
url,
{ sale_status: saleStatus },
......@@ -170,8 +200,13 @@ function changeSaleStatus(orderId, saleStatus, callback) {
cb,
);
},
function (err) {
callback(err || null);
function (err, body) {
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