InvoiceCore 0.4.0
dotnet add package InvoiceCore --version 0.4.0
NuGet\Install-Package InvoiceCore -Version 0.4.0
<PackageReference Include="InvoiceCore" Version="0.4.0" />
<PackageVersion Include="InvoiceCore" Version="0.4.0" />
<PackageReference Include="InvoiceCore" />
paket add InvoiceCore --version 0.4.0
#r "nuget: InvoiceCore, 0.4.0"
#:package InvoiceCore@0.4.0
#addin nuget:?package=InvoiceCore&version=0.4.0
#tool nuget:?package=InvoiceCore&version=0.4.0
InvoiceCore
Zero-dependency invoicing primitives for .NET. Predictable, documented rounding, multi-rate tax (inclusive and exclusive), status rules, JSON/CSV export. Bring your own storage and rendering.
Quick start
using System; // Console, DateOnly
using System.Collections.Generic; // List<T>
using InvoiceCore; // InvoiceService, CreateInvoiceRequest, CustomerInfo, LineItem, TaxRate
var svc = new InvoiceService();
var invoice = svc.Create(new CreateInvoiceRequest
{
InvoiceNumber = "INV-001",
IssuedDate = new DateOnly(2025, 1, 15),
DueDate = new DateOnly(2025, 2, 15),
CurrencyCode = "USD",
Customer = new CustomerInfo { Name = "Acme Corp" },
LineItems = new List<LineItem> { new LineItem { Description = "Consulting", Quantity = 2, UnitPrice = 50m } },
TaxRates = new List<TaxRate> { new TaxRate { Name = "VAT", Percentage = 20m } },
});
Console.WriteLine($"Subtotal : {invoice.Subtotal:C}"); // $100.00
Console.WriteLine($"VAT 20% : {invoice.TaxAmount:C}"); // $20.00
Console.WriteLine($"Total : {invoice.Total:C}"); // $120.00
Console.WriteLine(svc.ExportToJson(invoice));
JSON export
ExportToJson returns a camelCase JSON string. Two options control the output:
| Option | Default | Effect |
|---|---|---|
MoneyFormat |
MoneyFormat.String |
Monetary amounts as quoted decimal strings ("10.50") |
IncludeNulls |
false |
Omit null optional fields |
Default output (MoneyFormat.String, IncludeNulls=false):
{
"invoiceNumber": "INV-001",
"currencyCode": "USD",
"subtotal": "100.00",
"taxAmount": "20.00",
"total": "120.00",
"lineItems": [{ "unitPrice": "50.00", "total": "100.00", ... }],
...
}
Monetary strings are formatted to the currency's minor-unit precision using
InvariantCulture: JPY produces "1000", KWD produces "100.010". Percentages,
quantities, and exchangeRate stay as JSON numbers.
Legacy numeric output (restores pre-0.3.0 behaviour):
var json = svc.ExportToJson(invoice, new JsonExportOptions
{
MoneyFormat = MoneyFormat.Number,
IncludeNulls = true,
});
Breaking change in 0.3.0: The defaults changed from
MoneyFormat.Number + IncludeNulls=truetoMoneyFormat.String + IncludeNulls=false. Consumers that parse monetary fields as numbers or compare exact JSON byte-for-byte must update their code.
Tax arithmetic: worked example
Both modes produce the same correctly-rounded totals. Subtotal is always
tax-exclusive: this is the value most likely to surprise callers switching
between modes.
Exclusive mode (prices are net)
Line: Qty 2 × $50.00 = $100.00
───────
Subtotal (net) $100.00 ← tax-exclusive in both modes
VAT 20% on $100.00 = $20.00
───────
Total $120.00
Inclusive mode (same gross price, tax extracted)
using InvoiceCore; // TaxMode
// TaxMode = TaxMode.Inclusive, one line Qty 1 × $120.00, VAT 20%
Line total (gross): $120.00
Subtotal = Round($120.00 / 1.20) = $100.00 ← tax-exclusive net
VAT 20% = Round($100.00 × 0.20) = $ 20.00
Residual reconciliation keeps Total = $120.00 exactly
───────
Total $120.00
Note:
Subtotalis the tax-exclusive net in both modes. In Inclusive mode it is the back-calculated base, not the gross line-item figure.
Multiple rates are additive, never compounded:
Subtotal $1 000.00
VAT 20% $200.00
Levy 5% $ 50.00
───────
TaxAmount $ 250.00
Total $1 250.00
Per-line tax calculation
By default, InvoiceCore applies each tax rate once to the rounded subtotal
(TaxCalculationMethod.SubtotalFirst). Set TaxCalculationMethod.PerLine to round
each line's tax contribution separately and sum the results instead:
var invoice = svc.Create(new CreateInvoiceRequest
{
// ... other fields ...
TaxCalculationMethod = TaxCalculationMethod.PerLine,
});
When to use it: some accounting systems post a tax entry per line item to the ledger. Using the same rounding method avoids a mismatch between the invoice total and the sum of posted tax entries.
Worked example — 3 lines at £1.67 each, 20% UK VAT:
| Method | Subtotal | Tax | Total |
|---|---|---|---|
SubtotalFirst (default): Round(£5.01 × 0.20) |
£5.01 | £1.00 | £6.01 |
PerLine: 3 × Round(£1.67 × 0.20) = 3 × £0.33 |
£5.01 | £0.99 | £6.00 |
Both methods are permitted under
HMRC VATREC12030
and ATO GSTA 1999 s9-90. The maximum divergence between them is 1 minor unit per
invoice in SubtotalFirst mode.
Restriction:
TaxCalculationMethod.PerLinecombined withTaxMode.InclusivethrowsNotSupportedExceptionat invoice construction in this version. The mathematical reason is documented in docs/TAX-CONFORMANCE.md (per-line rounding residual analysis): for common retail prices such as £3.99 at 20% VAT, the per-line extraction residual accumulates to N minor units across N lines, which is outside the ±1 design of the inclusive-mode reconciliation rule.
Rounding policy
All money arithmetic uses MidpointRounding.AwayFromZero (half-up), routed
exclusively through a single internal Money.Round call site. There are no
ad-hoc Math.Round or decimal.Round calls anywhere in the library.
Minor-unit precision is resolved per ISO-4217:
| Digits | Codes (examples) |
|---|---|
| 0 | JPY, KRW, VND |
| 3 | KWD, BHD, OMR |
| 2 | USD, EUR, GBP and everything else |
Unknown codes default to 2 digits and never throw.
Tax compliance
InvoiceCore's rounding has been validated against two published tax authority sources. Full comparison tables and source links are in docs/TAX-CONFORMANCE.md.
Rounding rule
HMRC VATREC12030
and ATO GSTA 1999 s9-90
both specify the same rule: round to the nearest minor unit, half-up at the midpoint.
InvoiceCore uses MidpointRounding.AwayFromZero, which matches this exactly for positive amounts.
Subtotal-first vs per-line
InvoiceCore applies the tax rate to the rounded subtotal by default. Both HMRC and
the ATO permit per-line rounding; InvoiceCore also supports it via
TaxCalculationMethod.PerLine. See the Per-line tax calculation
section above for the full worked example and the restriction on inclusive mode.
HMRC truncation concession (Notice 700 §17.5) — not implemented
HMRC permits invoice traders to optionally round total VAT down to the nearest penny
(truncation, not half-up). InvoiceCore does not implement this. Callers who need it must
post-process TaxAmount. The concession is relevant only at the UK 5% reduced rate on
specific net values — for example, 5% of £0.30 = £0.015 exactly: InvoiceCore gives £0.02,
the concession allows £0.01. No divergence is possible at the standard 20% rate on
whole-penny net values, because 20% of any integer number of pence is never exactly 0.5p.
What this is not
- Not a payment processor. No partial payments, payment allocation, or credit notes. Negative quantities are rejected in v1.
- Not a PDF renderer. Use
InvoiceCore.Pdf(planned) for that. - Not an accounting ledger. No bank feeds, journal entries, or chart of accounts.
- Not a tax jurisdiction lookup. You supply the rate; InvoiceCore applies it correctly.
- Not an e-invoicing compliance layer. Peppol, ZATCA, and FatturaPA are out of scope.
- Not a recurring-invoice engine. No schedules, templates, or auto-numbering.
- Not a currency converter. An exchange rate can be stored on an invoice for reporting grouping; it is never applied.
- Not a mixed-rate line engine. Tax rates apply at invoice level; line items with differing VAT rates on the same invoice are not supported yet.
How this was built
See docs/PROCESS.md. The test suite was adversarially verified by mutation testing, which found four defects a green 202-test suite could not see.
Prior art
| Library | What it does | Why InvoiceCore is different |
|---|---|---|
InvoiceSdk |
Fluent PDF invoicing (.NET 6) | Depends on QuestPDF + ServiceStack.Text |
InvoicerNETCore, Invoicer, InvoiceGenerator.Core |
PDF generators | Not model libraries |
InvoiceCore replaces the 400 lines of subtly-wrong tax arithmetic every SaaS team writes by hand, with no dependencies, no rendering, and documented, tested rounding behaviour.
Roadmap
| Package | Status |
|---|---|
InvoiceCore |
v0.4.0 (current) |
InvoiceCore.Pdf |
Planned |
InvoiceCore.EfCore |
Planned |
InvoiceCore.Blazor |
Planned |
License
MIT © 2026 Aftab Bashir
| 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 is compatible. 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
- No dependencies.
-
net8.0
- No dependencies.
-
net9.0
- No dependencies.
NuGet packages
This package is not used by any NuGet packages.
GitHub repositories
This package is not used by any popular GitHub repositories.