hospitality.gross-profit-menu
Menu gross profit (GP%) on the VAT-exclusive price, and the menu price that reaches a target GP.
1.0.0 · published 2026-10-03 by charlie · Anterra
Pinned by 20 tests, run in TypeScript, Python and Rust.menuGrossProfit 10 · menuPriceForGp 10
What it does
Two functions for pricing a menu:
- `menuGrossProfit(cost, menuPrice, ...)`: the GP of a dish at its menu price. The VAT-inclusive price is split into VAT and net with `finance.tax.remove-vat`, and the GP and food cost are worked out on the net price with `finance.margin`. - `menuPriceForGp(cost, target, priceStep, ...)`: the lowest menu price that reaches a target GP, rounded up to a price step (50 for the next 50p), and the GP it actually makes.
The functions
A group: 2 functions that work together, each in its own file, each pinned by its own tests in TypeScript, Python and Rust. A project can install only the ones it calls.
- menu_gross_profit (cost: Money, menuPrice: Money, jurisdiction: string, category: string, onDate: date) -> MenuGrossProfit
- menu_price_for_gp (cost: Money, targetGpBasisPoints: int, priceStep: int, jurisdiction: string, category: string, onDate: date) -> MenuGrossProfit
The type it declares, generated into your project
/// A dish's price split into VAT and net, and the profit on the net.
#[derive(Debug, Clone, PartialEq, Eq)]
pub struct MenuGrossProfit {
/// VAT included
pub menu_price: Money,
pub vat: Money,
/// menuPrice less VAT
pub net_price: Money,
pub cost: Money,
/// netPrice less cost
pub gross_profit: Money,
/// grossProfit / netPrice; null when the net price is 0
pub gp_basis_points: Option<i64>,
/// cost / netPrice (food cost %), 10000 - gpBasisPoints
pub cost_basis_points: Option<i64>,
pub vat_rate_basis_points: i64,
}
Once installed, your code imports each one from the group's module.
Install
fune build
With that line in your source, in a Rust project (language rust in fune.project), fune build resolves it and its 6 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 hospitality.gross-profit-menu
That builds the whole group. To build only what you call, and whatever it uses inside the group:
fune add hospitality.gross-profit-menu --only menuGrossProfit
The manifest, vectors and README with only the Rust implementation. Install it without the registry with fune add ./hospitality.gross-profit-menu-1.0.0-rust.fune, or fetch it from a terminal with fune pull hospitality.gross-profit-menu@1.0.0:rust.
The whole function, every language, is one file too: hospitality.gross-profit-menu-1.0.0.fune, 26,994 bytes, sha256 f5fa6251ed00c56e15f51284d89af8438562aa2a63cb42e36a4dffcb17920e3b. 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 hospitality.gross-profit-menu.menuGrossProfit
// fune: before hospitality.gross-profit-menu.menuPriceForGp
after — your function gets the result and the arguments, and returns the final result.
// fune: after hospitality.gross-profit-menu.menuGrossProfit
// fune: after hospitality.gross-profit-menu.menuPriceForGp
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.margin in hospitality.gross-profit-menu
// fune: replace finance.markup-price in hospitality.gross-profit-menu
// fune: replace finance.tax.add-vat in hospitality.gross-profit-menu
// fune: replace finance.tax.remove-vat in hospitality.gross-profit-menu
// fune: replace math.round-div in hospitality.gross-profit-menu
// fune: replace money.amount in hospitality.gross-profit-menu
step — your function runs at a numbered point inside a function’s body, receives the in-scope values it names as parameters, and may return replacements. List the points with fune show hospitality.gross-profit-menu --steps.
// fune: step hospitality.gross-profit-menu.<fn> 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.
menuGrossProfit 10 tests
| Case | Arguments | Expected | |
|---|---|---|---|
| a 14.50 dish costing 3.50 at 20% VAT: GP on the net price is 71.03%, not the 75.86% on the menu price | £3.50, £14.50, GB, HOSPITALITY, 2026-09-23 | → | menu price £14.50, vat £2.42, net price £12.08, cost £3.50, gross profit £8.58, gp basis points 71.03%, cost basis points 28.97%, vat rate basis points 20% |
| cold takeaway food is zero-rated, so all of the price is net | £1.20, £4.50, GB, ZERO, 2026-09-23 | → | menu price £4.50, vat £0.00, net price £4.50, cost £1.20, gross profit £3.30, gp basis points 73.33%, cost basis points 26.67%, vat rate basis points 0% |
| the same dish during the 2021 hospitality VAT cut to 5% | £3.50, £14.50, GB, HOSPITALITY, 2021-01-15 | → | menu price £14.50, vat £0.69, net price £13.81, cost £3.50, gross profit £10.31, gp basis points 74.66%, cost basis points 25.34%, vat rate basis points 5% |
| Irish tourism and hospitality rate of 13.5% | €4.00, €18.00, IE, HOSPITALITY, 2026-09-23 | → | menu price €18.00, vat €2.14, net price €15.86, cost €4.00, gross profit €11.86, gp basis points 74.78%, cost basis points 25.22%, vat rate basis points 13.5% |
| a dish sold at a loss has a negative GP and a food cost over 100% | £15.00, £12.00, GB, HOSPITALITY, 2026-09-23 | → | menu price £12.00, vat £2.00, net price £10.00, cost £15.00, gross profit -£5.00, gp basis points -50%, cost basis points 150%, vat rate basis points 20% |
| a free dish has no GP percentage | £3.50, £0.00, GB, HOSPITALITY, 2026-09-23 | → | menu price £0.00, vat £0.00, net price £0.00, cost £3.50, gross profit -£3.50, gp basis points —, cost basis points —, vat rate basis points 20% |
| a negative cost is an error | -£0.01, £14.50, GB, HOSPITALITY, 2026-09-23 | → | error: cost must not be negative |
| a negative price is an error | £3.50, -£14.50, GB, HOSPITALITY, 2026-09-23 | → | error: menuPrice must not be negative |
| cost and price in different currencies is an error | €3.50, £14.50, GB, HOSPITALITY, 2026-09-23 | → | error: currency mismatch |
| an unknown VAT category is an error | £3.50, £14.50, GB, FOOD, 2026-09-23 | → | error: no VAT rule for GB/FOOD on 2026-09-23 |
menuPriceForGp 10 tests
| Case | Arguments | Expected | |
|---|---|---|---|
| 70% GP on a 3.50 cost: 11.67 net, 14.00 on the menu (3.50 / 0.3 = 11.67 on the menu would be 64%) | £3.50, 70%, 1, GB, HOSPITALITY, 2026-09-23 | → | menu price £14.00, vat £2.33, net price £11.67, cost £3.50, gross profit £8.17, gp basis points 70.01%, cost basis points 29.99%, vat rate basis points 20% |
| rounded up to the next 50p | £4.00, 70%, 50, GB, HOSPITALITY, 2026-09-23 | → | menu price £16.50, vat £2.75, net price £13.75, cost £4.00, gross profit £9.75, gp basis points 70.91%, cost basis points 29.09%, vat rate basis points 20% |
| rounded up to the next pound | £4.00, 70%, 100, GB, HOSPITALITY, 2026-09-23 | → | menu price £17.00, vat £2.83, net price £14.17, cost £4.00, gross profit £10.17, gp basis points 71.77%, cost basis points 28.23%, vat rate basis points 20% |
| a 0% target sells at cost plus VAT | £5.00, 0%, 1, GB, HOSPITALITY, 2026-09-23 | → | menu price £6.00, vat £1.00, net price £5.00, cost £5.00, gross profit £0.00, gp basis points 0%, cost basis points 100%, vat rate basis points 20% |
| zero-rated takeaway to 65% GP, on a 5p step | £1.00, 65%, 5, GB, ZERO, 2026-09-23 | → | menu price £2.90, vat £0.00, net price £2.90, cost £1.00, gross profit £1.90, gp basis points 65.52%, cost basis points 34.48%, vat rate basis points 0% |
| a dish that costs nothing is priced at nothing | £0.00, 70%, 1, GB, HOSPITALITY, 2026-09-23 | → | menu price £0.00, vat £0.00, net price £0.00, cost £0.00, gross profit £0.00, gp basis points —, cost basis points —, vat rate basis points 20% |
| a 100% target is an error | £3.50, 100%, 1, GB, HOSPITALITY, 2026-09-23 | → | error: targetGpBasisPoints must be a whole number from 0 to 9999 |
| a negative target is an error | £3.50, -0.01%, 1, GB, HOSPITALITY, 2026-09-23 | → | error: targetGpBasisPoints must be a whole number from 0 to 9999 |
| a zero price step is an error | £3.50, 70%, 0, GB, HOSPITALITY, 2026-09-23 | → | error: priceStep must be at least 1 |
| a negative cost is an error | -£3.50, 70%, 1, GB, HOSPITALITY, 2026-09-23 | → | error: cost must not be negative |
More from the author
They are a group because they are two directions of one calculation. `menuPriceForGp` answers with `menuGrossProfit`'s breakdown of the price it chose.
## GP is on the price without VAT
In UK hospitality, GP (gross profit percentage) is `(net price - cost) / net price`, where the net price excludes VAT. The VAT is collected for HMRC, not earned. A 14.50 dish costing 3.50 at 20% VAT has a net price of 12.08 and a GP of 71.03%. Working on the menu price gives 75.86%, which overstates it by five points. The food cost percentage is `10000 - gpBasisPoints`, so the two always add up to exactly 100%.
## The VAT rate is looked up, not assumed
The VAT rate comes from `finance.tax.vat-rate` for the jurisdiction, category and date of sale. Food eaten on the premises is `HOSPITALITY` (20% in the UK since April 2022, 5% and then 12.5% during the pandemic relief of 2020 to 2022). Cold takeaway food is usually `ZERO`. Which category a dish falls in under VAT Notice 709/1 is the caller's decision.
## Pricing for a target
1. The net price is the smallest that reaches the target on the net (`finance.markup-price` with basis `margin`, rounded up). 2. VAT is added with `finance.tax.add-vat`. 3. The menu price is rounded **up** to a multiple of `priceStep`.
Every rounding step goes up, so the GP returned is never below the target. It is usually a little above it, and the result shows by how much. A common mistake is `cost / (1 - target)` on the menu price: 3.50 / 0.3 = 11.67 on the menu is only a 64% GP once VAT is taken out.
## Errors
A negative cost or price, cost and price in different currencies, a target outside 0 to 9999, a step below 1, or no VAT rule for the category and date.