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
<PackageReference Include="Themia.ETax.Withholding" Version="0.35.3" />
<PackageVersion Include="Themia.ETax.Withholding" Version="0.35.3" />
<PackageReference Include="Themia.ETax.Withholding" />
paket add Themia.ETax.Withholding --version 0.35.3
#r "nuget: Themia.ETax.Withholding, 0.35.3"
#:package Themia.ETax.Withholding@0.35.3
#addin nuget:?package=Themia.ETax.Withholding&version=0.35.3
#tool nuget:?package=Themia.ETax.Withholding&version=0.35.3
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).WithholdingSnapshotSerializerwrites deterministic, ASCII-only versioned JSON and hashes the exact text. Store it in a byte-preserving column, neverjsonbor MySQLJSON.WithholdingSnapshotValidator: checks NEW input against anIncomeTypeCatalogue. 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 ofVatMath. 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 ofIncomeTypeEntry(section, sub-item, printed label, form row, allowed filing forms and payee kinds) the app selects from byCode. A certificate stores a copy of what is printed, so it renders the same after the catalogue changes.ContentHashis independent of entry order: pin it in a test for every version you ship, and publish a change as a newVersion, 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 throughAddThemiaETaxWithholding(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 answersSourceConflict, 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;FindByKeyForIssueAsyncmust serialise racing issuers of one key) andIWithholdingNumberSource(gapless, inside the transaction). Run the store throughThemia.ETax.Withholding.Testing. On PostgreSQL, the table's immutability trigger, same-payer replacement key and unique issue key come fromThemia.ETax.Withholding.Data(WithholdingCertificateGuards), together with a gaplessIWithholdingNumberSource. - Series:
IssueWithholdingCertificateRequest.Series(the form's เล่มที่ used as a short code such asCRorSF) gives each series its own gapless run per payer and year and is stored and printed as เล่มที่ (BookNumber). The number source must implementIWithholdingNumberSource.NextNumberInSeriesAsync(the default implementation refuses a series);Themia.ETax.Withholding.Datadoes. - Replace, never edit: a wrong certificate is cancelled by issuing revision N+1 with
Replacementset; the old one becomesCancelled(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) andCancelled(watermark). - The tax in words is printed in parentheses, as on the certificates in use, with
Themia.ETax.ThaiBahtText, and isTensOnly: หนึ่งร้อยหนึ่ง 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 (defaultWHT-2569-000123; four shapes and the year style are the adopter's options, seeThemia.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'sDescription, 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 aWithholdingSignatureException(DifferentImage,MissingImageif the certificate was signed and none is supplied,NotIssuedWithOneif 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
WithholdingCertificateServicefrom itsTimeProviderin Bangkok time (UTC+7), never supplied by the caller (a caller'sIssueDateis ignored, so a certificate cannot be back- or post-dated; a date paid after today is refused). The date paid printsdd/MM/BE-year(22/01/2567); the issue date under the signature printsdd / Thai month name / BE year(22 / พฤศจิกายน / 2567).
| 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.ETax (>= 0.35.3)
-
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.ETax (>= 0.35.3)
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.