Commit 6536143a by tdgiang

Fix final-review findings: normalize order total, re-check tolerance against…

Fix final-review findings: normalize order total, re-check tolerance against authoritative total, wrap HTTP errors, log orphan orders, add outbound timeout

- SobanleClient.createSale: coerce grand_total to a rounded Number and
  reject non-finite/non-positive values before it crosses into the epay
  signature and the DB amount, so a decimal-string API response can't
  desync the signed value from the stored/transmitted one.
- productComboPicker.computeLineTotal: coerce tax.rate with Number()
  instead of a strict typeof check, so a string rate is honored instead
  of silently defaulting to 0%.
- SobanleClient.createAutoOrder: reject with SOBANLE_TOTAL_EXCEEDS_TARGET
  and log the order context when the POS's authoritative grand_total
  exceeds targetAmount — the spec's tolerance was only ever checked
  against the local estimate, never the real total.
- SobanleClient.withAuthRetry: wrap non-2xx response bodies into real
  Error objects with a statusCode, instead of forwarding the raw body
  as err (previously every real HTTP failure logged `message: undefined`
  with no status code).
- admin.server.controller.js createTransaction: log the Sổ Bán Lẻ
  sobanleOrderId/total when the follow-up AdminTransaction DB write
  fails, so an order left orphaned in the POS can be found later.
- ApiRequest.js: add timeout: 15000 to the three *OtherUrlWithHeader
  functions so a hung Sổ Bán Lẻ call can't hang the admin's request
  indefinitely (also benefits the existing Appota caller).
