Help us shape the future of Go! Take the 2026 Go Developer Survey

vnsalary

package module
v0.6.1 Latest Latest
Warning

This package is not in the latest version of its module.

Go to latest
Published: Oct 21, 2026 License: MIT Imports: 5 Imported by: 0

README

vnsalary

CI Go Reference

Vietnam Gross ↔ Net Salary & PIT Calculator in pure Go.
Exact to the single VND. Analytical piecewise-linear inversion. Zero external dependencies.

📖 Đọc bản tiếng Việt tại đây (Vietnamese README)


🌟 Highlights

  • Zero External Dependencies: 100% built with the Go Standard Library.
  • Analytical Piecewise-Linear Inversion: Solves Net → Gross by identifying policy breakpoints and computing initial analytical approximations, combined with local boundary refinement (±50 VND) and monotonic binary search fallback to handle integer half-up discretization. Guarantees round-trip mathematical consistency: GrossToNet(NetToGross(net)).Net == net.
  • Exact Integer Arithmetic (int64 VND): Zero float64 for currency. Rounding is performed half-up independently on each mandatory insurance item and progressive tax bracket using integer arithmetic (n + d/2) / d, eliminating IEEE-754 floating-point errors.
  • Multi-Era Policy Engine & Legal Traceability: Automatically selects policy parameters based on the salary payment date using the Cash-basis principle (Law on PIT 109/2025/QH15 Art. 8.3; previously Circular 111/2013/TT-BTC Art. 8.2.b):
    • July 01, 2024: Base salary 2,340,000 VND (Decree 73/2024/ND-CP), regional minimums (Decree 74/2024/ND-CP), 7-bracket progressive tax schedule (Law on PIT), personal deductions of 11M / 4.4M (Resolution 954/2020/UBTVQH14).
    • January 01, 2026: New 5-bracket progressive tax schedule (Law on PIT No. 109/2025/QH15 Article 9), upgraded personal deductions of 15.5M / 6.2M (Resolution 110/2025/UBTVQH15), regional minimums (Decree 293/2025/ND-CP).
    • July 01, 2026: Base salary raised to 2,530,000 VND (Decree 161/2026/ND-CP), so the social & health insurance cap rises from 46.8M to 50.6M. Other parameters unchanged.
  • Concurrency-Safe & Immutable: PolicyAt returns deep copies of policies, preventing external modification of global slices. Input validation enforces bounded amounts (up to 10¹⁴ VND) and dependent limits (≤ 50) to prevent int64 overflow.
  • Comprehensive Employer Cost: Computes mandatory employer contributions (Social 17.5%, Health 3%, Unemployment 1%) and total company cost.
  • Annual PIT Finalization: Annual settles a tax year from its payments (several employers, bonus months, months without income, late dependents) and returns the amount to pay or refund; personal deduction for 12 months, medical and tuition deductions from 2026 (Decree 253/2026/ND-CP Art. 48–49).
  • CLI Included: Standalone command-line tool with human-readable formatting, policy diff comparison, annual finalization from a JSON file, and --json export.

📦 Installation

go get github.com/HuynhNhatKhanh/vnsalary

Requirements: Go 1.22+.


🚀 Quick Start

1. Gross to Net Calculation
package main

import (
	"fmt"
	"time"

	"github.com/HuynhNhatKhanh/vnsalary"
)

func main() {
	opts := vnsalary.Options{
		Dependents: 1,
		Region:     vnsalary.Region1,
		Date:       time.Date(2026, 1, 15, 0, 0, 0, 0, time.UTC),
	}

	result, err := vnsalary.GrossToNet(35_000_000, opts)
	if err != nil {
		panic(err)
	}

	fmt.Printf("Gross:      %d VND\n", result.Gross)
	fmt.Printf("Net:        %d VND\n", result.Net)
	fmt.Printf("Insurance:  %d VND\n", result.Insurance.Total)
	fmt.Printf("PIT Tax:    %d VND\n", result.Tax)
	fmt.Printf("Employer:   %d VND\n", result.Employer.TotalCost)
}
2. Net to Gross Calculation (Exact Inversion)
net := vnsalary.VND(30_843_750)
result, err := vnsalary.NetToGross(net, opts)
if err != nil {
	panic(err)
}

fmt.Printf("Required Gross: %d VND\n", result.Gross) // Output: 35000000 VND
3. Custom Insurance Base

Many employers register a contractual basic salary for insurance contributions:

insBase := vnsalary.VND(5_000_000)
opts := vnsalary.Options{
	InsuranceBase: &insBase,
	Dependents:    0,
	Region:        vnsalary.Region1,
	Date:          time.Date(2024, 8, 1, 0, 0, 0, 0, time.UTC),
}

result, err := vnsalary.GrossToNet(30_000_000, opts)
4. Overtime, Allowances and Bonuses

Gross is the total paid in the month. Tell the library which parts of it are overtime, allowances or a bonus with Options.Extras:

opts := vnsalary.Options{
	Region: vnsalary.Region1,
	Date:   time.Date(2026, 8, 10, 0, 0, 0, 0, time.UTC),
	Extras: vnsalary.Extras{
		Overtime: 5_000_000,  // tax-exempt from 2026, outside the insurance salary
		MealCash: 1_500_000,  // tax-exempt up to 1,200,000/month from 2026-07-01
		Bonus:    10_000_000, // taxed in the month paid, outside the insurance salary
	},
}
result, _ := vnsalary.GrossToNet(55_000_000, opts) // 55M includes the extras
fmt.Println(result.Exempt.Total, result.BonusTax)   // exempt amount; tax caused by the bonus
Extra PIT Insurance salary (when InsuranceBase is nil) Basis
Overtime (within legal hour limits) exempt from tax period 2026; before 2026 taxed, with a warning excluded Law 109/2025 Art. 4.8, Decree 253/2026 Art. 26
MealCash exempt up to 1,200,000/month from 2026-07-01; 730,000 before, with a warning (not confirmed) included Decree 253/2026 Art. 8.2.g
OtherExempt (phone, uniform, …) exempt; limits are the caller's responsibility included Decree 253/2026 Art. 8.2.đ
Bonus taxed in the month paid; Result.BonusTax = tax with it − tax without it excluded Decree 253/2026 Art. 8.2.i, 46.3

Overtime and bonuses vary month to month, so they are outside the salary "paid regularly and stably" that insurance is based on (Law 41/2024 Art. 31.1.b). If your contract also excludes the meal or other allowances, set InsuranceBase. NetToGross holds the extras fixed and returns the smallest total gross.

5. Annual PIT Finalization

Monthly withholding and the yearly tax differ when income is uneven, when you changed employers or did not work the whole year (the personal deduction still counts 12 months), or when dependents were registered late. Annual computes the yearly tax and what is left to pay or refund:

