Skip to content

Calculation Logic

How InvoicePDFs computes invoice totals with precision.

Overview

All calculations use decimal arithmetic (no floating point) with half-up rounding to 2 decimal places. This ensures accurate financial math even with complex tax and discount combinations.

Formula

total = subtotal - document_discount + tax_total + shipping_total

Calculation Order

Two passes, because a document-level discount is not known until every line has been read, but it has to affect the tax on every line.

Pass one — reduce each line to a taxable value
   a. gross = quantity × unit_price
   b. subtract the per-line discount
   c. extract inclusive tax, if any
   → what remains is the line's taxable value; the sum is the subtotal

Between the passes
   d. compute the document-level discount from the subtotal
   e. apportion it back across the lines, pro-rata by taxable value

Pass two — tax what is left
   f. apply each line's rates to (taxable value − its share of the discount),
      plus its share of the freight when shipping.taxable is set
   g. total = subtotal − document_discount + tax_total + shipping_total

Both kinds of discount reduce the tax

Where a discount is written does not change what is owed. 10% off a line and 10% off the invoice produce the same total, because the document-level discount is apportioned back into the taxable value before tax is applied.

This is not a preference. A discount granted at the time of supply is excluded from the taxable amount — EU VAT Directive Art. 79, CGST s.15(3)(a) — and a tax invoice must show the taxable value net of it (CGST Rule 46).

Worked Examples

Simple Invoice

{
  "line_items": [
    { "name": "Service", "quantity": "2", "unit_price": "50.00" }
  ]
}
Step Amount
Gross 2 × $50.00 = $100.00
Subtotal $100.00
Total $100.00

With Exclusive Tax

{
  "line_items": [
    {
      "name": "Service",
      "quantity": "1",
      "unit_price": "49.00",
      "taxes": [{ "name": "Sales Tax", "rate": "8.375", "inclusive": false }]
    }
  ]
}
Step Amount
Gross $49.00
Tax $49.00 × 8.375% = $4.10 (rounded)
Subtotal $49.00
Tax total $4.10
Total $53.10

With Inclusive Tax (VAT)

{
  "line_items": [
    {
      "name": "Product",
      "quantity": "1",
      "unit_price": "120.00",
      "taxes": [{ "name": "VAT", "rate": "20", "inclusive": true }]
    }
  ]
}
Step Amount
Gross $120.00
Inclusive VAT $120.00 × (20 / 120) = $20.00
Net (subtotal) $120.00 - $20.00 = $100.00
Tax total $20.00
Total $120.00

With Per-Line Discount + Tax

{
  "line_items": [
    {
      "name": "Service",
      "quantity": "1",
      "unit_price": "100.00",
      "discount": { "type": "percentage", "value": "20" },
      "taxes": [{ "name": "Tax", "rate": "10", "inclusive": false }]
    }
  ]
}
Step Amount
Gross $100.00
Line discount $100.00 × 20% = $20.00
Discounted gross $80.00
Tax $80.00 × 10% = $8.00
Subtotal $80.00
Discount total $20.00
Tax total $8.00
Total $88.00

Key insight

Per-line discounts are applied before tax calculation. This means tax is computed on the discounted amount, not the original price.

Full Example: Discounts + Tax + Shipping

{
  "line_items": [
    {
      "name": "Product A",
      "quantity": "2",
      "unit_price": "100.00",
      "discount": { "type": "percentage", "value": "10" },
      "taxes": [{ "name": "Tax", "rate": "10", "inclusive": false }]
    }
  ],
  "discounts": [
    { "type": "fixed", "value": "10.00" }
  ],
  "shipping": { "amount": "15.00" }
}
Step Amount
Gross 2 × $100.00 = $200.00
Line discount $200.00 × 10% = $20.00
Subtotal (taxable value) $180.00
Document discount $10.00
Taxed on $180.00 − $10.00 = $170.00
Tax $170.00 × 10% = $17.00
Shipping $15.00
Discount total $20.00 + $10.00 = $30.00
Total $180.00 − $10.00 + $17.00 + $15.00 = $202.00

This example changed

Tax here is $17.00, not $18.00, and the total $202.00, not $203.00. The document-level discount used to be subtracted after tax was computed, so the buyer was charged tax on $10.00 they were never billed for. The rate row on the printed invoice now cites $170.00 as the taxable value — the amount actually taxed.

Rounding

  • All monetary calculations round to 2 decimal places using half-up rounding
  • Rounding is applied per line item to avoid surprising totals
  • Tax is rounded per-line, then summed — not computed on the aggregate

Validation

Input that cannot be computed is refused with 422 unprocessable_entity, and the message names the field:

Rejected Why
"Infinity", "NaN" in any numeric field Not finite; the arithmetic cannot produce a number
quantity ≤ 0, negative unit_price, negative rate Not a quantity, price or rate
A percentage discount above 100 Cannot discount more than the whole
A fixed line discount above its line amount Would produce negative tax
Document discounts above the subtotal Same
{
  "error": {
    "status": 422,
    "code": "unprocessable_entity",
    "message": "line_items[0].quantity must be > 0"
  }
}

POST /api/v1/documents/validate runs the full calculation and discards the result, so it answers whether the payload can actually become a document — not just whether its fields look plausible. Anything it accepts, calculate and render will accept too.

Response Fields

Field Description
gross_subtotal Line amounts at list price, before any discount. What the Amount column on the printed invoice sums to
subtotal The taxable value: net of per-line discounts and of any tax bundled into the price
discount_total Every discount — per-line and document-level. Reported for information
document_discount_total Only the document-level discounts. This is the one the total formula subtracts
tax_total All tax, inclusive and exclusive
shipping_total Shipping amount (0 if none)
total subtotal − document_discount_total + tax_total + shipping_total

discount_total is not what the total subtracts

subtotal is already net of per-line discounts, so subtracting discount_total from it removes them a second time. Use document_discount_total for arithmetic, discount_total for display.

Both of these identities hold, and they are the two ways an invoice adds up:

total = subtotal       − document_discount_total + tax_total + shipping_total
total = gross_subtotal − discount_total          + tax_total + shipping_total

A rendered document also carries tax_lines: one row per rate, each with the taxable value it applies to and the tax it produced. Under GST a rate-wise breakup is part of what makes a document a tax invoice.

The POST /api/v1/documents/calculate endpoint wraps the breakdown under data.calculation:

{
  "data": {
    "calculation": {
      "subtotal": { "amount": "180.00", "currency": "USD" },
      "discount_total": { "amount": "30.00", "currency": "USD" },
      "document_discount_total": { "amount": "10.00", "currency": "USD" },
      "tax_total": { "amount": "17.00", "currency": "USD" },
      "shipping_total": { "amount": "15.00", "currency": "USD" },
      "total": { "amount": "202.00", "currency": "USD" }
    }
  }
}