Commit 41ebef95 by tdgiang

Add design spec for admin transaction management page

New self-contained module for staff to create Epay payment links for
customers and track their status, kept separate from the just-stabilized
/epay/* customer-facing flow to avoid regression risk.
parent eaaadb46
# Thiết kế: Trang Quản lý giao dịch (Admin Transaction Management)
**Ngày:** 2026-08-28
**Trạng thái:** Chờ review
## 1. Mục tiêu
Nhân viên cần 1 trang nội bộ để:
- **Thêm mới giao dịch**: nhập thông tin khách hàng (tên, SĐT, địa chỉ, số tiền) → hệ thống tạo link thanh toán Epay/MegaPay thật → nhân viên gửi link cho khách (Zalo/SMS/email) để khách tự thanh toán.
- **Lịch sử giao dịch**: xem danh sách các giao dịch đã tạo + trạng thái (chờ thanh toán / thành công / thất bại).
## 2. Ngoài phạm vi (không làm trong lần này)
- Không sửa/đụng vào flow `/epay/*` hiện tại (vừa ổn định, vừa test tiền thật thành công) — module mới hoàn toàn tách biệt.
- Không có hệ thống tài khoản nhiều người dùng/phân quyền — chỉ 1 mật khẩu chung (Basic Auth).
- Không có chức năng sửa/xoá/hoàn tiền giao dịch từ trang này.
- Không hỗ trợ các cổng thanh toán khác ngoài Epay/MegaPay.
- Không filter/tìm kiếm nâng cao ở trang lịch sử ban đầu — chỉ danh sách theo thời gian tạo mới nhất trước, có phân trang đơn giản.
## 3. Kiến trúc
### 3.1 Hạ tầng — MongoDB
Repo hiện chưa có `mongoose.connect()` nào (model `Order` tồn tại nhưng chết, không dùng). Thêm:
- Service `mongo` mới vào `docker-compose.yml` (image `mongo:7`, volume riêng để không mất dữ liệu khi container restart), chỉ expose nội bộ trong docker network (không public port ra ngoài).
- `config/express.js` (hoặc `server.js`) thêm `mongoose.connect(process.env.MONGO_URI)`.
- `.env` thêm `MONGO_URI` (mặc định trỏ vào service `mongo` trong compose, ví dụ `mongodb://mongo:27017/haiyen_admin`).
### 3.2 Bảo vệ truy cập
- Middleware Basic Auth áp dụng cho `/admin/transactions*` (danh sách + form thêm mới + API tạo giao dịch).
- Username/password lấy từ `.env` (`ADMIN_USER`, `ADMIN_PASSWORD`).
- 3 route công khai (khách hàng / MegaPay gọi vào, không thể yêu cầu họ đăng nhập): trang thanh toán, return, ipn.
### 3.3 Route
| Method | Path | Auth | Mô tả |
|---|---|---|---|
| GET | `/admin/transactions` | Basic Auth | Lịch sử giao dịch (bảng, phân trang) |
| GET | `/admin/transactions/new` | Basic Auth | Form thêm mới giao dịch |
| POST | `/admin/transactions` | Basic Auth | Tạo giao dịch, trả về link thanh toán |
| GET | `/admin/pay/:merTrxId` | Public | Trang khách bấm "Thanh toán" |
| GET | `/admin/epay/return` | Public | MegaPay redirect trình duyệt khách về |
| POST | `/admin/epay/ipn` | Public | MegaPay gọi server-to-server |
### 3.4 Model `AdminTransaction`
```js
{
merTrxId: String, // unique, index — dạng "HY_" + timeStamp + "_" + uuid8
transCode: String, // invoiceNo gửi MegaPay
customerName: String,
customerPhone: String,
customerAddress: String,
amount: Number,
payType: { type: String, default: "DC" },
status: { type: String, enum: ["pending", "success", "failed"], default: "pending" },
merchantToken: String, // tính 1 lần lúc tạo, dùng lại nguyên vẹn khi khách bấm link
timeStamp: String, // timeStamp gốc dùng để ký — phải khớp lúc verify callback
resultMsg: String, // lý do thất bại (nếu có), lấy từ resultMsg MegaPay trả về
paidAt: Date,
createdAt: Date, // timestamps: true
updatedAt: Date,
}
```
### 3.5 Luồng dữ liệu
1. NV vào `/admin/transactions/new`, điền tên/SĐT/địa chỉ/số tiền, submit.
2. `POST /admin/transactions`: sinh `merTrxId` (chống trùng bằng uuid, theo đúng cách đã sửa cho `/epay/create-payment-url`), tính `merchantToken` **một lần duy nhất** bằng hàm ký dùng chung với `epayCreatePaymentUrl` (tách hàm `signEpayRequest(timeStamp, merTrxId, amount, payToken)` ra dùng chung, không copy-paste công thức). Lưu record `status: "pending"`. Trả về `paymentUrl = https://<domain>/admin/pay/<merTrxId>`.
3. NV copy `paymentUrl`, gửi khách qua kênh khác (Zalo/SMS/email — ngoài phạm vi hệ thống này).
4. Khách bấm link bất kỳ lúc nào → `GET /admin/pay/:merTrxId` → tra DB theo `merTrxId`, dựng lại form MegaPay từ dữ liệu **đã lưu sẵn** (không tính lại `merchantToken`/`timeStamp`) → khách bấm "Thanh toán" → `openPayment()` sang MegaPay.
5. Khách hoàn tất → MegaPay redirect `GET /admin/epay/return` (hiện kết quả cho khách) + gọi `POST /admin/epay/ipn` (server-to-server, nguồn cập nhật trạng thái chính) → verify `merchantToken` bằng đúng công thức tài liệu MegaPay → tra `AdminTransaction` theo `merTrxId` → cập nhật `status`/`resultMsg`/`paidAt`.
6. NV vào `/admin/transactions` xem danh sách + trạng thái cập nhật. Mỗi dòng hiện lại `paymentUrl` (dựng từ `merTrxId` lưu sẵn) để NV copy gửi lại nếu khách làm mất link — không cần tạo giao dịch mới.
### 3.6 Xử lý lỗi
- `merTrxId` không tồn tại trong DB (link sai/đã bị xoá) → trang báo lỗi rõ ràng cho khách, không crash.
- `result`/dữ liệu callback thiếu trường → không được giả định luôn tồn tại (bài học từ bug crash `Cannot read properties of undefined` vừa gặp hôm nay ở `/epay/return`) — luôn kiểm tra tồn tại trước khi đọc `.data`/`.amount`.
- IPN gọi lặp lại (MegaPay retry khi timeout) → cập nhật idempotent theo `merTrxId` (ghi đè, không tạo bản ghi mới, không lỗi nếu gọi 2 lần).
- Chữ ký (`merchantToken`) không khớp → không cập nhật status thành success, ghi log để điều tra, trả lỗi rõ ràng cho MegaPay (không phải lỗi 200 giả như gap đã biết ở `/epay/ipn` cũ).
- MongoDB mất kết nối → route trả lỗi 503 rõ ràng, không để Node crash toàn bộ process.
### 3.7 Testing
Trước khi test tiền thật, lặp lại kiểu bộ test đã áp dụng cho `/epay/*`:
- Tạo giao dịch → verify `merchantToken` đúng công thức tài liệu.
- Giả lập callback chữ ký đúng → status chuyển `success` đúng.
- Giả lập chữ ký giả mạo → bị từ chối, status không đổi.
- Giả lập `merTrxId` không tồn tại → không crash.
- Giả lập IPN gọi 2 lần liên tiếp → không lỗi, không tạo trùng bản ghi.
- Test `GET /admin/transactions`/`POST /admin/transactions` không có Basic Auth → phải bị chặn (401).
- Test `GET /admin/pay/:id`, `return`, `ipn` không cần Basic Auth vẫn truy cập được (khách/MegaPay không có mật khẩu).
## 4. Việc cần làm khi triển khai (tóm tắt cho bước lập kế hoạch)
- `docker-compose.yml`: thêm service `mongo`.
- `.env` / `.env.example`: thêm `MONGO_URI`, `ADMIN_USER`, `ADMIN_PASSWORD`.
- Model mới `app/models/AdminTransaction.js`.
- Kết nối Mongoose thật trong `config/express.js`.
- Middleware Basic Auth cho nhóm route `/admin/transactions*`.
- Controller mới (tách file riêng, không phình thêm `core.server.controller.js` vốn đã 2300+ dòng) cho: tạo giao dịch, danh sách, trang thanh toán, return, ipn.
- 2 view mới (Swig): form thêm mới, bảng lịch sử.
- Tái sử dụng logic ký từ `epayCreatePaymentUrl` qua hàm dùng chung, không copy-paste.
- Bộ test cho toàn bộ luồng trước khi test tiền thật.
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