var payments []vnsalary.Payment
for month := 1; month <= 12; month++ {
	paid := time.Date(2026, time.Month(month), 28, 0, 0, 0, 0, time.UTC)
	r, _ := vnsalary.GrossToNet(30_000_000, vnsalary.Options{Region: vnsalary.Region1, Dependents: 1, Date: paid})
	payments = append(payments, r.Payment(paid)) // or fill Payment from a payslip
}
res, _ := vnsalary.Annual(2026, payments, vnsalary.AnnualOptions{
	DependentMonths: 12,         // one dependent for the whole year
	Healthcare:      10_000_000, // deductible from 2026, up to 23M/year
})
fmt.Println(res.Tax, res.Withheld, res.Balance) // Balance < 0: refundable
Deduction Amount Basis
Personal 12 × monthly, whatever the months worked Decree 253/2026 Art. 48.1.b
Dependents DependentMonths × monthly, from the month the duty of support arose Decree 253/2026 Art. 48.2.a
Charity, medical (≤ 23M), tuition (≤ 24M) per year, from tax period 2026; claiming them means filing yourself Decree 253/2026 Art. 49, 51.3

The yearly schedule is the monthly one × 12 (Law 109/2025 Art. 9.2). Supported tax years: 2025 onwards.

6. 10% Withholding (No Labour Contract, or Under 3 Months)

Payments to a resident without a labour contract or with one under 3 months are withheld at 10% of the whole payment, without family deductions, once the payment reaches the threshold of its date:

