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 / sourceNumbertext-Order id / number
origintext000_ORIGINORIGIN/ADJUSTMENT/REPLACEMENT
invoiceTypetext-Config snapshot (VAT/SALE/POS/…)
invoiceSymbol / invoiceCategory / yeartext/int/int-Serial snapshot
hasCqtCodebooleantrueHas tax-authority code
taxMethodtext-DEDUCTION/DIRECT/UNKNOWN
autoRelease/autoSign/autoSendCqtboolean-Policy snapshot
issuanceModetext-Snapshot of config mode
invoiceConfigMappingIdtext-Routing mapping
invoiceNumbertext-Assigned on success
issuanceStatustextPENDINGSee enum
retryCountint0-
parentIdtext-Corrected invoice (adjust/replace)
invoiceRequestIdtext-Buyer-info source
claimQrDataUrltext-Buyer-claim QR data URL
metadatajsonb-errorMessage, 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
requestedBytext-Captured-by
buyerInfojsonb{}TBuyerInfo (name, taxCode, address, …)
claimTokentext-Self-service token (UQ partial)
claimDeadlinetimestamptz-Claim window end
claimStatetext-PENDING/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
providerIdtext-InvoiceProvider
invoiceType/invoiceSymbol/invoiceCategory/yearmixed-Serial
issuanceModetext100_MANUALREAL_TIME/MANUAL/SCHEDULED/BUYER_SELF_SERVICE
issuanceModeMetadatajsonb-claimWindowMinutes, claimIssueTiming
autoRelease/autoSign/autoSendCqt/isSendMailbooleanfalseExport policy
retryMetadatajsonb{max:3, delays:[5,15,60]}Retry policy
defaultBuyerInfojsonb"Bán cho người tiêu dùng"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.