Dịch vụ Sổ sách — Hướng dẫn tích hợp Client
Phạm vi: Hướng dẫn này bao gồm các REST API (danh sách kỳ, kích hoạt tạo sổ, xem PDF, tải file) và thông báo WebSocket thời gian thực để cập nhật trạng thái job trực tiếp. Vòng đời DRAFT/FINALIZED (chốt sổ, điều chỉnh, snapshot) được ghi nhận là tính năng tương lai và không cần thiết cho luồng này.
Base URL (dev):
https://sgw.develop.bana.com.vn/v1/api/ledgerXác thực:Authorization: Bearer <jwt>trong mọi request.
Trạng thái nút bấm trên UI
Trong luồng hiện tại, chỉ jobStatus được dùng để xác định nút nào hiển thị. ledgerStatus không được dùng trong UI hiện tại — xem Bảng tra cứu trạng thái để biết thêm.
jobStatus | Nút Tạo sổ | Xem / Tải về | Tạo lại |
|---|---|---|---|
null | Hiện | — | — |
103_PENDING | — | — | — |
203_PROCESSING | — | — | — |
303_COMPLETED | — | Hiện | Hiện |
507_REJECTED | — | — | Hiện |
300_PARTIAL | — | Một phần | Hiện |
1. Danh sách Sổ kế toán
Batch Status (chính)
Trả về tất cả kỳ kế toán dự kiến trong năm, kể cả các kỳ chưa được tạo.
GET /ledgers/status/batch| Query param | Bắt buộc | Ghi chú |
|---|---|---|
merchantId | ✓ | |
year | — | Mặc định: năm hiện tại |
periodType | — | MONTHLY | QUARTERLY | YEARLY. Mặc định: MONTHLY |
types | — | Phân cách bởi dấu phẩy, ví dụ S1A-HKD,S2A-HKD. Lấy từ MerchantLedgerConfig.requiredLedgerTypes nếu bỏ qua |
Response:
{
"warnings": [],
"items": [
{
"type": "S1A-HKD",
"period": "2026-M1",
"periodType": "MONTHLY",
"ledgerStatus": "001_DRAFT",
"jobStatus": "303_COMPLETED",
"ledgerId": "7601234567890002",
"attemptCount": 1,
"failureReason": null
},
{
"type": "S1A-HKD",
"period": "2026-M2",
"periodType": "MONTHLY",
"ledgerStatus": "001_DRAFT",
"jobStatus": "507_REJECTED",
"ledgerId": "7601234567890003",
"attemptCount": 1,
"failureReason": {
"default": "Parse error: missing field 'totalRevenue'",
"en": null,
"vi": null,
"errorCode": "FETCH_DATA_ERROR"
}
},
{
"type": "S1A-HKD",
"period": "2026-M4",
"periodType": "MONTHLY",
"ledgerStatus": null,
"jobStatus": null,
"ledgerId": null,
"attemptCount": null,
"failureReason": null
}
]
}ledgerId: null có nghĩa là kỳ này chưa được tạo sổ.
failureReason — chi tiết lỗi có cấu trúc
Khi jobStatus là 507_REJECTED, failureReason là một object không null:
| Trường | Kiểu | Ghi chú |
|---|---|---|
default | string | Thông báo lỗi đọc được; luôn có giá trị |
en | string|null | Bản dịch tiếng Anh nếu worker đặt |
vi | string|null | Bản dịch tiếng Việt nếu worker đặt |
errorCode | string | Mã lỗi máy đọc được — xem Mã lỗi |
Ưu tiên hiển thị: ưu tiên vi, nếu null thì dùng en, cuối cùng dùng default. errorCode có thể dùng để tra chuỗi hiển thị phía client khi trường ngôn ngữ phía server là null.
Với tất cả trạng thái job khác, failureReason là null. warnings liệt kê các loại sổ hoặc loại kỳ mà merchant chưa được cấu hình:
{
"warnings": [
"Ledger type S2A-HKD is not in your configuration",
"Period type YEARLY is not configured for ledger type S1A-HKD"
],
"items": []
}Loại kỳ được hỗ trợ theo bậc thuế:
| Bậc | S1A-HKD | S2B/S2C-HKD | S2D-HKD |
|---|---|---|---|
| TIRE_1 (< 1 tỷ VND) | MONTHLY, QUARTERLY, YEARLY | — | — |
| TIRE_2 (1–10 tỷ VND) | MONTHLY, QUARTERLY | QUARTERLY | YEARLY |
| TIRE_3 (> 10 tỷ VND) | MONTHLY, QUARTERLY | MONTHLY hoặc QUARTERLY | YEARLY |
Search (phân trang, chỉ bản ghi đã tồn tại)
GET /ledgers/search| Query param | Bắt buộc | Ghi chú |
|---|---|---|
merchantId | ✓ | |
year | — | Mặc định: năm hiện tại |
type | ✓ | Một loại sổ duy nhất |
page | — | Mặc định: 1 |
size | — | 5–50. Mặc định: 5 |
Trả về { data: Ledger[], count: number }. Chỉ hiển thị các kỳ đã được tạo ít nhất một lần.
2. Tạo sổ
Tạo một kỳ
POST /ledgers/{ledgerType}/generatePath param: ledgerType — ví dụ S1A-HKD.
Request body:
| Trường | Bắt buộc | Ghi chú |
|---|---|---|
merchantId | ✓ | |
periodType | ✓ | MONTHLY | QUARTERLY | YEARLY |
periodValue | — | Tháng 1–12 cho MONTHLY; quý 1–4 cho QUARTERLY; bỏ qua cho YEARLY |
year | — | Mặc định: năm hiện tại |
Response:
| Trường | Ghi chú |
|---|---|
id | Ledger ID |
type | Loại sổ |
period | Chuỗi kỳ — ví dụ 2026-M3, 2026-Q1, 2026-Y |
action | created | skipped | retried — xem bên dưới |
job.status | Trạng thái job ban đầu |
Giá trị action:
| Giá trị | Ý nghĩa |
|---|---|
created | Job mới được enqueue |
skipped | Đã có job đang 103_PENDING hoặc 203_PROCESSING cho kỳ này |
retried | Job 507_REJECTED trước đó được re-enqueue |
Lỗi kiểm tra trước (HTTP 4xx/5xx, trả về trước khi job được tạo):
messageCode | HTTP | Khi nào |
|---|---|---|
server.core.ledger.tax_info_not_found | 404 | Merchant chưa cấu hình thông tin khai thuế |
server.core.ledger.failed_to_get_fetcher_service | 500 | Không tìm thấy data fetcher cho loại sổ này (lỗi hệ thống) |
Các lỗi này được trả về ngay lập tức — không có bản ghi sổ hoặc job nào được tạo.
Tạo hàng loạt
POST /ledgers/generate/batchEnqueue tất cả tổ hợp loại sổ + kỳ hợp lệ cho merchant. Các loại hoặc kỳ không được cấu hình sẽ bị bỏ qua với cảnh báo.
Request body:
| Trường | Bắt buộc | Ghi chú |
|---|---|---|
merchantId | ✓ | |
year | — | Mặc định: năm hiện tại |
periodType | — | Mặc định: MONTHLY |
types | — | Mảng loại sổ. Lấy từ cấu hình merchant nếu bỏ qua |
Response:
{
"total": 5,
"created": 3,
"skipped": 0,
"retried": 0,
"failed": 0,
"validationFailed": 2,
"validationErrors": [
{
"type": "S1A-HKD",
"period": "2026-M5",
"errorCode": "server.core.ledger.tax_info_not_found",
"message": "Merchant tax info not found"
}
],
"warnings": []
}validationFailed đếm các item không qua kiểm tra trước và bị bỏ qua hoàn toàn (không tạo job). failed đếm các item qua kiểm tra nhưng lệnh enqueue gặp lỗi không mong đợi. Các item trong validationErrors không được tính vào failed.
3. Trạng thái tạo sổ
GET /ledgers/{id}/statusTrả về trạng thái hiện tại của job tạo sổ cho ledger ID đã cho.
Các trường trong response:
| Trường | Ghi chú |
|---|---|
ledgerId | |
status | Trạng thái job hiện tại — xem Trạng thái job |
attemptCount | Số lần đã thử tạo |
processStartAt | Timestamp ISO khi worker bắt đầu |
processCompletedAt | Timestamp ISO khi worker hoàn thành |
failureReason | Khác null chỉ khi 507_REJECTED |
Để cập nhật thời gian thực, ưu tiên dùng thông báo WebSocket thay vì polling. Nếu WebSocket không khả dụng, gọi lại endpoint này mỗi 2–3 giây và dừng khi trạng thái đạt 303_COMPLETED hoặc 507_REJECTED.
4. Theo dõi thời gian thực qua WebSocket
Dịch vụ sổ sách phát thông báo chuyển trạng thái job qua WebSocket thông qua dịch vụ Signal. Đây là phương án ưu tiên hơn polling — nhận cập nhật UI ngay lập tức mà không cần gọi HTTP lặp lại.
Cơ chế hoạt động
Ledger Worker
│ phát vào Redis pub/sub
▼
Signal Service
│ phân phối đến client trong room
▼
Browser ClientDịch vụ sổ sách không tự host WebSocket server. Nó phát sự kiện vào kênh Redis chung; Signal service phân phối đến tất cả client đang kết nối trong room liên quan.
Kết nối
Kết nối đến WebSocket của Signal bằng EncryptedWebSocketClient từ @nx-app/core. Xem Hướng dẫn Web Browser Client của Signal để biết cách thiết lập kết nối đầy đủ (trao đổi khóa ECDH, xác thực, kết nối lại, heartbeat).
Room và Topic
Sử dụng các hằng số từ apps/core/src/socket/constants.ts:
import { WebSocketRooms, WebSocketTopics } from '@nx-app/core';
// Một room cho mỗi merchant — nhận mọi sự kiện job sổ sách của merchant đó
const room = WebSocketRooms.LEDGER_PROCESS.replace('{merchantId}', merchantId);
// → 'wr:ledger/760001234/process'
// Được phát mỗi khi trạng thái job thay đổi
const topic = WebSocketTopics.LEDGER_JOB_STATUS;
// → 'ws:observation.ledger.job.status'Vào và rời room
// Sau khi EncryptedWebSocketClient phát sự kiện 'connected':
client.joinRooms({ rooms: [room] });
// Khi unmount component hoặc rời màn hình sổ sách:
client.leaveRooms({ rooms: [room] });Lắng nghe sự kiện
client.on({
event: WebSocketTopics.LEDGER_JOB_STATUS,
handler: (payload: TLedgerJobStatusPayload) => {
// Khớp theo ledgerId (hoặc type + period nếu chưa có ID)
updateLedgerRow(payload.ledgerId, payload.jobStatus, payload.failureReason);
},
});Cấu trúc payload
interface TLedgerJobStatusPayload {
ledgerId: string; // ID bản ghi sổ
merchantId: string; // Merchant mà sổ thuộc về
type: string; // ví dụ 'S1A-HKD'
period: string; // ví dụ '2026-M3'
jobStatus: string; // '103_PENDING' | '203_PROCESSING' | '303_COMPLETED' | '507_REJECTED'
attemptCount: number; // Số lần đã thử tạo
failureReason: { // Khác null chỉ khi jobStatus === '507_REJECTED'
default: string;
errorCode: string;
en: string | null;
vi: string | null;
} | null;
}Trình tự sự kiện
POST /generate
│
▼
'103_PENDING' ← job được enqueue, chờ worker xử lý
│
▼
'203_PROCESSING' ← worker bắt đầu
│
├──▶ '303_COMPLETED' file sẵn sàng, có thể tải về
└──▶ '507_REJECTED' failureReason mô tả lỗiMỗi lần chuyển trạng thái phát đúng một sự kiện. Sự kiện 103_PENDING có thể đến trước hoặc sau khi POST /generate trả về — xử lý sự kiện theo bất kỳ thứ tự nào.
Phạm vi room: Tất cả job của cùng một merchant dùng chung một room. Lọc sự kiện nhận được theo
payload.ledgerId(hoặcpayload.type+payload.period) để khớp với dòng cụ thể trong UI.
Dự phòng bằng polling
Nếu kết nối WebSocket không khả dụng, dùng dự phòng bằng cách gọi GET /ledgers/{id}/status mỗi 2–3 giây và dừng khi jobStatus đạt 303_COMPLETED hoặc 507_REJECTED.
Tích hợp với UI
Áp dụng logic tương tự như bảng Trạng thái nút bấm ở đầu hướng dẫn — dùng payload.jobStatus làm giá trị jobStatus và payload.failureReason làm thông tin lỗi.
5. Xem PDF trên trình duyệt
Yêu cầu jobStatus = 303_COMPLETED.
GET /ledgers/{id}/download/pdf?disposition=inlineResponse: dữ liệu nhị phân PDF (Content-Type: application/pdf, Content-Disposition: inline; filename="..."). Trình duyệt hiển thị trực tiếp.
Lưu ý: query param
disposition=inlinelà bắt buộc để hiển thị inline. Bỏ qua sẽ mặc định làattachmentvà kích hoạt hộp thoại tải về.
Lỗi khi job chưa hoàn thành (HTTP 400):
{
"messageCode": "server.core.ledger.fetch_data_error",
"message": "Parse error: missing field 'totalRevenue'",
"extra": {
"failureReason": {
"default": "Parse error: missing field 'totalRevenue'",
"errorCode": "FETCH_DATA_ERROR"
}
}
}messageCode phản ánh lỗi cụ thể (ví dụ server.core.ledger.fetch_data_error) thay vì mã chung server.core.ledger.job_not_ready. Vì endpoint yêu cầu header Authorization, không thể dùng <iframe src> hay <a href> trực tiếp — client phải fetch binary qua JS và xử lý response (Blob URL).
6. Tải về PDF hoặc XLSX
Yêu cầu jobStatus = 303_COMPLETED.
GET /ledgers/{id}/download/pdf
GET /ledgers/{id}/download/xlsxResponse: file nhị phân (Content-Disposition: attachment; filename="...").
Định dạng tên file: {loại}_{kỳ}_v{phiên bản}.{định dạng} — ví dụ S1A-HKD_2026-M3_v1.pdf.
Tương tự endpoint xem PDF, client phải fetch qua JS với header Authorization.
7. Tạo lại sổ (Regenerate)
Tạo lại file cho sổ đang ở trạng thái 001_DRAFT.
POST /ledgers/{id}/regenerateĐiều kiện chặn — trả về 400 nếu:
- Sổ đang ở
200_FINALIZED— dùngPOST /{id}/reviseđể tạo phiên bản điều chỉnh trước - Sổ không ở
001_DRAFT - Job đang
103_PENDINGhoặc203_PROCESSING— chờ job hiện tại kết thúc
Response: { ledgerId, status, attemptCount }
So sánh Retry và Regenerate:
POST /{id}/retry | POST /{id}/regenerate | |
|---|---|---|
| Yêu cầu trạng thái sổ | Bất kỳ | Phải là 001_DRAFT |
| Yêu cầu trạng thái job | Phải là 507_REJECTED | Không được là 103_PENDING hoặc 203_PROCESSING |
| Trường hợp dùng | Phục hồi sau lỗi | Tạo lại sau khi dữ liệu nguồn thay đổi |
8. Bảng tra cứu trạng thái
Trạng thái job (jobStatus)
Đây là trạng thái duy nhất UI hiện tại cần dùng để điều khiển hiển thị nút bấm.
| Giá trị | Ý nghĩa |
|---|---|
null | Chưa có job — kỳ này chưa được tạo |
103_PENDING | Đã enqueue, chờ worker |
203_PROCESSING | Worker đang tạo PDF + XLSX |
303_COMPLETED | File sẵn sàng |
300_PARTIAL | Một định dạng thành công, một thất bại |
507_REJECTED | Tạo sổ thất bại — failureReason có chi tiết |
Mã lỗi (failureReason.errorCode)
Các giá trị này xuất hiện trong failureReason.errorCode khi worker bắt được lỗi đã biết.
errorCode | Phân loại | Khi nào xảy ra | Gợi ý hiển thị |
|---|---|---|---|
FETCH_DATA_ERROR | Hệ thống | Lấy dữ liệu từ service nguồn thất bại, hoặc dữ liệu không khớp schema | "Lỗi lấy dữ liệu — vui lòng thử lại" |
MERCHANT_TAX_INFO_NOT_FOUND | Nghiệp vụ | Thông tin khai thuế của merchant chưa được cấu hình | "Thiếu thông tin thuế — vui lòng cấu hình trước" |
FAILED_TO_GET_DATA_FETCHER_SERVICE | Hệ thống | Lỗi nội bộ: không tìm thấy service lấy dữ liệu cho loại sổ này | "Lỗi hệ thống — liên hệ hỗ trợ" |
ENQUEUE_FAILED | Hệ thống | Kafka producer không thể phát tin nhắn ledger.generate; job bị từ chối trước khi đến worker | "Không thể bắt đầu tạo sổ — vui lòng thử lại" |
JOB_EXECUTION_FAILED | Hệ thống | Lỗi không xác định trong quá trình thực thi worker, không thuộc mã lỗi cụ thể nào | "Tạo sổ thất bại không xác định — vui lòng thử lại hoặc liên hệ hỗ trợ" |
Trạng thái sổ (ledgerStatus)
ledgerStatus phản ánh vòng đời của chính bản ghi sổ (không phải job tạo). Nó được trả về trong response batch status và search. Luồng tạo/xem/tải chỉ cần job status ở trên; vòng đời finalize/revise được ghi lại làm tài liệu tham chiếu — xem Domain Model và Architecture — Máy trạng thái.
| Giá trị | Ý nghĩa |
|---|---|
DRAFT | Sửa được — có thể tạo file mới. Trạng thái mặc định. |
200_FINALIZED | Đã khoá bởi người dùng. Dùng POST /ledgers/{id}/revise để mở một revision mới. |
ARCHIVED | Bị thay thế bởi một revision đã finalize mới hơn |
400_SUBMITTED | Dành chỗ — đã nộp lên cơ quan thuế (chưa hiện thực) |
Trang liên quan
- API Events — tham chiếu payload Kafka/WS
- Generation Pipeline — luồng phía server
- REST endpoints — OpenAPI trực tiếp tại
/v1/api/ledger/doc/openapi.json