Skip to content

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.

jobStatusNút Tạo sổXem / Tải vềTạo lại
nullHiện
103_PENDING
203_PROCESSING
303_COMPLETEDHiệnHiện
507_REJECTEDHiện
300_PARTIALMột phầnHiệ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.

http
GET /ledgers/status/batch
Query paramBắt buộcGhi chú
merchantId
yearMặc định: năm hiện tại
periodTypeMONTHLY | QUARTERLY | YEARLY. Mặc định: MONTHLY
typesPhâ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:

json
{
  "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 jobStatus507_REJECTED, failureReason là một object không null:

TrườngKiểuGhi chú
defaultstringThông báo lỗi đọc được; luôn có giá trị
enstring|nullBản dịch tiếng Anh nếu worker đặt
vistring|nullBản dịch tiếng Việt nếu worker đặt
errorCodestringMã 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, failureReasonnull. warnings liệt kê các loại sổ hoặc loại kỳ mà merchant chưa được cấu hình:

json
{
  "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ậcS1A-HKDS2B/S2C-HKDS2D-HKD
TIRE_1 (< 1 tỷ VND)MONTHLY, QUARTERLY, YEARLY
TIRE_2 (1–10 tỷ VND)MONTHLY, QUARTERLYQUARTERLYYEARLY
TIRE_3 (> 10 tỷ VND)MONTHLY, QUARTERLYMONTHLY hoặc QUARTERLYYEARLY

Search (phân trang, chỉ bản ghi đã tồn tại)

http
GET /ledgers/search
Query paramBắt buộcGhi chú
merchantId
yearMặc định: năm hiện tại
typeMột loại sổ duy nhất
pageMặc định: 1
size5–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ỳ

http
POST /ledgers/{ledgerType}/generate

Path param: ledgerType — ví dụ S1A-HKD.

Request body:

TrườngBắt buộcGhi chú
merchantId
periodTypeMONTHLY | QUARTERLY | YEARLY
periodValueTháng 1–12 cho MONTHLY; quý 1–4 cho QUARTERLY; bỏ qua cho YEARLY
yearMặc định: năm hiện tại

Response:

TrườngGhi chú
idLedger ID
typeLoại sổ
periodChuỗi kỳ — ví dụ 2026-M3, 2026-Q1, 2026-Y
actioncreated | skipped | retried — xem bên dưới
job.statusTrạng thái job ban đầu

Giá trị action:

Giá trịÝ nghĩa
createdJob mới được enqueue
skippedĐã có job đang 103_PENDING hoặc 203_PROCESSING cho kỳ này
retriedJob 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):

messageCodeHTTPKhi nào
server.core.ledger.tax_info_not_found404Merchant chưa cấu hình thông tin khai thuế
server.core.ledger.failed_to_get_fetcher_service500Khô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

http
POST /ledgers/generate/batch

Enqueue 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ườngBắt buộcGhi chú
merchantId
yearMặc định: năm hiện tại
periodTypeMặc định: MONTHLY
typesMảng loại sổ. Lấy từ cấu hình merchant nếu bỏ qua

Response:

json
{
  "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ổ

http
GET /ledgers/{id}/status

Trả 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ườngGhi chú
ledgerId
statusTrạng thái job hiện tại — xem Trạng thái job
attemptCountSố lần đã thử tạo
processStartAtTimestamp ISO khi worker bắt đầu
processCompletedAtTimestamp ISO khi worker hoàn thành
failureReasonKhá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 Client

Dị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:

typescript
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

typescript
// 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

typescript
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

typescript
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ỗi

Mỗ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ặc payload.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ị jobStatuspayload.failureReason làm thông tin lỗi.

5. Xem PDF trên trình duyệt

Yêu cầu jobStatus = 303_COMPLETED.

http
GET /ledgers/{id}/download/pdf?disposition=inline

Response: 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=inline là bắt buộc để hiển thị inline. Bỏ qua sẽ mặc định là attachment và kích hoạt hộp thoại tải về.

Lỗi khi job chưa hoàn thành (HTTP 400):

json
{
  "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.

http
GET /ledgers/{id}/download/pdf
GET /ledgers/{id}/download/xlsx

Response: 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.

http
POST /ledgers/{id}/regenerate

Điều kiện chặn — trả về 400 nếu:

  • Sổ đang ở 200_FINALIZED — dùng POST /{id}/revise để tạo phiên bản điều chỉnh trước
  • Sổ không ở 001_DRAFT
  • Job đang 103_PENDING hoặc 203_PROCESSING — chờ job hiện tại kết thúc

Response: { ledgerId, status, attemptCount }

So sánh Retry và Regenerate:

POST /{id}/retryPOST /{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 jobPhải là 507_REJECTEDKhông được là 103_PENDING hoặc 203_PROCESSING
Trường hợp dùngPhục hồi sau lỗiTạ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
nullChưa có job — kỳ này chưa được tạo
103_PENDINGĐã enqueue, chờ worker
203_PROCESSINGWorker đang tạo PDF + XLSX
303_COMPLETEDFile sẵn sàng
300_PARTIALMột định dạng thành công, một thất bại
507_REJECTEDTạ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.

errorCodePhân loạiKhi nào xảy raGợi ý hiển thị
FETCH_DATA_ERRORHệ thốngLấ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_FOUNDNghiệ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_SERVICEHệ thốngLỗ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_FAILEDHệ thốngKafka 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_FAILEDHệ thốngLỗ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 ModelArchitecture — Máy trạng thái.

Giá trịÝ nghĩa
DRAFTSử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.
ARCHIVEDBị thay thế bởi một revision đã finalize mới hơn
400_SUBMITTEDDà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

Proprietary and Confidential. Unauthorized copying, distribution, or use of this software is strictly prohibited.