This commit is contained in:
2026-04-06 00:17:17 +07:00
commit 0f7e386e2a
15 changed files with 1283 additions and 0 deletions

1
.gitignore vendored Normal file
View File

@@ -0,0 +1 @@
node_modules/

428
README.md Normal file
View File

@@ -0,0 +1,428 @@
# Charging Station Mock API (json-server)
Mock server cho feature Charging Station theo tài liệu:
- `docs/charging_station_final_spec.md`
## 1. Cài dependency
```bash
cd mock/charging-station
bun install
```
## 2. Chạy server
```bash
bun run start
```
Mặc định chạy ở `http://localhost:3100`.
Có thể đổi port:
```bash
PORT=3200 bun run start
```
## 3. Static Files (Images)
Server serve static files từ folder `public/`. Cấu trúc:
```
mock/charging-station/
├── public/
│ └── images/
│ ├── banners/ # Ảnh banner (800x400 recommended)
│ │ ├── banner_summer_sale.png
│ │ ├── banner_cashback.png
│ │ └── banner_new_year.png
│ └── vouchers/ # Ảnh voucher (300x200 recommended)
│ ├── starbucks.png
│ ├── kfc.png
│ ├── grab.png
│ └── ...
├── db.json
└── server.js
```
**Cách thêm ảnh mới:**
1. Đặt file ảnh vào `public/images/banners/` hoặc `public/images/vouchers/`
2. Cập nhật `imageUrl` trong `db.json` với path tương đối: `/images/banners/ten_file.png`
3. Server sẽ tự động build full URL: `http://localhost:3100/images/banners/ten_file.png`
**Access trực tiếp:**
```bash
# Xem ảnh trong browser
open http://localhost:3100/images/banners/banner_summer_sale.png
```
## 4. Endpoints
Tất cả endpoint dùng `POST`:
| Endpoint | Mô tả |
|----------|-------|
| `POST /charging-station/execute` | Thực thi giao dịch nạp tiền |
| `POST /charging-station/getHomeData` | Lấy dữ liệu trang Home |
| `POST /charging-station/search` | Tìm kiếm với AI gợi ý |
| `POST /charging-station/reward/getList` | Danh sách ưu đãi/voucher |
## 5. Validation Rules
### `POST /charging-station/execute`
**Input validation:**
| Field | Rule | Regex |
|-------|------|-------|
| `accountNumber` | Bắt buộc, đúng **9 ký tự**, chỉ chứa số | `^[0-9]{9}$` |
| `amount` | Bắt buộc, > 0, tối đa **1,000,000,000** | — |
| `accountName` | Không bắt buộc, chỉ chữ cái và khoảng trắng | `^[a-zA-ZÀ-ỹ\s]*$` |
**Error codes:**
| Code | Điều kiện | Message |
|------|-----------|---------|
| `01` | Dữ liệu không hợp lệ / TK không tồn tại | Tùy context |
| `02` | Số dư không đủ | "Số dư không đủ" |
| `03` | Vượt hạn mức (> 1 tỷ) | "Số tiền tối đa là 1,000,000,000 VND" |
**Simulate scenarios:**
| Scenario | Cách test |
|----------|-----------|
| TK không tồn tại | `accountNumber` bắt đầu bằng `999` |
| Số dư không đủ | `accountNumber` bắt đầu bằng `111` + `amount > 500000000` |
| Vượt hạn mức | `amount > 1000000000` |
### `POST /charging-station/search`
- `query` rỗng: trả full default data (recent + suggested + chips).
- `query` có giá trị: filter `suggestions` theo `title/description`.
### `POST /charging-station/reward/getList`
- Hỗ trợ `category` để filter voucher/brand.
- Hỗ trợ `page` + `pageSize` để phân trang `vouchers`.
- Trả thêm `paging` để client dễ test load more.
## 6. Curl samples
### Execute success
```bash
curl -X POST http://localhost:3100/charging-station/execute \
-H "Content-Type: application/json" \
-d '{
"sessionId": "abc123",
"deviceId": "device-001",
"accountNumber": "012345678",
"amount": "1000000",
"accountName": "NGUYEN VAN A",
"isDefaultAccount": true
}'
```
### Execute - TK không tồn tại
```bash
curl -X POST http://localhost:3100/charging-station/execute \
-H "Content-Type: application/json" \
-d '{
"accountNumber": "999123456",
"amount": "1000000"
}'
```
### Execute - Số dư không đủ
```bash
curl -X POST http://localhost:3100/charging-station/execute \
-H "Content-Type: application/json" \
-d '{
"accountNumber": "111234567",
"amount": "600000000"
}'
```
### Execute - Vượt hạn mức
```bash
curl -X POST http://localhost:3100/charging-station/execute \
-H "Content-Type: application/json" \
-d '{
"accountNumber": "012345678",
"amount": "1500000000"
}'
```
### Execute - accountNumber sai format
```bash
curl -X POST http://localhost:3100/charging-station/execute \
-H "Content-Type: application/json" \
-d '{
"accountNumber": "12345",
"amount": "1000000"
}'
```
### Execute - accountName có ký tự đặc biệt
```bash
curl -X POST http://localhost:3100/charging-station/execute \
-H "Content-Type: application/json" \
-d '{
"accountNumber": "012345678",
"amount": "1000000",
"accountName": "Nguyen Van A@123"
}'
```
### Get Home Data
```bash
curl -X POST http://localhost:3100/charging-station/getHomeData \
-H "Content-Type: application/json" \
-d '{"sessionId":"abc123","deviceId":"device-001"}'
```
**Response structure:**
- `accounts[]` - Danh sách tài khoản cá nhân (DEFAULT, APPLE_PAY, OVERDRAFT, SALARY)
- `businessAccounts[]` - Danh sách tài khoản hộ kinh doanh
- `loyaltyPoints`, `cashbackAmount`, `memberLevel` - Thông tin loyalty
- `recentTransactions[]` - Giao dịch gần đây
### Search default
```bash
curl -X POST http://localhost:3100/charging-station/search \
-H "Content-Type: application/json" \
-d '{"sessionId":"abc123","query":"","page":1,"pageSize":20}'
```
### Search with query
```bash
curl -X POST http://localhost:3100/charging-station/search \
-H "Content-Type: application/json" \
-d '{"sessionId":"abc123","query":"chuyển tiền","page":1,"pageSize":20}'
```
### Reward - page 1 (có banners)
```bash
curl -X POST http://localhost:3100/charging-station/reward/getList \
-H "Content-Type: application/json" \
-d '{"sessionId":"abc123","page":1,"pageSize":5}'
```
**Response structure:**
- `banners[]` - Danh sách banner (chỉ trả về ở page 1)
- `vouchers[]` - Danh sách voucher (phân trang)
- `paging` - Thông tin phân trang
### Reward - load more (page 2+, không có banners)
```bash
curl -X POST http://localhost:3100/charging-station/reward/getList \
-H "Content-Type: application/json" \
-d '{"sessionId":"abc123","page":2,"pageSize":5}'
```
**Voucher fields:**
- `imageUrl` - URL ảnh voucher
- `content` - Nội dung/mô tả ưu đãi
- `expiry` - Thời hạn (rỗng = không giới hạn)
- `requireCardSpending` - `true` = yêu cầu chi tiêu thẻ
---
## 7. BASE_URL Configuration
| Environment | BASE_URL |
|-------------|----------|
| Local | `http://localhost:3100` |
| VPS Production | `https://api-mock.example.com` |
> **TODO:** Cập nhật VPS Production URL sau khi deploy.
---
## 8. Deploy lên VPS với PM2
### 8.1 Cài đặt môi trường trên VPS
```bash
# 1. Cài Bun (JavaScript runtime)
curl -fsSL https://bun.sh/install | bash
source ~/.bashrc
# 2. Cài Node.js (cần cho PM2)
curl -fsSL https://deb.nodesource.com/setup_20.x | sudo -E bash -
sudo apt-get install -y nodejs
# 3. Cài PM2 globally
sudo npm install -g pm2
# 4. Verify installations
bun --version
node --version
pm2 --version
```
### 8.2 Clone và setup project
```bash
# Clone repo (hoặc copy folder mock/charging-station)
cd /var/www
git clone <repo-url> mbbank-mock
cd mbbank-mock/mock/charging-station
# Cài dependencies
bun install
```
### 8.3 Chạy với PM2
```bash
# Start server với PM2
pm2 start ecosystem.config.js --env production
# Hoặc start trực tiếp
pm2 start server.js --name "charging-station-mock" --interpreter bun
# Xem logs
pm2 logs charging-station-mock
# Xem status
pm2 status
# Restart
pm2 restart charging-station-mock
# Stop
pm2 stop charging-station-mock
# Delete
pm2 delete charging-station-mock
```
### 8.4 Auto-start khi reboot VPS
```bash
# Generate startup script
pm2 startup
# Save current process list
pm2 save
```
### 8.5 Cấu hình Nginx (reverse proxy)
```nginx
# /etc/nginx/sites-available/charging-station-mock
server {
listen 80;
server_name api-mock.example.com;
location / {
proxy_pass http://127.0.0.1:3100;
proxy_http_version 1.1;
proxy_set_header Upgrade $http_upgrade;
proxy_set_header Connection 'upgrade';
proxy_set_header Host $host;
proxy_set_header X-Real-IP $remote_addr;
proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
proxy_set_header X-Forwarded-Proto $scheme;
proxy_cache_bypass $http_upgrade;
}
}
```
```bash
# Enable site
sudo ln -s /etc/nginx/sites-available/charging-station-mock /etc/nginx/sites-enabled/
# Test config
sudo nginx -t
# Reload nginx
sudo systemctl reload nginx
```
### 8.6 Cài SSL với Certbot (HTTPS)
```bash
# Cài Certbot
sudo apt install certbot python3-certbot-nginx
# Lấy SSL certificate
sudo certbot --nginx -d api-mock.example.com
# Auto-renew (đã tự động setup)
sudo certbot renew --dry-run
```
### 8.7 PM2 Commands Reference
| Command | Mô tả |
|---------|-------|
| `pm2 start ecosystem.config.js` | Start với config file |
| `pm2 start server.js --name app` | Start với tên custom |
| `pm2 list` | Xem danh sách processes |
| `pm2 logs [name]` | Xem logs |
| `pm2 monit` | Monitor realtime |
| `pm2 restart [name]` | Restart process |
| `pm2 reload [name]` | Zero-downtime reload |
| `pm2 stop [name]` | Stop process |
| `pm2 delete [name]` | Xóa process |
| `pm2 save` | Lưu process list |
| `pm2 startup` | Setup auto-start |
### 8.8 Đổi Port
```bash
# Cách 1: Environment variable
PORT=3200 pm2 start server.js --name "charging-station-mock" --interpreter bun
# Cách 2: Sửa ecosystem.config.js
# env_production: { PORT: 3200 }
pm2 start ecosystem.config.js --env production
```
---
## 9. Troubleshooting
### PM2 không nhận Bun
```bash
# Kiểm tra path của bun
which bun
# Output: /home/user/.bun/bin/bun
# Sử dụng full path
pm2 start server.js --interpreter /home/user/.bun/bin/bun
```
### Port đã được sử dụng
```bash
# Tìm process đang dùng port
sudo lsof -i :3100
# Kill process
sudo kill -9 <PID>
```
### Logs quá lớn
```bash
# Rotate logs
pm2 install pm2-logrotate
# Config logrotate
pm2 set pm2-logrotate:max_size 10M
pm2 set pm2-logrotate:retain 7
```

