Bỏ qua để đến nội dung

Tích hợp Hóa đơn điện tử

Tài liệu này là đặc tả riêng cho ADR-005. Quyết định kiến trúc (adapter pattern, độc lập nhà cung cấp, chỉ dùng nhà cung cấp được CQT chấp thuận) được ghi lại trong architecture.md §7. Tài liệu này bao gồm phần triển khai: giao diện adapter, các nhà cung cấp được hỗ trợ, mô hình dữ liệu, cấu hình tenant, vòng đời trạng thái và xử lý lỗi.


Adapter pattern độc lập nhà cung cấp. Mỗi nhà cung cấp được CQT chấp thuận có adapter triển khai riêng sau giao diện IEInvoiceProvider chung. Tenant cấu hình nhà cung cấp ưa thích của họ. Nền tảng resolve adapter đúng vào runtime từ cấu hình tenant. Không có logic đặc thù của nhà cung cấp nào rò rỉ vào mã domain.


Pháp luật Việt Nam (Nghị định 123/2020/NĐ-CP, Thông tư 78/2021/TT-BTC) yêu cầu doanh nghiệp phải phát hành hóa đơn điện tử có chữ ký số thông qua nhà cung cấp bên thứ ba được CQT chấp thuận. CQT duy trì danh sách các nhà cung cấp đã được chấp thuận; hiện có hơn một chục đơn vị. Mỗi nhà cung cấp có API riêng để gửi, kiểm tra trạng thái, điều chỉnh và thay thế hóa đơn — nhưng tất cả đều phải tạo ra hóa đơn tuân thủ cùng định dạng pháp lý và schema XML do CQT quy định.

BPO ERP phục vụ 50–100 khách hàng hiện tại đã có hợp đồng với các nhà cung cấp khác nhau. Nền tảng không thể yêu cầu dùng một nhà cung cấp duy nhất. Adapter pattern cho phép hỗ trợ nhà cung cấp mới bằng cách thêm một lớp adapter mà không cần sửa bất kỳ logic domain hay nghiệp vụ nào.


Các yêu cầu sau là bắt buộc và phải được đáp ứng bất kể nhà cung cấp nào được sử dụng:

Yêu cầuCăn cứ pháp lý
Hóa đơn phải được phát hành qua nhà cung cấp được CQT chấp thuậnNghị định 123/2020, Điều 10
Hóa đơn phải chứa: MST người bán, MST người mua, ký hiệu hóa đơn, số hóa đơn, ngày phát hành, chi tiết hàng hóa/dịch vụ, đơn giá, thuế suất VAT, tiền thuế VAT, tổng tiềnThông tư 78/2021, Phụ lục
Hóa đơn phải có chữ ký số của nhà cung cấp (và tùy chọn của người bán)Nghị định 123/2020, Điều 22
Khi hóa đơn đã ký có sai sót, người bán phải phát hành hóa đơn điều chỉnh để sửa các trường cụ thể, hoặc hóa đơn thay thế để hủy và phát hành lại toàn bộ. Cả hai loại phải tham chiếu số và ký hiệu hóa đơn gốc. Hóa đơn đã phát hành không thể hủy trực tiếp.Nghị định 123/2020, Điều 19
Dữ liệu hóa đơn phải được lưu trữ tối thiểu 10 nămLuật Kế toán 2015, Điều 40
Toàn bộ dữ liệu hóa đơn điện tử phải được lưu tại Việt NamLuật An ninh mạng 2018

Nền tảng quản lý vòng đời tích hợp. Nhà cung cấp thực hiện ký số và truyền lên CQT. BPO ERP chịu trách nhiệm: xây dựng payload hóa đơn đúng, gửi đến nhà cung cấp phù hợp, theo dõi trạng thái và lưu trữ kết quả.


Các nhà cung cấp sau được CQT chấp thuận và là mục tiêu triển khai adapter Phase 1. Thứ tự ưu tiên dựa trên hợp đồng của khách hàng hiện tại.

