Skip to content

Domain Model

Schemas + repositories are owned by @nx/core. Source: packages/core/src/models/schemas/invoice/* and .../tax/tax-info/*. The Invoice schema is invoice; TaxInfo lives in the shared tax schema. No cross-schema FKs — only InvoiceIssuance/InvoiceAuditTracing declare FKs (both to Invoice).

1. Full ERD

2. Entities

Invoice

PropertyValue
Tableinvoice.Invoice
Sourcepackages/core/src/models/schemas/invoice/invoice/schema.ts
Soft-deleteyes
Owner columninvoiceConfigMappingId (→ merchant via mapping)

Fields (selected):

FieldTypeRequiredDefaultDescription
idtextSnowflakePK
sourceTypetext001_SALE_ORDEROrigin doc type (cross-schema soft ref)
sourceId / sourceNumbertextOrder id / number
origintext000_ORIGINORIGIN/ADJUSTMENT/REPLACEMENT
invoiceTypetextConfig snapshot (VAT/SALE/POS/…)
invoiceSymbol / invoiceCategory / yeartext/int/intSerial snapshot
hasCqtCodebooleantrueHas tax-authority code
taxMethodtextDEDUCTION/DIRECT/UNKNOWN
autoRelease/autoSign/autoSendCqtbooleanPolicy snapshot
issuanceModetextSnapshot of config mode
invoiceConfigMappingIdtextRouting mapping
invoiceNumbertextAssigned on success
issuanceStatustextPENDINGSee enum
retryCountint0
parentIdtextCorrected invoice (adjust/replace)
invoiceRequestIdtextBuyer-info source
claimQrDataUrltextBuyer-claim QR data URL
metadatajsonberrorMessage, permanent, …

Status enum (issuanceStatus):

ValueDescription
PENDINGCreated, awaiting issuance (start state)
PROCESSINGWorker is calling provider
SUCCESSIssued; invoiceNumber assigned
FAILEDPermanent error / retries exhausted / DLQ
CANCELLEDCancelled post-issuance

Origin enum: 000_ORIGIN, 100_ADJUSTMENT, 200_REPLACEMENT.

Key indexes:

NameColumnsType
UQ partial(sourceType, sourceId) where origin=ORIGIN AND deletedAt IS NULLUnique partial
UQ partial(invoiceSymbol, invoiceNumber) where deletedAt IS NULLUnique partial
partial(invoiceConfigMappingId, createdAt) where PENDING AND issuanceMode=SCHEDULEDCron pickup

InvoiceRequest

PropertyValue
Tableinvoice.InvoiceRequest
Source.../invoice-request/schema.ts
Soft-deleteyes
FieldTypeRequiredDefaultDescription
sourceType/sourceId/sourceNumbertextSALE_ORDEROrigin doc
flowTypetext000_DIRECTDIRECT / 100_BUYER_CLAIM
statustextACTIVATEDACTIVATED/DEACTIVATED/CANCELLED
requestedBytextCaptured-by
buyerInfojsonb{}TBuyerInfo (name, taxCode, address, …)
claimTokentextSelf-service token (UQ partial)
claimDeadlinetimestamptzClaim window end
claimStatetextPENDING/CLAIMED/EXPIRED
claimSubmittedAttimestamptz

Invariant: one ACTIVATED request per (sourceType, sourceId) (UQ partial index).

InvoiceProvider

PropertyValue
Tableinvoice.InvoiceProvider
Soft-deleteyes
FieldTypeRequiredNotes
merchantInvoiceProfileIdtextOwner profile
providertextInvoiceProviders (VNPAY today)
environmenttextDEVELOPMENT/PRODUCTION
username / passwordtextpassword AES-256-GCM encrypted
webhookSecrettextencrypted
webhookUUID / webhookEventTypes / webhookStatustext/array/textwebhook registration

Invariant: UQ (merchantInvoiceProfileId, provider) and UQ name (both partial on deletedAt IS NULL).

InvoiceProviderConfig

FieldTypeRequiredDefaultNotes
providerIdtextInvoiceProvider
invoiceType/invoiceSymbol/invoiceCategory/yearmixedSerial
issuanceModetext100_MANUALREAL_TIME/MANUAL/SCHEDULED/BUYER_SELF_SERVICE
issuanceModeMetadatajsonbclaimWindowMinutes, claimIssueTiming
autoRelease/autoSign/autoSendCqt/isSendMailbooleanfalseExport policy
retryMetadatajsonb{max:3, delays:[5,15,60]}Retry policy
defaultBuyerInfojsonb"Người mua không lấy hoá đơn"Fallback buyer
deliveryChannelsjsonb[RECEIPT_QR]RECEIPT_QR/EMAIL/SMS

Invariant: UQ (providerId, invoiceSymbol, invoiceType) partial.

InvoiceConfigMapping

FieldTypeRequiredNotes
principalType / principalIdtexte.g. SALE_CHANNEL → channel id
providerConfigIdtextInvoiceProviderConfig
merchantIdtextOwner
statustextACTIVATED/…

Invariant: one active mapping per (principalType, principalId) (UQ partial).

InvoiceIssuance

FieldTypeRequiredNotes
invoiceIdtextFK → Invoice.id; UQ partial (one per invoice)
invoiceRequestIdtextFK → InvoiceRequest.id
providertextIssuing provider
externalId/externalRef/taxAuthorityCodetextProvider refs
providerStatustextVNPAY: new/released/signed/…
taxAuthorityStatusintVNPAY tvanStatus 1–8
requestIdtext${invoiceId}:${retryCount}
requestPayload/responsePayload/buyerInfoSnapshotjsonbProvider blobs

InvoiceAuditTracing

FieldTypeRequiredNotes
invoiceIdtextFK → Invoice.id
eventTypetexte.g. ISSUE_ATTEMPT
eventOutcometextdefault SUCCESS
issuanceStatusBefore/Aftertexttransition trace
message / detailstext/jsonbhuman + structured
triggeredBytextactor / system:*
occurredAttimestamptznow()

MerchantInvoiceProfile

FieldTypeRequiredNotes
merchantIdtextUQ partial (one profile per merchant)
taxInfoIdtexttax.TaxInfo
taxMethodtextDEDUCTION/DIRECT/UNKNOWN
businessTypetextHOUSEHOLD/BUSINESS
sharingPolicytextPRIVATE/ALL_BRANCHES/WHITELIST

TaxInfo (shared tax schema)

FieldTypeRequiredNotes
principalTypetextMerchant / Organizer
principalIdtext
taxCode / fullName / addressLinetextAuthoritative seller/buyer identity
cityCode/districtCode/wardsCode/fullAddresstextVN address
managingTaxAuthority/chapter/departmenttextGDT registry fields

Invariant: UQ (principalType, principalId) partial. Written by TaxInfoService.syncTaxInfo from merchant CDC — see Integration §3.

3. Cross-entity Invariants

InvariantEnforcement
One ORIGIN invoice per source orderUQ partial (sourceType, sourceId) where origin=ORIGIN AND deletedAt IS NULL
Unique official (invoiceSymbol, invoiceNumber) once assignedUQ partial index
One InvoiceIssuance per invoiceUQ partial (invoiceId); upserted per retry (latest wins)
One active InvoiceRequest per sourceUQ partial (sourceType, sourceId) where ACTIVATED
One config mapping per principalUQ partial (principalType, principalId)
TaxInfo is authoritative seller identitysyncTaxInfo upsert by (principalType, principalId); diff vs persisted row
(businessType, taxMethod) → allowed invoiceType setALLOWED_INVOICE_TYPES map (service-layer guard)
Adjustment/replacement chainInvoice.parentId self-reference (service-layer, not DB FK)

4. Soft-delete Behavior

BehaviorDetail
Read defaultdeletedAt IS NULL (SoftDeletableRepository)
Unique indexesAll partial on deletedAt IS NULL — soft-deleted rows free the slot
Hard-deleteNot used by default
RestoreNot implemented

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