332
bun.lock Normal file
View File

@@ -0,0 +1,332 @@
{
"lockfileVersion": 1,
"configVersion": 1,
"workspaces": {
"": {
"name": "charging-station-json-server",
"dependencies": {
"json-server": "^0.17.4",
},
"devDependencies": {
"nodemon": "^3.1.10",
},
},
},
"packages": {
"accepts": ["accepts@1.3.8", "", { "dependencies": { "mime-types": "~2.1.34", "negotiator": "0.6.3" } }, "sha512-PYAthTa2m2VKxuvSD3DPC/Gy+U+sOA1LAuT8mkmRuvw+NACSaeXEQ+NHcVF7rONl6qcaxV3Uuemwawk+7+SJLw=="],
"ansi-regex": ["ansi-regex@5.0.1", "", {}, "sha512-quJQXlTSUGL2LH9SUXo8VwsY4soanhgo6LNSm84E1LBcE8s3O0wpdiRzyR9z/ZZJMlMWv37qOOb9pdJlMUEKFQ=="],
"ansi-styles": ["ansi-styles@4.3.0", "", { "dependencies": { "color-convert": "^2.0.1" } }, "sha512-zbB9rCJAT1rbjiVDb2hqKFHNYLxgtk8NURxZ3IZwD3F6NtxbXZQCnnSi1Lkx+IDohdPlFp222wVALIheZJQSEg=="],
"anymatch": ["anymatch@3.1.3", "", { "dependencies": { "normalize-path": "^3.0.0", "picomatch": "^2.0.4" } }, "sha512-KMReFUr0B4t+D+OBkjR3KYqvocp2XaSzO55UcB6mgQMd3KbcE+mWTyvVV7D/zsdEbNnV6acZUutkiHQXvTr1Rw=="],
"array-flatten": ["array-flatten@1.1.1", "", {}, "sha512-PCVAQswWemu6UdxsDFFX/+gVeYqKAod3D3UVm91jHwynguOwAvYPhx8nNlM++NqRcK6CxxpUafjmhIdKiHibqg=="],
"balanced-match": ["balanced-match@4.0.4", "", {}, "sha512-BLrgEcRTwX2o6gGxGOCNyMvGSp35YofuYzw9h1IMTRmKqttAZZVU67bdb9Pr2vUHA8+j3i2tJfjO6C6+4myGTA=="],
"basic-auth": ["basic-auth@2.0.1", "", { "dependencies": { "safe-buffer": "5.1.2" } }, "sha512-NF+epuEdnUYVlGuhaxbbq+dvJttwLnGY+YixlXlME5KpQ5W3CnXA5cVTneY3SPbPDRkcjMbifrwmFYcClgOZeg=="],
"binary-extensions": ["binary-extensions@2.3.0", "", {}, "sha512-Ceh+7ox5qe7LJuLHoY0feh3pHuUDHAcRUeyL2VYghZwfpkNIy/+8Ocg0a3UuSoYzavmylwuLWQOf3hl0jjMMIw=="],
"body-parser": ["body-parser@1.20.4", "", { "dependencies": { "bytes": "~3.1.2", "content-type": "~1.0.5", "debug": "2.6.9", "depd": "2.0.0", "destroy": "~1.2.0", "http-errors": "~2.0.1", "iconv-lite": "~0.4.24", "on-finished": "~2.4.1", "qs": "~6.14.0", "raw-body": "~2.5.3", "type-is": "~1.6.18", "unpipe": "~1.0.0" } }, "sha512-ZTgYYLMOXY9qKU/57FAo8F+HA2dGX7bqGc71txDRC1rS4frdFI5R7NhluHxH6M0YItAP0sHB4uqAOcYKxO6uGA=="],
"brace-expansion": ["brace-expansion@5.0.5", "", { "dependencies": { "balanced-match": "^4.0.2" } }, "sha512-VZznLgtwhn+Mact9tfiwx64fA9erHH/MCXEUfB/0bX/6Fz6ny5EGTXYltMocqg4xFAQZtnO3DHWWXi8RiuN7cQ=="],
"braces": ["braces@3.0.3", "", { "dependencies": { "fill-range": "^7.1.1" } }, "sha512-yQbXgO/OSZVD2IsiLlro+7Hf6Q18EJrKSEsdoMzKePKXct3gvD8oLcOQdIzGupr5Fj+EDe8gO/lxc1BzfMpxvA=="],
"bytes": ["bytes@3.1.2", "", {}, "sha512-/Nf7TyzTx6S3yRJObOAV7956r8cr2+Oj8AC5dt8wSP3BQAoeX58NoHyCU8P8zGkNXStjTSi6fzO6F0pBdcYbEg=="],
"call-bind-apply-helpers": ["call-bind-apply-helpers@1.0.2", "", { "dependencies": { "es-errors": "^1.3.0", "function-bind": "^1.1.2" } }, "sha512-Sp1ablJ0ivDkSzjcaJdxEunN5/XvksFJ2sMBFfq6x0ryhQV/2b/KwFe21cMpmHtPOSij8K99/wSfoEuTObmuMQ=="],
"call-bound": ["call-bound@1.0.4", "", { "dependencies": { "call-bind-apply-helpers": "^1.0.2", "get-intrinsic": "^1.3.0" } }, "sha512-+ys997U96po4Kx/ABpBCqhA9EuxJaQWDQg7295H4hBphv3IZg0boBKuwYpt4YXp6MZ5AmZQnU/tyMTlRpaSejg=="],
"chalk": ["chalk@4.1.2", "", { "dependencies": { "ansi-styles": "^4.1.0", "supports-color": "^7.1.0" } }, "sha512-oKnbhFyRIXpUuez8iBMmyEa4nbj4IOQyuhc/wy9kY7/WVPcwIO9VA668Pu8RkO7+0G76SLROeyw9CpQ061i4mA=="],
"chokidar": ["chokidar@3.6.0", "", { "dependencies": { "anymatch": "~3.1.2", "braces": "~3.0.2", "glob-parent": "~5.1.2", "is-binary-path": "~2.1.0", "is-glob": "~4.0.1", "normalize-path": "~3.0.0", "readdirp": "~3.6.0" }, "optionalDependencies": { "fsevents": "~2.3.2" } }, "sha512-7VT13fmjotKpGipCW9JEQAusEPE+Ei8nl6/g4FBAmIm0GOOLMua9NDDo/DWp0ZAxCr3cPq5ZpBqmPAQgDda2Pw=="],
"cliui": ["cliui@8.0.1", "", { "dependencies": { "string-width": "^4.2.0", "strip-ansi": "^6.0.1", "wrap-ansi": "^7.0.0" } }, "sha512-BSeNnyus75C4//NQ9gQt1/csTXyo/8Sb+afLAkzAptFuMsod9HFokGNudZpi/oQV73hnVK+sR+5PVRMd+Dr7YQ=="],
"color-convert": ["color-convert@2.0.1", "", { "dependencies": { "color-name": "~1.1.4" } }, "sha512-RRECPsj7iu/xb5oKYcsFHSppFNnsj/52OVTRKb4zP5onXwVF3zVmmToNcOfGC+CRDpfK/U584fMg38ZHCaElKQ=="],
"color-name": ["color-name@1.1.4", "", {}, "sha512-dOy+3AuW3a2wNbZHIuMZpTcgjGuLU/uBL/ubcZF9OXbDo8ff4O8yVp5Bf0efS8uEoYo5q4Fx7dY9OgQGXgAsQA=="],
"compressible": ["compressible@2.0.18", "", { "dependencies": { "mime-db": ">= 1.43.0 < 2" } }, "sha512-AF3r7P5dWxL8MxyITRMlORQNaOA2IkAFaTr4k7BUumjPtRpGDTZpl0Pb1XCO6JeDCBdp126Cgs9sMxqSjgYyRg=="],
"compression": ["compression@1.8.1", "", { "dependencies": { "bytes": "3.1.2", "compressible": "~2.0.18", "debug": "2.6.9", "negotiator": "~0.6.4", "on-headers": "~1.1.0", "safe-buffer": "5.2.1", "vary": "~1.1.2" } }, "sha512-9mAqGPHLakhCLeNyxPkK4xVo746zQ/czLH1Ky+vkitMnWfWZps8r0qXuwhwizagCRttsL4lfG4pIOvaWLpAP0w=="],
"connect-pause": ["connect-pause@0.1.1", "", {}, "sha512-a1gSWQBQD73krFXdUEYJom2RTFrWUL3YvXDCRkyv//GVXc79cdW9MngtRuN9ih4FDKBtfJAJId+BbDuX+1rh2w=="],
"content-disposition": ["content-disposition@0.5.4", "", { "dependencies": { "safe-buffer": "5.2.1" } }, "sha512-FveZTNuGw04cxlAiWbzi6zTAL/lhehaWbTtgluJh4/E95DqMwTmha3KZN1aAWA8cFIhHzMZUvLevkw5Rqk+tSQ=="],
"content-type": ["content-type@1.0.5", "", {}, "sha512-nTjqfcBFEipKdXCv4YDQWCfmcLZKm81ldF0pAopTvyrFGVbcR6P/VAAd5G7N+0tTr8QqiU0tFadD6FK4NtJwOA=="],
"cookie": ["cookie@0.7.2", "", {}, "sha512-yki5XnKuf750l50uGTllt6kKILY4nQ1eNIQatoXEByZ5dWgnKqbnqmTrBE5B4N7lrMJKQ2ytWMiTO2o0v6Ew/w=="],
"cookie-signature": ["cookie-signature@1.0.7", "", {}, "sha512-NXdYc3dLr47pBkpUCHtKSwIOQXLVn8dZEuywboCOJY/osA0wFSLlSawr3KN8qXJEyX66FcONTH8EIlVuK0yyFA=="],
"cors": ["cors@2.8.6", "", { "dependencies": { "object-assign": "^4", "vary": "^1" } }, "sha512-tJtZBBHA6vjIAaF6EnIaq6laBBP9aq/Y3ouVJjEfoHbRBcHBAHYcMh/w8LDrk2PvIMMq8gmopa5D4V8RmbrxGw=="],
"debug": ["debug@4.4.3", "", { "dependencies": { "ms": "^2.1.3" } }, "sha512-RGwwWnwQvkVfavKVt22FGLw+xYSdzARwm0ru6DhTVA3umU5hZc28V3kO4stgYryrTlLpuvgI9GiijltAjNbcqA=="],
"depd": ["depd@2.0.0", "", {}, "sha512-g7nH6P6dyDioJogAAGprGpCtVImJhpPk/roCzdb3fIh61/s/nPsfR6onyMwkCAR/OlC3yBC0lESvUoQEAssIrw=="],
"destroy": ["destroy@1.2.0", "", {}, "sha512-2sJGJTaXIIaR1w4iJSNoN0hnMY7Gpc/n8D4qSCJw8QqFWXf7cuAgnEHxBpweaVcPevC2l3KpjYCx3NypQQgaJg=="],
"dunder-proto": ["dunder-proto@1.0.1", "", { "dependencies": { "call-bind-apply-helpers": "^1.0.1", "es-errors": "^1.3.0", "gopd": "^1.2.0" } }, "sha512-KIN/nDJBQRcXw0MLVhZE9iQHmG68qAVIBg9CqmUYjmQIhgij9U5MFvrqkUL5FbtyyzZuOeOt0zdeRe4UY7ct+A=="],
"ee-first": ["ee-first@1.1.1", "", {}, "sha512-WMwm9LhRUo+WUaRN+vRuETqG89IgZphVSNkdFgeb6sS/E4OrDIN7t48CAewSHXc6C8lefD8KKfr5vY61brQlow=="],
"emoji-regex": ["emoji-regex@8.0.0", "", {}, "sha512-MSjYzcWNOA0ewAHpz0MxpYFvwg6yjy1NG3xteoqz644VCo/RPgnr1/GGt+ic3iJTzQ8Eu3TdM14SawnVUmGE6A=="],
"encodeurl": ["encodeurl@2.0.0", "", {}, "sha512-Q0n9HRi4m6JuGIV1eFlmvJB7ZEVxu93IrMyiMsGC0lrMJMWzRgx6WGquyfQgZVb31vhGgXnfmPNNXmxnOkRBrg=="],
"errorhandler": ["errorhandler@1.5.2", "", { "dependencies": { "accepts": "~1.3.8", "escape-html": "~1.0.3" } }, "sha512-kNAL7hESndBCrWwS72QyV3IVOTrVmj9D062FV5BQswNL5zEdeRmz/WJFyh6Aj/plvvSOrzddkxW57HgkZcR9Fw=="],
"es-define-property": ["es-define-property@1.0.1", "", {}, "sha512-e3nRfgfUZ4rNGL232gUgX06QNyyez04KdjFrF+LTRoOXmrOgFKDg4BCdsjW8EnT69eqdYGmRpJwiPVYNrCaW3g=="],
"es-errors": ["es-errors@1.3.0", "", {}, "sha512-Zf5H2Kxt2xjTvbJvP2ZWLEICxA6j+hAmMzIlypy4xcBg1vKVnx89Wy0GbS+kf5cwCVFFzdCFh2XSCFNULS6csw=="],
"es-object-atoms": ["es-object-atoms@1.1.1", "", { "dependencies": { "es-errors": "^1.3.0" } }, "sha512-FGgH2h8zKNim9ljj7dankFPcICIK9Cp5bm+c2gQSYePhpaG5+esrLODihIorn+Pe6FGJzWhXQotPv73jTaldXA=="],
"escalade": ["escalade@3.2.0", "", {}, "sha512-WUj2qlxaQtO4g6Pq5c29GTcWGDyd8itL8zTlipgECz3JesAiiOKotd8JU6otB3PACgG6xkJUyVhboMS+bje/jA=="],
"escape-html": ["escape-html@1.0.3", "", {}, "sha512-NiSupZ4OeuGwr68lGIeym/ksIZMJodUGOSCZ/FSnTxcrekbvqrgdUxlJOMpijaKZVjAJrWrGs/6Jy8OMuyj9ow=="],
"etag": ["etag@1.8.1", "", {}, "sha512-aIL5Fx7mawVa300al2BnEE4iNvo1qETxLrPI/o05L7z6go7fCw1J6EQmbK4FmJ2AS7kgVF/KEZWufBfdClMcPg=="],
"express": ["express@4.22.1", "", { "dependencies": { "accepts": "~1.3.8", "array-flatten": "1.1.1", "body-parser": "~1.20.3", "content-disposition": "~0.5.4", "content-type": "~1.0.4", "cookie": "~0.7.1", "cookie-signature": "~1.0.6", "debug": "2.6.9", "depd": "2.0.0", "encodeurl": "~2.0.0", "escape-html": "~1.0.3", "etag": "~1.8.1", "finalhandler": "~1.3.1", "fresh": "~0.5.2", "http-errors": "~2.0.0", "merge-descriptors": "1.0.3", "methods": "~1.1.2", "on-finished": "~2.4.1", "parseurl": "~1.3.3", "path-to-regexp": "~0.1.12", "proxy-addr": "~2.0.7", "qs": "~6.14.0", "range-parser": "~1.2.1", "safe-buffer": "5.2.1", "send": "~0.19.0", "serve-static": "~1.16.2", "setprototypeof": "1.2.0", "statuses": "~2.0.1", "type-is": "~1.6.18", "utils-merge": "1.0.1", "vary": "~1.1.2" } }, "sha512-F2X8g9P1X7uCPZMA3MVf9wcTqlyNp7IhH5qPCI0izhaOIYXaW9L535tGA3qmjRzpH+bZczqq7hVKxTR4NWnu+g=="],
"express-urlrewrite": ["express-urlrewrite@1.4.0", "", { "dependencies": { "debug": "*", "path-to-regexp": "^1.0.3" } }, "sha512-PI5h8JuzoweS26vFizwQl6UTF25CAHSggNv0J25Dn/IKZscJHWZzPrI5z2Y2jgOzIaw2qh8l6+/jUcig23Z2SA=="],
"fill-range": ["fill-range@7.1.1", "", { "dependencies": { "to-regex-range": "^5.0.1" } }, "sha512-YsGpe3WHLK8ZYi4tWDg2Jy3ebRz2rXowDxnld4bkQB00cc/1Zw9AWnC0i9ztDJitivtQvaI9KaLyKrc+hBW0yg=="],
"finalhandler": ["finalhandler@1.3.2", "", { "dependencies": { "debug": "2.6.9", "encodeurl": "~2.0.0", "escape-html": "~1.0.3", "on-finished": "~2.4.1", "parseurl": "~1.3.3", "statuses": "~2.0.2", "unpipe": "~1.0.0" } }, "sha512-aA4RyPcd3badbdABGDuTXCMTtOneUCAYH/gxoYRTZlIJdF0YPWuGqiAsIrhNnnqdXGswYk6dGujem4w80UJFhg=="],
"forwarded": ["forwarded@0.2.0", "", {}, "sha512-buRG0fpBtRHSTCOASe6hD258tEubFoRLb4ZNA6NxMVHNw2gOcwHo9wyablzMzOA5z9xA9L1KNjk/Nt6MT9aYow=="],
"fresh": ["fresh@0.5.2", "", {}, "sha512-zJ2mQYM18rEFOudeV4GShTGIQ7RbzA7ozbU9I/XBpm7kqgMywgmylMwXHxZJmkVoYkna9d2pVXVXPdYTP9ej8Q=="],
"fsevents": ["fsevents@2.3.3", "", { "os": "darwin" }, "sha512-5xoDfX+fL7faATnagmWPpbFtwh/R77WmMMqqHGS65C3vvB0YHrgF+B1YmZ3441tMj5n63k0212XNoJwzlhffQw=="],
"function-bind": ["function-bind@1.1.2", "", {}, "sha512-7XHNxH7qX9xG5mIwxkhumTox/MIRNcOgDrxWsMt2pAr23WHp6MrRlN7FBSFpCpr+oVO0F744iUgR82nJMfG2SA=="],
"get-caller-file": ["get-caller-file@2.0.5", "", {}, "sha512-DyFP3BM/3YHTQOCUL/w0OZHR0lpKeGrxotcHWcqNEdnltqFwXVfhEBQ94eIo34AfQpo0rGki4cyIiftY06h2Fg=="],
"get-intrinsic": ["get-intrinsic@1.3.0", "", { "dependencies": { "call-bind-apply-helpers": "^1.0.2", "es-define-property": "^1.0.1", "es-errors": "^1.3.0", "es-object-atoms": "^1.1.1", "function-bind": "^1.1.2", "get-proto": "^1.0.1", "gopd": "^1.2.0", "has-symbols": "^1.1.0", "hasown": "^2.0.2", "math-intrinsics": "^1.1.0" } }, "sha512-9fSjSaos/fRIVIp+xSJlE6lfwhES7LNtKaCBIamHsjr2na1BiABJPo0mOjjz8GJDURarmCPGqaiVg5mfjb98CQ=="],
"get-proto": ["get-proto@1.0.1", "", { "dependencies": { "dunder-proto": "^1.0.1", "es-object-atoms": "^1.0.0" } }, "sha512-sTSfBjoXBp89JvIKIefqw7U2CCebsc74kiY6awiGogKtoSGbgjYE/G/+l9sF3MWFPNc9IcoOC4ODfKHfxFmp0g=="],
"glob-parent": ["glob-parent@5.1.2", "", { "dependencies": { "is-glob": "^4.0.1" } }, "sha512-AOIgSQCepiJYwP3ARnGx+5VnTu2HBYdzbGP45eLw1vr3zB3vZLeyed1sC9hnbcOc9/SrMyM5RPQrkGz4aS9Zow=="],
"gopd": ["gopd@1.2.0", "", {}, "sha512-ZUKRh6/kUFoAiTAtTYPZJ3hw9wNxx+BIBOijnlG9PnrJsCcSjs1wyyD6vJpaYtgnzDrKYRSqf3OO6Rfa93xsRg=="],
"graceful-fs": ["graceful-fs@4.2.11", "", {}, "sha512-RbJ5/jmFcNNCcDV5o9eTnBLJ/HszWV0P73bc+Ff4nS/rJj+YaS6IGyiOL0VoBYX+l1Wrl3k63h/KrH+nhJ0XvQ=="],
"has-flag": ["has-flag@3.0.0", "", {}, "sha512-sKJf1+ceQBr4SMkvQnBDNDtf4TXpVhVGateu0t918bl30FnbE2m4vNLX+VWe/dpjlb+HugGYzW7uQXH98HPEYw=="],
"has-symbols": ["has-symbols@1.1.0", "", {}, "sha512-1cDNdwJ2Jaohmb3sg4OmKaMBwuC48sYni5HUw2DvsC8LjGTLK9h+eb1X6RyuOHe4hT0ULCW68iomhjUoKUqlPQ=="],
"hasown": ["hasown@2.0.2", "", { "dependencies": { "function-bind": "^1.1.2" } }, "sha512-0hJU9SCPvmMzIBdZFqNPXWa6dqh7WdH0cII9y+CyS8rG3nL48Bclra9HmKhVVUHyPWNH5Y7xDwAB7bfgSjkUMQ=="],
"http-errors": ["http-errors@2.0.1", "", { "dependencies": { "depd": "~2.0.0", "inherits": "~2.0.4", "setprototypeof": "~1.2.0", "statuses": "~2.0.2", "toidentifier": "~1.0.1" } }, "sha512-4FbRdAX+bSdmo4AUFuS0WNiPz8NgFt+r8ThgNWmlrjQjt1Q7ZR9+zTlce2859x4KSXrwIsaeTqDoKQmtP8pLmQ=="],
"iconv-lite": ["iconv-lite@0.4.24", "", { "dependencies": { "safer-buffer": ">= 2.1.2 < 3" } }, "sha512-v3MXnZAcvnywkTUEZomIActle7RXXeedOR31wwl7VlyoXO4Qi9arvSenNQWne1TcRwhCL1HwLI21bEqdpj8/rA=="],
"ignore-by-default": ["ignore-by-default@1.0.1", "", {}, "sha512-Ius2VYcGNk7T90CppJqcIkS5ooHUZyIQK+ClZfMfMNFEF9VSE73Fq+906u/CWu92x4gzZMWOwfFYckPObzdEbA=="],
"inherits": ["inherits@2.0.4", "", {}, "sha512-k/vGaX4/Yla3WzyMCvTQOXYeIHvqOKtnqBduzTHpzpQZzAskKMhZ2K+EnBiSM9zGSoIFeMpXKxa4dYeZIQqewQ=="],
"ipaddr.js": ["ipaddr.js@1.9.1", "", {}, "sha512-0KI/607xoxSToH7GjN1FfSbLoU0+btTicjsQSWQlh/hZykN8KpmMf7uYwPW3R+akZ6R/w18ZlXSHBYXiYUPO3g=="],
"is-binary-path": ["is-binary-path@2.1.0", "", { "dependencies": { "binary-extensions": "^2.0.0" } }, "sha512-ZMERYes6pDydyuGidse7OsHxtbI7WVeUEozgR/g7rd0xUimYNlvZRE/K2MgZTjWy725IfelLeVcEM97mmtRGXw=="],
"is-extglob": ["is-extglob@2.1.1", "", {}, "sha512-SbKbANkN603Vi4jEZv49LeVJMn4yGwsbzZworEoyEiutsN3nJYdbO36zfhGJ6QEDpOZIFkDtnq5JRxmvl3jsoQ=="],
"is-fullwidth-code-point": ["is-fullwidth-code-point@3.0.0", "", {}, "sha512-zymm5+u+sCsSWyD9qNaejV3DFvhCKclKdizYaJUuHA83RLjb7nSuGnddCHGv0hk+KY7BMAlsWeK4Ueg6EV6XQg=="],
"is-glob": ["is-glob@4.0.3", "", { "dependencies": { "is-extglob": "^2.1.1" } }, "sha512-xelSayHH36ZgE7ZWhli7pW34hNbNl8Ojv5KVmkJD4hBdD3th8Tfk9vYasLM+mXWOZhFkgZfxhLSnrwRr4elSSg=="],
"is-number": ["is-number@7.0.0", "", {}, "sha512-41Cifkg6e8TylSpdtTpeLVMqvSBEVzTttHvERD741+pnZ8ANv0004MRL43QKPDlK9cGvNp6NZWZUBlbGXYxxng=="],
"is-promise": ["is-promise@2.2.2", "", {}, "sha512-+lP4/6lKUBfQjZ2pdxThZvLUAafmZb8OAxFb8XXtiQmS35INgr85hdOGoEs124ez1FCnZJt6jau/T+alh58QFQ=="],
"isarray": ["isarray@0.0.1", "", {}, "sha512-D2S+3GLxWH+uhrNEcoh/fnmYeP8E8/zHl644d/jdA0g2uyXvy3sb0qxotE+ne0LtccHknQzWwZEzhak7oJ0COQ=="],
"jju": ["jju@1.4.0", "", {}, "sha512-8wb9Yw966OSxApiCt0K3yNJL8pnNeIv+OEq2YMidz4FKP6nonSRoOXc80iXY4JaN2FC11B9qsNmDsm+ZOfMROA=="],
"json-parse-helpfulerror": ["json-parse-helpfulerror@1.0.3", "", { "dependencies": { "jju": "^1.1.0" } }, "sha512-XgP0FGR77+QhUxjXkwOMkC94k3WtqEBfcnjWqhRd82qTat4SWKRE+9kUnynz/shm3I4ea2+qISvTIeGTNU7kJg=="],
"json-server": ["json-server@0.17.4", "", { "dependencies": { "body-parser": "^1.19.0", "chalk": "^4.1.2", "compression": "^1.7.4", "connect-pause": "^0.1.1", "cors": "^2.8.5", "errorhandler": "^1.5.1", "express": "^4.17.1", "express-urlrewrite": "^1.4.0", "json-parse-helpfulerror": "^1.0.3", "lodash": "^4.17.21", "lodash-id": "^0.14.1", "lowdb": "^1.0.0", "method-override": "^3.0.0", "morgan": "^1.10.0", "nanoid": "^3.1.23", "please-upgrade-node": "^3.2.0", "pluralize": "^8.0.0", "server-destroy": "^1.0.1", "yargs": "^17.0.1" }, "bin": { "json-server": "lib/cli/bin.js" } }, "sha512-bGBb0WtFuAKbgI7JV3A864irWnMZSvBYRJbohaOuatHwKSRFUfqtQlrYMrB6WbalXy/cJabyjlb7JkHli6dYjQ=="],
"lodash": ["lodash@4.18.1", "", {}, "sha512-dMInicTPVE8d1e5otfwmmjlxkZoUpiVLwyeTdUsi/Caj/gfzzblBcCE5sRHV/AsjuCmxWrte2TNGSYuCeCq+0Q=="],
"lodash-id": ["lodash-id@0.14.1", "", {}, "sha512-ikQPBTiq/d5m6dfKQlFdIXFzvThPi2Be9/AHxktOnDSfSxE1j9ICbBT5Elk1ke7HSTgM38LHTpmJovo9/klnLg=="],
"lowdb": ["lowdb@1.0.0", "", { "dependencies": { "graceful-fs": "^4.1.3", "is-promise": "^2.1.0", "lodash": "4", "pify": "^3.0.0", "steno": "^0.4.1" } }, "sha512-2+x8esE/Wb9SQ1F9IHaYWfsC9FIecLOPrK4g17FGEayjUWH172H6nwicRovGvSE2CPZouc2MCIqCI7h9d+GftQ=="],
"math-intrinsics": ["math-intrinsics@1.1.0", "", {}, "sha512-/IXtbwEk5HTPyEwyKX6hGkYXxM9nbj64B+ilVJnC/R6B0pH5G4V3b0pVbL7DBj4tkhBAppbQUlf6F6Xl9LHu1g=="],
"media-typer": ["media-typer@0.3.0", "", {}, "sha512-dq+qelQ9akHpcOl/gUVRTxVIOkAJ1wR3QAvb4RsVjS8oVoFjDGTc679wJYmUmknUF5HwMLOgb5O+a3KxfWapPQ=="],
"merge-descriptors": ["merge-descriptors@1.0.3", "", {}, "sha512-gaNvAS7TZ897/rVaZ0nMtAyxNyi/pdbjbAwUpFQpN70GqnVfOiXpeUUMKRBmzXaSQ8DdTX4/0ms62r2K+hE6mQ=="],
"method-override": ["method-override@3.0.0", "", { "dependencies": { "debug": "3.1.0", "methods": "~1.1.2", "parseurl": "~1.3.2", "vary": "~1.1.2" } }, "sha512-IJ2NNN/mSl9w3kzWB92rcdHpz+HjkxhDJWNDBqSlas+zQdP8wBiJzITPg08M/k2uVvMow7Sk41atndNtt/PHSA=="],
"methods": ["methods@1.1.2", "", {}, "sha512-iclAHeNqNm68zFtnZ0e+1L2yUIdvzNoauKU4WBA3VvH/vPFieF7qfRlwUZU+DA9P9bPXIS90ulxoUoCH23sV2w=="],
"mime": ["mime@1.6.0", "", { "bin": { "mime": "cli.js" } }, "sha512-x0Vn8spI+wuJ1O6S7gnbaQg8Pxh4NNHb7KSINmEWKiPE4RKOplvijn+NkmYmmRgP68mc70j2EbeTFRsrswaQeg=="],
"mime-db": ["mime-db@1.52.0", "", {}, "sha512-sPU4uV7dYlvtWJxwwxHD0PuihVNiE7TyAbQ5SWxDCB9mUYvOgroQOwYQQOKPJ8CIbE+1ETVlOoK1UC2nU3gYvg=="],
"mime-types": ["mime-types@2.1.35", "", { "dependencies": { "mime-db": "1.52.0" } }, "sha512-ZDY+bPm5zTTF+YpCrAU9nK0UgICYPT0QtT1NZWFv4s++TNkcgVaT0g6+4R2uI4MjQjzysHB1zxuWL50hzaeXiw=="],
"minimatch": ["minimatch@10.2.5", "", { "dependencies": { "brace-expansion": "^5.0.5" } }, "sha512-MULkVLfKGYDFYejP07QOurDLLQpcjk7Fw+7jXS2R2czRQzR56yHRveU5NDJEOviH+hETZKSkIk5c+T23GjFUMg=="],
"morgan": ["morgan@1.10.1", "", { "dependencies": { "basic-auth": "~2.0.1", "debug": "2.6.9", "depd": "~2.0.0", "on-finished": "~2.3.0", "on-headers": "~1.1.0" } }, "sha512-223dMRJtI/l25dJKWpgij2cMtywuG/WiUKXdvwfbhGKBhy1puASqXwFzmWZ7+K73vUPoR7SS2Qz2cI/g9MKw0A=="],
"ms": ["ms@2.1.3", "", {}, "sha512-6FlzubTLZG3J2a/NVCAleEhjzq5oxgHyaCU9yYXvcLsvoVaHJq/s5xXI6/XXP6tz7R9xAOtHnSO/tXtF3WRTlA=="],
"nanoid": ["nanoid@3.3.11", "", { "bin": { "nanoid": "bin/nanoid.cjs" } }, "sha512-N8SpfPUnUp1bK+PMYW8qSWdl9U+wwNWI4QKxOYDy9JAro3WMX7p2OeVRF9v+347pnakNevPmiHhNmZ2HbFA76w=="],
"negotiator": ["negotiator@0.6.4", "", {}, "sha512-myRT3DiWPHqho5PrJaIRyaMv2kgYf0mUVgBNOYMuCH5Ki1yEiQaf/ZJuQ62nvpc44wL5WDbTX7yGJi1Neevw8w=="],
"nodemon": ["nodemon@3.1.14", "", { "dependencies": { "chokidar": "^3.5.2", "debug": "^4", "ignore-by-default": "^1.0.1", "minimatch": "^10.2.1", "pstree.remy": "^1.1.8", "semver": "^7.5.3", "simple-update-notifier": "^2.0.0", "supports-color": "^5.5.0", "touch": "^3.1.0", "undefsafe": "^2.0.5" }, "bin": { "nodemon": "bin/nodemon.js" } }, "sha512-jakjZi93UtB3jHMWsXL68FXSAosbLfY0In5gtKq3niLSkrWznrVBzXFNOEMJUfc9+Ke7SHWoAZsiMkNP3vq6Jw=="],
"normalize-path": ["normalize-path@3.0.0", "", {}, "sha512-6eZs5Ls3WtCisHWp9S2GUy8dqkpGi4BVSz3GaqiE6ezub0512ESztXUwUB6C6IKbQkY2Pnb/mD4WYojCRwcwLA=="],
"object-assign": ["object-assign@4.1.1", "", {}, "sha512-rJgTQnkUnH1sFw8yT6VSU3zD3sWmu6sZhIseY8VX+GRu3P6F7Fu+JNDoXfklElbLJSnc3FUQHVe4cU5hj+BcUg=="],
"object-inspect": ["object-inspect@1.13.4", "", {}, "sha512-W67iLl4J2EXEGTbfeHCffrjDfitvLANg0UlX3wFUUSTx92KXRFegMHUVgSqE+wvhAbi4WqjGg9czysTV2Epbew=="],
"on-finished": ["on-finished@2.4.1", "", { "dependencies": { "ee-first": "1.1.1" } }, "sha512-oVlzkg3ENAhCk2zdv7IJwd/QUD4z2RxRwpkcGY8psCVcCYZNq4wYnVWALHM+brtuJjePWiYF/ClmuDr8Ch5+kg=="],
"on-headers": ["on-headers@1.1.0", "", {}, "sha512-737ZY3yNnXy37FHkQxPzt4UZ2UWPWiCZWLvFZ4fu5cueciegX0zGPnrlY6bwRg4FdQOe9YU8MkmJwGhoMybl8A=="],
"parseurl": ["parseurl@1.3.3", "", {}, "sha512-CiyeOxFT/JZyN5m0z9PfXw4SCBJ6Sygz1Dpl0wqjlhDEGGBP1GnsUVEL0p63hoG1fcj3fHynXi9NYO4nWOL+qQ=="],
"path-to-regexp": ["path-to-regexp@0.1.13", "", {}, "sha512-A/AGNMFN3c8bOlvV9RreMdrv7jsmF9XIfDeCd87+I8RNg6s78BhJxMu69NEMHBSJFxKidViTEdruRwEk/WIKqA=="],
"picomatch": ["picomatch@2.3.2", "", {}, "sha512-V7+vQEJ06Z+c5tSye8S+nHUfI51xoXIXjHQ99cQtKUkQqqO1kO/KCJUfZXuB47h/YBlDhah2H3hdUGXn8ie0oA=="],
"pify": ["pify@3.0.0", "", {}, "sha512-C3FsVNH1udSEX48gGX1xfvwTWfsYWj5U+8/uK15BGzIGrKoUpghX8hWZwa/OFnakBiiVNmBvemTJR5mcy7iPcg=="],
"please-upgrade-node": ["please-upgrade-node@3.2.0", "", { "dependencies": { "semver-compare": "^1.0.0" } }, "sha512-gQR3WpIgNIKwBMVLkpMUeR3e1/E1y42bqDQZfql+kDeXd8COYfM8PQA4X6y7a8u9Ua9FHmsrrmirW2vHs45hWg=="],
"pluralize": ["pluralize@8.0.0", "", {}, "sha512-Nc3IT5yHzflTfbjgqWcCPpo7DaKy4FnpB0l/zCAW0Tc7jxAiuqSxHasntB3D7887LSrA93kDJ9IXovxJYxyLCA=="],
"proxy-addr": ["proxy-addr@2.0.7", "", { "dependencies": { "forwarded": "0.2.0", "ipaddr.js": "1.9.1" } }, "sha512-llQsMLSUDUPT44jdrU/O37qlnifitDP+ZwrmmZcoSKyLKvtZxpyV0n2/bD/N4tBAAZ/gJEdZU7KMraoK1+XYAg=="],
"pstree.remy": ["pstree.remy@1.1.8", "", {}, "sha512-77DZwxQmxKnu3aR542U+X8FypNzbfJ+C5XQDk3uWjWxn6151aIMGthWYRXTqT1E5oJvg+ljaa2OJi+VfvCOQ8w=="],
"qs": ["qs@6.14.2", "", { "dependencies": { "side-channel": "^1.1.0" } }, "sha512-V/yCWTTF7VJ9hIh18Ugr2zhJMP01MY7c5kh4J870L7imm6/DIzBsNLTXzMwUA3yZ5b/KBqLx8Kp3uRvd7xSe3Q=="],
"range-parser": ["range-parser@1.2.1", "", {}, "sha512-Hrgsx+orqoygnmhFbKaHE6c296J+HTAQXoxEF6gNupROmmGJRoyzfG3ccAveqCBrwr/2yxQ5BVd/GTl5agOwSg=="],
"raw-body": ["raw-body@2.5.3", "", { "dependencies": { "bytes": "~3.1.2", "http-errors": "~2.0.1", "iconv-lite": "~0.4.24", "unpipe": "~1.0.0" } }, "sha512-s4VSOf6yN0rvbRZGxs8Om5CWj6seneMwK3oDb4lWDH0UPhWcxwOWw5+qk24bxq87szX1ydrwylIOp2uG1ojUpA=="],
"readdirp": ["readdirp@3.6.0", "", { "dependencies": { "picomatch": "^2.2.1" } }, "sha512-hOS089on8RduqdbhvQ5Z37A0ESjsqz6qnRcffsMU3495FuTdqSm+7bhJ29JvIOsBDEEnan5DPu9t3To9VRlMzA=="],
"require-directory": ["require-directory@2.1.1", "", {}, "sha512-fGxEI7+wsG9xrvdjsrlmL22OMTTiHRwAMroiEeMgq8gzoLC/PQr7RsRDSTLUg/bZAZtF+TVIkHc6/4RIKrui+Q=="],
"safe-buffer": ["safe-buffer@5.2.1", "", {}, "sha512-rp3So07KcdmmKbGvgaNxQSJr7bGVSVk5S9Eq1F+ppbRo70+YeaDxkw5Dd8NPN+GD6bjnYm2VuPuCXmpuYvmCXQ=="],
"safer-buffer": ["safer-buffer@2.1.2", "", {}, "sha512-YZo3K82SD7Riyi0E1EQPojLz7kpepnSQI9IyPbHHg1XXXevb5dJI7tpyN2ADxGcQbHG7vcyRHk0cbwqcQriUtg=="],
"semver": ["semver@7.7.4", "", { "bin": { "semver": "bin/semver.js" } }, "sha512-vFKC2IEtQnVhpT78h1Yp8wzwrf8CM+MzKMHGJZfBtzhZNycRFnXsHk6E5TxIkkMsgNS7mdX3AGB7x2QM2di4lA=="],
"semver-compare": ["semver-compare@1.0.0", "", {}, "sha512-YM3/ITh2MJ5MtzaM429anh+x2jiLVjqILF4m4oyQB18W7Ggea7BfqdH/wGMK7dDiMghv/6WG7znWMwUDzJiXow=="],
"send": ["send@0.19.2", "", { "dependencies": { "debug": "2.6.9", "depd": "2.0.0", "destroy": "1.2.0", "encodeurl": "~2.0.0", "escape-html": "~1.0.3", "etag": "~1.8.1", "fresh": "~0.5.2", "http-errors": "~2.0.1", "mime": "1.6.0", "ms": "2.1.3", "on-finished": "~2.4.1", "range-parser": "~1.2.1", "statuses": "~2.0.2" } }, "sha512-VMbMxbDeehAxpOtWJXlcUS5E8iXh6QmN+BkRX1GARS3wRaXEEgzCcB10gTQazO42tpNIya8xIyNx8fll1OFPrg=="],
"serve-static": ["serve-static@1.16.3", "", { "dependencies": { "encodeurl": "~2.0.0", "escape-html": "~1.0.3", "parseurl": "~1.3.3", "send": "~0.19.1" } }, "sha512-x0RTqQel6g5SY7Lg6ZreMmsOzncHFU7nhnRWkKgWuMTu5NN0DR5oruckMqRvacAN9d5w6ARnRBXl9xhDCgfMeA=="],
"server-destroy": ["server-destroy@1.0.1", "", {}, "sha512-rb+9B5YBIEzYcD6x2VKidaa+cqYBJQKnU4oe4E3ANwRRN56yk/ua1YCJT1n21NTS8w6CcOclAKNP3PhdCXKYtQ=="],
"setprototypeof": ["setprototypeof@1.2.0", "", {}, "sha512-E5LDX7Wrp85Kil5bhZv46j8jOeboKq5JMmYM3gVGdGH8xFpPWXUMsNrlODCrkoxMEeNi/XZIwuRvY4XNwYMJpw=="],
"side-channel": ["side-channel@1.1.0", "", { "dependencies": { "es-errors": "^1.3.0", "object-inspect": "^1.13.3", "side-channel-list": "^1.0.0", "side-channel-map": "^1.0.1", "side-channel-weakmap": "^1.0.2" } }, "sha512-ZX99e6tRweoUXqR+VBrslhda51Nh5MTQwou5tnUDgbtyM0dBgmhEDtWGP/xbKn6hqfPRHujUNwz5fy/wbbhnpw=="],
"side-channel-list": ["side-channel-list@1.0.0", "", { "dependencies": { "es-errors": "^1.3.0", "object-inspect": "^1.13.3" } }, "sha512-FCLHtRD/gnpCiCHEiJLOwdmFP+wzCmDEkc9y7NsYxeF4u7Btsn1ZuwgwJGxImImHicJArLP4R0yX4c2KCrMrTA=="],
"side-channel-map": ["side-channel-map@1.0.1", "", { "dependencies": { "call-bound": "^1.0.2", "es-errors": "^1.3.0", "get-intrinsic": "^1.2.5", "object-inspect": "^1.13.3" } }, "sha512-VCjCNfgMsby3tTdo02nbjtM/ewra6jPHmpThenkTYh8pG9ucZ/1P8So4u4FGBek/BjpOVsDCMoLA/iuBKIFXRA=="],
"side-channel-weakmap": ["side-channel-weakmap@1.0.2", "", { "dependencies": { "call-bound": "^1.0.2", "es-errors": "^1.3.0", "get-intrinsic": "^1.2.5", "object-inspect": "^1.13.3", "side-channel-map": "^1.0.1" } }, "sha512-WPS/HvHQTYnHisLo9McqBHOJk2FkHO/tlpvldyrnem4aeQp4hai3gythswg6p01oSoTl58rcpiFAjF2br2Ak2A=="],
"simple-update-notifier": ["simple-update-notifier@2.0.0", "", { "dependencies": { "semver": "^7.5.3" } }, "sha512-a2B9Y0KlNXl9u/vsW6sTIu9vGEpfKu2wRV6l1H3XEas/0gUIzGzBoP/IouTcUQbm9JWZLH3COxyn03TYlFax6w=="],
"statuses": ["statuses@2.0.2", "", {}, "sha512-DvEy55V3DB7uknRo+4iOGT5fP1slR8wQohVdknigZPMpMstaKJQWhwiYBACJE3Ul2pTnATihhBYnRhZQHGBiRw=="],
"steno": ["steno@0.4.4", "", { "dependencies": { "graceful-fs": "^4.1.3" } }, "sha512-EEHMVYHNXFHfGtgjNITnka0aHhiAlo93F7z2/Pwd+g0teG9CnM3JIINM7hVVB5/rhw9voufD7Wukwgtw2uqh6w=="],
"string-width": ["string-width@4.2.3", "", { "dependencies": { "emoji-regex": "^8.0.0", "is-fullwidth-code-point": "^3.0.0", "strip-ansi": "^6.0.1" } }, "sha512-wKyQRQpjJ0sIp62ErSZdGsjMJWsap5oRNihHhu6G7JVO/9jIB6UyevL+tXuOqrng8j/cxKTWyWUwvSTriiZz/g=="],
"strip-ansi": ["strip-ansi@6.0.1", "", { "dependencies": { "ansi-regex": "^5.0.1" } }, "sha512-Y38VPSHcqkFrCpFnQ9vuSXmquuv5oXOKpGeT6aGrr3o3Gc9AlVa6JBfUSOCnbxGGZF+/0ooI7KrPuUSztUdU5A=="],
"supports-color": ["supports-color@5.5.0", "", { "dependencies": { "has-flag": "^3.0.0" } }, "sha512-QjVjwdXIt408MIiAqCX4oUKsgU2EqAGzs2Ppkm4aQYbjm+ZEWEcW4SfFNTr4uMNZma0ey4f5lgLrkB0aX0QMow=="],
"to-regex-range": ["to-regex-range@5.0.1", "", { "dependencies": { "is-number": "^7.0.0" } }, "sha512-65P7iz6X5yEr1cwcgvQxbbIw7Uk3gOy5dIdtZ4rDveLqhrdJP+Li/Hx6tyK0NEb+2GCyneCMJiGqrADCSNk8sQ=="],
"toidentifier": ["toidentifier@1.0.1", "", {}, "sha512-o5sSPKEkg/DIQNmH43V0/uerLrpzVedkUh8tGNvaeXpfpuwjKenlSox/2O/BTlZUtEe+JG7s5YhEz608PlAHRA=="],
"touch": ["touch@3.1.1", "", { "bin": { "nodetouch": "bin/nodetouch.js" } }, "sha512-r0eojU4bI8MnHr8c5bNo7lJDdI2qXlWWJk6a9EAFG7vbhTjElYhBVS3/miuE0uOuoLdb8Mc/rVfsmm6eo5o9GA=="],
"type-is": ["type-is@1.6.18", "", { "dependencies": { "media-typer": "0.3.0", "mime-types": "~2.1.24" } }, "sha512-TkRKr9sUTxEH8MdfuCSP7VizJyzRNMjj2J2do2Jr3Kym598JVdEksuzPQCnlFPW4ky9Q+iA+ma9BGm06XQBy8g=="],
"undefsafe": ["undefsafe@2.0.5", "", {}, "sha512-WxONCrssBM8TSPRqN5EmsjVrsv4A8X12J4ArBiiayv3DyyG3ZlIg6yysuuSYdZsVz3TKcTg2fd//Ujd4CHV1iA=="],
"unpipe": ["unpipe@1.0.0", "", {}, "sha512-pjy2bYhSsufwWlKwPc+l3cN7+wuJlK6uz0YdJEOlQDbl6jo/YlPi4mb8agUkVC8BF7V8NuzeyPNqRksA3hztKQ=="],
"utils-merge": ["utils-merge@1.0.1", "", {}, "sha512-pMZTvIkT1d+TFGvDOqodOclx0QWkkgi6Tdoa8gC8ffGAAqz9pzPTZWAybbsHHoED/ztMtkv/VoYTYyShUn81hA=="],
"vary": ["vary@1.1.2", "", {}, "sha512-BNGbWLfd0eUPabhkXUVm0j8uuvREyTh5ovRa/dyow/BqAbZJyC+5fU+IzQOzmAKzYqYRAISoRhdQr3eIZ/PXqg=="],
"wrap-ansi": ["wrap-ansi@7.0.0", "", { "dependencies": { "ansi-styles": "^4.0.0", "string-width": "^4.1.0", "strip-ansi": "^6.0.0" } }, "sha512-YVGIj2kamLSTxw6NsZjoBxfSwsn0ycdesmc4p+Q21c5zPuZ1pl+NfxVdxPtdHvmNVOQ6XSYG4AUtyt/Fi7D16Q=="],
"y18n": ["y18n@5.0.8", "", {}, "sha512-0pfFzegeDWJHJIAmTLRP2DwHjdF5s7jo9tuztdQxAhINCdvS+3nGINqPd00AphqJR/0LhANUS6/+7SCb98YOfA=="],
"yargs": ["yargs@17.7.2", "", { "dependencies": { "cliui": "^8.0.1", "escalade": "^3.1.1", "get-caller-file": "^2.0.5", "require-directory": "^2.1.1", "string-width": "^4.2.3", "y18n": "^5.0.5", "yargs-parser": "^21.1.1" } }, "sha512-7dSzzRQ++CKnNI/krKnYRV7JKKPUXMEh61soaHKg9mrWEhzFWhFnxPxGl+69cD1Ou63C13NUPCnmIcrvqCuM6w=="],
"yargs-parser": ["yargs-parser@21.1.1", "", {}, "sha512-tVpsJW7DdjecAiFpbIB1e3qxIQsE6NoPc5/eTdrbbIC4h0LVsWhnoa3g+m2HclBIujHzsxZ4VJVA+GUuc2/LBw=="],
"accepts/negotiator": ["negotiator@0.6.3", "", {}, "sha512-+EUsqGPLsM+j/zdChZjsnX51g4XrHFOIXwfnCVPGlQk/k5giakcKsuxCObBRu6DSm9opw/O6slWbJdghQM4bBg=="],
"basic-auth/safe-buffer": ["safe-buffer@5.1.2", "", {}, "sha512-Gd2UZBJDkXlY7GbJxfsE8/nvKkUEU1G38c1siN6QP6a9PT9MmHB8GnpscSmMJSoF8LOIrt8ud/wPtojys4G6+g=="],
"body-parser/debug": ["debug@2.6.9", "", { "dependencies": { "ms": "2.0.0" } }, "sha512-bC7ElrdJaJnPbAP+1EotYvqZsb3ecl5wi6Bfi6BJTUcNowp6cvspg0jXznRTKDjm/E7AdgFBVeAPVMNcKGsHMA=="],
"chalk/supports-color": ["supports-color@7.2.0", "", { "dependencies": { "has-flag": "^4.0.0" } }, "sha512-qpCAvRl9stuOHveKsn7HncJRvv501qIacKzQlO/+Lwxc9+0q2wLyv4Dfvt80/DPn2pqOBsJdDiogXGR9+OvwRw=="],
"compression/debug": ["debug@2.6.9", "", { "dependencies": { "ms": "2.0.0" } }, "sha512-bC7ElrdJaJnPbAP+1EotYvqZsb3ecl5wi6Bfi6BJTUcNowp6cvspg0jXznRTKDjm/E7AdgFBVeAPVMNcKGsHMA=="],
"express/debug": ["debug@2.6.9", "", { "dependencies": { "ms": "2.0.0" } }, "sha512-bC7ElrdJaJnPbAP+1EotYvqZsb3ecl5wi6Bfi6BJTUcNowp6cvspg0jXznRTKDjm/E7AdgFBVeAPVMNcKGsHMA=="],
"express-urlrewrite/path-to-regexp": ["path-to-regexp@1.9.0", "", { "dependencies": { "isarray": "0.0.1" } }, "sha512-xIp7/apCFJuUHdDLWe8O1HIkb0kQrOMb/0u6FXQjemHn/ii5LrIzU6bdECnsiTF/GjZkMEKg1xdiZwNqDYlZ6g=="],
"finalhandler/debug": ["debug@2.6.9", "", { "dependencies": { "ms": "2.0.0" } }, "sha512-bC7ElrdJaJnPbAP+1EotYvqZsb3ecl5wi6Bfi6BJTUcNowp6cvspg0jXznRTKDjm/E7AdgFBVeAPVMNcKGsHMA=="],
"method-override/debug": ["debug@3.1.0", "", { "dependencies": { "ms": "2.0.0" } }, "sha512-OX8XqP7/1a9cqkxYw2yXss15f26NKWBpDXQd0/uK/KPqdQhxbPa994hnzjcE2VqQpDslf55723cKPUOGSmMY3g=="],
"morgan/debug": ["debug@2.6.9", "", { "dependencies": { "ms": "2.0.0" } }, "sha512-bC7ElrdJaJnPbAP+1EotYvqZsb3ecl5wi6Bfi6BJTUcNowp6cvspg0jXznRTKDjm/E7AdgFBVeAPVMNcKGsHMA=="],
"morgan/on-finished": ["on-finished@2.3.0", "", { "dependencies": { "ee-first": "1.1.1" } }, "sha512-ikqdkGAAyf/X/gPhXGvfgAytDZtDbr+bkNUJ0N9h5MI/dmdgCs3l6hoHrcUv41sRKew3jIwrp4qQDXiK99Utww=="],
"send/debug": ["debug@2.6.9", "", { "dependencies": { "ms": "2.0.0" } }, "sha512-bC7ElrdJaJnPbAP+1EotYvqZsb3ecl5wi6Bfi6BJTUcNowp6cvspg0jXznRTKDjm/E7AdgFBVeAPVMNcKGsHMA=="],
"body-parser/debug/ms": ["ms@2.0.0", "", {}, "sha512-Tpp60P6IUJDTuOq/5Z8cdskzJujfwqfOTkrwIwj7IRISpnkJnT6SyJ4PCPnGMoFjC9ddhal5KVIYtAt97ix05A=="],
"chalk/supports-color/has-flag": ["has-flag@4.0.0", "", {}, "sha512-EykJT/Q1KjTWctppgIAgfSO0tKVuZUjhgMr17kqTumMl6Afv3EISleU7qZUzoXDFTAHTDC4NOoG/ZxU3EvlMPQ=="],
"compression/debug/ms": ["ms@2.0.0", "", {}, "sha512-Tpp60P6IUJDTuOq/5Z8cdskzJujfwqfOTkrwIwj7IRISpnkJnT6SyJ4PCPnGMoFjC9ddhal5KVIYtAt97ix05A=="],
"express/debug/ms": ["ms@2.0.0", "", {}, "sha512-Tpp60P6IUJDTuOq/5Z8cdskzJujfwqfOTkrwIwj7IRISpnkJnT6SyJ4PCPnGMoFjC9ddhal5KVIYtAt97ix05A=="],
"finalhandler/debug/ms": ["ms@2.0.0", "", {}, "sha512-Tpp60P6IUJDTuOq/5Z8cdskzJujfwqfOTkrwIwj7IRISpnkJnT6SyJ4PCPnGMoFjC9ddhal5KVIYtAt97ix05A=="],
"method-override/debug/ms": ["ms@2.0.0", "", {}, "sha512-Tpp60P6IUJDTuOq/5Z8cdskzJujfwqfOTkrwIwj7IRISpnkJnT6SyJ4PCPnGMoFjC9ddhal5KVIYtAt97ix05A=="],
"morgan/debug/ms": ["ms@2.0.0", "", {}, "sha512-Tpp60P6IUJDTuOq/5Z8cdskzJujfwqfOTkrwIwj7IRISpnkJnT6SyJ4PCPnGMoFjC9ddhal5KVIYtAt97ix05A=="],
"send/debug/ms": ["ms@2.0.0", "", {}, "sha512-Tpp60P6IUJDTuOq/5Z8cdskzJujfwqfOTkrwIwj7IRISpnkJnT6SyJ4PCPnGMoFjC9ddhal5KVIYtAt97ix05A=="],
}
}