Nhà cung cấpTrạng thái
VNPT InvoiceVNPTPhase 1
Viettel e-InvoiceVIETTELPhase 1
MISA AMIS e-InvoiceMISAPhase 1
Bkav e-InvoiceBKAVPhase 1
EasyInvoiceEASYPhase 1
MInvoiceMINVOICEPhase 1
FPT e-InvoiceFPTPhase 2 (nếu có nhu cầu)

Trước khi triển khai: xác nhận danh sách nhà cung cấp được CQT chấp thuận hiện tại tại https://www.gdt.gov.vn — danh sách thay đổi khi có cấp phép mới hoặc thu hồi. Adapter chỉ được xây dựng cho nhà cung cấp có trong danh sách được chấp thuận tại thời điểm tích hợp.

Để thêm nhà cung cấp mới chỉ cần: một lớp adapter mới triển khai IEInvoiceProvider và một hàng trong bảng EInvoiceProviders. Không cần thay đổi mã domain hay ứng dụng.


public interface IEInvoiceProvider
{
/// Mã nhà cung cấp ổn định khớp với EInvoiceProviders.ProviderCode
string ProviderCode { get; }
/// Gửi hóa đơn đến nhà cung cấp để ký và phát hành.
/// Trả về ID hóa đơn được nhà cung cấp gán và trạng thái ban đầu.
Task<IssueResult> IssueInvoiceAsync(
InvoiceRequest request,
TenantProviderConfig config,
CancellationToken ct);
/// Kiểm tra trạng thái hiện tại của hóa đơn đã gửi trước đó.
Task<StatusResult> GetInvoiceStatusAsync(
string providerInvoiceId,
TenantProviderConfig config,
CancellationToken ct);
/// Phát hành hóa đơn thay thế thay thế hoàn toàn hóa đơn gốc đã ký.
/// Hóa đơn gốc được đánh dấu Replaced; hóa đơn mới trải qua vòng đời ký bình thường.
Task<ReplaceResult> ReplaceInvoiceAsync(
string originalProviderInvoiceId,
InvoiceRequest replacementRequest,
TenantProviderConfig config,
CancellationToken ct);
/// Phát hành hóa đơn điều chỉnh sửa các trường cụ thể của hóa đơn gốc đã ký.
Task<AdjustResult> AdjustInvoiceAsync(
string originalProviderInvoiceId,
AdjustmentRequest request,
TenantProviderConfig config,
CancellationToken ct);
}

TenantProviderConfig mang thông tin xác thực đã giải mã cho tài khoản nhà cung cấp của tenant cụ thể (API key, endpoint, mã mẫu, tiền tố ký hiệu). Nó được resolve từ cơ sở dữ liệu khi bắt đầu mỗi thao tác và không bao giờ được cache lâu hơn thời gian sống của request.

InvoiceRequest mang tất cả các trường hóa đơn bắt buộc theo pháp luật ở định dạng trung lập với nhà cung cấp. Mỗi adapter ánh xạ định dạng này sang định dạng API cụ thể của nhà cung cấp. Mã domain xây dựng InvoiceRequest mà không cần biết nhà cung cấp nào sẽ nhận nó.


EInvoiceProviders
────────────────────────────────────────────────────────
ProviderId UNIQUEIDENTIFIER PK
ProviderCode NVARCHAR(20) NOT NULL UNIQUE -- 'VNPT', 'VIETTEL', v.v.
ProviderName NVARCHAR(100) NOT NULL
IsActive BIT NOT NULL
SupportedOps NVARCHAR(MAX) NULL
-- Mảng JSON các thao tác được hỗ trợ: ['Issue','Replace','Adjust','StatusPoll']
-- Nhà cung cấp không hỗ trợ kiểm tra trạng thái thời gian thực (chỉ batch) được đánh dấu ở đây.
TenantEInvoiceConfigs
────────────────────────────────────────────────────────
ConfigId UNIQUEIDENTIFIER PK
TenantId UNIQUEIDENTIFIER NOT NULL (RLS)
ProviderId UNIQUEIDENTIFIER NOT NULL FK → EInvoiceProviders
Environment NVARCHAR(20) NOT NULL -- 'Production' hoặc 'Sandbox'
ApiEndpoint NVARCHAR(500) NOT NULL
ApiKey NVARCHAR(MAX) NOT NULL -- mã hóa khi lưu (column-level)
TemplateCode NVARCHAR(100) NULL -- ID mẫu hóa đơn đặc thù của nhà cung cấp
SerialPrefix NVARCHAR(20) NOT NULL -- ví dụ: 'C23T' theo nhà cung cấp/CQT
IsDefault BIT NOT NULL -- một mặc định mỗi tenant
IsActive BIT NOT NULL

