Phân tích & thiết kế hệ thống · Xe khách Hà Giang

Cổng gửi vé VeXeRe cho nhà xe

Phân tích & thiết kế hệ thống

Nhiều đại lý gửi vé về một cổng của nhà xe; nhà xe xếp chỗ qua API VeXeRe hoặc thủ công; xuất vé từng khách và gửi qua SMS SIM thường, email, Zalo OA, Zalo cá nhân; thống kê và đối soát riêng từng đại lý. Xây mới hoàn toàn trên VPS Ubuntu + aaPanel theo kiến trúc lõi quản lý vé + mô-đun cắm/rút.

v0.2DỰ THẢO · 02.09.2026
Phiên bản
0.2 — dự thảo để chốt yêu cầu (0.2: xây mới hoàn toàn, bộ công nghệ, lõi + mô-đun cắm/rút, tài liệu đồng bộ)
Ngày
02/9/2026
Tài liệu đã phân tích
docs/tham-khao/HDSD AMS update 7.2022.pdf (28 trang, nền tảng đại lý AMS của VeXeRe); tài liệu API đối tác https://vdoc.vexere.net/#API (bản trích lưu tại docs/tham-khao/); phân tích công nghệ/nền tảng ở 01-phan-tich-cong-nghe-nen-tang.md
Tham khảo (chỉ tài liệu, mã mẫu)
Vé247 (D:\CODE\30_SamsungS22u), OSZalo-247 (d:\CODE\07_OSZalo_247), web đặt vé xekhachhagiang.com (d:\CODE\32_webVexe247\site-moi). Hệ thống mới xây hoàn toàn mới trên VPS Linux riêng, không dùng chung hạ tầng.
Trạng thái
Chưa có mã nguồn. Mục 11 liệt kê các quyết định cần người dùng chốt trước khi lập trình.

0. Tóm tắt điều hành

Hệ thống cần xây là một cổng tập trung của nhà xe để nhiều đại lý bán vé gửi vé về, nhà xe xếp chỗ (tự động qua API VeXeRe hoặc thủ công), rồi xuất vé cho từng khách và gửi qua SMS (SIM thường), email, Zalo OA, Zalo cá nhân, kèm thống kê và đối soát riêng từng đại lý. Đại lý được đẩy lên, thêm, sửa, xoá vé của mình trong giới hạn quyền theo trạng thái vé.

Năm kết luận chính sau khi đọc tài liệu:

  1. Tài liệu VeXeRe cung cấp là góc nhìn ĐẠI LÝ (AMS + API đối tác), không phải góc nhìn nhà xe (BMS). Gửi vé "lên VeXeRe" bằng API này nghĩa là đặt chỗ như một đại lý: có hạn mức, có chiết khấu, có chính sách huỷ của VeXeRe. Cần xác nhận với VeXeRe tài khoản API cấp cho nhà xe (mục 11, câu 1).
  2. API đủ để tự động hoá vòng đời vé: tìm chuyến → sơ đồ ghế → giữ chỗ → thanh toán → tra cứu → huỷ → sửa điểm đón/trả, và webhook báo ngược khi vé bị đổi giờ/huỷ/cập nhật giá cọc. Thiếu hai thứ: API đổi ngày/giờ (chỉ làm tay trên AMS) và API tra hạn mức.
  3. Giữ chỗ chưa thanh toán sẽ tự huỷ đến expired_time; chuyến bị tắt bán trực tuyến trước giờ khởi hành N giờ. Hệ thống phải có đồng hồ theo dõi hai mốc này, không được phó mặc cho người trực.
  4. Xây mới hoàn toàn trên VPS Linux riêng: mã, CSDL, tiến trình mới; Vé247/OSZalo/web chỉ là tài liệu nghiệp vụ và mã mẫu. Bộ công nghệ đề xuất (mục 4.7, phân tích sâu ở tài liệu 01): Node.js + TypeScript (NestJS), PostgreSQL, Redis + BullMQ, React + Vite (PWA); chạy trên aaPanel như các Node project gắn domain riêng (nginx + SSL của aaPanel), PostgreSQL/Redis/Google Drive cài từ App Store, sao lưu bằng Cron aaPanel lên Google Drive; Zalo cá nhân chạy trong worker riêng bằng zca-js; SMS qua máy Android + SIM chạy ứng dụng gateway.
  5. Đối soát phải khoá kỳ: mỗi vé mang ba con số (giá bán, tiền VeXeRe ghi nhận, hoa hồng đại lý) và một trạng thái cuối; chốt kỳ thì vé không sửa được nữa; biên bản xuất Excel theo đại lý.
  6. Lõi quản lý vé + mô-đun cắm/rút: lõi chỉ giữ thực thể vé, vòng đời, sự kiện, quyền, vết, cấu hình; mọi thứ khác (VeXeRe, từng kênh gửi, đẩy danh sách, đối soát, nguồn liên thông) là mô-đun có hợp đồng và phiên bản riêng, bật/tắt bằng cấu hình, nâng cấp không đụng lõi và không đụng nhau (mục 4.9). Tài liệu đi cùng mã: thiết kế, hướng dẫn sử dụng, bàn giao, kinh nghiệm nằm trong kho mã, cập nhật trong cùng một lần thay đổi và được CI kiểm tra (mục 4.10).

1. Bối cảnh và những gì tài liệu cho biết

1.1 Rút ra từ HDSD AMS (bản 7/2022)

AMS là phần mềm web cho đại lý đặt vé trên mọi nhà xe có bán trên VeXeRe. Các quy tắc nghiệp vụ ảnh hưởng trực tiếp đến thiết kế:

Quy tắc AMS Hệ quả cho hệ thống
Hạn mức: số tiền khả dụng; bấm Thanh toán thì trừ đúng Giá dành cho đại lý (= tổng tiền − chiết khấu). Hoàn lại khi huỷ theo phí huỷ theo giá đại lý. Phải theo dõi hạn mức, cảnh báo khi thấp; mỗi lần thanh toán/huỷ ghi một bút toán để đối soát với Báo cáo GD thay đổi hạn mức của AMS.
3 trạng thái vé: Đã đặt chỗ → Đã thanh toán → Đã huỷ. Vé đặt chỗ đến giờ hẹn huỷ chưa thanh toán thì tự huỷ và VeXeRe nhắn khách. Trạng thái nội bộ phải ánh xạ 1-1 và có bộ đếm ngược theo expired_time.
3 hình thức thanh toán: tại đại lý (trừ hạn mức), tại nhà xe (chỉ giữ chỗ), online (VeXeRe thu hộ, phí 1,1–2,5 %). Trường hình thức thanh toán trên vé quyết định có gọi pay hay không.
Sửa vé: vé đã thanh toán không được thêm ghế, không đổi sang ghế khác giá, không đổi ngày/giờ. Gần giờ chạy không nên sửa vì nhà xe đã in. Sau khi xếp, đại lý chỉ yêu cầu sửa; điều hành duyệt; hệ thống chặn các sửa AMS không cho phép.
Đổi ngày/giờ ngang giá: chỉ 1 lần/vé, không áp dụng vé cọc/coupon, vé đã đổi phí huỷ mặc định 100 %, đại lý chịu trách nhiệm nếu chuyến có phí huỷ > 0. Không có API tương ứng; "đổi chuyến" trong hệ thống = huỷ + đặt lại, có cảnh báo phí, hoặc điều hành làm tay trên AMS rồi ghi nhận lại.
Huỷ vé: tính phí huỷ theo giá gốc (khách chịu) và theo giá đại lý (đại lý chịu); có thể huỷ từng ghế. Màn huỷ phải hiện hai con số này trước khi xác nhận (API GET /v3/booking/refund cung cấp).
Tắt bán trực tuyến trước giờ khởi hành N giờ: chuyến ẩn khỏi hệ thống. Vé đẩy lên sát giờ phải chuyển sang luồng xếp thủ công (gọi nhà xe/điều hành).
"NX không báo mã ghế": ghế chọn chỉ tượng trưng, khách ra xe được xếp. Vé xuất cho khách phải ghi "ghế do nhà xe sắp xếp khi lên xe" thay vì số ghế.
Vé cọc (Lễ, Tết): thanh toán giá cọc; khi có giá chính thức thì chờ thu thêm/chờ hoàn tiền. Có trạng thái phụ deposit_status và xử lý webhook ticket_official_fare_updated.
Báo cáo tối đa 31 ngày; tìm vé tối đa 7 ngày theo ngày đặt hoặc ngày đi. Hệ thống tự lưu toàn bộ lịch sử, không dựa vào AMS để tra cứu cũ.
SMS/Email xác nhận của VeXeRe gửi khách (tuỳ chọn, có bản tiếng Anh); email chứa ticket code. Quyết định: có để VeXeRe tự nhắn khách song song với hệ thống hay không (mục 11, câu 6).
Tối đa 8 ghế/đơn (API). Đoàn > 8 khách tách nhiều booking, vẫn gom một "vé nhóm" khi gửi khách.

1.2 Rút ra từ tài liệu API đối tác (vdoc.vexere.net, đọc ngày 02/9/2026)

Môi trường: UAT (uat-api.vexere.net, uat-vroute.vexere.net, uat-account-service.vexere.net) và PRO (api.vexere.com, vroute.vexere.com, account-service.vexere.com). UAT có sẵn client_id/secret thử nghiệm trong tài liệu.

Xác thực: POST {account}/v3/token với grant_type=client_credentials; token sống 1 giờ; mọi API khác dùng Authorization: Bearer.

