Themia.ETax
0.35.1
dotnet add package Themia.ETax --version 0.35.1
NuGet\Install-Package Themia.ETax -Version 0.35.1
<PackageReference Include="Themia.ETax" Version="0.35.1" />
<PackageVersion Include="Themia.ETax" Version="0.35.1" />
<PackageReference Include="Themia.ETax" />
paket add Themia.ETax --version 0.35.1
#r "nuget: Themia.ETax, 0.35.1"
#:package Themia.ETax@0.35.1
#addin nuget:?package=Themia.ETax&version=0.35.1
#tool nuget:?package=Themia.ETax&version=0.35.1
Themia.ETax
Neutral core for Thai tax invoices, credit/debit notes and e-Tax (route A, e-Tax by Email). Targets net8.0 and net10.0.
No database, no HTTP: it holds the parts every issuing app must agree on, and it prints PDFs through Themia.Pdf.
TaxDocumentSnapshot: everything a document prints, frozen at issue (money in satang, rates in basis points).TaxDocumentSnapshotSerializerwrites it as deterministic, ASCII-only versioned JSON and hashes the exact text (SHA-256). Store the text in a byte-preserving column (PostgreSQLtext, SQL Servernvarchar(max), MySQLlongtext), neverjsonbor MySQLJSON: they reorder keys and the hash would no longer match.TaxDocumentSnapshotValidator: checks NEW input (totals, VAT arithmetic, withholding, credit/debit note and replacement references). Stored snapshots are never re-validated.TaxDocumentKey: idempotency key(Kind, SourceEventId, Revision).SourceEventIdis the triggering event: payment id for a tax invoice/receipt, refund id for a credit note, the debit note's own event id for a debit note. Keying a credit note by the payment would make a second partial refund look like a duplicate.VatMath:AddVat/Splitin satang and basis points, midpoints away from zero, identical to the decimal-baht algorithm it was promoted from.tests/Themia.ETax.Tests/Vectors/vatmath-parity.jsonholds parity vectors (midpoints included) that any decimal-baht adapter can run.ThaiBahtText.Format(satang, etMode): the amount in Thai words ("หนึ่งพันสองร้อยสามสิบสี่บาทห้าสิบหกสตางค์", "…บาทถ้วน"; under one baht omits the baht part). Both conventions for "เอ็ด" are in use, so the adopter chooses withThaiBahtTextEtMode:Always(default: หนึ่งร้อยเอ็ด, หนึ่งพันเอ็ดล้าน) orTensOnly(เอ็ดเฉพาะหลังหลักสิบ: สิบเอ็ด but หนึ่งร้อยหนึ่ง, หนึ่งพันหนึ่งล้าน). Pinned by the documented examples of the ThaiBahtText package (11, 101, 211, 1001, 1,001,000,000 in both modes); that package is not a dependency. Tax documents print it from template version"2"(below).ThaiTaxId/ThaiBranchCode: 13-digit id with optional mod-11 check digit, 5-digit branch.
The issue_date is always supplied by the caller (the Bangkok business date of the payment or refund), never derived from UTC.
Never log snapshot contents: they hold names and tax ids. Errors from this package carry codes and field paths only.
Rendering
TaxDocumentRendererrenders a hash-verified snapshot with the template version it was issued under (TaxDocumentTemplates.CurrentVersion="1"; a template change ships as a NEW version, never an edit) and returns aTaxDocumentRender: normalised PDF, its SHA-256 andRendererVersion(<browser version>|n<normaliser version>). Register withservices.AddThemiaETaxRendering(); it needs the Puppeteer renderer, soIVersionedPdfRendererresolves to it even when an adopter registers its ownIPdfRenderer. Rendering never validates and never logs.- Dates: stored Gregorian/ISO, printed with the Buddhist-era year (
14/10/2569), computed as year + 543 with invariant formatting, never from a Thai culture or calendar (their data can differ between hosts and would break byte-stable renders). - Marker:
TaxDocumentMarker.Originalonly on the send path,Copyfor every other render,CancelledCopyfor a cancelled document (adds a CANCELLED/ยกเลิก watermark). - Reproducible bytes: the PDF is printed untagged (a tagged PDF embeds a per-browser id counter) and its two date
fields are pinned to the issue date.
TaxDocumentEvidence.Checkis the resend rule: the same renderer version with a different hash throwsTaxDocumentRenderMismatchException(no exceptions to the rule); an unseen renderer version isNewRendererVersion(record new evidence, warn, resend). - Fonts: Sarabun Regular and Bold are embedded (Thai, basic Latin, Latin Extended), so output does not depend on the
host's fonts. The SIL Open Font License 1.1 text ships in the package (
licenses/Sarabun-OFL.txt) and as an embedded resource.TaxDocumentSnapshotValidatorrejects any text with a character the faces do not cover (TaxDocumentValidationCode.UnsupportedCharacter, field path only, never the value). CJK and other scripts are out of scope for template"1"; supporting them means a new template version with added fonts. - Proof:
tests/Themia.ETax.IntegrationTests(Category=Integration) re-renders on Chromium across time, fresh browsers, multiple pages, images and concurrency. An app that pointsThemiaPdfOptions.ExecutablePathat its own Chromium runs a build CI does not cover;RendererVersionrecords which one rendered.
Issuing and sending
Apps own their tables, migrations and the persistence port; Themia owns the rules.
- Ports the app implements:
ITaxDocumentStore(every transition is one conditional write),ITaxDocumentNumberSource(gapless number inside the caller's transaction),ITaxDocumentMailer(route A transport, returns a code, never a message), and optionallyITaxDocumentFailureHandler(alert when a document enters Failed). Prove your store withThemia.ETax.Testing: inheritTaxDocumentStoreConformanceTests, overrideCreateHarnessAsync, done. TaxDocumentService.IssueAsync(tx, request)runs in the caller's READ COMMITTED transaction: a duplicate key answersAlreadyIssuedbefore any number is allocated (on PostgreSQL the store'sFindByKeyForIssueAsyncmust takepg_advisory_xact_lockand run its SELECT as SEPARATE statements under READ COMMITTED: one statement keeps its pre-wait snapshot and misses the row the lock holder just committed); validation, the credit/debit-note precondition (the original was prepared, is not cancelled, same seller, same number) and a replacement's cancel all run before the number is taken. OnlyIssuedandAlreadyIssuedmean commit; every other outcome means roll back.SourceEventIdis the triggering event and must be globally unique across sellers: the key carries no seller, so a second seller reusing a key getsSourceConflict, never the other seller's document id.TaxDocumentSender.RunOnceAsync()(call it on a schedule; it is also the sweeper) claims due documents under a lease (IssuedorOriginalPrepared, no live lease, backoff elapsed), renders the ORIGINAL from the stored snapshot, records its evidence, re-checks in a conditional write that the document is still sendable (and extends the lease) and only then sends. A cancel (CancelForReplacementAsync) is refused while a live lease is held, so a cancelled document is never sent as ORIGINAL. The mailer runs underSendTimeout(default 60 s, must be shorter than the 2-minute lease) and must honour its cancellation token; a timeout is a transientsend_timeoutand the lease is deliberately NOT released (the next attempt is due no earlier than the lease's end), because a mailer that ignored its token may still be delivering and the lease is what keeps a cancel out; and an abandoned send that later faults or finishes is observed and logged (type and outcome only). A replacement must be the next revision of the same source event and kind. Delivery is at-least-once with exactly one ORIGINAL: if the process dies after sending, the lease lapses and the same ORIGINAL is re-rendered, checked against the evidence history, and resent with the same Message-ID (the document id).- Seller scope, fail closed: everything an app can expose by document id (
ITaxDocumentStore.GetAsync,RequeueAsync,UpdateBuyerEmailAsync, and the service'sRenderCopyAsync/RequeueAsync) takes the caller'ssellerId; the store looks the document up by (id, seller) and another seller's id answers exactly like a missing one. The app still authorizes first; this is the second lock. - Failure: permanent errors (and snapshot or template problems) fail at once; transient ones back off exponentially with jitter
and fail at
MaxAttempts(default 5). Failed is terminal for automation;RequeueAsyncis the only way back and logs the actor id. One ERROR log per entry into Failed, thenITaxDocumentFailureHandler. - Logging: ids, kinds, states, attempt counts and error codes only. A capturing-logger test proves no snapshot value, address or transport message reaches a log or an exception.
- Register:
services.AddThemiaETaxDocuments(o => o.RevenueDepartmentAddress = "...")after registering your store, number source and mailer.TaxDocumentServiceandTaxDocumentSenderare scoped (your store and number source normally are): resolve them from a scope, and in a scheduled job create one scope perRunOnceAsync.
Template version 2: the amount in Thai words
Template "1" stays frozen and renders every document issued under it byte-identically. Template "2" is template 1 plus a line under the totals,
ตัวอักษร / In words: หนึ่งพันเจ็ดสิบบาทถ้วน. The words are the document total: the grand total including VAT on a tax invoice/receipt, and the
credited (or debited) amount on a credit (debit) note. A document records the template version it was issued under, as before, and is always
rendered with it.
The adopter opts in for NEW documents; the version also fixes the "เอ็ด" convention, so it is frozen with each document: "2" reads a units digit of 1 as
"เอ็ด" (หนึ่งร้อยเอ็ด), "3" is TensOnly (หนึ่งร้อยหนึ่ง but ยี่สิบเอ็ด). The layout is the same.
services.AddThemiaETaxDocuments(...);
services.ConfigureThemiaETaxTemplates(o =>
{
o.TemplateVersion = TaxDocumentTemplates.LatestVersion; // "3" (หนึ่ง); "2" is เอ็ด; the default stays "1", so upgrading changes nothing by itself
});
ConfigureThemiaETaxTemplates may be called before or after the other AddThemiaETax* methods and validates at registration.
Changing TemplateVersion later only affects documents issued after the change: an issued document is always rendered under the version recorded on it,
so its words never change.
| Product | Versions Compatible and additional computed target framework versions. |
|---|---|
| .NET | net8.0 is compatible. net8.0-android was computed. net8.0-browser was computed. net8.0-ios was computed. net8.0-maccatalyst was computed. net8.0-macos was computed. net8.0-tvos was computed. net8.0-windows was computed. net9.0 was computed. net9.0-android was computed. net9.0-browser was computed. net9.0-ios was computed. net9.0-maccatalyst was computed. net9.0-macos was computed. net9.0-tvos was computed. net9.0-windows was computed. net10.0 is compatible. net10.0-android was computed. net10.0-browser was computed. net10.0-ios was computed. net10.0-maccatalyst was computed. net10.0-macos was computed. net10.0-tvos was computed. net10.0-windows was computed. |
-
net10.0
- Handlebars.Net (>= 2.1.6)
- Microsoft.Extensions.DependencyInjection (>= 10.0.9)
- Microsoft.Extensions.DependencyInjection.Abstractions (>= 10.0.9)
- Microsoft.Extensions.Logging (>= 10.0.9)
- Microsoft.Extensions.Logging.Abstractions (>= 10.0.9)
- Microsoft.Extensions.Options (>= 10.0.9)
- PuppeteerSharp (>= 25.1.2)
- Themia.Pdf (>= 0.35.1)
-
net8.0
- Handlebars.Net (>= 2.1.6)
- Microsoft.Extensions.DependencyInjection (>= 10.0.9)
- Microsoft.Extensions.DependencyInjection.Abstractions (>= 10.0.9)
- Microsoft.Extensions.Logging (>= 10.0.9)
- Microsoft.Extensions.Logging.Abstractions (>= 10.0.9)
- Microsoft.Extensions.Options (>= 10.0.9)
- PuppeteerSharp (>= 25.1.2)
- Themia.Pdf (>= 0.35.1)
NuGet packages (3)
Showing the top 3 NuGet packages that depend on Themia.ETax:
| Package | Downloads |
|---|---|
|
Themia.ETax.Testing
Conformance tests for Themia.ETax's ITaxDocumentStore: inherit the abstract class from your own xUnit project, supply a store factory, and your store gets the same transition and lease semantics as every other adopter's. |
|
|
Themia.ETax.Data
Database helpers for Themia.ETax: the required immutability trigger and same-seller replacement key (FluentMigrator, PostgreSQL) for an app's own tax document table, and the gapless document-number source over Themia's transactional sequence allocator. |
|
|
Themia.ETax.Withholding
Neutral Thai withholding tax certificate (50 bis) core: withholding math in satang, certificate snapshot with integrity hash, validator, versioned income-type catalogue structure and Thai baht text. |
GitHub repositories
This package is not used by any popular GitHub repositories.