Hỗ trợ nhiều cấu hình mỗi tenant: tenant có thể có cấu hình production và sandbox, hoặc cấu hình cho các nhà cung cấp khác nhau cho các loại chứng từ khác nhau (không phổ biến nhưng có thể).

EInvoices
────────────────────────────────────────────────────────
InvoiceId UNIQUEIDENTIFIER PK
TenantId UNIQUEIDENTIFIER NOT NULL (RLS)
SourceDocumentType NVARCHAR(50) NOT NULL -- 'CustomerInvoice', 'CreditNote'
SourceDocumentId UNIQUEIDENTIFIER NOT NULL FK → bảng nguồn
ConfigId UNIQUEIDENTIFIER NOT NULL FK → TenantEInvoiceConfigs
ProviderInvoiceId NVARCHAR(200) NULL -- ID nhà cung cấp gán sau khi gửi
InvoiceSerial NVARCHAR(20) NOT NULL -- ký hiệu do CQT gán (ví dụ: 'C23T')
InvoiceNumber NVARCHAR(20) NULL -- số do nhà cung cấp gán khi ký
InvoiceType NVARCHAR(20) NOT NULL DEFAULT 'Standard'
-- 'Standard' | 'Adjustment' | 'Replacement'
OriginalInvoiceId UNIQUEIDENTIFIER NULL FK → EInvoices
-- set cho loại Adjustment và Replacement; tham chiếu hóa đơn đang được sửa
Status NVARCHAR(30) NOT NULL
-- 'Draft' | 'Submitted' | 'Signed' | 'Issued' | 'Adjusted' | 'Replaced' | 'Failed'
IssuedAt DATETIMEOFFSET NULL -- khi nhà cung cấp ký và phát hành
SubmittedAt DATETIMEOFFSET NULL -- khi nền tảng gửi đến nhà cung cấp
LastPolledAt DATETIMEOFFSET NULL
RetryCount INT NOT NULL DEFAULT 0
ProviderResponseRaw NVARCHAR(MAX) NULL -- phản hồi thô của nhà cung cấp (JSON/XML)
CreatedAt DATETIMEOFFSET NOT NULL
EInvoiceStatusHistory
────────────────────────────────────────────────────────
HistoryId UNIQUEIDENTIFIER PK
InvoiceId UNIQUEIDENTIFIER NOT NULL FK → EInvoices
TenantId UNIQUEIDENTIFIER NOT NULL (RLS)
FromStatus NVARCHAR(30) NOT NULL
ToStatus NVARCHAR(30) NOT NULL
ChangedAt DATETIMEOFFSET NOT NULL
Notes NVARCHAR(500) NULL -- thông báo lỗi hoặc ghi chú của nhà cung cấp

┌─────────┐
│ Draft │ (hóa đơn nguồn đã tạo, chưa gửi)
└────┬────┘
│ IssueInvoiceAsync() được gọi
┌───────────┐
│ Submitted │ (đã gửi đến nhà cung cấp; chờ ký)
└─────┬─────┘
│ Nhà cung cấp ký
┌────────┐
│ Signed │ (đã ký bởi nhà cung cấp; CQT được thông báo)
└───┬────┘
│ CQT xác nhận / nhà cung cấp xác nhận
┌─────────┐
│ Issued │ ◄─── trạng thái thành công cuối cùng
└────┬────┘
┌────────┴────────┐
│ │
▼ ▼
┌──────────┐ ┌──────────┐
│ Replaced │ │ Adjusted │
└──────────┘ └──────────┘
(cả hai đều là trạng thái cuối cùng của hóa đơn gốc; hóa đơn thay thế/điều chỉnh
là một hàng EInvoices mới với vòng đời Draft → Issued riêng)
Failed ◄── bất kỳ chuyển đổi nào cũng có thể dẫn đến đây khi có lỗi nhà cung cấp không thể phục hồi

