Functional Weave
Code in Rust

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

  • redeem_gift_card(£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 change
  • redeem_gift_card(£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 due
  • redeem_gift_card(£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.

pub fn redeem_gift_card(balance: &Money, amount_due: &Money) -> GiftCardRedemption
balanceMoneywhat is left on the card, 0 or more
amount_dueMoneywhat the customer owes at the till, 0 or more, same currency
returnsGiftCardRedemption

The type it declares, generated into your project

/// What the card paid, what is left on it, and what the customer still owes.
#[derive(Debug, Clone, PartialEq, Eq)]
pub struct GiftCardRedemption {
    pub applied: Money,
    pub remaining_balance: Money,
    /// to be paid by another tender
    pub still_due: Money,
    pub fully_paid: bool,
}

Your code names it in one line, in the file that uses it

fune!(retail.gift-card-balance@^1);  // then call redeem_gift_card(…)
impl/rust.rs · 44 lines · open · raw

Imports name this capability’s declared dependencies, which fune builds next to it in your project; each one links to its page.

use super::funejson::Value;  ← the fune runtime: the JSON value the test vectors use; fune build keeps it only where a signature takes one
use super::money_add::subtract_money;  ← from money.add ^1.0.0 · built alongside by fune
use super::money_amount::{money_from_value, money_to_value, Money};  ← from money.amount ^1.0.0 · built alongside by fune
use super::money_compare::min_money;  ← from money.compare ^1.0.0 · built alongside by fune

/// Pay as much of `amount_due` as the card's balance allows.
///
/// # Panics
/// Panics on a negative balance or amount due, or mixed currencies.
pub fn redeem_gift_card(balance: &Money, amount_due: &Money) -> GiftCardRedemption {
    if balance.minor < 0 {
        panic!("balance must not be negative, received {}", balance.minor);
    }
    if amount_due.minor < 0 {
        panic!(
            "amountDue must not be negative, received {}; a refund to a gift card is a top-up",
            amount_due.minor
        );
    }
    let applied = min_money(balance, amount_due);
    let still_due = subtract_money(amount_due, &applied);
    GiftCardRedemption {
        remaining_balance: subtract_money(balance, &applied),
        fully_paid: still_due.minor == 0,
        applied,
        still_due,
    }
}

pub fn gift_card_redemption_to_value(r: &GiftCardRedemption) -> Value {
    Value::obj(vec![
        ("applied", money_to_value(&r.applied)),
        ("remainingBalance", money_to_value(&r.remaining_balance)),
        ("stillDue", money_to_value(&r.still_due)),
        ("fullyPaid", Value::Bool(r.fully_paid)),
    ])
}

pub fn fune_vector(args: &[Value]) -> Value {
    gift_card_redemption_to_value(&redeem_gift_card(
        &money_from_value(&args[0]),
        &money_from_value(&args[1]),
    ))
}

Install

fune build

With that line in your source, in a Rust project (language rust in fune.project), fune build resolves it and its 3 dependencies, pins them in fune.lock, downloads only the Rust 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. A crate’s build.rs runs it before every compile. Or pin a range in fune.project and build in one step:

fune add retail.gift-card-balance
Download for Rust retail.gift-card-balance-1.0.0-rust.fune · 8,219 bytes sha256 fc21ad5f997efaed41de8a046ffa6fb1c5a1b8fa8a0400295f4ddeb9ff60ccee

The manifest, vectors and README with only the Rust implementation. Install it without the registry with fune add ./retail.gift-card-balance-1.0.0-rust.fune, or fetch it from a terminal with fune pull retail.gift-card-balance@1.0.0:rust.

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.

CaseArgumentsExpected
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
CaseArgumentsExpected
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

PathBytes
README.md1,042
impl/python.py901
impl/rust.rs1,488
impl/typescript.ts878
vectors.json3,057