init
This commit is contained in:
237
server.js
Normal file
237
server.js
Normal file
@@ -0,0 +1,237 @@
|
||||
import path from 'node:path';
|
||||
import jsonServer from 'json-server';
|
||||
|
||||
const server = jsonServer.create();
|
||||
const router = jsonServer.router(path.join(import.meta.dir, 'db.json'));
|
||||
|
||||
// Serve static files từ folder public (images, etc.)
|
||||
const middlewares = jsonServer.defaults({
|
||||
logger: true,
|
||||
static: path.join(import.meta.dir, 'public'),
|
||||
});
|
||||
|
||||
const port = process.env.PORT || 3100;
|
||||
// BASE_URL có thể set từ env khi deploy lên VPS (vd: https://api-mock.example.com)
|
||||
const baseUrl = process.env.BASE_URL || `http://localhost:${port}`;
|
||||
|
||||
server.use(middlewares);
|
||||
server.use(jsonServer.bodyParser);
|
||||
|
||||
server.use((req, _res, next) => {
|
||||
req.body = req.body || {};
|
||||
next();
|
||||
});
|
||||
|
||||
const formatCurrencyVnd = (value) => {
|
||||
const number = Number(String(value || '').replace(/[^0-9]/g, ''));
|
||||
if (Number.isNaN(number) || number <= 0) {
|
||||
return '0';
|
||||
}
|
||||
|
||||
return new Intl.NumberFormat('en-US').format(number);
|
||||
};
|
||||
|
||||
const buildResult = ({ ok = true, responseCode = '00', message = 'Thành công' } = {}) => ({
|
||||
ok,
|
||||
responseCode,
|
||||
message,
|
||||
});
|
||||
|
||||
const normalizeText = (value) => String(value || '').toLowerCase().trim();
|
||||
|
||||
// Validation constants theo spec
|
||||
const ACCOUNT_NUMBER_LENGTH = 9;
|
||||
const MAX_AMOUNT = 1000000000; // 1 tỷ VND
|
||||
const ACCOUNT_NUMBER_REGEX = /^[0-9]{9}$/;
|
||||
const ACCOUNT_NAME_REGEX = /^[a-zA-ZÀ-ỹ\s]*$/;
|
||||
|
||||
server.post('/charging-station/execute', (req, res) => {
|
||||
const payload = req.body;
|
||||
const accountNumber = String(payload.accountNumber || '').trim();
|
||||
const accountName = String(payload.accountName || '').trim();
|
||||
const amountRaw = String(payload.amount || '').trim();
|
||||
const amountNumber = Number(amountRaw.replace(/[^0-9]/g, ''));
|
||||
|
||||
// V1, V4: Kiểm tra rỗng
|
||||
if (!accountNumber || !amountRaw || Number.isNaN(amountNumber) || amountNumber <= 0) {
|
||||
return res.status(200).json({
|
||||
result: buildResult({
|
||||
ok: false,
|
||||
responseCode: '01',
|
||||
message: 'Dữ liệu không hợp lệ',
|
||||
}),
|
||||
});
|
||||
}
|
||||
|
||||
// V2, V3: Kiểm tra accountNumber đúng 9 ký tự và chỉ chứa số
|
||||
if (!ACCOUNT_NUMBER_REGEX.test(accountNumber)) {
|
||||
return res.status(200).json({
|
||||
result: buildResult({
|
||||
ok: false,
|
||||
responseCode: '01',
|
||||
message: accountNumber.length !== ACCOUNT_NUMBER_LENGTH
|
||||
? `Số tài khoản phải có ${ACCOUNT_NUMBER_LENGTH} ký tự`
|
||||
: 'Số tài khoản chỉ được chứa số',
|
||||
}),
|
||||
});
|
||||
}
|
||||
|
||||
// V6: Kiểm tra accountName không chứa ký tự đặc biệt hoặc số
|
||||
if (accountName && !ACCOUNT_NAME_REGEX.test(accountName)) {
|
||||
return res.status(200).json({
|
||||
result: buildResult({
|
||||
ok: false,
|
||||
responseCode: '01',
|
||||
message: 'Tên chỉ được chứa chữ cái và khoảng trắng',
|
||||
}),
|
||||
});
|
||||
}
|
||||
|
||||
// V5: Kiểm tra amount không vượt quá 1 tỷ
|
||||
if (amountNumber > MAX_AMOUNT) {
|
||||
return res.status(200).json({
|
||||
result: buildResult({
|
||||
ok: false,
|
||||
responseCode: '03',
|
||||
message: `Số tiền tối đa là ${formatCurrencyVnd(MAX_AMOUNT)} VND`,
|
||||
}),
|
||||
});
|
||||
}
|
||||
|
||||
// Simulate: Số tài khoản bắt đầu bằng 999 không tồn tại
|
||||
if (accountNumber.startsWith('999')) {
|
||||
return res.status(200).json({
|
||||
result: buildResult({
|
||||
ok: false,
|
||||
responseCode: '01',
|
||||
message: 'Số tài khoản không tồn tại',
|
||||
}),
|
||||
});
|
||||
}
|
||||
|
||||
// Simulate: Số tiền > 500 triệu và TK bắt đầu bằng 111 → số dư không đủ
|
||||
if (amountNumber > 500000000 && accountNumber.startsWith('111')) {
|
||||
return res.status(200).json({
|
||||
result: buildResult({
|
||||
ok: false,
|
||||
responseCode: '02',
|
||||
message: 'Số dư không đủ',
|
||||
}),
|
||||
});
|
||||
}
|
||||
|
||||
const now = new Date();
|
||||
const transactionSuffix = String(now.getTime()).slice(-10);
|
||||
|
||||
return res.status(200).json({
|
||||
result: buildResult(),
|
||||
transactionId: `TXN${transactionSuffix}`,
|
||||
status: 'SUCCESS',
|
||||
message: 'Nạp tiền thành công',
|
||||
accountNumber,
|
||||
accountName,
|
||||
amount: formatCurrencyVnd(amountNumber),
|
||||
executedAt: now.toISOString(),
|
||||
refNo: `REF${String(now.getTime()).slice(-8)}`,
|
||||
});
|
||||
});
|
||||
|
||||
server.post('/charging-station/getHomeData', (_req, res) => {
|
||||
const db = router.db;
|
||||
const homeData = db.get('homeData').value();
|
||||
return res.status(200).json(homeData);
|
||||
});
|
||||
|
||||
server.post('/charging-station/search', (req, res) => {
|
||||
const db = router.db;
|
||||
const searchSeed = db.get('searchSeed').value();
|
||||
const query = normalizeText(req.body.query);
|
||||
|
||||
const baseResponse = {
|
||||
result: buildResult(),
|
||||
recentSearches: searchSeed.recentSearches,
|
||||
recommendedFeatures: searchSeed.recommendedFeatures,
|
||||
recommendedChips: searchSeed.recommendedChips,
|
||||
};
|
||||
|
||||
if (!query) {
|
||||
return res.status(200).json({
|
||||
...baseResponse,
|
||||
suggestions: searchSeed.suggestions,
|
||||
});
|
||||
}
|
||||
|
||||
const suggestions = searchSeed.suggestions.filter((item) => {
|
||||
const title = normalizeText(item.title);
|
||||
const description = normalizeText(item.description);
|
||||
return title.includes(query) || description.includes(query);
|
||||
});
|
||||
|
||||
const recentSearches = [req.body.query, ...searchSeed.recentSearches]
|
||||
.filter(Boolean)
|
||||
.filter((item, index, array) => array.indexOf(item) === index)
|
||||
.slice(0, 10);
|
||||
|
||||
return res.status(200).json({
|
||||
...baseResponse,
|
||||
suggestions,
|
||||
recentSearches,
|
||||
});
|
||||
});
|
||||
|
||||
// Helper: build full image URL từ relative path
|
||||
const buildImageUrl = (relativePath) => {
|
||||
if (!relativePath) return null;
|
||||
// Nếu đã là URL đầy đủ thì giữ nguyên
|
||||
if (relativePath.startsWith('http://') || relativePath.startsWith('https://')) {
|
||||
return relativePath;
|
||||
}
|
||||
return `${baseUrl}${relativePath.startsWith('/') ? '' : '/'}${relativePath}`;
|
||||
};
|
||||
|
||||
server.post('/charging-station/reward/getList', (req, res) => {
|
||||
const db = router.db;
|
||||
const rewardSeed = db.get('rewardSeed').value();
|
||||
const page = Number(req.body.page || 1);
|
||||
const pageSize = Number(req.body.pageSize || 10);
|
||||
|
||||
const safePage = Number.isNaN(page) || page < 1 ? 1 : page;
|
||||
const safePageSize = Number.isNaN(pageSize) || pageSize < 1 ? 10 : pageSize;
|
||||
|
||||
const start = (safePage - 1) * safePageSize;
|
||||
const end = start + safePageSize;
|
||||
|
||||
// Map vouchers với full image URL
|
||||
const vouchers = rewardSeed.vouchers.slice(start, end).map((v) => ({
|
||||
...v,
|
||||
imageUrl: buildImageUrl(v.imageUrl),
|
||||
}));
|
||||
|
||||
// Banners chỉ trả về ở page 1, với full image URL
|
||||
const banners =
|
||||
safePage === 1
|
||||
? rewardSeed.banners.map((b) => ({
|
||||
...b,
|
||||
imageUrl: buildImageUrl(b.imageUrl),
|
||||
}))
|
||||
: [];
|
||||
|
||||
return res.status(200).json({
|
||||
result: buildResult(),
|
||||
banners,
|
||||
vouchers,
|
||||
paging: {
|
||||
page: safePage,
|
||||
pageSize: safePageSize,
|
||||
total: rewardSeed.vouchers.length,
|
||||
hasMore: end < rewardSeed.vouchers.length,
|
||||
},
|
||||
});
|
||||
});
|
||||
|
||||
server.use(router);
|
||||
|
||||
server.listen(port, () => {
|
||||
// eslint-disable-next-line no-console
|
||||
console.log(`Charging Station mock API is running at http://localhost:${port}`);
|
||||
});
|
||||
Reference in New Issue
Block a user