Co-Authored-By: 's avatarClaude Sonnet 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_017KPzWwuTEeX2vXGXvyGn4q
parent b6e12424
...@@ -75,7 +75,10 @@ exports.createTransaction = function (req, res) { ...@@ -75,7 +75,10 @@ exports.createTransaction = function (req, res) {
}, },
function (err, tx) { function (err, tx) {
if (err) { if (err) {
console.error("createTransaction: DB error:", err.message); console.error(
"createTransaction: DB error (orphan Sổ Bán Lẻ order sobanleOrderId=" +
order.orderId + " total=" + order.orderTotal + "):", err.message
);
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;
......
...@@ -247,7 +247,8 @@ exports.postOtherUrlWithHeader = function (apiName, param, headers, callback) { ...@@ -247,7 +247,8 @@ exports.postOtherUrlWithHeader = function (apiName, param, headers, callback) {
method: 'POST', method: 'POST',
uri: apiName, uri: apiName,
headers: headers, headers: headers,
json: param json: param,
timeout: 15000
}; };
if (param && param.token) { if (param && param.token) {
options['auth'] = { options['auth'] = {
...@@ -280,7 +281,8 @@ exports.getOtherUrlWithHeader = function (apiName, param, headers, callback) { ...@@ -280,7 +281,8 @@ exports.getOtherUrlWithHeader = function (apiName, param, headers, callback) {
method: 'GET', method: 'GET',
uri: url, uri: url,
headers: headers, headers: headers,
json: true json: true,
timeout: 15000
}; };
request(options, function (err, httpResponse, body) { request(options, function (err, httpResponse, body) {
var statusCode = httpResponse ? httpResponse.statusCode : undefined; var statusCode = httpResponse ? httpResponse.statusCode : undefined;
...@@ -303,7 +305,8 @@ exports.patchOtherUrlWithHeader = function (apiName, param, headers, callback) { ...@@ -303,7 +305,8 @@ exports.patchOtherUrlWithHeader = function (apiName, param, headers, callback) {
method: 'PATCH', method: 'PATCH',
uri: apiName, uri: apiName,
headers: headers, headers: headers,
json: param json: param,
timeout: 15000
}; };
request(options, function (err, httpResponse, body) { request(options, function (err, httpResponse, body) {
var statusCode = httpResponse ? httpResponse.statusCode : undefined; var statusCode = httpResponse ? httpResponse.statusCode : undefined;
......
...@@ -39,6 +39,16 @@ exports._resetTokenCacheForTest = function () { ...@@ -39,6 +39,16 @@ exports._resetTokenCacheForTest = function () {
tokenCache.expiresAt = 0; tokenCache.expiresAt = 0;
}; };
function toError(raw, statusCode) {
if (raw instanceof Error) {
return raw;
}
var msg = raw && raw.message ? raw.message : "SOBANLE_HTTP_ERROR";
var e = new Error(msg + " (status=" + statusCode + ")");
e.statusCode = statusCode;
return e;
}
function withAuthRetry(makeRequest, callback) { function withAuthRetry(makeRequest, callback) {
exports.getToken(function (err, token) { exports.getToken(function (err, token) {
if (err) { if (err) {
...@@ -51,16 +61,16 @@ function withAuthRetry(makeRequest, callback) { ...@@ -51,16 +61,16 @@ function withAuthRetry(makeRequest, callback) {
if (loginErr) { if (loginErr) {
return callback(loginErr); return callback(loginErr);
} }
makeRequest(freshToken, function (err3, body3) { makeRequest(freshToken, function (err3, body3, statusCode3) {
if (err3) { if (err3) {
return callback(err3); return callback(toError(err3, statusCode3));
} }
callback(null, body3); callback(null, body3);
}); });
}); });
} }
if (err2) { if (err2) {
return callback(err2); return callback(toError(err2, statusCode));
} }
callback(null, body); callback(null, body);
}); });
...@@ -112,7 +122,11 @@ function createSale(lines, customerData, callback) { ...@@ -112,7 +122,11 @@ function createSale(lines, customerData, callback) {
if (!body || !body.data || typeof body.data.id === "undefined" || typeof body.data.grand_total === "undefined") { if (!body || !body.data || typeof body.data.id === "undefined" || typeof body.data.grand_total === "undefined") {
return callback(new Error("SOBANLE_CREATE_SALE_INVALID")); return callback(new Error("SOBANLE_CREATE_SALE_INVALID"));
} }
callback(null, { orderId: body.data.id, orderTotal: body.data.grand_total }); var total = Math.round(Number(body.data.grand_total));
if (!isFinite(total) || total <= 0) {
return callback(new Error("SOBANLE_CREATE_SALE_INVALID"));
}
callback(null, { orderId: body.data.id, orderTotal: total });
}); });
} }
exports.createSale = createSale; exports.createSale = createSale;
...@@ -136,7 +150,19 @@ function createAutoOrder(customerData, targetAmount, callback) { ...@@ -136,7 +150,19 @@ function createAutoOrder(customerData, targetAmount, callback) {
if (!combo) { if (!combo) {
return callback(new Error("NO_PRODUCT_COMBO_MATCH")); return callback(new Error("NO_PRODUCT_COMBO_MATCH"));
} }
exports.createSale(combo.lines, customerData, callback); exports.createSale(combo.lines, customerData, function (err2, order) {
if (err2) {
return callback(err2);
}
if (order.orderTotal > targetAmount) {
console.error(
"createAutoOrder: Sổ Bán Lẻ order total exceeds target (orphan order sobanleOrderId=" +
order.orderId + " orderTotal=" + order.orderTotal + " targetAmount=" + targetAmount + ")"
);
return callback(new Error("SOBANLE_TOTAL_EXCEEDS_TARGET"));
}
callback(null, order);
});
}); });
} }
exports.createAutoOrder = createAutoOrder; exports.createAutoOrder = createAutoOrder;
...@@ -16,7 +16,10 @@ function shuffle(list, random) { ...@@ -16,7 +16,10 @@ function shuffle(list, random) {
} }
function computeLineTotal(product, qty) { function computeLineTotal(product, qty) {
var rate = product.tax && typeof product.tax.rate === "number" ? product.tax.rate : 0; var rate = product.tax ? Number(product.tax.rate) : 0;
if (!isFinite(rate)) {
rate = 0;
}
return Math.round(qty * product.price * (1 + rate / 100)); return Math.round(qty * product.price * (1 + rate / 100));
} }
......
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