246
db.json Normal file
View File

@@ -0,0 +1,246 @@
{
"homeData": {
"result": {
"ok": true,
"responseCode": "00",
"message": "Thành công"
},
"accounts": [
{
"accountNumber": "012345678",
"accountName": "NGUYEN VAN A",
"type": "DEFAULT",
"balance": "151,000,000",
"currency": "VND",
"isDefault": true
},
{
"accountNumber": "012345679",
"accountName": "NGUYEN VAN A",
"type": "APPLE_PAY",
"balance": "25,500,000",
"currency": "VND",
"isDefault": false
},
{
"accountNumber": "012345680",
"accountName": "NGUYEN VAN A",
"type": "OVERDRAFT",
"balance": "50,000,000",
"currency": "VND",
"isDefault": false
},
{
"accountNumber": "012345681",
"accountName": "NGUYEN VAN A",
"type": "SALARY",
"balance": "88,200,000",
"currency": "VND",
"isDefault": false
},
{
"accountNumber": "012345682",
"accountName": "NGUYEN VAN A",
"type": "DEFAULT",
"balance": "5,000",
"currency": "USD",
"isDefault": false
},
{
"accountNumber": "012345683",
"accountName": "NGUYEN VAN A",
"type": "DEFAULT",
"balance": "2,800",
"currency": "EUR",
"isDefault": false
}
],
"businessAccounts": [
{
"accountNumber": "198765432",
"accountName": "NGUYEN VAN A",
"businessName": "Cửa hàng Tạp hóa Minh Anh",
"balance": "320,500,000",
"currency": "VND"
},
{
"accountNumber": "198765433",
"accountName": "NGUYEN VAN A",
"businessName": "Quán Cafe Sunrise",
"balance": "85,200,000",
"currency": "VND"
},
{
"accountNumber": "198765434",
"accountName": "NGUYEN VAN A",
"businessName": "Shop Online ABC",
"balance": "12,500",
"currency": "USD"
}
],
"loyaltyPoints": "2,450",
"cashbackAmount": "888,000",
"memberLevel": "GOLD",
"recentTransactions": [
{
"transactionId": "TXN20260325001234",
"amount": "1,000,000",
"status": "SUCCESS",
"executedAt": "2026-03-25T10:30:00Z",
"description": "Nạp tiền tài khoản",
"accountNumber": "012345678"
},
{
"transactionId": "TXN20260325001235",
"amount": "500,000",
"status": "SUCCESS",
"executedAt": "2026-03-24T08:20:00Z",
"description": "Nạp tiền tài khoản",
"accountNumber": "012345678"
},
{
"transactionId": "TXN20260325001236",
"amount": "2,200,000",
"status": "PENDING",
"executedAt": "2026-03-22T12:00:00Z",
"description": "Nạp tiền tài khoản",
"accountNumber": "012345678"
}
]
},
"searchSeed": {
"recentSearches": [
"Chuyển tiền nhanh",
"Nạp điện thoại",
"Thanh toán hóa đơn"
],
"recommendedFeatures": [
{
"code": "MOVE_MONEY",
"name": "Chuyển tiền",
"iconCode": "moveMoney"
},
{
"code": "TOPUP",
"name": "Nạp tiền ĐT",
"iconCode": "topup"
},
{
"code": "DEPOSIT",
"name": "Tiết kiệm",
"iconCode": "deposit"
},
{
"code": "LOAN",
"name": "Vay vốn",
"iconCode": "loan"
}
],
"recommendedChips": [
{
"id": "C1",
"label": "Dán chuyển tiền",
"isNew": true,
"actionCode": "TRANSFER_LABEL"
},
{
"id": "C2",
"label": "Vay tiêu dùng",
"isNew": false,
"actionCode": "CONSUMER_LOAN"
},
{
"id": "C3",
"label": "Nạp điện thoại tự động",
"isNew": true,
"actionCode": "AUTO_TOPUP"
}
],
"suggestions": [
{
"id": "SUG001",
"title": "Chuyển tiền nhanh 24/7",
"description": "Chuyển tiền tức thì đến mọi ngân hàng, không mất phí",
"type": "FEATURE",
"actionCode": "TRANSFER"
},
{
"id": "SUG002",
"title": "Ưu đãi hoàn tiền 10% khi nạp điện thoại",
"description": "Áp dụng từ 01/03 - 31/03/2026 cho thẻ MB",
"type": "PROMOTION",
"actionCode": "TOPUP_PHONE"
},
{
"id": "SUG003",
"title": "Mẹo quản lý chi tiêu thông minh",
"description": "Thiết lập ngân sách theo tuần để tối ưu tiền nhàn rỗi",
"type": "TIP",
"actionCode": "SPENDING_TIP"
},
{
"id": "SUG004",
"title": "Mở sổ tiết kiệm online",
"description": "Lãi suất cao hơn quầy, gửi tiền chỉ 30 giây",
"type": "FEATURE",
"actionCode": "OPEN_DEPOSIT"
}
]
},
"rewardSeed": {
"banners": [
{
"id": "BN001",
"imageUrl": "/images/banners/banner_01.png",
"redirectUrl": "https://www.mbbank.com.vn/promotion/summer-sale"
},
{
"id": "BN002",
"imageUrl": "/images/banners/banner_01.png",
"redirectUrl": "https://www.mbbank.com.vn/promotion/cashback-50"
},
{
"id": "BN003",
"imageUrl": "/images/banners/banner_01.png",
"redirectUrl": "https://www.mbbank.com.vn/promotion/new-year-2026"
}
],
"vouchers": [
{
"id": "V001",
"imageUrl": "/images/vouchers/starbucks.png",
"content": "Giảm 40% tại Starbucks cho đơn từ 100K",
"expiry": "Hạn: 25/06/2026",
"requireCardSpending": false
},
{
"id": "V002",
"imageUrl": "/images/vouchers/kfc.png",
"content": "Giảm 68K cho combo gà rán KFC",
"expiry": "",
"requireCardSpending": true
},
{
"id": "V003",
"imageUrl": "/images/vouchers/bamboo.png",
"content": "Đồng giá 39K",
"expiry": "Hạn: 15/05/2026",
"requireCardSpending": false
},
{
"id": "V004",
"imageUrl": "/images/vouchers/kfc_2.png",
"content": "Giảm 68K",
"expiry": "Hạn: 30/04/2026",
"requireCardSpending": true
},
{
"id": "V005",
"imageUrl": "/images/vouchers/thai_air.png",
"content": "Vé giảm 50%",
"expiry": "Hạn: 30/09/2026",
"requireCardSpending": false
}
]
}
}

21
ecosystem.config.js Normal file
View File

@@ -0,0 +1,21 @@
module.exports = {
apps: [
{
name: 'charging-station-mock',
script: 'server.js',
interpreter: 'bun',
instances: 1,
autorestart: true,
watch: false,
max_memory_restart: '500M',
env: {
NODE_ENV: 'development',
PORT: 3100,
},
env_production: {
NODE_ENV: 'production',
PORT: 3100,
},
},
],
};

18
package.json Normal file
View File

@@ -0,0 +1,18 @@
{
"name": "charging-station-json-server",
"version": "1.0.0",
"private": true,
"type": "module",
"description": "Mock API server for Charging Station feature",
"main": "server.js",
"scripts": {
"start": "bun server.js",
"start:watch": "nodemon --watch db.json --watch server.js --exec \"bun server.js\""
},
"dependencies": {
"json-server": "^0.17.4"
},
"devDependencies": {
"nodemon": "^3.1.10"
}
}

View File

Binary file not shown.

After

Width:  |  Height:  |  Size: 324 KiB

View File

Binary file not shown.

After

Width:  |  Height:  |  Size: 13 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 33 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 170 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 43 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 26 KiB

237
server.js Normal file
View 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}`);
});