Nhóm Endpoint Dùng để
Khu vực GET /v3/area/city, /district, /city_district; GET {route}/v2/area Ánh xạ tên tỉnh/huyện của nhà xe ↔ id VeXeRe (đã có lưu ý sáp nhập đơn vị hành chính 2025).
Tìm chuyến GET {route}/v2/route?filter[from,to,date,online_ticket…]&page&pagesize≤20&sort Lấy trip_code của chuyến đúng nhà xe, đúng giờ.
Sơ đồ ghế GET /v3/trip/seat_map?trip_code Ghế trống, giá từng ghế, max_total_seats, danh sách điểm đón/trung chuyển/trả (id, phụ thu, min_customer, điểm cố định/không cố định).
Chính sách GET /v3/company_policy/config_detail, GET /v3/cancellation/policy Ghi vào vé gửi khách (hành lý, trẻ em, huỷ).
Giữ chỗ POST /v3/booking/reserve (form-urlencoded; trip_code, seats = mã ghế đầy đủ cách nhau dấu phẩy, customer_phone/name/email, pickup+pickup_id hoặc transfer+transfer_id, drop_off_info+drop_off_point_id hoặc drop_off_transfer_info+arrive_transfer_id, note) → booking_code, code (ticket code), tickets[], partner_code (mã nhà xe cấp để lên xe). Bước "xếp vé tự động".
Tra vé GET /v3/booking?code=status (1 giữ chỗ, 4 đã thanh toán, 5 hoàn/huỷ), expired_time, amount_booking, vehicle{number,driver,phone_info}, pickup_date, pickup_guide, partner_code, all_tickets_cancelled Làm giàu vé trước khi gửi khách (biển số, tài xế, giờ đón); đối soát.
Thanh toán POST /v3/booking/pay (code, transaction_id = mã đối soát phía ta) → vxr_transaction_id Trừ hạn mức; khứ hồi gửi mảng.
Huỷ GET /v3/booking/refund?code=booking (xem phí/hoàn, cancelable, bậc phí theo giờ) rồi POST /v3/booking/refund (code = ticket code) Huỷ có xem trước phí.
Sửa PUT /v3/booking/{code}/agent_api — chỉ customer_name/email, điểm đón/trung chuyển/trả cùng loại, cùng mức phụ thu Sửa nhẹ sau khi xếp.
Coupon GET /v3/campaign/coupon, POST /v3/booking/{code}/coupon Giai đoạn sau.
Nhà xe GET /v3/company/{id}/info_for_agent, /image, đánh giá Hiển thị.
Webhook (VeXeRe → ta) POST tới URL đăng ký, header x-signature = HMAC-SHA256(client_secret, chuỗi key=value&… theo thứ tự quy định); 3 sự kiện ticket_updated, ticket_cancelled, ticket_official_fare_updated; phải trả 200 {"code":"00"}; VeXeRe thử lại 4 lần, yêu cầu idempotent. Đồng bộ ngược khi nhà xe đổi giờ/huỷ trên BMS.
Webview Trang đặt vé nhúng của VeXeRe cho đối tác Không dùng (ta có UI riêng).

Mã lỗi cần xử lý riêng: 3403 SEAT_IS_NOT_AVAILABLE (chọn lại ghế, thử tối đa 3 lần), 3308 TRIP_IS_NOT_BOOKABLE (chuyển thủ công), 3317 BOOKING_EXPIRED, 3301 BOOKING_PAID, 3302 BOOKING_CANCELLED, 3101/3115/3118/3120 tham số sai (lỗi dữ liệu, không thử lại).

Điểm mâu thuẫn trong tài liệu cần hỏi VeXeRe: mục Update ticket info ghi "booking status = 2 (booked)" trong khi Get booking ghi "1-Reserve". Thiết kế coi giá trị trả về của GET /v3/booking là chuẩn.

1.3 Hệ thống hiện có: chỉ là tài liệu tham khảo và mã mẫu

Cổng gửi vé được xây mới hoàn toàn trên một VPS Linux riêng: mã nguồn mới, CSDL mới, tiến trình mới; không dùng chung máy chủ, CSDL hay thư viện lúc chạy với Vé247/OSZalo. Ba hệ thống cũ chỉ đóng vai trò tài liệu nghiệp vụ và mã mẫu để rút ngắn thiết kế chi tiết:

Hệ thống cũ Tham khảo gì Không dùng gì
Vé247 (D:\CODE\30_SamsungS22u) Lược đồ vé và cách chuẩn hoá SĐT/giờ/ngày/loại chỗ (server/src/server.js mục tao-ve, brain.js); quy trình phát hành an toàn (kiểm tra tham chiếu trước khi triển khai); cách app Android giữ kết nối máy chủ. Không chạy chung VPS/CSDL; app S22U không sửa để phục vụ cổng.
OSZalo-247 (d:\CODE\07_OSZalo_247) Cách dùng zca-js: đăng nhập QR, lưu phiên, giám sát kết nối, gửi tin/ảnh, nhận sự kiện đã xem, giới hạn nhịp gửi (src/channels/zaloLive.ts). Không gọi sang OSZalo lúc chạy; cổng có worker Zalo riêng.
xekhachhagiang.com (d:\CODE\32_webVexe247\site-moi) Cách khách mô tả nhu cầu (tuyến, điểm đón), dòng vé chuẩn, mã tra cứu ngắn. Không phụ thuộc.

Sau khi cổng ổn định, các hệ thống cũ có thể đẩy vé vào cổng qua API liên thông (mục 4.6) với tư cách một "nguồn". Đó là tích hợp tuỳ chọn, không phải điều kiện để cổng chạy.


2. Mục tiêu, phạm vi, tác nhân

2.1 Mục tiêu đo được

  • Đại lý nhập một vé dưới 30 giây trên điện thoại; đẩy danh sách 50 vé dưới 2 phút kể cả sửa lỗi.
  • Vé hợp lệ được xếp tự động qua API và gửi khách trong vòng 2 phút (trường hợp ghế còn, chuyến còn bán online).
  • Khách nhận vé qua ít nhất một kênh ≥ 99 % (SMS là kênh bảo đảm, các kênh khác là bổ sung).
  • Mọi thay đổi vé đều có vết (ai, lúc nào, trước/sau).
  • Chốt đối soát tháng cho một đại lý dưới 10 phút, có biên bản Excel.

2.2 Ngoài phạm vi (phiên bản 1)

  • Không thay thế AMS/BMS của VeXeRe; không quản lý sơ đồ ghế của nhà xe.
  • Không thu tiền online (dùng kênh thanh toán online của VeXeRe nếu cần).
  • Không xây app di động gốc cho đại lý (web mobile-first/PWA là đủ).
  • Không tích hợp cổng SMS thương hiệu (brandname) — chỉ SIM thường theo yêu cầu.

2.3 Tác nhân

Tác nhân Là ai Việc chính
Quản trị nhà xe Chủ/kế toán nhà xe Cấu hình VeXeRe, kênh gửi, tuyến, đại lý, hoa hồng; xem toàn bộ; chốt đối soát.
Điều hành (bàn xếp vé) Người trực tổng đài/điều độ Nhận vé đẩy lên, xếp chỗ (tự động/thủ công), duyệt yêu cầu sửa/huỷ, xử lý sự cố gửi.
Đại lý (chủ đại lý, nhân viên đại lý) Điểm bán, cộng tác viên Đẩy lên, thêm, sửa, xoá vé của đại lý mình; theo dõi trạng thái; xem thống kê/đối soát của mình.
Nguồn hệ thống Vé247, OSZalo, web Đẩy vé bằng API với khoá riêng; được đối xử như một đại lý đặc biệt.
Khách hàng Người đi xe Nhận vé qua kênh; tra cứu vé bằng mã/SĐT (trang công khai tối giản).
VeXeRe Hệ thống ngoài Nhận đặt chỗ/thanh toán/huỷ; gửi webhook.

3. Nghiệp vụ và quy trình

3.1 Vòng đời một vé

stateDiagram-v2
    [*] --> NHAP: đại lý tạo (form/danh sách/API)
    NHAP --> DA_DAY: đại lý bấm "Đẩy lên"
    DA_DAY --> DANG_XEP: điều hành nhận / máy tự xếp
    DANG_XEP --> DA_XEP: có ghế + mã VeXeRe (hoặc xếp tay)
    DANG_XEP --> TU_CHOI: hết chỗ / chuyến ẩn / dữ liệu sai
    TU_CHOI --> NHAP: đại lý sửa, đẩy lại
    DA_XEP --> DA_XUAT: bộ xuất vé dựng nội dung
    DA_XUAT --> DA_GUI: ≥ 1 kênh gửi thành công
    DA_GUI --> HOAN_THANH: qua giờ khởi hành + 1 ngày
    DA_XEP --> HUY: huỷ (đại lý yêu cầu → điều hành duyệt / webhook VeXeRe)
    DA_XUAT --> HUY
    DA_GUI --> HUY
    DA_DAY --> HUY: đại lý tự huỷ khi chưa xếp
    HUY --> [*]
    HOAN_THANH --> [*]

Quy tắc quyền theo trạng thái (chi tiết ở mục 7):

Trạng thái Đại lý sửa Đại lý xoá/huỷ Điều hành
NHAP, DA_DAY, TU_CHOI Sửa tự do Xoá mềm tự do Xem, xếp
DANG_XEP Khoá (đang giữ chỗ) Không Xếp/từ chối
DA_XEP, DA_XUAT, DA_GUI Chỉ gửi yêu cầu sửa (tên, email, điểm đón/trả, ghi chú) Chỉ gửi yêu cầu huỷ, thấy phí huỷ dự kiến Duyệt → gọi API sửa/huỷ; gửi lại vé
HUY, HOAN_THANH Không Không Chỉ ghi chú
Vé thuộc kỳ đối soát đã chốt Không Không Không (phải mở kỳ)

Trạng thái gửi theo từng kênh là lớp riêng trên vé: mỗi vé có N bản ghi gui_tin (kênh, người nhận, CHO_GUI → DANG_GUI → DA_GUI/LOI → DA_XEM nếu kênh báo được). Vé đạt DA_GUI khi có ít nhất một kênh DA_GUI.

3.2 Luồng 1 — Đại lý nhập thủ công

  1. Đại lý mở cổng trên điện thoại → Vé mới. Form gồm: SĐT khách (bắt buộc, 10 số), tên khách, số khách, loại chỗ (ghế/giường/cabin/…), tuyến (chọn từ danh sách tuyến nhà xe), ngày đi, giờ đi (chọn trong lịch chuyến), điểm đón, điểm trả, ghi chú cho nhà xe, giá bán, tiền đã thu (đã thu tại đại lý / thu tại xe / chuyển khoản), kênh gửi cho khách (mặc định theo cấu hình đại lý), email (nếu chọn kênh email).
  2. Hệ thống kiểm tra tức thì: trùng SĐT + ngày + giờ (cảnh báo, không chặn); giờ đi đã qua mốc tắt bán (cảnh báo "sẽ xếp thủ công"); điểm đón có trong danh sách của chuyến.
  3. Bấm Lưu nháp (NHAP) hoặc Đẩy lên (DA_DAY). Đẩy lên tạo mã vé nội bộ dạng VX-YYMMDD-NNNN.
  4. Đại lý thấy vé trong Vé của tôi với trạng thái thời gian thực (SSE).

3.3 Luồng 2 — Đẩy danh sách

Ba cách nhập, cùng một bộ kiểm tra:

Cách Mô tả Ai dùng
Tải Excel/CSV theo mẫu Mẫu cột ở Phụ lục C; tải mẫu trống từ hệ thống. Đại lý có sổ Excel.
Dán văn bản nhiều dòng Mỗi dòng một vé theo dòng vé chuẩn của Vé247 (0912… · Nguyễn A · 2 giường · 17h30 2/9 · HG→HN · đón BX Hà Giang). Bộ tách dòng viết mới trong packages/shared, tham khảo cách chuẩn hoá của Vé247. Đại lý quen chat Zalo.
API POST /api/dai-ly/ve/lo Mảng JSON, khoá API của đại lý/nguồn. Vé247, OSZalo, web.

