Commit 503cc251 by tdgiang

update code

parent 4bdc896a
......@@ -7,7 +7,10 @@ var tokenCache = { token: null, expiresAt: 0 };
function login(callback) {
var url = config.sobanle.base_url + "/login";
ApiRequest.postOtherUrl(url, { login: config.sobanle.username, password: config.sobanle.password }, function (err, body) {
ApiRequest.postOtherUrl(
url,
{ login: config.sobanle.username, password: config.sobanle.password },
function (err, body) {
if (err) {
return callback(err);
}
......@@ -22,7 +25,8 @@ function login(callback) {
var earlyRefreshMs = Math.min(60000, expiresInMs * 0.1);
tokenCache.expiresAt = Date.now() + expiresInMs - earlyRefreshMs;
callback(null, body.access_token);
});
},
);
}
exports.login = login;
......@@ -78,10 +82,19 @@ function withAuthRetry(makeRequest, callback) {
}
function getProducts(callback) {
withAuthRetry(function (token, cb) {
var url = config.sobanle.base_url + "/products?page=1&is_active=true&per_page=1000&";
ApiRequest.getOtherUrlWithHeader(url, {}, { Authorization: "Bearer " + token }, cb);
}, function (err, body) {
withAuthRetry(
function (token, cb) {
var url =
config.sobanle.base_url +
"/products?page=1&is_active=true&per_page=1000&";
ApiRequest.getOtherUrlWithHeader(
url,
{},
{ Authorization: "Bearer " + token },
cb,
);
},
function (err, body) {
if (err) {
return callback(err);
}
......@@ -89,12 +102,14 @@ function getProducts(callback) {
return callback(new Error("SOBANLE_PRODUCTS_INVALID"));
}
callback(null, body.data);
});
},
);
}
exports.getProducts = getProducts;
function createSale(lines, customerData, callback) {
withAuthRetry(function (token, cb) {
withAuthRetry(
function (token, cb) {
var url = config.sobanle.base_url + "/sales";
var payload = {
warehouse_id: config.sobanle.warehouse_id,
......@@ -111,15 +126,25 @@ function createSale(lines, customerData, callback) {
customer_data: customerData,
payment_receiver: "",
payment_note: "",
sale_note: "Tạo tự động từ payment-gate",
staff_note: "",
};
ApiRequest.postOtherUrlWithHeader(url, payload, { Authorization: "Bearer " + token }, cb);
}, function (err, body) {
ApiRequest.postOtherUrlWithHeader(
url,
payload,
{ Authorization: "Bearer " + token },
cb,
);
},
function (err, body) {
if (err) {
return callback(err);
}
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"));
}
var total = Math.round(Number(body.data.grand_total));
......@@ -127,18 +152,28 @@ function createSale(lines, customerData, callback) {
return callback(new Error("SOBANLE_CREATE_SALE_INVALID"));
}
callback(null, { orderId: body.data.id, orderTotal: total });
});
},
);
}
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) {
withAuthRetry(function (token, cb) {
var url = config.sobanle.base_url + "/sales/change-sale-status/" + orderId;
ApiRequest.patchOtherUrlWithHeader(url, { sale_status: saleStatus }, { Authorization: "Bearer " + token }, cb);
}, function (err) {
withAuthRetry(
function (token, cb) {
var url =
config.sobanle.base_url + "/sales/change-sale-status/" + orderId;
ApiRequest.patchOtherUrlWithHeader(
url,
{ sale_status: saleStatus },
{ Authorization: "Bearer " + token },
cb,
);
},
function (err) {
callback(err || null);
});
},
);
}
exports.changeSaleStatus = changeSaleStatus;
......@@ -158,7 +193,12 @@ function createAutoOrder(customerData, targetAmount, callback) {
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 + ")"
order.orderId +
" orderTotal=" +
order.orderTotal +
" targetAmount=" +
targetAmount +
")",
);
return callback(new Error("SOBANLE_TOTAL_EXCEEDS_TARGET"));
}
......
......@@ -31,10 +31,12 @@
### Task 1: `ApiRequest.js` — header-aware GET/PATCH + status code passthrough
**Files:**
- Modify: `app/libs/ApiRequest.js`
- Test: `scratch/test-sobanle-apirequest-headers.js`
**Interfaces:**
- Produces: `ApiRequest.getOtherUrlWithHeader(apiName, param, headers, callback)` and `ApiRequest.patchOtherUrlWithHeader(apiName, param, headers, callback)` — both call `callback(err, body, statusCode)` (3rd arg is the HTTP status code, `undefined` on network-level `err`). `ApiRequest.postOtherUrlWithHeader` (existing function) is extended to also pass `statusCode` as a 3rd callback arg — existing callers (`core.server.controller.js:2041`) are unaffected since they only read the first 2 args.
- Consumed by: Task 5 (`SobanleClient.js``getProducts`, `createSale`, `changeSaleStatus`).
......@@ -112,19 +114,19 @@ Add at the end of the file (before the final blank line), and update `postOtherU
```js
exports.postOtherUrlWithHeader = function (apiName, param, headers, callback) {
if (typeof (param) === 'function') {
if (typeof param === "function") {
callback = param;
param = {};
}
var options = {
method: 'POST',
method: "POST",
uri: apiName,
headers: headers,
json: param
json: param,
};
if (param && param.token) {
options['auth'] = {
'bearer': param.token
options["auth"] = {
bearer: param.token,
};
}
// request ato API Host
......@@ -141,19 +143,19 @@ exports.postOtherUrlWithHeader = function (apiName, param, headers, callback) {
};
exports.getOtherUrlWithHeader = function (apiName, param, headers, callback) {
if (typeof (param) === 'function') {
if (typeof param === "function") {
callback = param;
param = {};
}
var url = apiName;
for (var k in param) {
url += k + '=' + param[k] + '&';
url += k + "=" + param[k] + "&";
}
var options = {
method: 'GET',
method: "GET",
uri: url,
headers: headers,
json: true
json: true,
};
request(options, function (err, httpResponse, body) {
var statusCode = httpResponse ? httpResponse.statusCode : undefined;
......@@ -168,15 +170,15 @@ exports.getOtherUrlWithHeader = function (apiName, param, headers, callback) {
};
exports.patchOtherUrlWithHeader = function (apiName, param, headers, callback) {
if (typeof (param) === 'function') {
if (typeof param === "function") {
callback = param;
param = {};
}
var options = {
method: 'PATCH',
method: "PATCH",
uri: apiName,
headers: headers,
json: param
json: param,
};
request(options, function (err, httpResponse, body) {
var statusCode = httpResponse ? httpResponse.statusCode : undefined;
......@@ -213,10 +215,12 @@ git commit -m "Add header-aware GET/PATCH helpers with status code passthrough t
### Task 2: `productComboPicker.js` — tax-aware product combo algorithm
**Files:**
- Create: `app/libs/productComboPicker.js`
- Test: `scratch/test-product-combo-picker.js`
**Interfaces:**
- Produces: `pickProductCombo(products, targetAmount, options)``{ lines: [{product_id, qty}], total: Number }` or `null`. `options` (all optional): `{ tolerance = 50000, maxAttempts = 20, maxQtyPerLine = 3, random = Math.random }`. Also exports `computeLineTotal(product, qty)``Math.round(qty * product.price * (1 + rate/100))`.
- Consumed by: Task 6 (`SobanleClient.createAutoOrder`).
......@@ -317,13 +321,17 @@ function shuffle(list, random) {
}
function computeLineTotal(product, qty) {
var rate = product.tax && typeof product.tax.rate === "number" ? product.tax.rate : 0;
var rate =
product.tax && typeof product.tax.rate === "number" ? product.tax.rate : 0;
return Math.round(qty * product.price * (1 + rate / 100));
}
function pickProductCombo(products, targetAmount, options) {
options = options || {};
var tolerance = typeof options.tolerance === "number" ? options.tolerance : DEFAULT_TOLERANCE;
var tolerance =
typeof options.tolerance === "number"
? options.tolerance
: DEFAULT_TOLERANCE;
var maxAttempts = options.maxAttempts || DEFAULT_MAX_ATTEMPTS;
var random = options.random || Math.random;
var maxQtyPerLine = options.maxQtyPerLine || DEFAULT_MAX_QTY_PER_LINE;
......@@ -392,11 +400,13 @@ git commit -m "Add tax-aware random product combo picker for Sổ Bán Lẻ auto
### Task 3: `config/env/all.js` + `.env` — Sổ Bán Lẻ config block
**Files:**
- Modify: `config/env/all.js`
- Modify: `.env`
- Test: `scratch/test-sobanle-config.js`
**Interfaces:**
- Produces: `config.sobanle = { base_url, username, password, warehouse_id, biller_id, account_id, currency_id }`.
- Consumed by: Task 4, 5 (`SobanleClient.js`).
......@@ -471,10 +481,12 @@ Note: confirm `.env` is already git-ignored or already tracked-with-secrets per
### Task 4: `SobanleClient.js` — token cache, login, `getToken`
**Files:**
- Create: `app/libs/SobanleClient.js`
- Test: `scratch/test-sobanle-client-token.js`
**Interfaces:**
- Produces: `SobanleClient.login(callback)``(err, token)`, always performs a real login call and refreshes the cache. `SobanleClient.getToken(callback)``(err, token)`, returns the cached token if not expired, otherwise calls `exports.login`. `SobanleClient._resetTokenCacheForTest()` — test-only helper that clears the module-level cache between scratch-script scenarios.
- Consumed by: Task 5, 6, and (indirectly) Task 8, 9.
- Internal calls to `login`/`getToken` from other functions in this file (later tasks) MUST go through `exports.login(...)` / `exports.getToken(...)`, not a local reference, so tests can monkey-patch them.
......@@ -546,7 +558,10 @@ var tokenCache = { token: null, expiresAt: 0 };
function login(callback) {
var url = config.sobanle.base_url + "/login";
ApiRequest.postOtherUrl(url, { login: config.sobanle.username, password: config.sobanle.password }, function (err, body) {
ApiRequest.postOtherUrl(
url,
{ login: config.sobanle.username, password: config.sobanle.password },
function (err, body) {
if (err) {
return callback(err);
}
......@@ -561,7 +576,8 @@ function login(callback) {
var earlyRefreshMs = Math.min(60000, expiresInMs * 0.1);
tokenCache.expiresAt = Date.now() + expiresInMs - earlyRefreshMs;
callback(null, body.access_token);
});
},
);
}
exports.login = login;
......@@ -599,10 +615,12 @@ git commit -m "Add SobanleClient token cache and login"
### Task 5: `SobanleClient.js` — `getProducts`, `createSale`, `changeSaleStatus` with 401 retry
**Files:**
- Modify: `app/libs/SobanleClient.js`
- Test: `scratch/test-sobanle-client-api.js`
**Interfaces:**
- Produces: `SobanleClient.getProducts(callback)``(err, products)`. `SobanleClient.createSale(lines, customerData, callback)``(err, { orderId, orderTotal })`. `SobanleClient.changeSaleStatus(orderId, callback)``(err)`. All three transparently retry once (re-login, then retry the original call) on a `401` response.
- Consumes: `exports.getToken` from Task 4.
- Consumed by: Task 6 (`createAutoOrder`), Task 8, 9 (controller).
......@@ -745,10 +763,19 @@ function withAuthRetry(makeRequest, callback) {
}
function getProducts(callback) {
withAuthRetry(function (token, cb) {
var url = config.sobanle.base_url + "/products?page=1&is_active=true&per_page=1000&";
ApiRequest.getOtherUrlWithHeader(url, {}, { Authorization: "Bearer " + token }, cb);
}, function (err, body) {
withAuthRetry(
function (token, cb) {
var url =
config.sobanle.base_url +
"/products?page=1&is_active=true&per_page=1000&";
ApiRequest.getOtherUrlWithHeader(
url,
{},
{ Authorization: "Bearer " + token },
cb,
);
},
function (err, body) {
if (err) {
return callback(err);
}
......@@ -756,12 +783,14 @@ function getProducts(callback) {
return callback(new Error("SOBANLE_PRODUCTS_INVALID"));
}
callback(null, body.data);
});
},
);
}
exports.getProducts = getProducts;
function createSale(lines, customerData, callback) {
withAuthRetry(function (token, cb) {
withAuthRetry(
function (token, cb) {
var url = config.sobanle.base_url + "/sales";
var payload = {
warehouse_id: config.sobanle.warehouse_id,
......@@ -778,29 +807,52 @@ function createSale(lines, customerData, callback) {
customer_data: customerData,
payment_receiver: "",
payment_note: "",
sale_note: "Tạo tự động từ payment-gate",
staff_note: "",
};
ApiRequest.postOtherUrlWithHeader(url, payload, { Authorization: "Bearer " + token }, cb);
}, function (err, body) {
ApiRequest.postOtherUrlWithHeader(
url,
payload,
{ Authorization: "Bearer " + token },
cb,
);
},
function (err, body) {
if (err) {
return callback(err);
}
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"));
}
callback(null, { orderId: body.data.id, orderTotal: body.data.grand_total });
callback(null, {
orderId: body.data.id,
orderTotal: body.data.grand_total,
});
},
);
}
exports.createSale = createSale;
function changeSaleStatus(orderId, callback) {
withAuthRetry(function (token, cb) {
var url = config.sobanle.base_url + "/sales/change-sale-status/" + orderId;
ApiRequest.patchOtherUrlWithHeader(url, { sale_status: 4 }, { Authorization: "Bearer " + token }, cb);
}, function (err) {
withAuthRetry(
function (token, cb) {
var url =
config.sobanle.base_url + "/sales/change-sale-status/" + orderId;
ApiRequest.patchOtherUrlWithHeader(
url,
{ sale_status: 4 },
{ Authorization: "Bearer " + token },
cb,
);
},
function (err) {
callback(err || null);
});
},
);
}
exports.changeSaleStatus = changeSaleStatus;
```
......@@ -825,10 +877,12 @@ git commit -m "Add SobanleClient getProducts/createSale/changeSaleStatus with 40
### Task 6: `SobanleClient.createAutoOrder` — orchestration
**Files:**
- Modify: `app/libs/SobanleClient.js`
- Test: `scratch/test-sobanle-client-auto-order.js`
**Interfaces:**
- Produces: `SobanleClient.createAutoOrder(customerData, targetAmount, callback)``(err, { orderId, orderTotal })`. Fetches products, picks a combo via `productComboPicker.pickProductCombo`, creates the sale. Fails with `NO_PRODUCT_COMBO_MATCH` without ever calling `createSale` if no combo fits.
- Consumes: `exports.getProducts`, `exports.createSale` from Task 5; `pickProductCombo` from Task 2.
- Consumed by: Task 8 (`admin.server.controller.js#createTransaction`).
......@@ -947,10 +1001,12 @@ git commit -m "Add SobanleClient.createAutoOrder orchestration"
### Task 7: `AdminTransaction` model — `sobanleOrderId` field
**Files:**
- Modify: `app/models/AdminTransaction.js`
- Test: `scratch/test-admin-transaction-sobanle-field.js`
**Interfaces:**
- Produces: `AdminTransaction` schema gains `sobanleOrderId: { type: String, default: null }`.
- Consumed by: Task 8, 9.
......@@ -1035,10 +1091,12 @@ git commit -m "Add sobanleOrderId field to AdminTransaction"
### Task 8: Wire `createTransaction` to `SobanleClient.createAutoOrder`
**Files:**
- Modify: `app/controllers/admin.server.controller.js`
- Test: `scratch/test-admin-create-transaction-sobanle.js`
**Interfaces:**
- Consumes: `SobanleClient.createAutoOrder(customerData, targetAmount, callback)` (Task 6), `AdminTransaction` (Task 7).
- Produces: `createTransaction` now calls Sổ Bán Lẻ first; `AdminTransaction.amount` = `order.orderTotal`; `AdminTransaction.sobanleOrderId` = `String(order.orderId)`. On Sổ Bán Lẻ failure: `502 {code:"99", data:"SOBANLE_ORDER_FAILED"}`, no DB row written. Response contract for the success path is unchanged (`{code:"00", data:{merTrxId, paymentUrl}}`).
......@@ -1148,7 +1206,11 @@ exports.createTransaction = function (req, res) {
}
var targetAmount = parseInt(rawAmount, 10);
if (!isFinite(targetAmount) || targetAmount <= 0 || String(targetAmount) !== String(rawAmount).trim()) {
if (
!isFinite(targetAmount) ||
targetAmount <= 0 ||
String(targetAmount) !== String(rawAmount).trim()
) {
return res.status(400).json({ code: "99", data: "INVALID_AMOUNT" });
}
......@@ -1164,10 +1226,18 @@ exports.createTransaction = function (req, res) {
email: null,
};
SobanleClient.createAutoOrder(customerData, targetAmount, function (sobanleErr, order) {
SobanleClient.createAutoOrder(
customerData,
targetAmount,
function (sobanleErr, order) {
if (sobanleErr) {
console.error("createTransaction: SobanleClient error:", sobanleErr.message);
return res.status(502).json({ code: "99", data: "SOBANLE_ORDER_FAILED" });
console.error(
"createTransaction: SobanleClient error:",
sobanleErr.message,
);
return res
.status(502)
.json({ code: "99", data: "SOBANLE_ORDER_FAILED" });
}
var amount = order.orderTotal;
......@@ -1180,7 +1250,7 @@ exports.createTransaction = function (req, res) {
merTrxId,
config.epay.merchant_id,
amount,
config.epay.encode_key
config.epay.encode_key,
);
AdminTransaction.create(
......@@ -1206,9 +1276,10 @@ exports.createTransaction = function (req, res) {
code: "00",
data: { merTrxId: tx.merTrxId, paymentUrl: paymentUrl },
});
}
},
);
},
);
});
};
```
......@@ -1233,10 +1304,12 @@ git commit -m "Create Sổ Bán Lẻ order before payment transaction in createT
### Task 9: Wire `epayIPN` to `SobanleClient.changeSaleStatus`
**Files:**
- Modify: `app/controllers/admin.server.controller.js`
- Test: `scratch/test-admin-epay-ipn-sobanle.js`
**Interfaces:**
- Consumes: `SobanleClient.changeSaleStatus(orderId, callback)` (Task 5).
- Produces: after `epayIPN` marks a transaction `success`, it fire-and-forgets a call to `SobanleClient.changeSaleStatus(tx.sobanleOrderId, ...)`. A failure there is logged only — the IPN still responds `200 {code:"00", data:"Success"}` immediately, unaffected by the Sổ Bán Lẻ call's outcome or latency.
......@@ -1333,16 +1406,18 @@ Expected: fails — `changeSaleStatus called with correct orderId: false` (`chan
Inside the `AdminTransaction.updateOne(...)` callback (after the `updateErr` check, before `return res.status(200)...`), add:
```js
if (newStatus === "success" && tx.sobanleOrderId) {
if (newStatus === "success" && tx.sobanleOrderId) {
SobanleClient.changeSaleStatus(tx.sobanleOrderId, function (sobanleErr) {
if (sobanleErr) {
console.error(
"epayIPN: SobanleClient.changeSaleStatus failed for orderId=" + tx.sobanleOrderId + ":",
sobanleErr.message
"epayIPN: SobanleClient.changeSaleStatus failed for orderId=" +
tx.sobanleOrderId +
":",
sobanleErr.message,
);
}
});
}
}
```
(`SobanleClient` is already required at the top of the file from Task 8.)
......
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