InvoiceCore 0.4.0

dotnet add package InvoiceCore --version 0.4.0
                    
NuGet\Install-Package InvoiceCore -Version 0.4.0
                    
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="InvoiceCore" Version="0.4.0" />
                    
For projects that support PackageReference, copy this XML node into the project file to reference the package.
<PackageVersion Include="InvoiceCore" Version="0.4.0" />
                    
Directory.Packages.props
<PackageReference Include="InvoiceCore" />
                    
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 InvoiceCore --version 0.4.0
                    
#r "nuget: InvoiceCore, 0.4.0"
                    
#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 InvoiceCore@0.4.0
                    
#: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=InvoiceCore&version=0.4.0
                    
Install as a Cake Addin
#tool nuget:?package=InvoiceCore&version=0.4.0
                    
Install as a Cake Tool

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.

NuGet CI License: MIT


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=true to MoneyFormat.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: Subtotal is 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.PerLine combined with TaxMode.Inclusive throws NotSupportedException at 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 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. 
Compatible target framework(s)
Included target framework(s) (in package)
Learn more about Target Frameworks and .NET Standard.
  • 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.

Version Downloads Last Updated
0.4.0 44 8/25/2026
0.3.0 92 8/19/2026
0.2.0 99 8/18/2026
0.1.1 86 8/18/2026
0.1.0 97 8/16/2026