Quy trình: tải lên → hệ thống dựng bảng xem trước có cột Kết quả kiểm tra (hợp lệ / cảnh báo / lỗi + lý do) → đại lý sửa tại chỗ trong bảng → bấm Đẩy các dòng hợp lệ → tạo một (lo_day) và N vé DA_DAY; dòng lỗi giữ lại trong lô để sửa tiếp. Toàn bộ hoặc không: không, vì thực tế một dòng sai không nên chặn 49 dòng đúng; nhưng lô ghi rõ số dòng thành công/lỗi.

Kiểm tra bắt buộc: SĐT hợp lệ; ngày ≥ hôm nay; giờ có trong lịch chuyến của tuyến; số khách 1–8 mỗi dòng (hơn thì tự tách); trùng lặp trong cùng lô (SĐT + ngày + giờ) hỏi gộp hay tách.

3.4 Luồng 3 — Xếp vé

Ba chế độ, cấu hình theo tuyến hoặc theo vé:

sequenceDiagram
    autonumber
    participant DL as Đại lý
    participant C as Cổng gửi vé
    participant DH as Điều hành
    participant VX as VeXeRe API
    DL->>C: Đẩy vé (DA_DAY)
    C->>C: Tìm tuyến → from_id/to_id, company_id
    C->>VX: GET /v2/route (ngày, from, to, online_ticket=1)
    VX-->>C: danh sách chuyến của nhà xe
    C->>C: Khớp giờ đi (± dung sai) → trip_code
    C->>VX: GET /v3/trip/seat_map?trip_code
    VX-->>C: ghế trống, giá, điểm đón/trả
    C->>C: Chọn ghế theo luật (loại chỗ, tầng, kề nhau)
    alt Tự động hoàn toàn
        C->>VX: POST /v3/booking/reserve
        VX-->>C: booking_code, ticket code, partner_code
        opt Đại lý đã thu tiền
            C->>VX: POST /v3/booking/pay (transaction_id = mã vé nội bộ)
        end
        C->>VX: GET /v3/booking?code (biển số, tài xế, giờ đón)
        C->>C: DA_XEP → xuất vé → gửi
    else Bán tự động (mặc định giai đoạn đầu)
        C->>DH: Đề xuất ghế + giá, chờ bấm "Xác nhận"
        DH->>C: Xác nhận / đổi ghế / từ chối
        C->>VX: reserve (+ pay) như trên
    else Thủ công (chuyến ẩn, hết ghế, lỗi API)
        DH->>DH: Xếp trên AMS/BMS hoặc gọi nhà xe
        DH->>C: Nhập mã vé/ghế/biển số → DA_XEP
    end
    C-->>DL: Trạng thái đổi thời gian thực

Luật chọn ghế (cấu hình được): đúng loại chỗ đại lý ghi; ưu tiên ghế kề nhau cho nhóm; ưu tiên tầng dưới cho người già/trẻ em nếu ghi chú; không lấy ghế có phụ thu khác giá bán trừ khi đại lý cho phép; nếu chuyến có nhiều mức giá thì hiện chênh lệch cho điều hành. Khi 3403 SEAT_IS_NOT_AVAILABLE → tải lại sơ đồ, chọn lại, tối đa 3 lần rồi chuyển bán tự động.

Đồng hồ theo dõi: vé đã reserve chưa payexpired_time → nhắc điều hành trước 30 phút và 10 phút; đến hạn mà chưa quyết → tự chuyển TU_CHOI với lý do "hết hạn giữ chỗ" và báo đại lý. Vé đẩy lên sau mốc tắt bán của chuyến → vào hàng đợi thủ công ngay, không gọi API.

3.5 Luồng 4 — Xuất vé và gửi đa kênh

Nội dung vé (một nguồn dữ liệu, nhiều khuôn hiển thị): tên nhà xe, mã vé nội bộ + mã VeXeRe (ticket code, partner_code nếu có), tên khách, số khách, tuyến, ngày giờ khởi hành, giờ đón dự kiến + điểm đón + địa chỉ, số ghế (hoặc "nhà xe xếp chỗ khi lên xe"), biển số + SĐT xe (nếu VeXeRe trả), giá vé + đã thu/chưa thu, chính sách huỷ tóm tắt, số hotline, liên kết tra cứu ngắn.

Khuôn theo kênh:

Kênh Khuôn Ghi chú
SMS SIM thường Văn bản không dấu, ≤ 160 ký tự (1 phân đoạn GSM-7); nếu cần dấu thì ≤ 70 ký tự/phân đoạn (UCS-2) Có dấu tốn 2–3 phân đoạn; mặc định không dấu, có liên kết tra cứu.
Email HTML có bảng, kèm mã QR nội bộ, đính kèm PDF tuỳ chọn Cần SPF/DKIM cho tên miền gửi.
Zalo OA ZNS mẫu "xác nhận vé" (gửi theo SĐT, không cần khách theo dõi OA) hoặc tin OA thường nếu khách đã tương tác ZNS phải đăng ký mẫu và được duyệt; có phí/tin (chưa xác minh mức phí hiện hành).
Zalo cá nhân Văn bản có dấu + ảnh vé (PNG dựng từ khuôn) Qua worker-zalo (zca-js); chỉ tới được khi tìm thấy tài khoản theo SĐT; tin từ người lạ có thể rơi vào "Tin nhắn chờ".

Chính sách gửi: mỗi đại lý có thứ tự kênh mặc định (ví dụ Zalo cá nhân → SMS; hoặc SMS + Email). Bộ gửi chạy hàng đợi: thử kênh 1; nếu kênh 1 không thể gửi (không tìm thấy Zalo, email sai) thì tự chuyển kênh dự phòng; nếu kênh 1 đã gửi thì các kênh đánh dấu "song song" vẫn gửi. Giới hạn nhịp riêng từng kênh (mục 4.3). Mọi lần gửi lưu nội dung đã gửi để tra lại đúng chữ khách nhận.

Gửi lại: khi vé đổi (giờ, ghế, xe) → tự dựng bản "VÉ CẬP NHẬT" và gửi lại theo cùng kênh đã thành công trước đó; khi huỷ → "VÉ ĐÃ HUỶ" kèm phí/hoàn.

3.6 Luồng 5 — Sửa, huỷ, đổi chuyến

Việc Trước khi xếp Sau khi xếp (có mã VeXeRe)
Sửa tên/email/điểm đón/trả/ghi chú Đại lý sửa thẳng Đại lý gửi yêu cầu sửa → điều hành duyệt → PUT /v3/booking/{code}/agent_api (chỉ điểm cùng loại, cùng phụ thu) → gửi lại vé
Đổi số khách/loại chỗ Sửa thẳng Không sửa được trên vé đã thanh toán → huỷ ghế thừa (refund từng ticket) hoặc đặt thêm booking mới
Đổi ngày/giờ Sửa thẳng Không có API. Hai lựa chọn hiện cho điều hành: (a) huỷ + đặt lại, hiện phí huỷ dự kiến; (b) điều hành đổi tay trên AMS (ngang giá, 1 lần, phí huỷ 100 % về sau) rồi bấm "Đã đổi tay" và nhập giờ mới.
Huỷ Đại lý huỷ thẳng, xoá mềm Đại lý gửi yêu cầu huỷ → hệ thống gọi GET /refund hiện phí huỷ theo giá gốc (khách chịu) và theo giá đại lý (đại lý chịu) → điều hành duyệt → POST /refund (ticket code) → HUY → báo khách + đại lý → ghi bút toán hoàn hạn mức
Xoá Xoá mềm (giữ vết) Không xoá; chỉ huỷ

Điều hành có thể uỷ quyền cho một đại lý tin cậy tự duyệt yêu cầu của mình (cờ tu_duyet trên đại lý).

3.7 Luồng 6 — Đồng bộ ngược từ VeXeRe

  • Webhook: nhận ticket_updated (nhà xe đổi giờ/giờ đón) → cập nhật vé, gửi "VÉ CẬP NHẬT"; ticket_cancelledHUY, ghi cancel_fee/refund_amount, báo đại lý + khách; ticket_official_fare_updated (vé cọc) → đánh dấu chờ thu thêm/hoàn với hạn surcharge_time_deadline, báo đại lý. Xác thực x-signature, lưu sự kiện thô, idempotent theo (event_type, booking_code, event_timestamp), trả 200 {"code":"00"} ngay rồi xử lý nền.
  • Quét định kỳ (dự phòng webhook lỗi): mỗi 15 phút với vé có ngày đi ≤ 2 ngày tới gọi GET /v3/booking?code=&checking_status_only=1; mỗi giờ với vé xa hơn. Khác trạng thái → xử lý như webhook.
  • Làm giàu trước giờ chạy: 3 giờ trước khởi hành gọi GET /v3/booking lấy biển số/tài xế/SĐT xe; nếu mới có thì gửi khách tin "XE CỦA BẠN" (tuỳ chọn cấu hình).

3.8 Luồng 7 — Thống kê và đối soát theo đại lý

Số liệu trên mỗi vé (đóng băng khi chốt kỳ):

  • gia_ban: giá đại lý bán cho khách (đại lý nhập).
  • gia_ncc: giá nhà cung cấp chỗ ghi nhận (với VeXeRe là amount_booking, giá bán lẻ VeXeRe).
  • gia_dai_ly (bảng mô-đun vexere): giá dành cho đại lý (sau chiết khấu) — lấy từ AMS/GET booking nếu API trả; nếu không thì tính theo % chiết khấu cấu hình.
  • hoa_hong: hoa hồng nhà xe trả đại lý (theo % hoặc số cố định, cấu hình theo đại lý × tuyến × loại chỗ).
  • phi_huy, hoan_tien khi huỷ.
  • hinh_thuc_thu: đã thu tại đại lý / thu tại xe / chuyển khoản nhà xe.

Công thức mặc định (điều chỉnh được ở cấu hình): phai_nop = Σ(gia_ban − hoa_hong) cho vé đã thu tại đại lý và không huỷ; vé huỷ có phí thì phai_nop += phi_huy_dai_ly_chiu. Vé thu tại xe không tính vào phải nộp nhưng vẫn tính hoa hồng.