res, _ := vnsalary.FlatWithholding(8_000_000, time.Date(2026, 9, 10, 0, 0, 0, 0, time.UTC))
fmt.Println(res.Withheld, res.Net) // 800000 7200000
Payment date Threshold per payment Basis
before 2026-07-01 2,000,000 Circular 111/2013 Art. 25.1.i
from 2026-07-01 5,000,000 (below it, 10% only at the individual's request) Decree 253/2026 Art. 50.2

The withholding is a prepayment: res.Payment(date) goes into Annual, which refunds it when the year's income is below the deductions. Not modelled: the commitment that lets an individual defer withholding, and compulsory insurance, which depends on the contract.

7. Foreign Employees

Foreign nationals pay no unemployment insurance (it covers Vietnamese citizens only); social and health insurance stay:

res, _ := vnsalary.GrossToNet(60_000_000, vnsalary.Options{
	Region: vnsalary.Region1, Date: time.Date(2026, 8, 10, 0, 0, 0, 0, time.UTC), Foreigner: true,
})
// res.Insurance.Unemployment == 0, res.Employer.Unemployment == 0

A foreign employee pays social insurance on a contract of 12 months or more (Law 41/2024 Art. 2.2); the exemptions (intra-company transfer, retirement age at signing, treaties) are not modelled. Foreigner changes insurance only: tax still follows residence (183 days in Vietnam, or a regular place to stay).

8. Non-Residents

A non-resident pays 20% of taxable income from work in Vietnam, with no family or insurance deductions (Decree 253/2026 Art. 64; Law on PIT 2007 Art. 26 before). Residence is the caller's call: 183 days or more in Vietnam in the calendar year or in 12 consecutive months, or a regular place to stay (Decree 253/2026 Art. 4).

res, _ := vnsalary.GrossToNet(60_000_000, vnsalary.Options{
	Region: vnsalary.Region1, Date: time.Date(2026, 8, 10, 0, 0, 0, 0, time.UTC),
	Foreigner: true, NonResident: true,
})
fmt.Println(res.Tax, res.Net) // 12000000 43193000

NonResident changes tax only; combine it with Foreigner for a foreign national. NetToGross still returns the smallest gross: with extras, a non-resident's net can fall by 1 VND as gross rises (the flat tax does not shrink when insurance grows), so it searches a 32 VND window below the first match.

9. Employer Cost Options

Options.Employer changes the employer's cost only, never the employee's net:

rate := int64(3) // 0.3%, if your accident insurance rate was approved
res, _ := vnsalary.GrossToNet(30_000_000, vnsalary.Options{
	Region: vnsalary.Region1, Date: time.Date(2026, 8, 10, 0, 0, 0, 0, time.UTC),
	Employer: vnsalary.EmployerOptions{AccidentPerMille: &rate, UnionFee: true},
})
fmt.Println(res.Employer.Social, res.Employer.UnionFee) // 5190000 600000
Option Default Basis
AccidentPerMille 5 (0.5%) occupational accident insurance, part of employer social insurance Decree 58/2020 Art. 4.1.a; 0.3% when approved
UnionFee off; when set, 2% of the social insurance salary is added to TotalCost Law on Trade Unions 50/2024 Art. 29.1.b; due with or without a grassroots union

Parameter From 01/07/2024 From 01/01/2026 From 01/07/2026 Legal Source
Base Salary / Reference 2,340,000 VND 2,340,000 VND 2,530,000 VND Decree 73/2024/ND-CP; Decree 161/2026/ND-CP
Social & Health Cap 46,800,000 VND (20 × Base) 46,800,000 VND 50,600,000 VND until 30/06/2025: Law on Social Insurance 2014 Art. 89.3, Law on Health Insurance Art. 14.2; from 01/07/2025: Law on Social Insurance No. 41/2024/QH15 Art. 31.1.đ (reference level = base salary, Art. 141.13), Law on Health Insurance Art. 14.5 as amended by Law No. 51/2024/QH15
Unemployment Cap (Region I) 99,200,000 VND (20 × 4.96M) 106,200,000 VND (20 × 5.31M) 106,200,000 VND Law on Employment 2013 Art. 58 (until 2025), Law on Employment No. 74/2025/QH15 Art. 34.2 (from 01/01/2026); Decree 74/2024 & Decree 293/2025
Personal Deduction 11,000,000 VND / month 15,500,000 VND / month 15,500,000 VND / month Resolution 954/2020 (2024) & Resolution 110/2025 (2026)
Dependent Deduction 4,400,000 VND / person / month 6,200,000 VND / person / month 6,200,000 VND / person / month Resolution 954/2020 (2024) & Resolution 110/2025 (2026)
PIT Schedule 7 brackets (5% – 35%) 5 brackets (5% – 35%) 5 brackets Law on PIT 2007 (2024) & Law on PIT No. 109/2025/QH15 Art. 9 (2026)
2026 Progressive PIT Tax Schedule (Law 109/2025/QH15 Art. 9)
Bracket Monthly Assessable Income Tax Rate
1 Up to 10,000,000 VND 5%
2 Over 10,000,000 to 30,000,000 VND 10%
3 Over 30,000,000 to 60,000,000 VND 20%
4 Over 60,000,000 to 100,000,000 VND 30%
5 Over 100,000,000 VND 35%

Cash Basis Principle: taxable income from salaries is determined at the time the income is paid (Law on PIT No. 109/2025/QH15 Art. 8.3; before that Circular 111/2013/TT-BTC Art. 8.2.b, replaced by Circular 87/2026/TT-BTC from 01/07/2026). Salary for December 2025 paid on January 05, 2026 is subject to the 2026 policy, as the tax authority confirmed in January 2026.

🔢 Rounding Convention

All amounts are whole VND (int64); no floating point is used anywhere.

Amount Rule
Each insurance item, employee (8%, 1.5%, 1%) and employer (17.5%, 3%, 1%) rate × capped salary, rounded half-up to 1 VND separately
PIT tax of each bracket rounded half-up to 1 VND, then summed
Net, totals exact sums of the rounded items
Net rounded to 1,000 VND not applied (company policy); use RoundToThousand if you need it

Half-up means x.5 VND goes up: 1.5% of 20,000,300 is 300,004.5, so 300,005 (a golden case).

The legal texts set rates and caps but no rounding step, and calculators differ (some round only the totals, some truncate). The effect is a few VND at most. Example, gross 20,000,028 in 2026: the insurance items are 1,600,002.24 + 300,000.42 + 200,000.28, so per item 2,100,002 but once on the total 2,100,003, and the net differs by 1 VND (TopCV gives the same net as vnsalary here, checked on 2026-10-13). In one sample (gross 20.0M–20.2M, 2026, no dependents) the two conventions gave nets 1 VND apart for about 28% of amounts. A difference of 1,000 VND or more is not rounding: please open an issue.


⚠️ Limitations

vnsalary covers employees on labour contracts, resident or not, Vietnamese or foreign; payments without a contract or under one shorter than 3 months go through FlatWithholding. Check these before relying on it for a real payroll:

  • Exempt allowances are taken as given. Extras.Overtime must be pay for hours within the legal limits (pay for extra hours belongs in gross only), and the limits of Extras.OtherExempt (phone, uniform, …) follow your employer's regulation; the library does not check them.
  • Not covered: for foreign employees, the social insurance exemptions (intra-company transfer, retirement age, treaties); for non-residents working in and outside Vietnam, splitting income by days in Vietnam (Decree 253/2026 Art. 64.3): pass the Vietnam part.
  • An insurance salary below the regional minimum wage is computed as given, with a warning. A full-time employee must be paid at least that minimum, so Result.Warnings reports code insurance_salary_below_regional_minimum (the CLI prints it with [!]). It is not an error because part-time contracts can legally be lower.
  • Employer accident insurance defaults to 0.5% and the 2% trade union fee is off by default; set Options.Employer (CLI --accident-rate, --union-fee) to match your employer.
  • Region is the place of work, not the head office or the employee's home. The library does not infer it; pass the region where the employee works.

Planned work is tracked in the milestones; ideas and requests are welcome as issues.


🛠️ Command-Line Interface (CLI)

Install CLI
go install github.com/HuynhNhatKhanh/vnsalary/cmd/vnsalary@latest

(Or compile directly: go build -o vnsalary ./cmd/vnsalary)

Homebrew (macOS, Linux) and Scoop (Windows):

brew install HuynhNhatKhanh/tap/vnsalary
scoop bucket add huynhnhatkhanh https://github.com/HuynhNhatKhanh/homebrew-tap
scoop install huynhnhatkhanh/vnsalary

Without a package manager: download a prebuilt binary for Linux, macOS or Windows (amd64/arm64) from the latest release, check it against checksums.txt, and run vnsalary version.

Commands
1. Calculate Gross to Net
vnsalary gross 35000000 -d 1 -r 1 --date 2026-01-15
# Also supports flags before positional args:
vnsalary gross -d 1 35000000 --date 2026-01-15
2. Calculate Net to Gross
vnsalary net 30843750 -d 1 -r 1 --date 2026-01-15
# Outputs exactly 35,000,000 VND Gross
3. Compare Policy Diff
vnsalary diff 35000000 -d 1 -r 1                                   # default: 2024-07-01 vs 2026-01-01
vnsalary diff 60000000 --from 2026-06-30 --to 2026-07-01           # any two payment dates
Overtime, allowances and bonuses

The amount is the total paid in the month; flags say which parts are extras:

vnsalary gross 50000000 --bonus 20000000 --date 2026-08-10        # shows the tax caused by the bonus
vnsalary gross 55000000 --overtime 5000000 --meal 1500000 --other-exempt 800000 --date 2026-08-10
Foreign employees
vnsalary gross 60000000 --foreigner --date 2026-08-10                # no unemployment insurance
vnsalary gross 60000000 --foreigner --non-resident --date 2026-08-10 # 20% flat, no deductions
vnsalary gross 30000000 --accident-rate 0.3 --union-fee              # employer cost with 0.3% accident insurance and the 2% union fee
10% withholding
vnsalary flat 8000000 --date 2026-09-10                         # 800,000 withheld; below the threshold it says so
Annual PIT finalization

vnsalary annual reads the payments of a tax year from a JSON file (- reads stdin). Give each payment either its gross (withholding is computed with the file's dependents and region; bonus, overtime, meal, other_exempt, ins_base as in gross) or the amounts on the payslip (taxable_income, insurance, tax_withheld):

{
  "year": 2026,
  "dependents": 1,
  "healthcare": 10000000,
  "payments": [
    {"date": "2026-01-28", "gross": 40000000},
    {"date": "2026-08-28", "taxable_income": 25000000, "insurance": 2625000, "tax_withheld": 343750}
  ]
}
vnsalary annual payments.json            # prints TO PAY or REFUNDABLE

dependent_months defaults to dependents × 12; charity, healthcare and education are yearly amounts. Unknown fields are rejected, so a typo cannot silently drop a deduction.

4. Language Selection

Default interface is in Vietnamese; --lang en switches the whole CLI to English (help, errors, tables, and amounts formatted as 35,000,000 VND). JSON output is language-independent:

vnsalary gross 35000000 -d 1 --lang en
5. JSON Output

Append --json to any command for programmatic integration:

vnsalary gross 35000000 --json

⚡ Performance & Benchmarks

Example run (go test -bench=. -benchmem, linux/amd64, Intel i7-10700K; numbers vary by machine and run):

BenchmarkGrossToNet-16      3850897           329.0 ns/op         336 B/op          3 allocs/op
BenchmarkNetToGross-16       111998         12749   ns/op       12920 B/op        133 allocs/op
  • Forward calculation: sub-microsecond; allocations come from the bracket lines and the copied policy reference in the result.
  • Inverse calculation: interpolation on the linear segment, then an ascending scan of ±50 VND extended down to the smallest gross with the same net, with a binary-search fallback. See docs/design.md for why the result is always the smallest gross.

🧪 Testing

The library is verified with a comprehensive test suite:

  • Golden Test Dataset (testdata/golden.json): 40 cases, each with a source and verified_at field stating how it was checked (hand calculation, an independent reference implementation, or an external calculator). 12 cases were matched to the VND against the TopCV calculator (2026-10-13 and 2026-10-15); cases not yet cross-checked with an external tool say so explicitly. 14 cases use odd amounts such as 23,456,789 so that every rounding step is exercised; 14 pin each insurance item. 4 cases cover foreign employees and non-residents.
  • Round-Trip Invariance Testing: for every gross g, NetToGross(Net(g)) returns the smallest gross with the same net, so Net(NetToGross(Net(g))) == Net(g) and NetToGross(Net(g)).Gross <= g (several grosses can share one net because of rounding).
  • Policy invariants: TestPoliciesInvariants fails if a new policy breaks an assumption of the inverse algorithm (ordering, social/health cap below unemployment cap, top bracket 35%, sources present).
  • Fuzz Testing: Includes FuzzGrossToNet_NetToGross verifying property invariance on arbitrary inputs.
  • Security & Integrity: Verifies immutability of PolicyAt slices, overflow bounds checking (> 10¹⁴ VND), and negative net rejection when insurance exceeds gross.
  • CLI: every command, flag spelling, language and exit code is tested in-process; table output is pinned by snapshot files.
  • CI: Go 1.22 (minimum), oldstable and stable, with the race detector, go vet, golangci-lint, gofmt and a fuzz smoke run.
go test -cover ./...

Statement coverage at v0.6.1: library 97.7%, CLI 96.4%.


🤝 Contributing

Law changes and calculation mismatches are the most useful reports; both have issue templates. See CONTRIBUTING.md for how a new policy is added and how versions are numbered, CHANGELOG.md for releases, and docs/design.md for the design and its decisions.


📄 License

MIT License. See LICENSE for details.

Documentation

Overview

Package vnsalary provides an exact Gross-to-Net and Net-to-Gross salary and personal income tax (PIT / Thuế TNCN) calculator according to Vietnamese labor and taxation regulations.

The library determines statutory parameters based on the income payment date (Date): taxable income from salaries is determined when the income is paid (Law on PIT No. 109/2025/QH15, Article 8.3; previously Circular 111/2013/TT-BTC, Article 8.2.b).

All monetary values are represented as integer Vietnamese Dong (VND int64) with half-up integer rounding independently applied to mandatory insurance items and progressive tax brackets. No floating-point arithmetic (zero float64) and zero external dependencies (Go Standard Library only).

Index

Examples

Constants

View Source
const (
	// MaxGross is the maximum supported salary amount (100 trillion VND) to guard against int64 overflow.
	MaxGross VND = 100_000_000_000_000

	// MaxDependents is the maximum allowed number of dependents.
	MaxDependents = 50

	// MaxAnnualPayments is the maximum number of payments Annual accepts for one tax year.
	MaxAnnualPayments = 1000

	// FirstAnnualYear is the first tax year Annual supports: the policy data
	// starts on 2024-07-01, so 2025 is the first year it covers in full.
	FirstAnnualYear = 2025
)
View Source
const (
	// WarnOvertimeTaxedBeforeExemption: before tax period 2026 only the part of
	// overtime pay above normal-hours pay was exempt (Circular 111/2013/TT-BTC
	// Art. 3.1.i). The library cannot split Extras.Overtime, so it taxes all of
	// it; put the exempt part in Extras.OtherExempt instead.
	WarnOvertimeTaxedBeforeExemption = "overtime_taxed_before_2026"

	// WarnMealCapUnverified: the meal allowance cap for this payment date could
	// not be confirmed from a legal text; the policy's cap is applied.
	WarnMealCapUnverified = "meal_cap_unverified"

	// WarnInsuranceSalaryIncludesAllowances: InsuranceBase is nil, so MealCash
	// and OtherExempt are included in the insurance salary. Set InsuranceBase if
	// the employment contract excludes them.
	WarnInsuranceSalaryIncludesAllowances = "insurance_salary_includes_allowances"
)

Warning codes for Options.Extras.

View Source
const WarnDependentsIgnoredNonResident = "dependents_ignored_non_resident"

WarnDependentsIgnoredNonResident is reported when Dependents is set for a non-resident, who has no family deductions; the dependents are ignored.

View Source
const WarnHealthEducationNotDeductible = "health_education_not_deductible"

WarnHealthEducationNotDeductible is reported when Healthcare or Education is given for a tax year before 2026, when these expenses were not deductible; they are ignored.

View Source
const WarnInsuranceSalaryBelowRegionalMinimum = "insurance_salary_below_regional_minimum"

WarnInsuranceSalaryBelowRegionalMinimum is reported when the salary used for insurance (InsuranceBase, or gross when InsuranceBase is nil) is positive but below the regional minimum wage. A full-time employee must be paid at least that minimum (Labour Code 2019, Art. 90.2), so a lower insurance salary usually means a part-time contract or an input mistake. It is a warning, not an error: the legal floor for the social insurance salary itself is the reference level (Law on Social Insurance 41/2024/QH15, Art. 31.1), which is lower than the regional minimum.

Variables

View Source
var (
	// ErrInvalidAmount indicates that a negative amount was supplied for gross or net salary.
	ErrInvalidAmount = errors.New("vnsalary: amount must be non-negative")

	// ErrAmountTooLarge indicates that an amount exceeds the supported maximum (100 trillion VND).
	ErrAmountTooLarge = errors.New("vnsalary: amount exceeds supported maximum (100,000,000,000,000 VND)")

	// ErrInvalidDependents indicates that the dependents count is out of range [0, 50].
	ErrInvalidDependents = errors.New("vnsalary: dependents count must be between 0 and 50")

	// ErrInvalidRegion indicates that the minimum wage region is outside [1, 4].
	ErrInvalidRegion = errors.New("vnsalary: region must be between 1 and 4")

	// ErrDateOutOfRange indicates that the effective date is prior to July 01, 2024.
	ErrDateOutOfRange = errors.New("vnsalary: date must be on or after 2024-07-01")

	// ErrDateZero indicates that a zero time value was provided for the payment date.
	ErrDateZero = errors.New("vnsalary: date cannot be zero")

	// ErrExtrasExceedGross indicates that Options.Extras add up to more than gross;
	// gross is the total paid in the month and must include them.
	ErrExtrasExceedGross = errors.New("vnsalary: extras exceed gross (gross must include them)")

	// ErrInsuranceExceedsGross indicates that the mandatory employee insurance contribution exceeds the gross salary.
	ErrInsuranceExceedsGross = errors.New("vnsalary: insurance contribution exceeds gross salary")

	// ErrInvalidAccidentRate indicates that EmployerOptions.AccidentPerMille is outside [0, 10].
	ErrInvalidAccidentRate = errors.New("vnsalary: accident insurance rate must be between 0 and 10 per mille")

	// ErrYearOutOfRange indicates that Annual was asked for a tax year before FirstAnnualYear.
	ErrYearOutOfRange = errors.New("vnsalary: tax year must be 2025 or later")

	// ErrPaymentOutsideYear indicates that a payment passed to Annual is dated outside the tax year.
	ErrPaymentOutsideYear = errors.New("vnsalary: payment date is outside the tax year")

	// ErrTooManyPayments indicates that Annual received more than MaxAnnualPayments payments.
	ErrTooManyPayments = errors.New("vnsalary: too many payments (maximum 1000 per tax year)")

	// ErrInvalidDependentMonths indicates that AnnualOptions.DependentMonths is outside [0, MaxDependents*12].
	ErrInvalidDependentMonths = errors.New("vnsalary: dependent months must be between 0 and 600")

	// ErrNetUnreachable indicates that no integer gross amount maps to this exact net salary due to half-up discretization.
	ErrNetUnreachable = errors.New("vnsalary: net salary is unreachable due to integer rounding")
)

Functions

This section is empty.

Types

type AnnualDeductions added in v0.4.0

type AnnualDeductions struct {
	Personal   VND `json:"personal"`   // 12 months of the personal deduction, whatever the months worked
	Dependents VND `json:"dependents"` // DependentMonths × the monthly dependent deduction
	Charity    VND `json:"charity"`
	Healthcare VND `json:"healthcare"` // After the yearly cap
	Education  VND `json:"education"`  // After the yearly cap
	Total      VND `json:"total"`
}

AnnualDeductions itemizes the deductions applied at finalization.

type AnnualOptions added in v0.4.0

type AnnualOptions struct {
	// DependentMonths is the number of months claimed for all dependents
	// together (2 dependents for the whole year = 24). At finalization a
	// dependent counts from the month the duty of support arose, even if
	// registered later in the year (Decree 253/2026/NĐ-CP Art. 48.2.a).
	DependentMonths int
	Charity         VND // Contributions to licensed charity, humanitarian and study-promotion funds (Decree 253/2026/NĐ-CP Art. 49.1)
	Healthcare      VND // Medical expenses of the taxpayer and dependents; capped by Policy.HealthcareDeductionCap
	Education       VND // Tuition of the taxpayer and dependents; capped by Policy.EducationDeductionCap
}

AnnualOptions holds the yearly deductions claimed at finalization. The zero value claims only the personal deduction.

type AnnualResult added in v0.4.0

type AnnualResult struct {
	Year             int              `json:"year"`
	TaxableIncome    VND              `json:"taxable_income"` // Sum of the payments' taxable income
	Insurance        VND              `json:"insurance"`      // Sum of the payments' compulsory insurance
	Deductions       AnnualDeductions `json:"deductions"`
	AssessableIncome VND              `json:"assessable_income"` // Taxable income minus insurance and deductions, not below zero
	Tax              VND              `json:"tax"`               // Tax due for the year on the yearly schedule
	Brackets         []BracketLine    `json:"brackets"`
	Withheld         VND              `json:"withheld"` // Sum of the payments' withheld tax
	Balance          VND              `json:"balance"`  // Tax - Withheld: positive = to pay, negative = refundable
	Policy           PolicyRef        `json:"policy"`
	Warnings         []Warning        `json:"warnings,omitempty"`
}

AnnualResult is the PIT finalization (quyết toán thuế TNCN) of a tax year.

func Annual added in v0.4.0

func Annual(year int, payments []Payment, opts AnnualOptions) (AnnualResult, error)

Annual computes the PIT finalization of a resident individual's salary income for a tax year: the yearly tax on the sum of all payments, compared with the tax already withheld.

Monthly withholding and the yearly tax differ when income is uneven (a bonus month), when the taxpayer changed employers or did not work the whole year (the personal deduction always counts 12 months at finalization, Decree 253/2026/NĐ-CP Art. 48.1.b), or when dependents were registered late. Pass the payments from every payer in the year, including those withheld at the 10% flat rate.

The yearly schedule is the monthly schedule with every limit multiplied by 12 (Law on PIT 109/2025/QH15 Art. 9.2). Claiming Charity, Healthcare or Education from tax period 2026 means the taxpayer must file the finalization personally instead of authorizing the employer (Decree 253/2026/NĐ-CP Art. 51.3).

Example

Changed employer in 2026: 40M a month from January to June, no income in July, 25M a month from August. Each employer withheld with a monthly personal deduction; at finalization the personal deduction counts all 12 months, so part of the tax is refunded.

package main

import (
	"fmt"
	"time"

	"github.com/HuynhNhatKhanh/vnsalary"
)

func main() {
	var payments []vnsalary.Payment
	for month := 1; month <= 12; month++ {
		gross := vnsalary.VND(40_000_000)
		switch {
		case month == 7:
			continue
		case month > 7:
			gross = 25_000_000
		}
		paid := time.Date(2026, time.Month(month), 28, 0, 0, 0, 0, time.UTC)
		res, err := vnsalary.GrossToNet(gross, vnsalary.Options{Region: vnsalary.Region1, Date: paid})
		if err != nil {
			fmt.Println(err)
			return
		}
		payments = append(payments, res.Payment(paid))
	}

	res, err := vnsalary.Annual(2026, payments, vnsalary.AnnualOptions{})
	if err != nil {
		fmt.Println(err)
		return
	}
	fmt.Println("Personal deduction:", res.Deductions.Personal)
	fmt.Println("Tax for the year:  ", res.Tax)
	fmt.Println("Tax withheld:      ", res.Withheld)
	fmt.Println("Balance:           ", res.Balance)
}
Output:
Personal deduction: 186000000
Tax for the year:   8067500
Tax withheld:       10898750
Balance:            -2831250

type BracketLine

type BracketLine struct {
	Bracket     int `json:"bracket"`      // Tax bracket index (1, 2, ...)
	TaxablePart VND `json:"taxable_part"` // Income portion assessed within this bracket
	RatePercent int `json:"rate_percent"` // Tax rate percentage (5, 10, 15, ...)
	Tax         VND `json:"tax"`          // Tax amount accrued at this bracket
}

BracketLine details personal income tax paid at a specific progressive bracket.

type Deductions

type Deductions struct {
	Personal   VND `json:"personal"`   // Taxpayer personal deduction (giảm trừ bản thân)
	Dependents VND `json:"dependents"` // Qualifying dependents deduction (giảm trừ người phụ thuộc)
	Total      VND `json:"total"`      // Total deduction amount
}

Deductions itemizes personal and family tax relief deductions (giảm trừ gia cảnh).

type EmployerCost

type EmployerCost struct {
	Social         VND `json:"social"`              // Social insurance (BHXH): 17% + the accident rate, 17.5% by default (14% pension/death, 3% sickness/maternity, 0.5% occupational accident unless EmployerOptions.AccidentPerMille)
	Health         VND `json:"health"`              // Health insurance (BHYT) 3%
	Unemployment   VND `json:"unemployment"`        // Unemployment insurance (BHTN) 1%, none for a foreigner
	TotalInsurance VND `json:"total_insurance"`     // Total employer contribution (21.5% by default; 20.5% for a foreigner; less with a lower accident rate)
	UnionFee       VND `json:"union_fee,omitempty"` // Trade union fee (2%), when EmployerOptions.UnionFee is set
	TotalCost      VND `json:"total_cost"`          // Total employer cost = Gross + TotalInsurance + UnionFee
}

EmployerCost itemizes statutory employer contributions and total company expenditure.

type EmployerOptions added in v0.6.0

type EmployerOptions struct {
	// AccidentPerMille overrides the occupational accident insurance rate, in
	// thousandths (3 = 0.3%, approved for some employers under Decree
	// 58/2020/NĐ-CP); nil = the policy rate (0.5%). Allowed: 0 to 10.
	AccidentPerMille *int64

	// UnionFee adds the trade union fee (2% of the social insurance salary,
	// Law on Trade Unions 50/2024/QH15 Art. 29.1.b) to EmployerCost.TotalCost.
	// Employers pay it whether or not they have a grassroots union.
	UnionFee bool
}

EmployerOptions are settings of the employer that change its cost but not the employee's net.

Example

An employer approved for the 0.3% accident insurance rate, including the 2% trade union fee in its cost. The employee's net is unchanged.

package main

import (
	"fmt"
	"time"

	"github.com/HuynhNhatKhanh/vnsalary"
)

func main() {
	rate := int64(3) // 0.3%
	res, err := vnsalary.GrossToNet(30_000_000, vnsalary.Options{
		Region:   vnsalary.Region1,
		Date:     time.Date(2026, 8, 10, 0, 0, 0, 0, time.UTC),
		Employer: vnsalary.EmployerOptions{AccidentPerMille: &rate, UnionFee: true},
	})
	if err != nil {
		fmt.Println(err)
		return
	}
	fmt.Println("Employer social insurance:", res.Employer.Social)
	fmt.Println("Trade union fee:          ", res.Employer.UnionFee)
	fmt.Println("Total employer cost:      ", res.Employer.TotalCost)
}
Output:
Employer social insurance: 5190000
Trade union fee:           600000
Total employer cost:       36990000

type ExemptIncome added in v0.3.0

type ExemptIncome struct {
	Overtime VND `json:"overtime"`
	Meal     VND `json:"meal"`
	Other    VND `json:"other"`
	Total    VND `json:"total"`
}

ExemptIncome itemizes the parts of gross excluded from taxable income (thu nhập không tính thuế).

type Extras added in v0.3.0

type Extras struct {
	Overtime    VND // Overtime and night-work pay within the legal hour limits; fully tax-exempt from tax period 2026 (pay for hours beyond the limits belongs in gross only)
	MealCash    VND // Cash meal allowance; tax-exempt up to Policy.MealCashCap
	OtherExempt VND // Other allowances exempt under the employer's regulation (phone, uniform, travel...); the caller is responsible for their limits
	Bonus       VND // Bonus paid this month; taxable, outside the insurance salary
}

Extras are amounts already included in gross (gross is the total paid in the month) that the law treats differently from base salary. The zero value reproduces the calculation of earlier versions.

When InsuranceBase is nil, the insurance salary is gross minus Overtime and Bonus: both vary from month to month, so they are outside the salary "paid regularly and stably" (Law on Social Insurance 41/2024/QH15 Art. 31.1.b). MealCash and OtherExempt stay in the insurance salary; set InsuranceBase if the employment contract excludes them.

type FlatResult added in v0.5.0

type FlatResult struct {
	Amount    VND       `json:"amount"`    // Payment before tax
	Threshold VND       `json:"threshold"` // Per-payment amount from which withholding is required
	Required  bool      `json:"required"`  // Amount >= Threshold
	Withheld  VND       `json:"withheld"`  // 10% of Amount when Required, otherwise 0
	Net       VND       `json:"net"`       // Amount - Withheld
	Policy    PolicyRef `json:"policy"`
}

FlatResult is the 10% withholding on one payment to a resident without a labour contract or with one under 3 months.

func FlatWithholding added in v0.5.0

func FlatWithholding(amount VND, date time.Time) (FlatResult, error)

FlatWithholding computes the PIT a payer withholds from one payment of salary, wages, fees or other income to a resident individual without a labour contract or with one under 3 months, including payments after the contract ended: 10% of the whole payment when it reaches the threshold of the payment date (Decree 253/2026/NĐ-CP Art. 50.2 from 2026-07-01: 5,000,000; Circular 111/2013/TT-BTC Art. 25.1.i before: 2,000,000), without family deductions.

Below the threshold withholding is not required; from 2026-07-01 the payer may still withhold 10% when the individual asks (Art. 50.2). An individual whose only income is of this kind and who expects no tax after deductions may give the payer a commitment to defer withholding; that case is not modelled. Compulsory insurance, which depends on the contract, is not computed. The tax withheld is a prepayment: pass FlatResult.Payment to Annual to settle the year.

Example

A one-off payment of 8,000,000 in September 2026 to someone without a labour contract: 10% is withheld because the payment reaches the 5,000,000 threshold. With no other income in the year, finalization refunds it.

package main

import (
	"fmt"
	"time"

	"github.com/HuynhNhatKhanh/vnsalary"
)

func main() {
	paid := time.Date(2026, 9, 10, 0, 0, 0, 0, time.UTC)
	res, err := vnsalary.FlatWithholding(8_000_000, paid)
	if err != nil {
		fmt.Println(err)
		return
	}
	fmt.Println("Withheld:", res.Withheld, "Net:", res.Net)

	annual, err := vnsalary.Annual(2026, []vnsalary.Payment{res.Payment(paid)}, vnsalary.AnnualOptions{})
	if err != nil {
		fmt.Println(err)
		return
	}
	fmt.Println("Balance at finalization:", annual.Balance)
}
Output:
Withheld: 800000 Net: 7200000
Balance at finalization: -800000

func (FlatResult) Payment added in v0.5.0

func (r FlatResult) Payment(date time.Time) Payment

Payment returns r as a payment made on date, for use with Annual.

type Insurance

type Insurance struct {
	Social       VND `json:"social"`       // Social insurance (BHXH) 8%
	Health       VND `json:"health"`       // Health insurance (BHYT) 1.5%
	Unemployment VND `json:"unemployment"` // Unemployment insurance (BHTN) 1%
	Total        VND `json:"total"`        // Total employee insurance contribution
}

Insurance itemizes mandatory employee contributions.

type Options

type Options struct {
	InsuranceBase *VND      // nil = based on gross salary; non-nil = contractual insurance salary (lương đóng bảo hiểm)
	Dependents    int       // Qualifying dependents count (0..50) (số người phụ thuộc)
	Region        Region    // Statutory minimum wage region (1..4) (vùng lương tối thiểu)
	Date          time.Time // Salary payment date; selects the policy in force (cash basis, Law on PIT 109/2025/QH15 Art. 8.3)
	Extras        Extras    // Parts of gross treated differently from base salary; zero value = none

	// Foreigner marks a foreign national: no unemployment insurance (BHTN) for
	// the employee or the employer, because that insurance covers Vietnamese
	// citizens only (Law on Employment 74/2025/QH15 Art. 2.1; Law 38/2013/QH13
	// Art. 3.1 before 2026). Social and health insurance stay: a foreign
	// employee on a contract of 12 months or more pays social insurance (Law on
	// Social Insurance 41/2024/QH15 Art. 2.2). Its exemptions (intra-company
	// transfer, retirement age at signing, treaties) are not modelled; set
	// InsuranceBase to 0 in those cases only if health insurance is also not due.
	Foreigner bool

	// NonResident marks a non-resident for PIT: 20% of taxable income from
	// work in Vietnam, without family or insurance deductions (Decree
	// 253/2026/NĐ-CP Art. 64; Law on PIT 2007 Art. 26 before 2026). A resident
	// is present in Vietnam 183 days or more in the calendar year or in 12
	// consecutive months, or has a regular place to stay there (Decree
	// 253/2026/NĐ-CP Art. 4); the caller decides. Insurance is unchanged.
	NonResident bool

	// Employer holds employer-side settings that change EmployerCost only;
	// the zero value keeps earlier results.
	Employer EmployerOptions
}

Options specifies optional calculation parameters.

type Payment added in v0.4.0

type Payment struct {
	Date          time.Time // Payment date; the payment belongs to the tax year of this date (cash basis)
	TaxableIncome VND       // Taxable income (thu nhập chịu thuế): gross minus exempt income
	Insurance     VND       // Compulsory insurance paid by the employee (BHXH, BHYT, BHTN)
	TaxWithheld   VND       // PIT withheld by the payer, at the progressive rates or the 10% flat rate
}

Payment is one salary payment of a tax year, as calculated by GrossToNet or as printed on a payslip or a withholding certificate.

type Policy

type Policy struct {
	EffectiveFrom time.Time
	Ref           PolicyRef

	// Base salary (lương cơ sở / mức tham chiếu) and statutory ceiling for social & health insurance (20 x base salary)
	BaseSalary            VND
	MaxSocialHealthSalary VND

	// Regional minimum wages for Regions I, II, III, IV (index 1..4) used to cap unemployment insurance (20 x regional min)
	RegionalMinSalaries [5]VND

	// Monthly personal family deductions (giảm trừ gia cảnh)
	PersonalDeduction  VND // Personal taxpayer deduction (giảm trừ bản thân)
	DependentDeduction VND // Deduction per qualifying dependent (giảm trừ người phụ thuộc)

	// Progressive PIT schedule (biểu thuế lũy tiến từng phần)
	Brackets []TaxBracket

	// Salary PIT schedule of non-residents: one bracket, a flat rate on
	// taxable income without deductions (see Options.NonResident).
	NonResidentBrackets []TaxBracket

	// Income excluded from PIT (thu nhập không tính thuế), see Extras.
	MealCashCap         VND  // Cash meal allowance excluded from taxable income up to this amount per month
	MealCashCapVerified bool // False when the cap for this period could not be confirmed from a legal text
	OvertimeFullyExempt bool // Overtime and night-work pay within the legal limits is fully excluded (from tax period 2026)

	// Yearly caps on medical and tuition expenses of the taxpayer and dependents
	// deductible at annual finalization (from tax period 2026); zero = not deductible.
	HealthcareDeductionCap VND
	EducationDeductionCap  VND

	// Payments of this amount or more to a resident without a labour contract
	// or with one under 3 months are withheld at 10% (see FlatWithholding).
	FlatWithholdingThreshold VND

	// Employer occupational accident and disease insurance (TNLĐ-BNN), in
	// thousandths of the social insurance salary (5 = 0.5%); part of the
	// employer's social insurance. EmployerOptions.AccidentPerMille overrides it.
	AccidentPerMille int64

	// Trade union fee (kinh phí công đoàn) paid by the employer, in
	// thousandths of the social insurance salary (20 = 2%); added to
	// EmployerCost only when EmployerOptions.UnionFee is set.
	UnionFeePerMille int64
}

Policy contains all statutory parameters effective for a given period.

func PolicyAt

func PolicyAt(date time.Time) (Policy, error)

PolicyAt returns the statutory policy parameter set effective at the given payment date. The returned value is an independent deep copy: modifying it will not affect any subsequent calculations. Returns ErrDateOutOfRange if date is prior to July 01, 2024.

func (Policy) MaxUnemploymentSalary

func (p Policy) MaxUnemploymentSalary(region Region) VND

MaxUnemploymentSalary returns the statutory cap for unemployment insurance (20 x regional minimum wage).

func (Policy) RegionalMinSalary

func (p Policy) RegionalMinSalary(region Region) VND

RegionalMinSalary returns the statutory minimum wage for the specified region.

type PolicyRef

type PolicyRef struct {
	EffectiveFrom string   `json:"effective_from"`
	Description   string   `json:"description"`    // Description in English
	DescriptionVI string   `json:"description_vi"` // Description in Vietnamese (mô tả tiếng Việt)
	Sources       []string `json:"sources"`        // Official statutory legal documents in Vietnamese
}

PolicyRef provides references to statutory decrees and enacted legislation.

type Region

type Region int

Region represents the statutory minimum wage region (Vùng lương tối thiểu: 1, 2, 3, 4).

const (
	Region1 Region = 1
	Region2 Region = 2
	Region3 Region = 3
	Region4 Region = 4
)

type Result

type Result struct {
	Gross            VND           `json:"gross"`
	Net              VND           `json:"net"`
	Insurance        Insurance     `json:"insurance"`
	TaxableIncome    VND           `json:"taxable_income"`    // Total taxable income (thu nhập chịu thuế)
	AssessableIncome VND           `json:"assessable_income"` // Income assessed for PIT after insurance and deductions (thu nhập tính thuế)
	Deductions       Deductions    `json:"deductions"`
	Tax              VND           `json:"tax"`
	Brackets         []BracketLine `json:"brackets"`
	Employer         EmployerCost  `json:"employer"`
	Policy           PolicyRef     `json:"policy"`
	Exempt           *ExemptIncome `json:"exempt,omitempty"`    // Exempt parts of gross; set only when Options.Extras is used
	BonusTax         VND           `json:"bonus_tax,omitempty"` // Tax caused by Extras.Bonus: tax with the bonus minus tax without it
	Warnings         []Warning     `json:"warnings,omitempty"`  // Inputs computed as given but worth checking; never change amounts
}

Result holds the comprehensive breakdown of a salary calculation.

Example (Warnings)

Warnings flag inputs that are computed as given but worth checking; they never change the amounts.

package main

import (
	"fmt"
	"time"

	"github.com/HuynhNhatKhanh/vnsalary"
)

func main() {
	opts := vnsalary.Options{
		Region: vnsalary.Region1,
		Date:   time.Date(2026, 3, 15, 0, 0, 0, 0, time.UTC),
	}

	res, err := vnsalary.GrossToNet(3_000_000, opts)
	if err != nil {
		fmt.Printf("error: %v\n", err)
		return
	}

	for _, w := range res.Warnings {
		fmt.Println(w.Code)
	}
	fmt.Printf("Net: %d VND\n", res.Net)

}
Output:
insurance_salary_below_regional_minimum
Net: 2685000 VND

func GrossToNet

func GrossToNet(gross VND, opts Options) (Result, error)

GrossToNet converts Gross salary to Net take-home pay and itemizes all deductions.

Example
package main

import (
	"fmt"
	"time"

	"github.com/HuynhNhatKhanh/vnsalary"
)

func main() {
	opts := vnsalary.Options{
		Dependents: 1,
		Region:     vnsalary.Region1,
		Date:       time.Date(2026, 1, 15, 0, 0, 0, 0, time.UTC),
	}

	res, err := vnsalary.GrossToNet(35_000_000, opts)
	if err != nil {
		fmt.Printf("error: %v\n", err)
		return
	}

	fmt.Printf("Gross: %d VND\n", res.Gross)
	fmt.Printf("BHXH (8%%): %d VND\n", res.Insurance.Social)
	fmt.Printf("BHYT (1.5%%): %d VND\n", res.Insurance.Health)
	fmt.Printf("BHTN (1%%): %d VND\n", res.Insurance.Unemployment)
	fmt.Printf("Total Insurance: %d VND\n", res.Insurance.Total)
	fmt.Printf("Tax: %d VND\n", res.Tax)
	fmt.Printf("Net: %d VND\n", res.Net)

}
Output:
Gross: 35000000 VND
BHXH (8%): 2800000 VND
BHYT (1.5%): 525000 VND
BHTN (1%): 350000 VND
Total Insurance: 3675000 VND
Tax: 481250 VND
Net: 30843750 VND
Example (Allowances)

Overtime pay and allowances inside gross are excluded from taxable income under the rules in force on the payment date.

package main

import (
	"fmt"
	"time"

	"github.com/HuynhNhatKhanh/vnsalary"
)

func main() {
	opts := vnsalary.Options{
		Dependents: 1,
		Region:     vnsalary.Region1,
		Date:       time.Date(2026, 8, 10, 0, 0, 0, 0, time.UTC),
		Extras: vnsalary.Extras{
			Overtime:    5_000_000, // fully exempt from tax period 2026
			MealCash:    1_500_000, // exempt up to 1,200,000 from 2026-07-01
			OtherExempt: 800_000,   // phone, uniform... under the employer's regulation
		},
	}

	res, err := vnsalary.GrossToNet(55_000_000, opts)
	if err != nil {
		fmt.Printf("error: %v\n", err)
		return
	}

	fmt.Printf("Exempt: overtime %d + meal %d + other %d = %d VND\n",
		res.Exempt.Overtime, res.Exempt.Meal, res.Exempt.Other, res.Exempt.Total)
	fmt.Printf("Taxable income: %d VND\n", res.TaxableIncome)
	fmt.Printf("Net: %d VND\n", res.Net)

}
Output:
Exempt: overtime 5000000 + meal 1200000 + other 800000 = 7000000 VND
Taxable income: 48000000 VND
Net: 48145000 VND
Example (Bonus)

A bonus is part of the gross paid in the month. It is taxed in that month but is not part of the insurance salary; BonusTax is the tax the bonus adds.

package main

import (
	"fmt"
	"time"

	"github.com/HuynhNhatKhanh/vnsalary"
)

func main() {
	opts := vnsalary.Options{
		Region: vnsalary.Region1,
		Date:   time.Date(2026, 8, 10, 0, 0, 0, 0, time.UTC),
		Extras: vnsalary.Extras{Bonus: 20_000_000}, // included in the 50M gross
	}

	res, err := vnsalary.GrossToNet(50_000_000, opts)
	if err != nil {
		fmt.Printf("error: %v\n", err)
		return
	}

	fmt.Printf("Insurance: %d VND (on 30M salary, not the bonus)\n", res.Insurance.Total)
	fmt.Printf("Tax: %d VND, of which caused by the bonus: %d VND\n", res.Tax, res.BonusTax)
	fmt.Printf("Net: %d VND\n", res.Net)

}
Output:
Insurance: 3150000 VND (on 30M salary, not the bonus)
Tax: 2770000 VND, of which caused by the bonus: 2135000 VND
Net: 44080000 VND

func NetToGross

func NetToGross(net VND, opts Options) (Result, error)

NetToGross solves the minimum integer Gross salary that yields the exact Net salary.

Step 1: Analytical piecewise linear interpolation on the interval [bps[i], bps[i+1]] containing net to derive gApprox. Step 2: Ascending scan within +/- searchRadius (50 VND) around gApprox; the first match is then extended downward to the smallest gross with the same net. Step 3 (Fallback): Monotonic binary search on [segmentLow, segmentHigh] to guarantee complete coverage.

Example
package main

import (
	"fmt"
	"time"

	"github.com/HuynhNhatKhanh/vnsalary"
)

func main() {
	opts := vnsalary.Options{
		Dependents: 1,
		Region:     vnsalary.Region1,
		Date:       time.Date(2026, 1, 15, 0, 0, 0, 0, time.UTC),
	}

	res, err := vnsalary.NetToGross(30_843_750, opts)
	if err != nil {
		fmt.Printf("error: %v\n", err)
		return
	}

	fmt.Printf("Net: %d VND -> Gross: %d VND\n", res.Net, res.Gross)

}
Output:
Net: 30843750 VND -> Gross: 35000000 VND

func (Result) Payment added in v0.4.0

func (r Result) Payment(date time.Time) Payment

Payment returns r as a payment made on date, for use with Annual.

type TaxBracket

type TaxBracket struct {
	Limit         VND   // Upper limit of assessable income in this bracket (e.g. 5,000,000 VND; math.MaxInt64 for the top bracket)
	RateNumerator int64 // Tax rate numerator (e.g. 5)
	RateDenom     int64 // Tax rate denominator (e.g. 100)
}

TaxBracket represents a single bracket in the progressive personal income tax schedule.

type VND

type VND int64

VND is an amount in Vietnamese Dong as a whole number. Salaries are non-negative; differences between two results (for example in a policy comparison) may be negative.

func RoundToThousand

func RoundToThousand(v VND) VND

RoundToThousand rounds a non-negative VND amount to the nearest thousand (1,000 VND) half-up. Convenient for payroll systems with cash-disbursement rounding policies.

type Warning added in v0.2.0

type Warning struct {
	Code    string `json:"code"`
	Message string `json:"message"`
}

Warning flags an input that is computed as given but is probably not what the caller intended. Warnings never change any amount.

Directories

Path Synopsis
cmd
vnsalary command
Command vnsalary converts Vietnamese gross and net salaries from the command line, using the github.com/HuynhNhatKhanh/vnsalary library.
Command vnsalary converts Vietnamese gross and net salaries from the command line, using the github.com/HuynhNhatKhanh/vnsalary library.

Jump to

Keyboard shortcuts

? : This menu
/ : Search site
f or F : Jump to
y or Y : Canonical URL