retail.gift-card-balance
Apply a payment from a gift card, redeeming part of the balance or all of it, and say what is still due.
1.0.0 · published 2026-10-03 by charlie · Anterra
Pinned by 11 tests, run in TypeScript, Python and Rust.
What it does
A gift card pays for as much of the bill as its balance allows. The card pays `min(balance, amountDue)`; the rest stays on the card or is left for another tender (card, cash, a second gift card).
The result carries all three amounts, applied, remaining balance and still due, so the till never derives one from the other two, and `applied + remainingBalance` is always the old balance and `applied + stillDue` always the amount due, to the minor unit.
For example
redeemGiftCard(£50.00, £30.00)→ applied £30.00, remaining balance £20.00, still due £0.00, fully paid true the card covers the bill and keeps the changeredeemGiftCard(£20.00, £35.50)→ applied £20.00, remaining balance £0.00, still due £15.50, fully paid false partial redemption: the card is emptied and the rest is still dueredeemGiftCard(£25.00, £25.00)→ applied £25.00, remaining balance £0.00, still due £0.00, fully paid true balance equal to the bill empties the card exactly
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 redeemGiftCard(balance: Money, amountDue: Money): GiftCardRedemption
| balance | Money | what is left on the card, 0 or more |
| amountDue | Money | what the customer owes at the till, 0 or more, same currency |
| returns | GiftCardRedemption |
The type it declares, generated into your project
/** What the card paid, what is left on it, and what the customer still owes. */
export interface GiftCardRedemption {
readonly applied: Money;
readonly remainingBalance: Money;
/** to be paid by another tender */
readonly stillDue: Money;
readonly fullyPaid: boolean;
}
Your code names it in one line, in the file that uses it
import { redeemGiftCard } from "#fune/retail.gift-card-balance@^1";
Imports name this capability’s declared dependencies, which fune builds next to it in your project; each one links to its page.
import { subtractMoney } from "./money_add.ts"; ← from money.add ^1.0.0 · built alongside by fune
import { type Money } from "./money_amount.ts"; ← from money.amount ^1.0.0 · built alongside by fune
import { minMoney } from "./money_compare.ts"; ← from money.compare ^1.0.0 · built alongside by fune
import { type GiftCardRedemption } from "./retail_gift_card_balance_types.ts";
/** Pay as much of `amountDue` as the card's balance allows. */
export function redeemGiftCard(balance: Money, amountDue: Money): GiftCardRedemption {
if (balance.minor < 0) throw new RangeError(`balance must not be negative, received ${balance.minor}`);
if (amountDue.minor < 0) {
throw new RangeError(`amountDue must not be negative, received ${amountDue.minor}; a refund to a gift card is a top-up`);
}
const applied = minMoney(balance, amountDue);
const stillDue = subtractMoney(amountDue, applied);
return {
applied,
remainingBalance: subtractMoney(balance, applied),
stillDue,
fullyPaid: stillDue.minor === 0,
};
}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 retail.gift-card-balance
The manifest, vectors and README with only the TypeScript implementation. Install it without the registry with fune add ./retail.gift-card-balance-1.0.0-typescript.fune, or fetch it from a terminal with fune pull retail.gift-card-balance@1.0.0:typescript.
The whole function, every language, is one file too: retail.gift-card-balance-1.0.0.fune, 10,092 bytes, sha256 96c516afc5ce0a9054f4fde768709d56a54ec0f25dbb87aaa57d283cf0ffdfe3. 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 retail.gift-card-balance
after — your function gets the result and the arguments, and returns the final result.
// fune: after retail.gift-card-balance
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 money.add in retail.gift-card-balance
// fune: replace money.amount in retail.gift-card-balance
// fune: replace money.compare in retail.gift-card-balance
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 retail.gift-card-balance --steps.
// fune: step retail.gift-card-balance 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 | |
|---|---|---|---|
| the card covers the bill and keeps the change | £50.00, £30.00 | → | applied £30.00, remaining balance £20.00, still due £0.00, fully paid true |
| partial redemption: the card is emptied and the rest is still due | £20.00, £35.50 | → | applied £20.00, remaining balance £0.00, still due £15.50, fully paid false |
| balance equal to the bill empties the card exactly | £25.00, £25.00 | → | applied £25.00, remaining balance £0.00, still due £0.00, fully paid true |
| one penny short leaves one penny to pay | £9.99, £10.00 | → | applied £9.99, remaining balance £0.00, still due £0.01, fully paid false |
| an empty card pays nothing | £0.00, £10.00 | → | applied £0.00, remaining balance £0.00, still due £10.00, fully paid false |
| nothing due leaves the balance alone | £50.00, £0.00 | → | applied £0.00, remaining balance £50.00, still due £0.00, fully paid true |
| both zero | £0.00, £0.00 | → | applied £0.00, remaining balance £0.00, still due £0.00, fully paid true |
| yen has no minor unit, same arithmetic | ¥3,000, ¥4,500 | → | applied ¥3,000, remaining balance ¥0, still due ¥1,500, fully paid false |
| a negative balance is an error | -£0.01, £10.00 | → | error: balance must not be negative |
| a negative amount due is an error, not a silent top-up | £10.00, -£5.00 | → | error: amountDue must not be negative |
Show the other 1 test
| Case | Arguments | Expected | |
|---|---|---|---|
| a euro card cannot pay a sterling bill | €10.00, £5.00 | → | error: currency mismatch: GBP and EUR |
More from the author
## What it does not do
- Refunds onto a gift card are top-ups, a different operation with its own rules (maximum balance, fraud checks), so a negative `amountDue` is an error rather than a silent credit. - Expiry, dormancy fees and activation are the card issuer's rules and are not modelled; check them before calling this. (In the UK, gift cards often carry an expiry date set by the issuer; there is no statutory minimum.) - Currencies must match. A card in euros cannot pay a bill in pounds without a conversion, which is `money.convert`'s job.
Files
| Path | Bytes |
|---|---|
| README.md | 1,042 |
| impl/python.py | 901 |
| impl/rust.rs | 1,488 |
| impl/typescript.ts | 878 |
| vectors.json | 3,057 |