Kỳ đối soát: tuần hoặc tháng theo đại lý. Quy trình: quản trị tạo kỳ → hệ thống liệt kê vé theo ngày đi trong kỳ, ba cột đối chiếu (hệ thống, VeXeRe qua GET booking/nhập file Báo cáo chi tiết vé xuất từ AMS, đại lý xác nhận) → dòng lệch được tô và có ô ghi chú xử lý → chốt kỳ (khoá mọi vé trong kỳ) → xuất biên bản Excel (tổng hợp + chi tiết) → đại lý bấm Xác nhận trên cổng (ghi thời điểm, người). Mở lại kỳ chỉ quản trị làm, có lý do, ghi vết.

Bảng thống kê (đại lý thấy của mình, quản trị thấy tất cả, lọc ngày/tuần/tháng/khoảng): số vé, số ghế, doanh thu, hoa hồng, tỉ lệ huỷ, phí huỷ; theo tuyến, theo giờ, theo loại chỗ; tỉ lệ gửi thành công theo kênh và thời gian trung bình từ đẩy lên → đã gửi; xếp hạng đại lý; tồn đọng (vé DA_DAY quá 15 phút, vé giữ chỗ sắp hết hạn, tin gửi lỗi).


4. Kiến trúc hệ thống

4.1 Tổng thể

flowchart LR
    subgraph NguoiDung[Người dùng]
        DL[Đại lý
web mobile-first / PWA] DH[Điều hành
bàn xếp vé] QT[Quản trị] KH[Khách
trang tra cứu] end subgraph VPS[VPS Ubuntu + aaPanel] CADDY[nginx của aaPanel
HTTPS, reverse proxy từ Node project] API[api — NestJS
REST + SSE, RBAC, audit] WK[worker — NestJS
xếp vé, gửi tin, đồng bộ, đồng hồ hết hạn] WZ[worker-zalo
zca-js, phiên Zalo cá nhân] PG[(PostgreSQL 16)] RD[(Redis 7
BullMQ, phiên, cache token)] SMSG[sms-gateway server
tự host] end subgraph Kenh[Kênh gửi] SIM[Máy Android + SIM
app SMS Gateway] MAIL[SMTP] OA[Zalo OA / ZNS] end subgraph Ngoai[Ngoài] VX[VeXeRe API] NG[Nguồn tuỳ chọn:
Vé247 / OSZalo / web] end DL & DH & QT & KH --> CADDY --> API NG -->|API khoá riêng| CADDY VX -->|webhook HMAC| CADDY API <--> PG API <--> RD WK <--> PG WK <--> RD WZ <--> RD WK <--> VX WK --> SMSG --> SIM SIM -->|đã gửi / đã nhận / lỗi| SMSG WK --> MAIL & OA

Nguyên tắc: một kho mã (monorepo), ba tiến trình Node (api, worker, worker-zalo) dùng chung PostgreSQL và Redis; giao diện là SPA tĩnh do Caddy phục vụ; mọi việc nền (xếp vé, gửi tin, quét trạng thái, nhắc hết hạn) là job BullMQ có thử lại và có giới hạn nhịp theo hàng đợi. Tách worker-zalo để phiên Zalo cá nhân đứt không kéo API đứt. Mã viết bằng TypeScript; tên miền nghiệp vụ tiếng Việt không dấu (ve, daiLy, guiTin) để khớp tài liệu này. Quy mô vài trăm vé/ngày chạy thoải mái trên một VPS nhỏ; tách dịch vụ sau nếu lớn. Bên trong, mã tổ chức theo lõi quản lý vé + mô-đun cắm/rút (mục 4.9): apiworker chỉ là hai "vỏ" nạp lõi cùng các mô-đun đang bật.

4.2 Mô-đun

Thành phần Thuộc Trách nhiệm Ghi chú
Thực thể vé, máy trạng thái, bus sự kiện, quyền, vết, cấu hình, registry mô-đun Lõi Mọi lệnh tạo/đẩy/xếp/huỷ/sửa vé đi qua đây và phát sự kiện ve.* packages/core, mục 4.9
cong-dai-ly (giao diện đại lý) Lõi (vỏ) + màn hình do mô-đun đóng góp Đăng nhập, form vé, vé của tôi, yêu cầu sửa/huỷ; menu tự hiện theo mô-đun đang bật SPA React (PWA); trạng thái qua SSE
ban-xep-ve Lõi Hàng đợi vé DA_DAY, gọi nhà cung cấp chỗ đang bật (thủ công là mặc định, có sẵn trong lõi), đồng hồ hết hạn, duyệt yêu cầu, xử lý tin lỗi Màn hình máy tính, phím tắt
quan-tri Lõi (khung) + trang cấu hình do mô-đun đóng góp Đại lý, người dùng, tuyến, điểm đón/trả, nhật ký, bật/tắt mô-đun
gui-tin (điều phối gửi) Lõi Hàng đợi theo kênh, nhịp, dự phòng, thử lại, gửi lại khi đổi/huỷ; gọi kênh đang bật qua hợp đồng KenhGui Một hàng đợi BullMQ mỗi kênh; bảng gui_tin
vexere Mô-đun (NhaCungCapCho + BaoCao) Bọc API (token 55 phút, thử lại có lùi, mã lỗi → hành động, nhật ký gọi/đáp), tìm chuyến, chọn ghế, giữ chỗ, thanh toán, huỷ, sửa; webhook nhận (HMAC, chống trùng); quét định kỳ, làm giàu biển số; ánh xạ tuyến/điểm ↔ id VeXeRe UAT/PRO bằng .env; tắt mô-đun → cổng vẫn xếp thủ công
xuat-ve Mô-đun (KhuonVe) Khuôn theo kênh, dựng PNG/PDF, liên kết tra cứu ngắn Khuôn sửa được trong quản trị
sms-gateway, email, zalo-oa, zalo-ca-nhan Mô-đun (KenhGui, mỗi kênh một mô-đun) Gửi, nhận trạng thái gửi/đã xem, trang cấu hình riêng zalo-ca-nhan chạy tiến trình worker-zalo riêng
day-danh-sach Mô-đun (NguonVe) Excel/CSV/dán, bảng xem trước, lô
api-nguon Mô-đun (NguonVe) API cho Vé247/OSZalo/web đẩy vé, webhook ra báo trạng thái Khoá theo nguồn
thong-ke, doi-soat Mô-đun (BaoCao) Tổng hợp theo đại lý/tuyến/kênh; kỳ đối soát, khoá kỳ (qua lệnh khoá vé của lõi), Excel exceljs

4.3 Thiết kế từng kênh gửi

Tiêu chí SMS SIM thường Email Zalo OA (ZNS) Zalo cá nhân
Cách gửi worker gọi REST của máy chủ SMS gateway tự host; máy Android + SIM chạy ứng dụng gateway nhận lệnh, gửi bằng SIM, báo đã gửi/đã nhận/lỗi qua webhook về worker nodemailer SMTP (tên miền nhà xe hoặc dịch vụ gửi) Gọi API OA với mẫu ZNS đã duyệt, gửi theo SĐT Job vào hàng đợi zalo-ca-nhan; worker-zalo (zca-js) tìm UID theo SĐT, gửi bằng tài khoản bán vé của nhà xe, báo msgId/đã xem
Độ tin cậy tới khách Cao (mọi máy) Trung bình (khách ít đọc) Cao nếu ZNS Trung bình (tin người lạ vào "chờ")
Chi phí Gói SIM; cần theo dõi số dư Rất thấp Phí/tin ZNS (chưa xác minh) Không
Giới hạn Nhà mạng chặn spam SIM thường: giữ ≤ 1 tin/5 giây, trần ngày cấu hình (mặc định 200), nội dung có mã vé khác nhau; số cụ thể chưa xác minh theo từng nhà mạng Uy tín tên miền (SPF/DKIM/DMARC) Mẫu phải duyệt; OA phải xác thực Nhịp tự đặt theo kinh nghiệm OSZalo (20/phút, 150/giờ, 600/ngày; không có con số chính thức); nguy cơ khoá tài khoản nếu gửi hàng loạt tới người lạ
Nhận biết thất bại RESULT_ERROR_* từ Android; không có DELIVERED sau 10 phút → nghi ngờ SMTP bounce Mã lỗi API "không tìm thấy người dùng", lỗi gửi
Vai trò đề xuất Kênh bảo đảm, luôn bật dự phòng Bổ sung cho khách doanh nghiệp/đoàn Giai đoạn 3 Kênh ưu tiên cho khách đã kết bạn/đã chat với tài khoản bán vé

Thiết kế kênh SMS (chi tiết): dùng ứng dụng SMS Gateway for Android (mã nguồn mở) trên một máy Android + SIM đặt cố định, cùng máy chủ gateway tự host trong Docker Compose (ứng dụng có chế độ máy chủ riêng); worker gọi REST gửi tin kèm mã tham chiếu, nhận webhook đã gửi/đã nhận/lỗi để cập nhật gui_tin; nhịp gửi do BullMQ giới hạn (mặc định 1 tin/5 giây, trần ngày cấu hình); tạm dừng hàng đợi khi 5 lỗi liên tiếp và báo quản trị; hỗ trợ nhiều máy SIM (mỗi máy một tài khoản gateway, cổng chia đều). Nếu ứng dụng gateway không đáp ứng, viết app Kotlin riêng theo cùng giao thức (kinh nghiệm từ app Vé247). Cần kiểm tra lại phiên bản ứng dụng gateway hiện hành trước khi chốt.

4.4 Bộ kết nối VeXeRe — quy ước

  • Token cache 55 phút, làm mới sớm; nhận 401 thì làm mới một lần rồi thử lại.
  • Mọi lệnh ghi (reserve, pay, refund, agent_api) là một giao dịch nội bộtransaction_id = mã vé nội bộ + hậu tố hành động, để đối soát với vxr_transaction_id.
  • Không bao giờ gọi pay mà chưa lưu booking_code; không gọi refund mà chưa lưu kết quả GET refund để hiện phí.
  • Ghi đầy đủ yêu cầu/đáp (che SĐT một phần) vào mod_vexere_nhat_ky với thời gian phản hồi để theo dõi.
  • Bảng ánh xạ mod_vexere_anh_xa: tuyến nội bộ → from_id/to_id (area), company_id nhà xe, danh sách giờ, dung sai khớp giờ (mặc định ±10 phút), danh sách điểm đón/trả nội bộ ↔ pickup_id/drop_off_point_id (đồng bộ từ seat_map, quản trị chỉnh tay).
  • Kiểm thử: chạy trên UAT với client_id/secret thử nghiệm trong tài liệu; bộ ca kiểm thử theo mục Testcases của tài liệu; sang PRO khi có tài khoản thật.

4.5 Webhook nhận từ VeXeRe