Các chuyển đổi được ghi lại trong EInvoiceStatusHistory. Cột EInvoices.Status phản ánh trạng thái hiện tại. Trạng thái chỉ được cập nhật thông qua EInvoiceService — không bao giờ trực tiếp.


TenantEInvoiceConfig mặc định của tenant được sử dụng trừ khi bị ghi đè. Thứ tự resolve:

  1. Nếu loại chứng từ có ánh xạ cấu hình rõ ràng (tương lai), dùng cấu hình đó.
  2. Nếu không, dùng cấu hình IsDefault = 1 của tenant.
  3. Nếu không có cấu hình active, thao tác phát hành thất bại với lỗi NoProviderConfigured — hiển thị cho người dùng trong UI trước khi thực hiện gửi.

Trường Environment trong cấu hình kiểm soát việc dùng thông tin xác thực sandbox hay production. Môi trường sandbox dùng trong quá trình onboard tenant và kiểm thử; chuyển sang production chỉ cần thay đổi một cài đặt cấu hình, không liên quan đến code.


CustomerInvoiceService.IssueEInvoice(invoiceId)
├─ Tải CustomerInvoice từ DB
├─ Xác thực hóa đơn ở trạng thái có thể gửi (đã xác nhận, chưa phát hành)
├─ Resolve TenantEInvoiceConfig (mặc định hoặc rõ ràng)
├─ Xây dựng InvoiceRequest (định dạng trung lập nhà cung cấp)
├─ Resolve IEInvoiceProvider từ DI container theo ProviderCode
├─ Tạo hàng EInvoices với Status = 'Draft'
├─ Gọi provider.IssueInvoiceAsync(request, config)
│ ├─ Thành công → cập nhật Status = 'Submitted', lưu ProviderInvoiceId
│ └─ Thất bại → cập nhật Status = 'Failed', lưu lỗi, publish sự kiện InvoiceIssueFailed
└─ Background job kiểm tra trạng thái tiếp quản cho các nhà cung cấp bất đồng bộ

Việc gửi không được tự động thử lại khi thất bại — domain service hiển thị lỗi cho người dùng để họ có thể thử lại thủ công sau khi sửa dữ liệu. Lỗi mạng tạm thời (timeout, 503) được thử lại với backoff theo cấp số nhân trong adapter, tối đa 3 lần, trước khi trả về thất bại cho domain service.


Một số nhà cung cấp xác nhận ký hóa đơn bất đồng bộ. Background job kiểm tra các hóa đơn đang chờ:

  • Tần suất: mỗi 5 phút cho các hóa đơn ở trạng thái Submitted
  • Số lần thử tối đa: 48 giờ kiểm tra (576 lần ở khoảng 5 phút), sau đó chuyển sang Failed với ghi chú ProviderTimeout
  • Khi thay đổi trạng thái: cập nhật EInvoices.Status, thêm vào EInvoiceStatusHistory, publish sự kiện domain EInvoiceStatusChanged
  • Khi Issued: kích hoạt sự kiện kế toán CustomerInvoiceIssued (nếu chưa được kích hoạt)

Hóa đơn đã phát hành ở Việt Nam không thể hủy trực tiếp. Sai sót được sửa bằng cách phát hành hóa đơn mới tham chiếu đến hóa đơn gốc. Nghị định 123/2020 Điều 19 định nghĩa hai loại:

Hóa đơn thay thế được dùng khi hóa đơn gốc có sai sót cần một chứng từ hoàn toàn mới (ví dụ: sai người mua, sai hàng hóa, sai loại hóa đơn). Hóa đơn thay thế tạo ra một hàng EInvoices mới với InvoiceType = 'Replacement'OriginalInvoiceId trỏ đến hóa đơn gốc. Hóa đơn thay thế trải qua vòng đời gửi tiêu chuẩn (Draft → Submitted → Signed → Issued). Khi hóa đơn thay thế đạt Issued, hóa đơn gốc được đánh dấu Replaced và trở thành bất biến.

