Domain Model
Schema + repository do
@nx/coresở hữu. Nguồn:packages/core/src/models/schemas/invoice/*và.../tax/tax-info/*. SchemaInvoicelàinvoice;TaxInfonằm trong schema dùng chungtax. Không có FK xuyên schema — chỉInvoiceIssuance/InvoiceAuditTracingkhai báo FK (cả hai tớiInvoice).
1. ERD đầy đủ
2. Entities
Invoice
| Thuộc tính | Giá trị |
|---|---|
| Table | invoice.Invoice |
| Source | packages/core/src/models/schemas/invoice/invoice/schema.ts |
| Soft-delete | có |
| Owner column | invoiceConfigMappingId (→ merchant qua mapping) |
Trường (chọn lọc):
| Trường | Kiểu | Bắt buộc | Mặc định | Mô tả |
|---|---|---|---|---|
id | text | ✓ | Snowflake | PK |
sourceType | text | ✓ | 001_SALE_ORDER | Loại chứng từ gốc (soft ref xuyên schema) |
sourceId / sourceNumber | text | ✓ | — | Id / số đơn hàng |
origin | text | ✓ | 000_ORIGIN | ORIGIN/ADJUSTMENT/REPLACEMENT |
invoiceType | text | ✓ | — | Snapshot config (VAT/SALE/POS/…) |
invoiceSymbol / invoiceCategory / year | text/int/int | ✓ | — | Snapshot serial |
hasCqtCode | boolean | ✓ | true | Có mã cơ quan thuế |
taxMethod | text | ✓ | — | DEDUCTION/DIRECT/UNKNOWN |
autoRelease/autoSign/autoSendCqt | boolean | ✓ | — | Snapshot chính sách |
issuanceMode | text | ✓ | — | Snapshot mode của config |
invoiceConfigMappingId | text | ✓ | — | Mapping định tuyến |
invoiceNumber | text | — | Gán khi thành công | |
issuanceStatus | text | ✓ | PENDING | Xem enum |
retryCount | int | ✓ | 0 | — |
parentId | text | — | Hoá đơn được sửa (adjust/replace) | |
invoiceRequestId | text | — | Nguồn thông tin người mua | |
claimQrDataUrl | text | — | QR data URL cho buyer-claim | |
metadata | jsonb | — | errorMessage, permanent, … |
Enum trạng thái (issuanceStatus):
| Giá trị | Mô tả |
|---|---|
PENDING | Đã tạo, chờ phát hành (trạng thái khởi đầu) |
PROCESSING | Worker đang gọi provider |
SUCCESS | Đã phát hành; gán invoiceNumber |
FAILED | Lỗi vĩnh viễn / cạn retry / DLQ |
CANCELLED | Huỷ sau khi phát hành |
Enum origin: 000_ORIGIN, 100_ADJUSTMENT, 200_REPLACEMENT.
Index chính:
| Tên | Cột | Loại |
|---|---|---|
| UQ partial | (sourceType, sourceId) where origin=ORIGIN AND deletedAt IS NULL | Unique partial |
| UQ partial | (invoiceSymbol, invoiceNumber) where deletedAt IS NULL | Unique partial |
| partial | (invoiceConfigMappingId, createdAt) where PENDING AND issuanceMode=SCHEDULED | Cron nhặt |
InvoiceRequest
| Thuộc tính | Giá trị |
|---|---|
| Table | invoice.InvoiceRequest |
| Source | .../invoice-request/schema.ts |
| Soft-delete | có |
| Trường | Kiểu | Bắt buộc | Mặc định | Mô tả |
|---|---|---|---|---|
sourceType/sourceId/sourceNumber | text | ✓ | SALE_ORDER | Chứng từ gốc |
flowType | text | ✓ | 000_DIRECT | DIRECT / 100_BUYER_CLAIM |
status | text | ✓ | ACTIVATED | ACTIVATED/DEACTIVATED/CANCELLED |
requestedBy | text | ✓ | — | Người nhập |
buyerInfo | jsonb | ✓ | {} | TBuyerInfo (name, taxCode, address, …) |
claimToken | text | — | Token tự khai báo (UQ partial) | |
claimDeadline | timestamptz | — | Hết cửa sổ claim | |
claimState | text | — | PENDING/CLAIMED/EXPIRED | |
claimSubmittedAt | timestamptz | — | — |
Bất biến: một request ACTIVATED mỗi (sourceType, sourceId) (UQ partial index).
InvoiceProvider
| Thuộc tính | Giá trị |
|---|---|
| Table | invoice.InvoiceProvider |
| Soft-delete | có |
| Trường | Kiểu | Bắt buộc | Ghi chú |
|---|---|---|---|
merchantInvoiceProfileId | text | ✓ | Profile sở hữu |
provider | text | ✓ | InvoiceProviders (hiện tại VNPAY) |
environment | text | ✓ | DEVELOPMENT/PRODUCTION |
username / password | text | ✓ | password mã hoá AES-256-GCM |
webhookSecret | text | đã mã hoá | |
webhookUUID / webhookEventTypes / webhookStatus | text/array/text | đăng ký webhook |
Bất biến: UQ (merchantInvoiceProfileId, provider) và UQ name (cả hai partial trên deletedAt IS NULL).
InvoiceProviderConfig
| Trường | Kiểu | Bắt buộc | Mặc định | Ghi chú |
|---|---|---|---|---|
providerId | text | ✓ | — | → InvoiceProvider |
invoiceType/invoiceSymbol/invoiceCategory/year | hỗn hợp | ✓ | — | Serial |
issuanceMode | text | ✓ | 100_MANUAL | REAL_TIME/MANUAL/SCHEDULED/BUYER_SELF_SERVICE |
issuanceModeMetadata | jsonb | — | claimWindowMinutes, claimIssueTiming | |
autoRelease/autoSign/autoSendCqt/isSendMail | boolean | ✓ | false | Chính sách xuất |
retryMetadata | jsonb | ✓ | {max:3, delays:[5,15,60]} | Chính sách retry |
defaultBuyerInfo | jsonb | ✓ | "Người mua không lấy hoá đơn" | Người mua fallback |
deliveryChannels | jsonb | ✓ | [RECEIPT_QR] | RECEIPT_QR/EMAIL/SMS |
Bất biến: UQ (providerId, invoiceSymbol, invoiceType) partial.
InvoiceConfigMapping
| Trường | Kiểu | Bắt buộc | Ghi chú |
|---|---|---|---|
principalType / principalId | text | ✓ | vd SALE_CHANNEL → id kênh |
providerConfigId | text | ✓ | → InvoiceProviderConfig |
merchantId | text | ✓ | Chủ sở hữu |
status | text | ✓ | ACTIVATED/… |
Bất biến: một mapping active mỗi (principalType, principalId) (UQ partial).
InvoiceIssuance
| Trường | Kiểu | Bắt buộc | Ghi chú |
|---|---|---|---|
invoiceId | text | ✓ | FK → Invoice.id; UQ partial (một mỗi hoá đơn) |
invoiceRequestId | text | FK → InvoiceRequest.id | |
provider | text | ✓ | Provider phát hành |
externalId/externalRef/taxAuthorityCode | text | Ref của provider | |
providerStatus | text | VNPAY: new/released/signed/… | |
taxAuthorityStatus | int | VNPAY tvanStatus 1–8 | |
requestId | text | ${invoiceId}:${retryCount} | |
requestPayload/responsePayload/buyerInfoSnapshot | jsonb | Blob của provider |
InvoiceAuditTracing
| Trường | Kiểu | Bắt buộc | Ghi chú |
|---|---|---|---|
invoiceId | text | ✓ | FK → Invoice.id |
eventType | text | ✓ | vd ISSUE_ATTEMPT |
eventOutcome | text | ✓ | mặc định SUCCESS |
issuanceStatusBefore/After | text | vết chuyển trạng thái | |
message / details | text/jsonb | con người + có cấu trúc | |
triggeredBy | text | ✓ | actor / system:* |
occurredAt | timestamptz | ✓ | now() |
MerchantInvoiceProfile
| Trường | Kiểu | Bắt buộc | Ghi chú |
|---|---|---|---|
merchantId | text | ✓ | UQ partial (một profile mỗi merchant) |
taxInfoId | text | ✓ | → tax.TaxInfo |
taxMethod | text | ✓ | DEDUCTION/DIRECT/UNKNOWN |
businessType | text | ✓ | HOUSEHOLD/BUSINESS |
sharingPolicy | text | ✓ | PRIVATE/ALL_BRANCHES/WHITELIST |
TaxInfo (schema dùng chung tax)
| Trường | Kiểu | Bắt buộc | Ghi chú |
|---|---|---|---|
principalType | text | ✓ | Merchant / Organizer |
principalId | text | ✓ | — |
taxCode / fullName / addressLine | text | ✓ | Định danh người bán/người mua có thẩm quyền |
cityCode/districtCode/wardsCode/fullAddress | text | Địa chỉ VN | |
managingTaxAuthority/chapter/department | text | Trường registry GDT |
Bất biến: UQ (principalType, principalId) partial. Ghi bởi TaxInfoService.syncTaxInfo từ merchant CDC — xem Integration §3.
3. Bất biến xuyên entity
| Bất biến | Cách thực thi |
|---|---|
| Một hoá đơn ORIGIN mỗi đơn nguồn | UQ partial (sourceType, sourceId) where origin=ORIGIN AND deletedAt IS NULL |
Duy nhất (invoiceSymbol, invoiceNumber) chính thức khi đã gán | UQ partial index |
Một InvoiceIssuance mỗi hoá đơn | UQ partial (invoiceId); upsert theo mỗi retry (mới nhất thắng) |
Một InvoiceRequest active mỗi nguồn | UQ partial (sourceType, sourceId) where ACTIVATED |
| Một config mapping mỗi principal | UQ partial (principalType, principalId) |
| TaxInfo là định danh người bán có thẩm quyền | syncTaxInfo upsert theo (principalType, principalId); diff vs hàng đã lưu |
(businessType, taxMethod) → tập invoiceType cho phép | map ALLOWED_INVOICE_TYPES (guard tầng service) |
| Chuỗi điều chỉnh/thay thế | Invoice.parentId tự tham chiếu (tầng service, không FK DB) |
4. Hành vi Soft-delete
| Hành vi | Chi tiết |
|---|---|
| Mặc định đọc | deletedAt IS NULL (SoftDeletableRepository) |
| Unique index | Tất cả partial trên deletedAt IS NULL — hàng đã soft-delete giải phóng slot |
| Hard-delete | Mặc định không dùng |
| Restore | Chưa hiện thực |