import path from 'node:path'; import { fileURLToPath } from 'node:url'; import jsonServer from 'json-server'; const __filename = fileURLToPath(import.meta.url); const __dirname = path.dirname(__filename); const server = jsonServer.create(); const router = jsonServer.router(path.join(__dirname, 'db.json')); // Serve static files từ folder public (images, etc.) const middlewares = jsonServer.defaults({ logger: true, static: path.join(__dirname, '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}`); });