POST /api/vexere/webhook → đọc thô body → dựng chuỗi theo thứ tự trường trong tài liệu → HMAC-SHA256(client_secret) → so với x-signature (so sánh thời gian hằng) → sai thì 401 và ghi nhật ký → đúng thì lưu mod_vexere_su_kien (chống trùng) → trả ngay 200 {"code":"00","message":"Webhook received"} → xử lý nền. URL webhook đăng ký với VeXeRe qua đầu mối kỹ thuật (tài liệu không có API tự đăng ký).

4.6 API liên thông cho nguồn tuỳ chọn (Vé247, OSZalo, web)

Tích hợp tuỳ chọn, bật khi cần; cổng không phụ thuộc các hệ thống này.

  • POST /api/nguon/ve (khoá theo nguồn): nhận vé theo lược đồ tương thích tao-ve của Vé247 (maNguon, soDienThoai, soKhach, loaiGhe, tuyenTu, tuyenDen, ngayDi, gio, diemDon, diemTra, yeuCauKhac, nguon) → trả veId, maVe. Idempotent theo nguon + maNguon.
  • POST /api/nguon/ve/{id}/huy, PUT /api/nguon/ve/{id}: như đại lý.
  • Webhook ra POST {url nguồn} khi vé đổi trạng thái, để hệ thống nguồn cập nhật phía mình.

4.7 Công nghệ đề xuất

Tiêu chí chọn: một người bảo trì được; chạy trên một VPS Linux bằng Docker; kênh Zalo cá nhân chỉ có thư viện Node (zca-js) nên một ngôn ngữ cho toàn bộ tiết kiệm nhất; có sẵn hàng đợi có nhịp, lịch, RBAC, audit; phục vụ nhiều đại lý ghi đồng thời và đối soát bằng SQL.

Lớp Chọn Vì sao (và lựa chọn đã bỏ)
Ngôn ngữ TypeScript trên Node.js 22 LTS Một ngôn ngữ cho api, worker, web, kịch bản. Chọn Python/Go/PHP vẫn phải kèm một tiến trình Node cho Zalo cá nhân.
Khung máy chủ NestJS 11 (adapter Fastify) Có sẵn mô-đun, guard (RBAC), interceptor (audit), @nestjs/schedule, @nestjs/bullmq, @nestjs/swagger, kiểm tra đầu vào; khớp 11 mô-đun mục 4.2. Fastify thuần nhẹ hơn nhưng phải tự dựng các lớp này.
CSDL PostgreSQL 16 Nhiều đại lý ghi đồng thời; khoá hàng (SELECT … FOR UPDATE) khi hai điều hành xếp cùng vé; ràng buộc FK/UNIQUE (chống trùng webhook); JSONB cho payload; window function cho đối soát; pg_dump. SQLite đủ cho một máy nhưng yếu khi nhiều người ghi và khó tách tiến trình.
ORM, di trú Prisma (+ SQL thô cho báo cáo) Lược đồ khai báo, di trú có phiên bản, kiểu an toàn; báo cáo phức tạp viết SQL qua $queryRaw. Lược đồ chia theo tệp: lõi một tệp, mỗi mô-đun một tệp (bảng mod_<ten>_*), gom lại khi dựng.
Hàng đợi, lịch BullMQ trên Redis 7 Thử lại có lùi, job trễ (nhắc hết hạn giữ chỗ), job lặp (quét định kỳ), giới hạn nhịp theo hàng đợi (đúng nhu cầu SMS/Zalo), Bull Board để xem hàng đợi.
Phiên, cache Redis Phiên đăng nhập (cookie HttpOnly) lưu Redis; cache token VeXeRe 55 phút; khoá phân tán khi xếp.
Giao diện React 19 + Vite + TypeScript, Ant Design 5, TanStack Query, vite-plugin-pwa Ant Design mạnh về bảng/biểu mẫu back-office, có locale tiếng Việt; PWA để đại lý "cài" lên màn hình điện thoại; TanStack Query nhận cập nhật từ SSE.
Thời gian thực SSE Một chiều máy chủ → trình duyệt là đủ (trạng thái vé, hàng đợi bàn xếp); qua Caddy không cần cấu hình riêng; đơn giản hơn WebSocket.
Kiểm tra dữ liệu zod trong packages/shared Một lược đồ dùng chung cho web và api (form vé, tệp đẩy danh sách, API nguồn).
Gọi VeXeRe undici (fetch) + lớp bọc thử lại trong packages/vexere-client Nhẹ; ghi nhật ký yêu cầu/đáp; có bản giả lập (mock) để chạy test không cần UAT.
Xuất vé satori + @resvg/resvg-js (PNG), pdf-lib (PDF), Handlebars (văn bản/HTML) Dựng ảnh vé từ JSX không cần Chromium; nhẹ cho VPS nhỏ.
Email nodemailer + SMTP (Zoho Mail / Brevo / Google Workspace) Cần SPF/DKIM/DMARC cho tên miền gửi.
SMS SIM thường Máy Android + SIM chạy SMS Gateway for Android (mã nguồn mở) + máy chủ gateway tự host REST gửi tin, webhook trạng thái, nhiều máy; không phải viết app Android ngay. Phương án B: app Kotlin riêng.
Zalo cá nhân zca-js trong tiến trình worker-zalo riêng Thư viện không chính thức, có thể đứt phiên → tách tiến trình, giám sát, đăng nhập lại bằng QR trên trang quản trị.
Zalo OA API chính thức Zalo OA / ZNS Giai đoạn 3.
Excel exceljs Biên bản đối soát, mẫu tệp đẩy danh sách.
Xác thực, phân quyền Phiên cookie + CASL Diễn đạt được "đại lý chỉ thấy vé của đại lý mình", "sửa khi trạng thái ≤ DA_DAY"; khoá API băm cho nguồn.
Nhật ký, giám sát pino (JSON) ra log Node project, /health, Monitor/Cron của aaPanel, cảnh báo Zalo/Telegram; Uptime Kuma tuỳ chọn Đủ cho một VPS; chưa cần Prometheus/Grafana ở bản 1.
Triển khai aaPanel Website → Node project (kiểu PM2) ×3, gắn domain riêng → reverse proxy nginx + SSL tự động; Node/PostgreSQL/Redis/Google Drive từ App Store; sao lưu bằng Cron aaPanel lên Google Drive Chỉ dùng tính năng có sẵn của aaPanel, ít thao tác tay; không cần Docker (tài liệu 01, mục 5).
Kiểm thử Vitest (đơn vị), Supertest (API), Playwright (luồng chính) Bộ ca theo mục Testcases của tài liệu VeXeRe chạy trên UAT.

Phương án đã cân nhắc và lý do không chọn:

  • Node + Fastify + SQLite một tiến trình (khuôn Vé247): triển khai đơn giản nhất, nhưng yếu khi nhiều đại lý ghi đồng thời, không có hàng đợi có nhịp, khó tách worker Zalo. Vẫn là phương án dự phòng nếu muốn tối giản (mục 11, câu 12).
  • Python Django: quản trị và ORM sẵn, nhưng Zalo cá nhân vẫn cần tiến trình Node; hai ngôn ngữ cho một người bảo trì.
  • PHP Laravel (+ Filament): nhanh cho back-office, cùng vấn đề hai ngôn ngữ.
  • Go: nhị phân gọn, nhưng làm giao diện/tích hợp chậm hơn, không có thư viện Zalo.

Cấu hình VPS tối thiểu: 2 vCPU, 4 GB RAM, 40 GB SSD, Ubuntu 24.04. Ước dùng: PostgreSQL ~300 MB, Redis ~50 MB, ba tiến trình Node ~450 MB, Caddy ~30 MB.

4.8 Cấu trúc mã nguồn (monorepo, pnpm workspaces)

cong-gui-ve/
  apps/
    api/              NestJS: nạp lõi + các mô-đun đang bật; REST + SSE
    worker/           NestJS không HTTP: chạy job BullMQ của lõi và mô-đun, cron
    worker-zalo/      tiến trình riêng của mô-đun zalo-ca-nhan (zca-js)
    web/              React + Vite + AntD: vỏ giao diện lõi + màn hình do mô-đun đóng góp
  packages/
    core/             thực thể, máy trạng thái, bus sự kiện, quyền, vết, cấu hình, registry mô-đun
    core-contracts/   interface điểm mở rộng, kiểu sự kiện, phiên bản hợp đồng (semver)
    modules/
      vexere/  sms-gateway/  email/  zalo-oa/  zalo-ca-nhan/  xuat-ve/
      day-danh-sach/  api-nguon/  thong-ke/  doi-soat/
        (mỗi mô-đun: manifest.ts, src/, prisma/ (bảng mod_*), test/,
         README.md, HUONG-DAN-SU-DUNG.md, CHANGELOG.md)
    shared/           zod schema, chuẩn hoá SĐT/giờ/ngày, tiện ích dùng chung
    db/               gom lược đồ lõi + mô-đun, di trú, Prisma client
    vexere-client/    bọc API VeXeRe (dùng bởi mô-đun vexere), có mock
  deploy/             khoi-dong.cjs, cap-nhat.sh, nginx-them.conf (GĐ0); sao-luu.sh, khoi-phuc.sh,
                      kiem-tra-khoi-phuc.sh, .env.example, HUONG-DAN-AAPANEL.md (đã có)
  scripts/            phien-ban.py, dong-bo-git.ps1 / .sh, kiem-tra-tai-lieu.py (đã có)
  .githooks/          pre-commit kiểm tra tài liệu;  .github/workflows/ CI kiểm tra tài liệu
  package.json        nguồn phiên bản toàn hệ thống (semver)
  docs/
    thiet-ke/         tài liệu này, 01-phan-tich-cong-nghe-nen-tang.md + mỗi mô-đun một tệp + ADR
    huong-dan/        theo vai trò: dai-ly.md, dieu-hanh.md, quan-tri.md (+ ảnh chụp)
    ban-giao/         HANDOVER-LOG.md
    kinh-nghiem/      LESSONS.md
    tham-khao/        bản trích tài liệu VeXeRe/AMS
    CHANGELOG.md
  CLAUDE.md           chỉ dẫn cho AI: đọc tài liệu nào đầu phiên, cập nhật tài liệu nào khi đổi mã

Khung docs/, CLAUDE.md đã tạo sẵn trong repo này; tài liệu thiết kế hiện nằm ở docs/00-…md và sẽ chuyển vào docs/thiet-ke/ khi khởi tạo monorepo.

4.9 Lõi quản lý vé và mô-đun cắm/rút

Ý tưởng: lõi là "hệ điều hành vé", mô-đun là "ứng dụng" cài lên. Lõi không biết VeXeRe, không biết SMS; nó chỉ biết vé, trạng thái, sự kiện, quyền, vết. Một mô-đun nâng cấp, thêm hay gỡ không làm lõi và mô-đun khác phải đổi mã.