Hóa đơn điều chỉnh được dùng khi chỉ cần sửa các trường cụ thể (ví dụ: số lượng, đơn giá, tiền thuế VAT). Hóa đơn điều chỉnh tạo ra một hàng EInvoices mới với InvoiceType = 'Adjustment'OriginalInvoiceId trỏ đến hóa đơn gốc. Khi đã phát hành, hóa đơn gốc được đánh dấu Adjusted và trở thành bất biến. Adapter phải tuân thủ các quy định đặc thù của từng nhà cung cấp về định dạng điều chỉnh, khác nhau giữa các nhà cung cấp.


Loại lỗiXử lý
Timeout API nhà cung cấp (<30 giây)Thử lại trong adapter (tối đa 3 lần, backoff theo cấp số nhân), sau đó trả về thất bại
Nhà cung cấp trả về lỗi validation (ví dụ: MST không hợp lệ)Hiển thị lỗi cho người dùng; không thử lại; sửa dữ liệu và gửi lại thủ công
Nhà cung cấp trả về lỗi xác thực (API key không hợp lệ)Cảnh báo quản trị viên tenant; tạm dừng gửi cho đến khi sửa cấu hình
Nhà cung cấp không khả dụng (5xx)Thử lại trong adapter; nếu hết lần thử, hiển thị thất bại; background job sẽ thử lại qua kiểm tra trạng thái
Hóa đơn đã gửi (trùng lặp)Adapter kiểm tra ProviderInvoiceId trước khi gửi; ghi log và hiển thị cảnh báo
CQT từ chối sau khi kýGhi nhận qua kiểm tra trạng thái; chuyển sang Failed; thông báo người dùng với mã lỗi CQT

Tất cả lỗi được lưu trong EInvoiceStatusHistory.Notes và phản hồi thô của nhà cung cấp trong EInvoices.ProviderResponseRaw để debug.


  • API key trong TenantEInvoiceConfigs.ApiKey được mã hóa khi lưu bằng SQL Server column-level encryption. Chúng chỉ được giải mã trong tiến trình server, không bao giờ trả về cho client hay ghi log.
  • Các lệnh gọi API nhà cung cấp sử dụng TLS 1.2+ và xác thực chứng chỉ nhà cung cấp.
  • Thông tin xác thực sandbox không bao giờ được dùng trong môi trường production — trường Environment được thực thi trong adapter.
  • Dịch vụ gửi hóa đơn điện tử chạy dưới danh tính người dùng tenant để ghi nhật ký kiểm toán, nhưng API key của nhà cung cấp không lấy từ JWT của người dùng — nó chỉ được tải từ cấu hình phía server.

  1. Hợp đồng API nhà cung cấp — API của từng nhà cung cấp phải được xem xét và ánh xạ sang InvoiceRequest trước khi xây dựng adapter. Tài liệu API cho VNPT, Viettel, MISA và FPT phải được lấy trực tiếp từ các nhà cung cấp.

  2. Quản lý dải số hóa đơn — CQT cấp dải số hóa đơn cho doanh nghiệp. Quy trình đăng ký số hóa đơn và quản lý dải theo tenant cần được thiết kế (có thể là quy trình quản trị viên tenant).

  3. Hóa đơn B2C và B2B — Yêu cầu pháp lý có khác biệt nhỏ giữa hóa đơn cho người tiêu dùng và doanh nghiệp (ví dụ: MST người mua là tùy chọn cho B2C dưới ngưỡng nhất định). Mô hình InvoiceRequest cần có cờ và các adapter phải xử lý điều này.

  4. Quản lý mẫu hóa đơn đặc thù nhà cung cấp — Một số nhà cung cấp yêu cầu tenant đăng ký trước mẫu hóa đơn. Trường TemplateCode trong TenantEInvoiceConfigs giải quyết điều này, nhưng luồng UI onboarding cần được thiết kế.

  5. Gửi nhiều hóa đơn cùng lúc — Một số nhà cung cấp hỗ trợ gửi batch để tăng hiệu suất. Điều này được hoãn cho đến khi gửi đơn lẻ ổn định.