donation item, shop soft open
All checks were successful
Deploy / build-deploy (push) Successful in 4m11s

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
This commit is contained in:
Jorijn van der Graaf 2026-08-17 11:04:03 +02:00
commit abbd616b40
23 changed files with 2898 additions and 209 deletions

View file

@ -53,11 +53,60 @@ int main() {
Check(NetFromGross(GrossFromNet(713)) == 713, "vat: gross-up round-trips");
Check(GrossFromNet(100) == 121, "vat: €1.00 -> €1.21 exactly");
Check(GrossFromNet(0) == 0, "vat: gross-up zero");
// The half-up term (+5000) caught at sub-cent scale, once each way:
// 3 × 1.21 = 3.63 must land on 4, 2 × 1.21 = 2.42 must land on 2. Plain
// truncation would give 3 for the first, so this is what pins the term.
Check(GrossFromNet(3) == 4, "vat: gross-up rounds .63 up");
Check(GrossFromNet(2) == 2, "vat: gross-up rounds .42 down");
// The round-trip is the property that makes "cost plus, eat nothing" true,
// and it is applied to every EU carrier bracket — so it sets the shipping
// cents on every EU order. If either rounding constant drifted, the shop
// would remit VAT on a grossed-up rate that no longer nets back to the
// carrier's own cost, losing or pocketing a cent on every parcel. A single
// case cannot catch that; sweep the range a shipping rate lives in.
//
// Why it must hold for every n: GrossFromNet(n) is 1.21n rounded half up,
// so it sits within 0.5 of 1.21n. Dividing back by 1.21 therefore lands
// within 0.5/1.21 ≈ 0.413 of n — never far enough to reach the next
// half-up boundary, so NetFromGross returns n exactly.
{
std::string broke;
for (std::int64_t n = 0; n <= 2000; ++n) {
if (NetFromGross(GrossFromNet(n)) != n && broke.empty()) {
broke = std::format("net {} -> gross {} -> net {}", n,
GrossFromNet(n), NetFromGross(GrossFromNet(n)));
}
}
Check(broke.empty(),
"vat: gross-up round-trips for every net from €0.00 to €20.00", broke);
}
// ── zones and membership ──────────────────────────────────────────
Check(IsEuCountry("NL") && IsEuCountry("DE") && IsEuCountry("FR"), "eu: members");
// The whole roster, restated here rather than borrowed from EuCountries()
// — a list that checks itself proves nothing. IsEuCountry is the single
// switch in ComputeTotals between charging the VAT-inclusive price and
// charging a zero-rated export net, so a member quietly lost to a rebase,
// or typed as EL instead of GR, bills that country's buyers ~17% under the
// order's worth while the shop still owes NL OSS VAT on the sale. Money
// out the door, per order, with nothing else in the repo watching.
constexpr std::array<std::string_view, 27> members{
"AT", "BE", "BG", "HR", "CY", "CZ", "DE", "DK", "EE", "ES", "FI",
"FR", "GR", "HU", "IE", "IT", "LT", "LU", "LV", "MT", "NL", "PL",
"PT", "RO", "SE", "SI", "SK",
};
Check(EuCountries().size() == 27, "eu: 27 member states, no more and no fewer");
for (const std::string_view cc : members) {
Check(IsEuCountry(cc), "eu: member state recognised", cc);
}
Check(!IsEuCountry("GB"), "eu: UK left");
Check(!IsEuCountry("CH") && !IsEuCountry("NO"), "eu: EFTA is not EU");
// Three of the easiest false positives: IS shares the single market
// through the EEA, UA and TR are candidates (TR is even in the customs
// union). None of that is membership, and none of it makes a sale
// domestic for VAT.
Check(!IsEuCountry("IS"), "eu: EEA membership is not EU membership");
Check(!IsEuCountry("UA") && !IsEuCountry("TR"), "eu: candidates are not members");
Check(!IsEuCountry("CA") && !IsEuCountry("US"), "eu: north america");
Check(!IsEuCountry("nl"), "eu: lowercase is not a member (normalise first)");
Check(ZoneFor("NL") == Zone::Nl, "zone: home");
@ -113,6 +162,25 @@ int main() {
Check(LadderFor(table, "BR").empty(), "table: unlisted country is empty");
}
// A zero-priced bracket, which is what a Sendcloud row with a missing or
// unparseable price field becomes by the time it reaches here. RateFor
// spends `best == 0` as its "nothing covers this weight" sentinel, so a
// zero-cent band can never win — and that collision is doing real work: it
// makes the lookup fail CLOSED (ShippingTable::Find returns 0, checkout
// answers 422 and refuses) instead of shipping a €580 parcel worldwide for
// nothing. It is an accident of the sentinel choice rather than a stated
// rule, which is exactly why it needs a test standing over it: "prefer the
// cheapest band" is a tempting simplification that would give the parcel
// away.
{
const std::vector<ShipBracket> onlyFree{ { 2000, 0 } };
Check(RateFor(onlyFree, 700) == 0,
"brackets: a zero-priced band reads as no price, never as free");
const std::vector<ShipBracket> withFree{ { 2000, 0 }, { 10000, 1650 } };
Check(RateFor(withFree, 700) == 1650,
"brackets: a real price wins over a zero-priced band that also carries it");
}
// ── order totals ──────────────────────────────────────────────────
// NL: gross + shipping, VAT included in both.
@ -141,10 +209,51 @@ int main() {
auto nl2 = ComputeTotals(57500, 3, 1500, "NL");
Check(nl2.goods == 172500 && nl2.total == 174000, "totals: qty multiplies gross");
// VAT is derived ONCE, from the taxable total — never from a sum of line
// nets. ComputeTotals says so above (nl.vatCharged comes off goods +
// shipping as one number); these four make the reason a test rather than a
// comment, because NetFromGross is NOT additive across lines. Each division
// rounds half up on its own remainder, and the remainders do not have to
// agree:
//
// NetFromGross(56330) = (563'300'000 + 6050) / 12100 = 46554 rem 2650
// NetFromGross( 400) = ( 4'000'000 + 6050) / 12100 = 331 rem 950
// NetFromGross(56730) = (567'300'000 + 6050) / 12100 = 46884 rem 9650
//
// 56330 + 400 is 56730, but 46554 + 331 is 46885 — one cent ABOVE the net
// the combined total yields. That gap is not exotic: across the real price
// grid (three variants × 1..28 units × the shipping ladder) roughly a
// quarter of the combinations hit it.
//
// It matters because the invoice prints a "Subtotal (ex VAT)" that is
// NetFromGross of the whole total, with the VAT line derived from that
// subtotal — and non-additivity is exactly why its shipping line is the
// REMAINDER of that subtotal after the goods net, never NetFromGross of
// the shipping on its own. The day someone "tidies" the remainder into a
// third independent rounding, a GPG-signed tax document starts
// disagreeing with itself by a cent on a quarter of the price grid.
Check(NetFromGross(56330) == 46554, "vat: net of a goods line");
Check(NetFromGross(400) == 331, "vat: net of a shipping line");
Check(NetFromGross(56730) == 46884, "vat: net of the two taken together");
Check(NetFromGross(56330) + NetFromGross(400) != NetFromGross(56730),
"vat: line nets do not sum to the total net — derive VAT once, from the total");
// ── indicative conversion ─────────────────────────────────────────
// €580.00 at 1.0834 USD/EUR = $628.37 -> 628 whole units.
Check(ConvertIndicative(58000, 1'083'400) == 628, "fx: converts to whole units");
Check(ConvertIndicative(58000, 1'000'000) == 580, "fx: identity rate");
// Both cases above land far from the rounding boundary and would pass
// under plain truncation too, which leaves the +50'000'000 term — the only
// thing making this round rather than truncate — entirely unpinned. Drop
// it and every quoted foreign price shifts DOWN by up to a whole unit, on
// the number a non-euro buyer reads before deciding to order. So take the
// boundary head-on: €1.00 at a rate of exactly 1.5 is 1.5 units.
Check(ConvertIndicative(100, 1'500'000) == 2, "fx: exactly half rounds up");
Check(ConvertIndicative(100, 1'499'999) == 1,
"fx: one millionth below half rounds down");
Check(ConvertIndicative(100, 1'400'000) == 1, "fx: .4 of a unit rounds down");
// The term must not conjure a unit out of nothing, either.
Check(ConvertIndicative(0, 1'083'400) == 0, "fx: zero converts to zero");
auto gbp = CurrencyFor("GB");
Check(gbp.has_value() && gbp->code == "GBP", "fx: GB -> GBP");
Check(!CurrencyFor("DE").has_value(), "fx: euro country has no conversion");
@ -172,6 +281,32 @@ int main() {
Check(r.Find("XXX") == 0, "rates: absent is zero");
Check(LoadRates("garbage").microPerEur.empty(), "rates: malformed input yields none");
// Above is the whole-document failure; this is the per-ENTRY one, which is
// the case CI actually produces. LoadRates admits a rate only when it is a
// JSON number AND strictly positive, and that guard is the single thing
// standing between a bad rates.json and a printed price: the views and the
// order handler only re-check `rate > 0` before formatting, so a negative
// that slipped through here would render "≈ £-628" on every shop card.
// One entry per rejected shape — zero, negative, a number sent as a
// string, and null.
const Rates bad = LoadRates(
R"({"date":"2026-08-04","micro_per_eur":{"USD":0,"GBP":-860000,)"
R"("CHF":"940000","SEK":null}})");
Check(bad.date == "2026-08-04", "rates: a readable date survives unusable entries");
Check(bad.microPerEur.empty(),
"rates: zero, negative, string and null entries are all refused");
Check(bad.Find("USD") == 0 && bad.Find("GBP") == 0 && bad.Find("CHF") == 0 &&
bad.Find("SEK") == 0,
"rates: a refused entry is indistinguishable from an absent one");
// Refusal is per entry, not per document — one unusable rate must not take
// its healthy siblings down with it, or a single ECB hiccup blanks every
// localised price on the site instead of just the one currency's.
const Rates partial = LoadRates(
R"({"date":"2026-08-04","micro_per_eur":{"GBP":-860000,"NOK":11700000}})");
Check(partial.microPerEur.size() == 1, "rates: only the bad entry is dropped");
Check(partial.Find("NOK") == 11'700'000, "rates: the valid sibling still loads");
Check(partial.Find("GBP") == 0, "rates: the negative sibling does not");
if (failures != 0) {
std::println(std::cerr, "{} check(s) failed", failures);
return 1;