Themia.ETax.Withholding 0.35.3

dotnet add package Themia.ETax.Withholding --version 0.35.3
                    
NuGet\Install-Package Themia.ETax.Withholding -Version 0.35.3
                    
This command is intended to be used within the Package Manager Console in Visual Studio, as it uses the NuGet module's version of Install-Package.
<PackageReference Include="Themia.ETax.Withholding" Version="0.35.3" />
                    
For projects that support PackageReference, copy this XML node into the project file to reference the package.
<PackageVersion Include="Themia.ETax.Withholding" Version="0.35.3" />
                    
Directory.Packages.props
<PackageReference Include="Themia.ETax.Withholding" />
                    
Project file
For projects that support Central Package Management (CPM), copy this XML node into the solution Directory.Packages.props file to version the package.
paket add Themia.ETax.Withholding --version 0.35.3
                    
#r "nuget: Themia.ETax.Withholding, 0.35.3"
                    
#r directive can be used in F# Interactive and Polyglot Notebooks. Copy this into the interactive tool or source code of the script to reference the package.
#:package Themia.ETax.Withholding@0.35.3
                    
#:package directive can be used in C# file-based apps starting in .NET 10 preview 4. Copy this into a .cs file before any lines of code to reference the package.
#addin nuget:?package=Themia.ETax.Withholding&version=0.35.3
                    
Install as a Cake Addin
#tool nuget:?package=Themia.ETax.Withholding&version=0.35.3
                    
Install as a Cake Tool

Themia.ETax.Withholding

Neutral core for issuing Thai withholding tax certificates (หนังสือรับรองการหักภาษี ณ ที่จ่าย, form 50 ทวิ). Targets net8.0 and net10.0. No database, no HTTP. Builds on Themia.ETax (tax id and branch validation, party and replacement records, the snapshot exception types). Phase 2 of coord #0158 / request #0164.