flowchart TB
    subgraph LOI[Lõi quản lý vé — packages/core]
        TT[Thực thể & máy trạng thái vé]
        SK[Bus sự kiện ve.*]
        QQ[Quyền · phiên · vết · cấu hình]
        RG[Registry mô-đun
manifest · phiên bản · bật/tắt · cô lập lỗi] HD[Hợp đồng — core-contracts
NhaCungCapCho · KenhGui · NguonVe · KhuonVe · BaoCao · TrangQuanTri] end subgraph MOD[Mô-đun cắm/rút — packages/modules/*] M1[vexere
NhaCungCapCho · BaoCao · webhook] M2[sms-gateway · email · zalo-oa · zalo-ca-nhan
KenhGui] M3[day-danh-sach · api-nguon
NguonVe] M4[xuat-ve
KhuonVe] M5[thong-ke · doi-soat
BaoCao] end M1 & M2 & M3 & M4 & M5 -. thực thi hợp đồng .-> HD HD --> RG SK -- phát sự kiện --> M1 & M2 & M4 & M5 M3 -- lệnh tạo/sửa vé --> TT M1 -- kết quả xếp chỗ --> TT M2 -- kết quả gửi --> TT

Lõi (packages/core) gồm:

  • Thực thể chuẩn: ve, dai_ly, nguoi_dung, tuyen, diem_don_tra, ve_lich_su, yeu_cau_thay_doi, gui_tin, su_kien, mo_dun, cai_dat (mục 5).
  • Máy trạng thái vé (mục 3.1) và các lệnh chuyển trạng thái có kiểm tra quyền: taoVe, dayVe, xepVe, tuChoi, xuatVe, ghiKetQuaGui, huyVe, suaVe, khoaVe, moKhoaVe.
  • Bus sự kiện: ve.tao, ve.day, ve.xep, ve.tu_choi, ve.xuat, ve.gui, ve.sua, ve.huy, ve.het_han, ve.khoa; trong tiến trình qua event-emitter, liên tiến trình qua BullMQ; mọi sự kiện lưu bảng su_kien để phát lại cho mô-đun bật muộn hoặc xử lý lỗi.
  • Nhà cung cấp chỗ mặc định thu-cong (điều hành nhập ghế/mã tay) nằm trong lõi, nên lõi tự chạy được khi không có mô-đun nào.
  • Registry mô-đun: đọc manifest (tên, phiên bản, yeuCauLoi: ">=1.2 <2", điểm mở rộng cung cấp, quyền cần, bảng sở hữu, di trú, trang cấu hình), bật/tắt theo bảng mo_dun, kiểm tra tương thích khi khởi động, cô lập lỗi (mô-đun ném lỗi → ghi nhật ký, đánh dấu lỗi, lõi và mô-đun khác chạy tiếp).
  • Dịch vụ chung mô-đun gọi qua interface: quyền, phiên, vết, cấu hình (có mã hoá bí mật), nhật ký, hàng đợi, SSE.

Sáu điểm mở rộng (interface TypeScript trong packages/core-contracts):

Điểm mở rộng Hợp đồng tối thiểu Cài đặt
NhaCungCapCho xep(ve) → {ghe, maChoNgoai, giaNcc, hetHan?}, huy(ve) → {phi, hoan}, sua(ve, thayDoi), traCuu(ve) thu-cong (lõi), vexere; sau này BMS khác
KenhGui gui(tin) → {maNgoai}, nhanTrangThai(payload) → {maNgoai, trangThai}, gioiHan() sms-gateway, email, zalo-oa, zalo-ca-nhan
NguonVe nhan(duLieu) → lệnh taoVe/suaVe/huyVe + baoNguoc(suKien) form (lõi), day-danh-sach, api-nguon
KhuonVe dung(ve, kenh, loaiTin) → {vanBan, html?, png?, pdf?} xuat-ve
BaoCao danhSach() → [báo cáo], chay(thamSo) → bảng/tệp thong-ke, doi-soat, vexere (hạn mức, giao dịch)
TrangQuanTri Đóng góp mục menu, trang cấu hình, ô trạng thái sức khoẻ mọi mô-đun có cấu hình

Quy tắc ràng buộc (kiểm tra bằng lint và CI):

  1. Mô-đun chỉ phụ thuộc core-contractsshared; không import mô-đun khác; mô-đun nói chuyện với nhau qua sự kiện lõi.
  2. Mô-đun sở hữu bảng riêng tiền tố mod_<ten>_, chỉ tham chiếu lõi qua ve_id/dai_ly_id/tuyen_id; không thêm cột vào bảng lõi; cần lưu thêm thì dùng thuoc_tinh_mo_rong (JSONB, khoá theo tên mô-đun).
  3. Phiên bản: lõi và từng mô-đun đánh semver riêng; lõi giữ tương thích ngược trong cùng phiên bản lớn; đổi hợp đồng = tăng phiên bản lớn kèm bảng chuyển đổi; mô-đun khai báo dải phiên bản lõi hỗ trợ; registry từ chối nạp mô-đun không tương thích và nêu rõ lý do.
  4. Di trú CSDL của mô-đun nằm trong thư mục mô-đun, chạy theo thứ tự lõi → mô-đun; tắt mô-đun không xoá dữ liệu; xoá dữ liệu là thao tác riêng có xác nhận.
  5. Kiểm thử hợp đồng: mỗi điểm mở rộng có bộ test chuẩn (ví dụ KenhGui: gửi thành công, thất bại, webhook trùng, quá nhịp); mô-đun mới phải qua bộ test đó trước khi được nạp.
  6. Bật/tắt không cần dựng lại; giao diện tự ẩn/hiện menu, trang cấu hình, cột báo cáo theo mô-đun đang bật.
  7. Mọi gọi từ lõi sang mô-đun có thời gian chờ và bắt lỗi; mô-đun chậm/hỏng không chặn máy trạng thái.

Ví dụ vận hành: nhà xe muốn thử kênh Telegram → viết mô-đun telegram thực thi KenhGui, chạy bộ test hợp đồng, bật trong quản trị; lõi, VeXeRe, SMS không đổi một dòng. Nâng lõi 1.x → 2.0 vì đổi hợp đồng NhaCungCapCho → chỉ mô-đun vexere phải cập nhật; các kênh gửi vẫn chạy.

4.10 Tài liệu đồng bộ với mã

Nguyên tắc: một thay đổi chưa xong nếu tài liệu chưa đổi theo. Bốn bộ tài liệu sống trong kho mã, cùng nhánh, cùng lần cập nhật với mã:

Bộ Ở đâu Nội dung Ai đọc
Thiết kế docs/thiet-ke/ (tài liệu này + mỗi mô-đun một tệp + ADR) Kiến trúc, hợp đồng, dữ liệu; ADR ngắn cho mỗi quyết định: bối cảnh, quyết định, hệ quả Người phát triển, AI
Hướng dẫn sử dụng docs/huong-dan/ theo vai trò + HUONG-DAN-SU-DUNG.md trong từng mô-đun Từng màn hình, thao tác, ảnh chụp; mục "Có gì mới" theo phiên bản Đại lý, điều hành, quản trị; nút ? trong app trỏ đúng mục
Bàn giao docs/ban-giao/HANDOVER-LOG.md Việc đang dở, cách chạy, vị trí cấu hình (không chứa bí mật), điều cần biết khi tiếp quản; xoá mục khi xong Người tiếp quản, AI đầu phiên
Kinh nghiệm docs/kinh-nghiem/LESSONS.md Lỗi đã gặp và cách xử lý, quyết định đã đảo và vì sao Người phát triển, AI

Cơ chế bảo đảm đồng bộ toàn hệ thống:

  1. Định nghĩa hoàn thành cho mọi thay đổi: mã + test + di trú + cập nhật đúng bộ tài liệu bị ảnh hưởng + một dòng CHANGELOG.md (theo Keep a Changelog, ghi phiên bản lõi/mô-đun).
  2. CI chặn: kịch bản kiểm tra khi thay đổi đụng packages/core, packages/modules/<x> hoặc apps/web mà không đụng docs/ hay README/HUONG-DAN-SU-DUNG của mô-đun tương ứng thì thất bại, trừ khi gắn nhãn khong-doi-tai-lieu kèm lý do. Mẫu PR có ô kiểm bốn bộ tài liệu.
  3. Sinh tự động những gì sinh được, không viết tay hai lần: OpenAPI từ NestJS Swagger → docs/thiet-ke/api.md; ERD từ lược đồ Prisma; bảng mô-đun và phiên bản từ manifest; sơ đồ trạng thái từ hằng số máy trạng thái; ma trận quyền từ định nghĩa CASL. Chạy trong CI, tệp sinh ra commit kèm.
  4. Phiên bản tài liệu = phiên bản phần mềm: đầu mỗi tài liệu ghi "áp dụng cho lõi x.y / mô-đun z"; trang Giới thiệu trong app hiện phiên bản và liên kết đúng bản hướng dẫn; nâng lõi lên phiên bản lớn mới thì CI bắt mọi mô-đun cập nhật dòng tương thích trong README.
  5. Đồng bộ với bộ nhớ AI: CLAUDE.md trong kho chỉ dẫn đầu phiên đọc HANDOVER-LOG.md + LESSONS.md, cuối phiên cập nhật; khớp thói quen bàn giao đang dùng ở các dự án khác.
  6. Rà soát định kỳ: mỗi lần phát hành chạy danh mục "tài liệu nào nhắc đến màn hình/API/bảng vừa đổi" (tìm theo tên) và duyệt; hằng quý đọc lại hướng dẫn cùng một đại lý thật.

5. Thiết kế dữ liệu

Dữ liệu chia hai lớp theo mục 4.9: bảng lõi (mô-đun chỉ đọc/ghi qua dịch vụ lõi) và bảng mô-đun (tiền tố mod_<ten>_, mô-đun toàn quyền, tham chiếu lõi qua ve_id/dai_ly_id/tuyen_id).

erDiagram
    DAI_LY ||--o{ NGUOI_DUNG : "có"
    DAI_LY ||--o{ VE : "sở hữu"
    TUYEN ||--o{ VE : "thuộc"
    TUYEN ||--o{ DIEM_DON_TRA : "có"
    VE ||--o{ VE_LICH_SU : "vết"
    VE ||--o{ YEU_CAU_THAY_DOI : "yêu cầu"
    VE ||--o{ GUI_TIN : "gửi"
    VE ||--o{ SU_KIEN : "phát"
    VE ||--o| MOD_VEXERE_DAT_CHO : "mô-đun vexere"
    VE ||--o{ MOD_VEXERE_GIAO_DICH : "mô-đun vexere"
    MOD_DAY_DANH_SACH_LO ||--o{ VE : "mô-đun đẩy danh sách"
    MOD_DOI_SOAT_KY ||--o{ MOD_DOI_SOAT_CT : "gồm"
    MOD_DOI_SOAT_CT }o--|| VE : "ghi nhận"

Bảng lõi

Bảng Cột chính
dai_ly id, ma, ten, sdt, email, loai (dai_ly / nguon_he_thong), hoa_hong_json, kenh_mac_dinh_json, tu_duyet, api_key_hash, trang_thai, thuoc_tinh_mo_rong, tao_luc
nguoi_dung id, dai_ly_id (null = nhà xe), ten_dang_nhap, mat_khau_hash, ho_ten, vai_tro (quan_tri / dieu_hanh / dai_ly_chu / dai_ly_nv), trang_thai
tuyen id, ten, tu_ten, den_ten, gio_json, dung_sai_phut, che_do_xep (tu_dong / ban_tu_dong / thu_cong), tat_ban_truoc_phut, thuoc_tinh_mo_rong
diem_don_tra id, tuyen_id, loai (don / tra / trung_chuyen), ten, dia_chi, phu_thu, thuoc_tinh_mo_rong
ve id, ma_ve, dai_ly_id, nguoi_tao_id, nguon (tên mô-đun NguonVe), ma_nguon, ten_khach, sdt, email, tuyen_id, ngay_di, gio_di, so_khach, loai_cho, diem_don_id, diem_don_text, diem_tra_id, diem_tra_text, ghi_chu, gia_ban, hinh_thuc_thu, kenh_json, trang_thai, ly_do_tu_choi, nha_cung_cap_cho (thu_cong / vexere / …), ma_cho_ngoai, ghe_json, xe_bien_so, xe_sdt, gio_don_du_kien, gia_ncc, hoa_hong, phi_huy, hoan_tien, khoa_luc, khoa_boi (mô-đun + lý do), thuoc_tinh_mo_rong, tao_luc, sua_luc, xoa_luc
ve_lich_su id, ve_id, nguoi_id, mo_dun, hanh_dong, truoc_json, sau_json, luc
yeu_cau_thay_doi id, ve_id, loai (sua / huy / doi_chuyen), noi_dung_json, phi_du_kien, trang_thai (cho_duyet / da_duyet / tu_choi), nguoi_duyet_id, ly_do, tao_luc, duyet_luc
gui_tin id, ve_id, kenh (tên mô-đun KenhGui), nguoi_nhan, loai_tin (ve_moi / cap_nhat / huy / xe_cua_ban), noi_dung, trang_thai, so_lan, loi, ma_ngoai, tao_luc, gui_luc, xem_luc
su_kien id, ten (ve.tao / ve.xep / …), ve_id, du_lieu_json, phat_luc, xu_ly_json (mô-đun nào đã xử lý, kết quả) — nhật ký sự kiện để phát lại
mo_dun ten, phien_ban, bat, cau_hinh_json (bí mật mã hoá), trang_thai, loi_cuoi, cap_nhat_luc — registry mô-đun
cai_dat khoá-giá trị cấu hình lõi; phiên đăng nhập lưu Redis, không có bảng

Bảng mô-đun

Mô-đun Bảng Cột chính
vexere mod_vexere_dat_cho ve_id, trip_code, booking_code, ticket_code, partner_code, status, expired_time, amount_booking, gia_dai_ly, is_deposit, deposit_status, dong_bo_luc
mod_vexere_giao_dich id, ve_id, loai (reserve / pay / refund / update), transaction_id, vxr_transaction_id, so_tien, phi, yeu_cau_json, dap_json, ma_loi, luc
mod_vexere_su_kien id, event_type, booking_code, event_timestamp, payload_json, chu_ky_hop_le, xu_ly_luc, ket_qua (UNIQUE event_type + booking_code + event_timestamp)
mod_vexere_nhat_ky id, endpoint, method, ma_http, thoi_gian_ms, tom_tat, luc
mod_vexere_anh_xa tuyen_id → from_id, to_id, company_id; diem_don_tra_id → point_id, ten_vxr, dong_bo_luc
sms-gateway mod_sms_thiet_bi id, ten, so_sim, tai_khoan_gateway, lan_cuoi_hoi, so_gui_hom_nay, tran_ngay, trang_thai
zalo-ca-nhan mod_zalo_tai_khoan id, ten, uid, phien_ma_hoa, trang_thai, lan_cuoi_song, nhip_json
xuat-ve mod_xuat_ve_mau id, kenh, loai_tin, ten, noi_dung, kich_hoat, phien_ban
day-danh-sach mod_day_danh_sach_lo id, dai_ly_id, nguoi_id, cach (excel / dan), ten_tep, so_dong, so_hop_le, so_loi, ket_qua_json, tao_luc
doi-soat mod_doi_soat_ky id, dai_ly_id, tu_ngay, den_ngay, so_ve, doanh_thu, hoa_hong, phai_nop, trang_thai (mo / da_chot / da_xac_nhan), tep_xlsx, chot_boi, chot_luc, xac_nhan_luc
mod_doi_soat_ct ky_id, ve_id, gia_ban, gia_ncc, hoa_hong, phi_huy, phai_nop, lech_json, ghi_chu
thong-ke (không bảng riêng) Materialized view làm mới theo lịch

Chỉ mục: ve(dai_ly_id, trang_thai), ve(ngay_di, gio_di), ve(sdt), ve(nguon, ma_nguon) UNIQUE, gui_tin(trang_thai, kenh), mod_vexere_dat_cho(booking_code), mod_vexere_su_kien(booking_code).


6. Giao diện

Đại lý (điện thoại là chính): Vé mới (form một cột, chọn nhanh tuyến/giờ đã dùng gần đây), Đẩy danh sách (tải tệp/dán, bảng xem trước sửa tại chỗ), Vé của tôi (thẻ vé, màu theo trạng thái, lọc ngày đi, tìm SĐT, nút Sửa/Huỷ/Gửi lại tuỳ trạng thái), Thống kê của tôi, Đối soát (kỳ, biên bản, nút Xác nhận).

Điều hành (máy tính): Bàn xếp vé hai cột — trái là hàng đợi DA_DAY nhóm theo chuyến (ngày/giờ/tuyến), phải là chi tiết vé + sơ đồ ghế VeXeRe + nút Xác nhận/Đổi ghế/Từ chối/Xếp tay; Yêu cầu chờ duyệt; Tin gửi lỗi; Sắp hết hạn giữ chỗ (đếm ngược); Tra cứu mọi vé.

Quản trị: Tuyến & điểm (đồng bộ id VeXeRe), Đại lý & hoa hồng, Kênh gửi (trạng thái SIM, OSZalo, OA, SMTP; bộ đếm ngày), Khuôn tin (xem trước theo kênh), Người dùng & quyền, Nhật ký.

Khách: trang /v/{mã ngắn} hiện vé (không cần đăng nhập, mã ngắn khó đoán 8 ký tự), cập nhật theo trạng thái mới nhất, nút "Gọi nhà xe".

Điểm UX quan trọng: trạng thái đổi tức thời (SSE); thao tác trên điện thoại một tay; chữ to, nút to; màu trạng thái thống nhất toàn hệ thống.


7. Phân quyền và an toàn

Quyền Quản trị Điều hành Đại lý chủ Đại lý NV Nguồn hệ thống
Tạo/đẩy vé ✓ (đại lý mình) ✓ (đại lý mình) ✓ (API)
Sửa/xoá vé chưa xếp chỉ vé mình tạo
Yêu cầu sửa/huỷ vé đã xếp
Duyệt yêu cầu, xếp vé, gọi API VeXeRe nếu tu_duyet
Xem vé tất cả tất cả đại lý mình đại lý mình vé của nguồn
Thống kê tất cả tất cả đại lý mình đại lý mình
Tạo/chốt/mở kỳ đối soát xác nhận kỳ của mình
Cấu hình hệ thống, kênh, khuôn tin, người dùng quản lý NV của mình

An toàn:

  • Mật khẩu băm (scrypt/argon2), phiên cookie HttpOnly, khoá API đại lý băm; giới hạn lần đăng nhập sai.
  • Bí mật (client_secret VeXeRe, SMTP, OA token, khoá OSZalo, khoá SIM) trong .env, không commit; giá trị trong kenh_cau_hinh mã hoá bằng khoá máy chủ.
  • Mọi hành động ghi vào ve_lich_su/nhật ký; xoá là xoá mềm.
  • Dữ liệu cá nhân (SĐT, tên, email) theo Nghị định 13/2023/NĐ-CP: chỉ dùng cho việc đi xe; tự ẩn SĐT trong thống kê tổng hợp; lịch xoá/ẩn danh vé sau N tháng (mặc định 24) cấu hình được; thông báo xử lý dữ liệu trên trang tra cứu.
  • Webhook: chữ ký HMAC, chống phát lại theo event_timestamp, giới hạn tốc độ theo IP.
  • HTTPS bắt buộc (aaPanel + Let's Encrypt), CSDL sao lưu hằng ngày ra thư mục ngoài web root như Vé247, giữ 30 bản.

8. Vận hành

  • Triển khai: VPS Ubuntu + aaPanel. Ba Node project (cgv-api gắn domain đề xuất ve.xekhachhagiang.vn với Mapping + SSL, cgv-worker, cgv-zalo), Node 22 / PostgreSQL / Redis / Google Drive cài từ App Store. Cập nhật: git pull + Restart trong panel (tệp khởi động tự cài phụ thuộc, di trú, dựng web) hoặc nút Cập nhật trong app. Tệp .env chỉ có trên VPS. Chi tiết: tài liệu 01 mục 5, deploy/HUONG-DAN-AAPANEL.md.
  • Giám sát: /health (CSDL, Redis, token VeXeRe, hàng đợi, worker-zalo, SMS gateway lần liên hệ cuối, webhook nhận cuối, sao lưu cuối); aaPanel Monitor + Cron Access URL; bot Zalo/Telegram báo sự cố; Bull Board xem hàng đợi (chỉ quản trị). Cảnh báo khi: máy SIM im > 10 phút, tin lỗi > 5 liên tiếp, vé giữ chỗ sắp hết hạn, VeXeRe lỗi 5xx liên tục, pay lỗi (nghi hạn mức thấp), phiên Zalo đứt, sao lưu lỗi.
  • Hạn mức VeXeRe: không có API tra, nên hệ thống tự ước tính (nạp − đã thanh toán + đã hoàn) và quản trị nhập lại số thực từ AMS khi lệch.
  • Sao lưu/khôi phục: Cron aaPanel — Shell deploy/sao-luu.sh (02:00, pg_dump + gói cấu hình mã hoá) → Backup Directory lên Google Drive giữ 30 bản (02:40) → deploy/kiem-tra-khoi-phuc.sh hằng tuần; Redis không cần sao lưu; khôi phục bằng deploy/khoi-phuc.sh.
  • Nhật ký: pino JSON ra log Node project, Cron Cut Log giữ 30 tệp; che SĐT một phần.
  • Bảo mật máy chủ: chỉ mở 22 (khoá SSH, tắt mật khẩu), 80, 443; UFW + fail2ban; PostgreSQL/Redis chỉ trong mạng nội bộ Docker; cập nhật bảo mật hệ điều hành tự động.

9. Lộ trình và ước lượng

Giai đoạn Nội dung Ước lượng
GĐ0 — Nền tảng Monorepo, Docker Compose, PostgreSQL/Redis, lõi (thực thể, máy trạng thái, bus sự kiện, quyền, vết, registry mô-đun) + hợp đồng core-contracts + bộ test hợp đồng, khung NestJS + React, CI (kể cả kiểm tra tài liệu), bốn bộ tài liệu khởi tạo, VPS + tên miền + HTTPS, sao lưu tự động 1–1,5 tuần
GĐ1 — Lõi vé & gửi bảo đảm Form vé, đẩy danh sách (Excel/dán), bàn xếp vé thủ công, xuất vé, gửi SMS SIM (máy Android + app gateway) + Zalo cá nhân (worker-zalo), trang tra cứu, thống kê cơ bản, API liên thông nguồn 2–3 tuần
GĐ2 — Tự động với VeXeRe Bộ kết nối đầy đủ trên UAT, ánh xạ tuyến/điểm, xếp bán tự động → tự động, thanh toán, huỷ/sửa qua API, webhook, quét định kỳ, làm giàu biển số, email 2–3 tuần (+ thời gian VeXeRe cấp tài khoản PRO và đăng ký webhook)
GĐ3 — Đối soát & OA Kỳ đối soát, khoá kỳ, biên bản Excel, xác nhận đại lý, đối chiếu với báo cáo AMS, Zalo OA/ZNS, cảnh báo vận hành, ẩn danh dữ liệu 2 tuần

Ước lượng tính theo một người làm chính với Claude Code, thử nghiệm thật với 1–2 đại lý ngay cuối GĐ1.


10. Rủi ro và giảm thiểu

Rủi ro Mức Giảm thiểu
VeXeRe không cấp API đối tác cho nhà xe, hoặc cấp với chiết khấu/hạn mức bất lợi Cao Hỏi ngay (mục 11). GĐ1 chạy được không cần API (xếp thủ công); bộ kết nối là adapter, thay được bằng cách khác sau.
SIM thường bị nhà mạng chặn khi gửi nhiều Trung bình Nhịp chậm, trần ngày, nội dung cá nhân hoá, nhiều SIM luân phiên, dự phòng Zalo; theo dõi tỉ lệ DELIVERED.
Zalo cá nhân bị khoá do gửi tới người lạ Trung bình Chỉ gửi Zalo cá nhân cho khách đã chat/kết bạn; còn lại đi SMS; nhịp gửi tự đặt theo kinh nghiệm OSZalo; worker-zalo tách riêng, đăng nhập lại bằng QR.
Vé giữ chỗ hết hạn tự huỷ mà không ai thấy Cao Đồng hồ hết hạn, nhắc 2 lần, tự chuyển TU_CHOI và báo đại lý.
Tài liệu API có điểm không nhất quán (trạng thái 1/2) Thấp Kiểm chứng trên UAT; coi GET booking là chuẩn.
Đại lý nhập sai tuyến/giờ hàng loạt Trung bình Chọn từ danh sách, xem trước lô, cảnh báo trùng, giới hạn sửa sau khi xếp.
Sai lệch đối soát do huỷ/đổi sau khi chốt Trung bình Khoá kỳ; thay đổi sau chốt đi vào kỳ sau dưới dạng điều chỉnh có vết.
Vận hành thêm PostgreSQL/Redis/ba Node project trên aaPanel cho một người Thấp Mọi thứ cài từ App Store và Website của aaPanel, sao lưu và cảnh báo tự động bằng Cron, hướng dẫn từng bước trong deploy/HUONG-DAN-AAPANEL.md.
Ứng dụng SMS gateway mã nguồn mở đổi giao thức hoặc ngừng phát triển Thấp Bọc trong một adapter; phương án B là app Kotlin riêng theo cùng giao thức.

11. Câu hỏi cần chốt trước khi lập trình

  1. Tài khoản VeXeRe: nhà xe đã có (hoặc xin được) client_id/secret API đối tác chưa? Chiết khấu và hạn mức thế nào? Hay mỗi đại lý dùng tài khoản AMS riêng (khi đó hệ thống chỉ xếp thủ công và ghi nhận)?
  2. company_id của nhà xe trên VeXeRe và danh sách tuyến/giờ chính thức để dựng bảng ánh xạ.
  3. Thanh toán: đại lý thu tiền rồi hệ thống pay ngay (trừ hạn mức), hay chỉ giữ chỗ và thu tại xe? Ai chịu phí huỷ theo giá đại lý?
  4. Gửi SMS: mua một máy Android + SIM riêng đặt cố định (đề xuất) chạy ứng dụng SMS Gateway mã nguồn mở, hay tự viết app? Dự kiến bao nhiêu tin/ngày, mấy SIM?
  5. Zalo OA đã có và đã xác thực chưa? Có ngân sách ZNS không, hay để giai đoạn sau?
  6. Có để VeXeRe tự nhắn SMS/email cho khách song song không? Nếu không, customer_email gửi lên VeXeRe dùng hộp thư của nhà xe.
  7. VPSđã chốt 02/9/2026: VPS mới riêng, Ubuntu + aaPanel. Còn lại: nhà cung cấp, cấu hình (đề xuất 2 vCPU / 4 GB / 40 GB SSD, Ubuntu 24.04), tên miền (đề xuất ve.xekhachhagiang.vn), ai giữ quyền root?
  8. Hoa hồng đại lý: % hay số cố định, theo tuyến/loại chỗ? Kỳ đối soát tuần hay tháng?
  9. Nguồn tuỳ chọn: có cần Vé247/OSZalo/web đẩy vé vào cổng ngay GĐ1 không, hay để sau khi cổng ổn định?
  10. Đại lý được xoá vé tới trạng thái nào: chỉ trước khi xếp (đề xuất) hay cả sau khi xếp nếu chưa gửi khách?
  11. Có cần vé khứ hồivé cọc Lễ/Tết ở phiên bản 1 không?
  12. Bộ công nghệ: chốt theo mục 4.7 (NestJS + PostgreSQL + Redis/BullMQ + React/Vite + Docker Compose) hay muốn tối giản hơn (Fastify + SQLite một tiến trình, chấp nhận hạn chế đã nêu)?
  13. Mức "cắm/rút" mong muốn: mô-đun nằm trong cùng kho mã, bật/tắt bằng cấu hình không cần dựng lại (đề xuất, mục 4.9) hay cần cài/gỡ gói mô-đun lúc chạy như plugin thật (phức tạp và rủi ro hơn nhiều)?

Đã chốt ngày 02/9/2026 (trả lời trực tiếp): VPS mới riêng Ubuntu + aaPanel (câu 7); ứng dụng đại lý bản 1 là PWA cài từ trình duyệt; đồng bộ git: Claude tự chạy scripts/dong-bo-git.ps1 cuối mỗi lượt làm việc; AI bản 1: đọc danh sách vé từ văn bản dán và ảnh chụp (tài liệu 01, mục 6). Các câu còn lại vẫn mở.


Phụ lục A — Ánh xạ trạng thái VeXeRe ↔ hệ thống

VeXeRe status Ý nghĩa Trạng thái nội bộ Hành động
1 Reserve (giữ chỗ, có expired_time) DA_XEP (chưa thanh toán) Theo dõi hết hạn
4 Paid DA_XEP (đã thanh toán) Xuất & gửi
5 Refund/huỷ HUY Báo khách + đại lý, bút toán hoàn
is_deposit=1, deposit_status 1/2/3/4 Vé cọc: chưa có giá / chờ thu thêm / hoàn / xong Cờ phụ trên vé Báo đại lý thu thêm/hoàn theo surcharge_time_deadline

Phụ lục B — Khuôn tin mẫu

SMS (không dấu, ≤ 160 ký tự) XKH: Ve 17h30 02/9 Ha Giang-Ha Noi, 2 giuong A5 A6, don BX Ha Giang 17h15. Ma ve NO97T9. Xe 24B-123.45 0912xxxxxx. Xem: xkh.vn/v/AB12CD34

Zalo cá nhân / OA (có dấu, kèm ảnh vé)

🎫 VÉ XE KHÁCH HÀ GIANG
Khách: Nguyễn Văn A (0912…)
Chuyến: 17h30 Thứ Tư 02/9/2026 · Hà Giang → Hà Nội
Chỗ: 2 giường A5, A6 (tầng dưới)
Đón: 17h15 tại Bến xe Hà Giang
Xe: 24B-123.45 · 0912 xxx xxx
Mã vé: NO97T9 · Đã thanh toán 400.000đ
Huỷ trước 12 giờ không mất phí. Tra cứu: xkh.vn/v/AB12CD34

Email: tiêu đề Vé xe 17h30 02/9 Hà Giang → Hà Nội — mã NO97T9; thân HTML bảng như trên, mã QR nội bộ, nút "Xem vé".

Phụ lục C — Mẫu cột tệp đẩy danh sách

Cột Bắt buộc Ví dụ Ghi chú
SĐT 0912345678 10 số, tự bỏ khoảng trắng/dấu chấm
Tên khách Nguyễn Văn A
Số khách 2 1–8/dòng
Loại chỗ giường / ghế / cabin Chuẩn hoá như Vé247
Tuyến Hà Giang - Hà Nội Khớp danh sách tuyến
Ngày đi 02/9/2026 Cho phép 2/9, 02-09-2026
Giờ đi 17h30 Khớp lịch chuyến
Điểm đón BX Hà Giang Khớp danh sách; không khớp → cảnh báo
Điểm trả Mỹ Đình
Giá bán 200000 Trống → giá mặc định tuyến
Đã thu dai_ly / xe / ck Mặc định theo đại lý
Email a@gmail.com Khi gửi email
Kênh gửi zalo,sms Trống → mặc định đại lý
Ghi chú Người già, cần tầng dưới Gửi nhà xe