finance.credit-note.calculate
Credit note against an original invoice, by units or by amount, with VAT at the original tax point's rates.
1.0.0 · published 2026-10-03 by charlie · Anterra
Pinned by 21 tests, run in TypeScript, Python and Rust.
What it does
A credit note reverses all or part of an invoice already issued. The rule that matters, and the one that is easy to get wrong, is the VAT rate: a credit note adjusts the original supply and its original VAT charge (HMRC VAT Notice 700, "The VAT guide", paragraph 18.2, https://www.gov.uk/guidance/vat-guide-notice-700), so its VAT is at the rate that applied at the **original invoice's tax point**, not the rate on the day the credit note is written. Crediting a 2010 invoice today gives 17.5% VAT, because that was the rate then. The rates themselves come from finance.tax.vat-rate through finance.invoice.calculate.
Each credit is against one line of the original invoice, either:
For example
calculateCreditNote(original lines ×3, credits ×1, GB, 2026-09-16)→ currency GBP, lines ×1, subtotal -£1,500.00, tax total -£300.00, total -£1,800.00, tax breakdown ×1 all three days of consultancy creditedcalculateCreditNote(original lines ×3, credits ×1, GB, 2010-06-01)→ currency GBP, lines ×1, subtotal -£500.00, tax total -£87.50, total -£587.50, tax breakdown ×1 a 2010 invoice credited today gets 2010's 17.5%, not today's 20%calculateCreditNote(original lines ×3, credits ×2, GB, 2026-09-16)→ currency GBP, lines ×2, subtotal -£525.00, tax total -£100.00, total -£625.00, tax breakdown ×2 units from two lines at two rates, grouped by rate
The function
The same function in TypeScript, Python and Rust, pinned by the same tests. Pick your language; the choice follows you around the registry.
export function calculateCreditNote(originalLines: readonly InvoiceLine[], credits: readonly CreditRequest[], jurisdiction: string, originalTaxPoint: string): Invoice
| originalLines | InvoiceLine[] | the lines of the invoice being credited, as it was issued |
| credits | CreditRequest[] | what to credit, line by line |
| jurisdiction | string | as on the original invoice |
| originalTaxPoint | date | the original invoice's tax point, which fixes the VAT rates |
| returns | Invoice | the credit note, every amount negative |
The type it declares, generated into your project
/** What to credit against one line of the original invoice: units, or a net amount. */
export interface CreditRequest {
/** 1-based line number on the original invoice */
readonly line: number;
/** units to credit at the original price and discount; null when crediting an amount */
readonly quantity: number | null;
/** a net amount to credit against the line, e.g. a price reduction; null when crediting units */
readonly net: Money | null;
}
Your code names it in one line, in the file that uses it
import { calculateCreditNote } from "#fune/finance.credit-note.calculate@^1";
Imports name this capability’s declared dependencies, which fune builds next to it in your project; each one links to its page.
import { calculateInvoice } from "./finance_invoice_calculate.ts"; ← from finance.invoice.calculate ^1.0.0 · built alongside by fune
import { type Invoice, type InvoiceLine } from "./finance_invoice_calculate_types.ts";
import { lineTotal } from "./finance_invoice_line_total.ts"; ← from finance.invoice.line-total ^1.0.0 · built alongside by fune
import { money } from "./money_amount.ts"; ← from money.amount ^1.0.0 · built alongside by fune
import { type CreditRequest } from "./finance_credit_note_calculate_types.ts";
/**
* A credit note against an invoice, with VAT at the original tax point.
*
* Every credit becomes a negative invoice line and the whole note is
* calculated by finance.invoice.calculate at the original date, so the VAT
* rates, rounding and grouping are the invoice's own.
*/
export function calculateCreditNote(
originalLines: readonly InvoiceLine[],
credits: readonly CreditRequest[],
jurisdiction: string,
originalTaxPoint: string,
): Invoice {
if (credits.length === 0) {
throw new RangeError("a credit note needs at least one line");
}
const credited = originalLines.map(() => 0);
const lines: InvoiceLine[] = credits.map((credit) => {
const n = credit.line;
if (!Number.isInteger(n) || n < 1 || n > originalLines.length) {
throw new RangeError(`credit refers to line ${n}, but the invoice has ${originalLines.length} lines`);
}
const original = originalLines[n - 1];
if (original.quantity <= 0) {
throw new RangeError(`line ${n} of the invoice is not a sale that can be credited`);
}
if ((credit.quantity === null) === (credit.net === null)) {
throw new RangeError(`credit for line ${n} needs exactly one of quantity and net`);
}
let line: InvoiceLine;
if (credit.quantity !== null) {
if (!Number.isInteger(credit.quantity) || credit.quantity <= 0) {
throw new RangeError(`credit quantity for line ${n} must be greater than zero`);
}
line = { ...original, quantity: -credit.quantity };
} else {
const net = credit.net!;
if (net.currency !== original.unitPrice.currency) {
throw new RangeError(`credit for line ${n} must be in ${original.unitPrice.currency}`);
}
if (net.minor <= 0) {
throw new RangeError(`credit amount for line ${n} must be greater than zero`);
}
line = {
description: original.description,
unitPrice: money(-net.minor, net.currency),
quantity: 1,
discountBasisPoints: 0,
taxCategory: original.taxCategory,
};
}
credited[n - 1] -= lineTotal(line.unitPrice, line.quantity, line.discountBasisPoints).minor;
if (credited[n - 1] > lineTotal(original.unitPrice, original.quantity, original.discountBasisPoints).minor) {
throw new RangeError(`credits against line ${n} exceed what it was invoiced for`);
}
return line;
});
return calculateInvoice(lines, jurisdiction, originalTaxPoint);
}Install
fune build
With that line in your source, in a TypeScript project (language typescript in fune.project), fune build resolves it and its 3 dependencies, pins them in fune.lock, downloads only the TypeScript package of each, and builds the code above into your project’s .fune/build, one readable file per capability with a header linking back here. Or pin a range in fune.project and build in one step:
fune add finance.credit-note.calculate
The manifest, vectors and README with only the TypeScript implementation. Install it without the registry with fune add ./finance.credit-note.calculate-1.0.0-typescript.fune, or fetch it from a terminal with fune pull finance.credit-note.calculate@1.0.0:typescript.
The whole function, every language, is one file too: finance.credit-note.calculate-1.0.0.fune, 36,097 bytes, sha256 4d2a4b47688bccd58137edab5e068912269c299b06b57e5e31bc2ef66d97ec31. It installs into a project of any language.
Customise it in your app
The seams this capability offers. Put a marker directly above a function of your own and fune build wires it into the built code; the package on the registry is not changed, the built file’s header lists it under CUSTOMISED, and fune hooks lists every hook in the project. How hooks work.
before — your function gets the arguments and returns them, changed or not, or throws to refuse the call.
// fune: before finance.credit-note.calculate
after — your function gets the result and the arguments, and returns the final result.
// fune: after finance.credit-note.calculate
replace — inside this capability’s code only, calls to a dependency go to your function, with the same signature. Other capabilities that use it are unaffected; write in * to replace it everywhere.
// fune: replace finance.invoice.calculate in finance.credit-note.calculate
// fune: replace finance.invoice.line-total in finance.credit-note.calculate
// fune: replace money.amount in finance.credit-note.calculate
step — your function runs at a numbered point inside the function’s body, receives the in-scope values it names as parameters, and may return replacements. List the points with fune show finance.credit-note.calculate --steps.
// fune: step finance.credit-note.calculate after <n|label>
Tests
A version published now needs at least 8 tests for every function, and one that expects the error for each function that throws; the registry refuses it otherwise. fune verify --all runs each case in TypeScript, Python and Rust, and a project runs them again with fune verify. This page lists the cases; it does not run them. The exact JSON is vectors.json.
| Case | Arguments | Expected | |
|---|---|---|---|
| all three days of consultancy credited | original lines ×3, credits ×1, GB, 2026-09-16 | → | currency GBP, lines ×1, subtotal -£1,500.00, tax total -£300.00, total -£1,800.00, tax breakdown ×1 |
| a 2010 invoice credited today gets 2010's 17.5%, not today's 20% | original lines ×3, credits ×1, GB, 2010-06-01 | → | currency GBP, lines ×1, subtotal -£500.00, tax total -£87.50, total -£587.50, tax breakdown ×1 |
| units from two lines at two rates, grouped by rate | original lines ×3, credits ×2, GB, 2026-09-16 | → | currency GBP, lines ×2, subtotal -£525.00, tax total -£100.00, total -£625.00, tax breakdown ×2 |
| a price reduction credited by amount, VAT added at the line's category | original lines ×3, credits ×1, GB, 2026-09-16 | → | currency GBP, lines ×1, subtotal -£10.00, tax total -£2.00, total -£12.00, tax breakdown ×1 |
| a discounted line credited by units keeps its discount and mirrors the invoice exactly | original lines ×3, credits ×1, GB, 2026-09-16 | → | currency GBP, lines ×1, subtotal -£89.99, tax total -£18.00, total -£107.99, tax breakdown ×1 |
| the whole invoice credited gives its totals negated | original lines ×3, credits ×3, GB, 2026-09-16 | → | currency GBP, lines ×3, subtotal -£1,614.99, tax total -£318.00, total -£1,932.99, tax breakdown ×2 |
| crediting a line's full net by amount is allowed | original lines ×3, credits ×1, GB, 2026-09-16 | → | currency GBP, lines ×1, subtotal -£25.00, tax total £0.00, total -£25.00, tax breakdown ×1 |
| units and an amount against one line, up to its net | original lines ×3, credits ×2, GB, 2026-09-16 | → | currency GBP, lines ×2, subtotal -£1,500.00, tax total -£300.00, total -£1,800.00, tax breakdown ×1 |
| hospitality credited at the covid rate of the original tax point | original lines ×1, credits ×1, GB, 2020-12-25 | → | currency GBP, lines ×1, subtotal -£50.00, tax total -£2.50, total -£52.50, tax breakdown ×1 |
| crediting more units than were invoiced is refused | original lines ×3, credits ×1, GB, 2026-09-16 | → | error: credits against line 1 exceed what it was invoiced for |
Show the other 11 tests
| Case | Arguments | Expected | |
|---|---|---|---|
| two credits that together exceed the line are refused | original lines ×3, credits ×2, GB, 2026-09-16 | → | error: credits against line 1 exceed what it was invoiced for |
| an amount above the line's net is refused | original lines ×3, credits ×1, GB, 2026-09-16 | → | error: credits against line 2 exceed what it was invoiced for |
| line 0 does not exist | original lines ×3, credits ×1, GB, 2026-09-16 | → | error: credit refers to line 0, but the invoice has 3 lines |
| line 4 does not exist | original lines ×3, credits ×1, GB, 2026-09-16 | → | error: credit refers to line 4, but the invoice has 3 lines |
| both a quantity and an amount is refused | original lines ×3, credits ×1, GB, 2026-09-16 | → | error: credit for line 1 needs exactly one of quantity and net |
| neither a quantity nor an amount is refused | original lines ×3, credits ×1, GB, 2026-09-16 | → | error: credit for line 1 needs exactly one of quantity and net |
| a zero quantity is refused | original lines ×3, credits ×1, GB, 2026-09-16 | → | error: credit quantity for line 1 must be greater than zero |
| a negative amount is refused | original lines ×3, credits ×1, GB, 2026-09-16 | → | error: credit amount for line 1 must be greater than zero |
| an amount in another currency is refused | original lines ×3, credits ×1, GB, 2026-09-16 | → | error: credit for line 1 must be in GBP |
| an empty credit note is refused | original lines ×3, , GB, 2026-09-16 | → | error: a credit note needs at least one line |
| a line that was itself a credit cannot be credited | original lines ×1, credits ×1, GB, 2026-09-16 | → | error: line 1 of the invoice is not a sale that can be credited |
More from the author
- **by units** (`quantity`): returned goods or days not worked, credited at the original unit price and the original discount; or - **by amount** (`net`): a price reduction or goodwill credit, a net amount taken off that line, VAT added at the line's tax category.
Set exactly one of the two. The credit note is then calculated by finance.invoice.calculate from negative lines, so it is an `Invoice` with every amount negative, VAT per line and grouped by rate exactly as the invoice was, and crediting the whole invoice line by line gives back its totals negated. Each credit line keeps the original line's description.
Credits are checked against the original: a line can not be credited for more than its net total, counting every credit against it in this note (units and amounts together); a credit against a line that was itself a credit is refused. Credits made on earlier credit notes are not known here; pass only what is still available, or check the history first.
Crediting units one at a time can round differently from the whole line when the line had a percentage discount (a 10% discount on 3 x 1.99 is rounded once on 5.97, not three times on 1.99), so the last unit of a discounted line may be refused by a penny. Credit the remaining units together, or the remaining amount by `net`.
Files
| Path | Bytes |
|---|---|
| README.md | 2,029 |
| impl/python.py | 3,142 |
| impl/rust.rs | 3,814 |
| impl/typescript.ts | 2,790 |
| vectors.json | 18,819 |