The package holds the snapshot, validator, math, Thai baht text, the catalogue structure, the issue service with its ports and the renderer for the official layout.

  • WithholdingCertificateSnapshot: everything the certificate prints, frozen at issue (money in satang, rates in basis points). WithholdingSnapshotSerializer writes deterministic, ASCII-only versioned JSON and hashes the exact text. Store it in a byte-preserving column, never jsonb or MySQL JSON.
  • WithholdingSnapshotValidator: checks NEW input against an IncomeTypeCatalogue. Stored snapshots are never re-validated. Errors carry codes and field paths only (names and tax ids are never echoed).
  • WithholdingMath.Withhold(baseSatang, rateBasisPoints): ceiling to the satang, looking at every digit after the second decimal (the Revenue Department's rule: 10.115, 10.114 and 10.1101 all become 10.12; an exact 10.11 stays). This is NOT the rule of VatMath. Which base the rate applies to and which rate applies to a payment are the caller's business rules (the base is still an open accountant question, #0164 Q10); this package only multiplies.
  • IncomeTypeCatalogue: a versioned, immutable set of IncomeTypeEntry (section, sub-item, printed label, form row, allowed filing forms and payee kinds) the app selects from by Code. A certificate stores a copy of what is printed, so it renders the same after the catalogue changes. ContentHash is independent of entry order: pin it in a test for every version you ship, and publish a change as a new Version, never an edit. Themia ships no catalogue content and no rates: which income types an app uses, how each maps to a line of the form (WithholdingFormRow, the numbered lines of the official certificate) and at what rate it is withheld are the adopter's, supplied through AddThemiaETaxWithholding(catalogue) and the rate passed per payment.
  • Amount in words: the total tax is printed in Thai words with Themia.ETax.ThaiBahtText (see that package).

Dates are supplied by the caller (the Bangkok business date). Never log snapshot contents.

Issuing

WithholdingCertificateService.IssueAsync(tx, request) issues inside your transaction (READ COMMITTED) and commits nothing. Adopter checklist: roll back on every outcome except Issued / AlreadyIssued; a refused replacement may already have written in your transaction (the replaced certificate is cancelled before the new number and the final validation, so a later refusal leaves that cancel pending until you roll back).

  • Key: WithholdingCertificateKey(SourceEventId, Revision). The source event is the payout the tax was withheld from and must be unique across payers (a key found for another payer answers SourceConflict, never that payer's id). A duplicate is answered before a number is consumed.
  • Ports you implement: IWithholdingCertificateStore (your tables; payer-scoped reads that fail closed; FindByKeyForIssueAsync must serialise racing issuers of one key) and IWithholdingNumberSource (gapless, inside the transaction). Run the store through Themia.ETax.Withholding.Testing. On PostgreSQL, the table's immutability trigger, same-payer replacement key and unique issue key come from Themia.ETax.Withholding.Data (WithholdingCertificateGuards), together with a gapless IWithholdingNumberSource.
  • Series: IssueWithholdingCertificateRequest.Series (the form's เล่มที่ used as a short code such as CR or SF) gives each series its own gapless run per payer and year and is stored and printed as เล่มที่ (BookNumber). The number source must implement IWithholdingNumberSource.NextNumberInSeriesAsync (the default implementation refuses a series); Themia.ETax.Withholding.Data does.
  • Replace, never edit: a wrong certificate is cancelled by issuing revision N+1 with Replacement set; the old one becomes Cancelled (final) and points at its replacement. Cancel-and-reissue under a new number that cites the old one is acceptable (a 50 ทวิ is not a tax invoice, so no rule fixes how it is corrected; the corrected certificate only has to carry what Director-General Notice on income tax No. 62 requires). If the tax was already filed and the filed data is wrong too, the filing is corrected separately with the Revenue Department's form ป.ป.01, and the certificate and the filed data must agree: nothing here amends a filing automatically, so the app uses the cancelled and replacement certificates of a period to raise it. (Secondary sources; an accountant confirms.) Revision 1 may not carry a replacement; later revisions must.
  • Same snapshot in, same snapshot stored: the service copies the snapshot through the persisted format first, so changes to your lists while it runs cannot reach what was validated.
  • Register with services.AddThemiaETaxWithholding(catalogue). Logs hold ids and states only.

Rendering (WithholdingCertificateRenderer)

services.AddThemiaETaxWithholdingRendering() registers the renderer and the Puppeteer PDF renderer it prints through. It prints the official layout (หนังสือรับรองการหักภาษี ณ ที่จ่าย, form 50 ทวิ) on one A4 page with the embedded Sarabun, in template version "1" (frozen by golden-hash tests: a layout change ships as template "2", never an edit). The same stored certificate and the same markings give the same bytes: no clock, no culture, PDF dates pinned to the issue date, dates printed in the Buddhist era (15/10/2569).

  • Markings are chosen when rendering and never stored: WithholdingCopy.Copy1 / Copy2 (both copy lines are printed as on the form and the rendered copy is marked), WithholdingRenderMode.ReplacementCopy ("ใบแทน" at the top: a copy of the same certificate and number for a lost or damaged one, not a new certificate; the certifying signature is the issuer's, outside this document) and Cancelled (watermark).
  • The tax in words is printed in parentheses, as on the certificates in use, with Themia.ETax.ThaiBahtText, and is TensOnly: หนึ่งร้อยหนึ่ง but ยี่สิบเอ็ด. That is fixed by the template version, not an option, so a re-render of an issued certificate never changes its words.
  • RenderStoredAsync(record, copy, mode) verifies the stored snapshot against its hash first and prints a cancelled certificate as cancelled whatever mode is asked.
  • The book number (เล่มที่) is empty by default and prints empty unless the adopter supplies a series (or BookNumber); the certificate number is the adopter's number source's (default WHT-2569-000123; four shapes and the year style are the adopter's options, see Themia.ETax.Withholding.Data).
  • Each income goes on the line of its WithholdingFormRow; the lines that say "(ระบุ)" (the section 3 เตรส line, "other", and the two other-dividend lines) print the row's Description, or the catalogue label when there is none. A certificate has at most one income per line. The form's two tax id boxes are the 13-digit id and the old 10-digit id (PayerLegacyTaxId / PayeeLegacyTaxId); the form has no branch box, so the branch is kept in the snapshot (the monthly return attachments ask for it) but not printed.
  • Signature: the adopter passes the image BYTES as a WithholdingSignatureImage (PNG or JPEG, at most 256 KiB, checked on construction); there is no URL and no fetch. Pass it at issue (IssueWithholdingCertificateRequest.Signature): only its SHA-256 is recorded, in an immutable column next to the snapshot and outside its hash, and the app keeps the image. RenderStoredAsync(record, …, signature) then refuses any other image with a WithholdingSignatureException (DifferentImage, MissingImage if the certificate was signed and none is supplied, NotIssuedWithOne if it was issued without), so an old certificate is never re-rendered with a new signer's signature, without its own, or with one it never had. A preview from a snapshot (RenderAsync) has no record to check against and prints what it is given. The image is printed on the "ลงชื่อ … ผู้จ่ายเงิน" line; the company stamp is optional on the form and is never printed. Whether an electronic copy needs a signature or stamp at all is an open accountant question (#0164 Q9).
  • Dates: the issue date is the date the certificate record is created, set by WithholdingCertificateService from its TimeProvider in Bangkok time (UTC+7), never supplied by the caller (a caller's IssueDate is ignored, so a certificate cannot be back- or post-dated; a date paid after today is refused). The date paid prints dd/MM/BE-year (22/01/2567); the issue date under the signature prints dd / Thai month name / BE year (22 / พฤศจิกายน / 2567).
Product 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. 
Compatible target framework(s)
Included target framework(s) (in package)
Learn more about Target Frameworks and .NET Standard.

NuGet packages (2)

Showing the top 2 NuGet packages that depend on Themia.ETax.Withholding:

Package Downloads
Themia.ETax.Withholding.Testing

Conformance tests for Themia.ETax.Withholding's IWithholdingCertificateStore: inherit the abstract class from your own xUnit project, supply a store factory, and your store gets the same key, cancel and payer-scope semantics as every other adopter's.

Themia.ETax.Withholding.Data

Database helpers for Themia.ETax.Withholding: the required immutability trigger, same-payer replacement key and issue-key uniqueness (FluentMigrator, PostgreSQL) for an app's own withholding certificate table, and the gapless certificate-number source over Themia's transactional sequence allocator.

GitHub repositories

This package is not used by any popular GitHub repositories.

Version Downloads Last Updated
0.35.3 0 10/22/2026
0.35.2 46 10/20/2026
0.35.1 95 10/17/2026
0.35.0 94 10/17/2026
0.34.0 97 10/17/2026
0.33.0 105 10/17/2026