/* catcrafts.net Copyright (C) 2026 Catcrafts The source code of this website is made available for viewing purposes only. No permission is granted to copy, modify, distribute, or create derivative works. */ // The money layer: formatting, the VAT arithmetic, zones and the sale policy, // the carrier weight brackets, order totals, indicative currency conversion // and the ECB rates loader. Every price the site shows or charges goes // through these functions. import std; import Catcrafts.Shared; using namespace Catcrafts; namespace { int failures = 0; void Check(bool ok, std::string_view what, std::string_view got = {}) { if (ok) return; ++failures; std::println(std::cerr, "FAIL: {}{}{}", what, got.empty() ? "" : " got: ", got); } } // namespace int main() { using namespace Catcrafts::Money; // ── formatting ──────────────────────────────────────────────────── Check(FormatMinor(58000) == "580.00", "money: wire format"); Check(FormatMinor(47934) == "479.34", "money: wire format with cents"); Check(FormatMinor(5) == "0.05", "money: sub-unit"); Check(FormatMinor(0) == "0.00", "money: zero"); Check(FormatEuro(58000) == "€580", "money: whole euros displayed bare"); Check(FormatEuro(47934) == "€479.34", "money: cents displayed when present"); // ── VAT arithmetic ──────────────────────────────────────────────── // €580.00 gross at 21%: net = 58000/1.21 = 47933.88... -> 47934 half-up. Check(NetFromGross(58000) == 47934, "vat: net from €580 gross"); // The derived pair must reconstruct plausibly: net + vat == gross. Check(58000 - NetFromGross(58000) == 10066, "vat: vat portion exact"); Check(NetFromGross(0) == 0, "vat: zero"); Check(NetFromGross(121) == 100, "vat: €1.21 -> €1.00 exactly"); // The gross-up direction, used to charge carrier costs without eating // the VAT slice: €7.13 cost -> €8.63 charged, and the pair round-trips. Check(GrossFromNet(713) == 863, "vat: gross from €7.13 net"); 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 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"); Check(ZoneFor("DE") == Zone::Eu, "zone: eu"); Check(ZoneFor("GB") == Zone::World, "zone: world"); // ── destinations the shop refuses ───────────────────────────────── // Zones still classify US and CA (the arithmetic is destination-blind, and // keeping it that way means one policy switch, not two); the sale is what // stops, in SellsTo. // Two hard gates, and the shipping one is an ALLOW-list. The assertion that // matters most is therefore the DEFAULT: a country nobody has cleared // refuses. This is the test that fails if the gate is ever "simplified" back // into a deny-list, which would silently reopen most of the world. Check(SellsTo("NL") && SellsTo("CH") && SellsTo("AU") && SellsTo("HK") && SellsTo("SG") && SellsTo("RS") && SellsTo("ME") && SellsTo("AL") && SellsTo("XK") && SellsTo("GE"), "policy: every cleared destination sells"); // Both were cleared once and refuted on verification. JP because using a // non-giteki handset is a Radio Act offence for the BUYER; NZ because its // radio regulator's supplier duties expressly reach a website seller and an // overseas company cannot register to comply. Asserted by name so a future // "these look fine, add them back" cannot pass silently. Check(!SellsTo("JP") && !SellsTo("NZ"), "policy: refuted destinations stay refuted"); Check(!SellsTo("DE") && !SellsTo("FR") && !SellsTo("BE"), "policy: uncleared member states refuse by default"); Check(!SellsTo("KR") && !SellsTo("MX") && !SellsTo("ZA") && !SellsTo("XX"), "policy: uncleared and unknown codes refuse by default"); Check(!SellsTo("NO") && !SellsTo("IS"), "policy: the EEA inherits the EU's distance-seller duties, so it waits"); Check(!SellsTo("GB"), "policy: GB waits on its EA small-producer entries"); Check(!SellsTo("TR") && !SellsTo("IN") && !SellsTo("BR"), "policy: IMEI and type-approval destinations refuse"); // North America is refused for regulatory reasons now, not insurance ones — // it simply is not on the list, and there is no separate category for it. Check(!SellsTo("US") && !SellsTo("CA"), "policy: north america refused"); // Matching is on the normalised code, as everywhere else here — but note the // direction the allow-list fails in. Under the old deny-list, "us" missed the // blocked entry and SOLD; now an unnormalised code is simply absent from the // list and refuses. Callers still uppercase first (ValidateCheckout does), // but the consequence of forgetting is a lost sale rather than a shipment to // a country the shop cannot serve. Check(!SellsTo("us") && !SellsTo("nl"), "policy: an unnormalised code fails closed, not open"); Check(ZoneFor("US") == Zone::World, "zone: refused countries still classify"); Check(ShipsTo("NL") && !ShipsTo("US") && !ShipsTo("DE"), "policy: ShipsTo is the allow-list on its own"); // Form::kShipsToMessage names these countries in prose for the buy page, and // prose cannot be generated from ISO codes. If this fails because a country // was opened, update that sentence too — the two must not drift. Check(ShippableCountries().size() == 10, "policy: opening a country means updating Form::kShipsToMessage as well"); // Sanctioned destinations refuse through the same gate, but stay // distinguishable — the checkout error names the law for one and pending // paperwork for the other, and conflating them would tell a Russian buyer // to email and ask. Check(!SellsTo("RU") && !SellsTo("BY") && !SellsTo("KP"), "sanctions: RU, BY and KP refused"); Check(IsSanctioned("RU") && IsSanctioned("BY") && IsSanctioned("KP"), "sanctions: the list knows its members"); Check(!IsSanctioned("US") && !IsSanctioned("DE") && !IsSanctioned("NL"), "sanctions: an uncleared country is not a sanctioned one"); // Belt and braces: a sanctioned code must never reach the allow-list, or the // wrong sentence would be shown for a criminal-law refusal. for (const std::string_view cc : SanctionedCountries()) { Check(!ShipsTo(cc), "sanctions: never on the shipping list", cc); } // ── carrier weight brackets ─────────────────────────────────────── // The only shipping prices that exist. A ladder covering 2 kg / 10 kg / // 20 kg, with the 20 kg band deliberately CHEAPER than the 10 kg one — // real carrier tariffs do that, and picking the tightest band rather than // the cheapest one that carries the parcel would overcharge for it. { const std::vector ladder{ { 2000, 895 }, { 10000, 1650 }, { 20000, 1490 } }; Check(RateFor(ladder, 700) == 895, "brackets: one unit takes the 2 kg band"); Check(RateFor(ladder, 2000) == 895, "brackets: the ceiling is inclusive"); Check(RateFor(ladder, 2001) == 1490, "brackets: cheapest band that CARRIES it, not the tightest"); Check(RateFor(ladder, 20001) == 0, "brackets: above every band is no price"); Check(RateFor({}, 700) == 0, "brackets: an uncovered country has no price"); Check(MaxUnitsFor(ladder, 700) == 28, "brackets: units that fit one parcel"); Check(MaxUnitsFor(ladder, 25000) == 0, "brackets: a unit heavier than every band fits nothing"); Check(MaxUnitsFor(ladder, 0) == 0, "brackets: no weight, no answer"); Check(MaxUnitsFor({}, 700) == 0, "brackets: no ladder, nothing fits"); // The table-level lookups the handler and the page both go through. const std::vector table{ { "NL", ladder }, { "JP", { { 2000, 4250 } } } }; Check(RateFor(LadderFor(table, "NL"), 700) == 895, "table: NL priced"); Check(RateFor(LadderFor(table, "JP"), 2100) == 0, "table: JP has one light band, so two units are unshippable"); 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 onlyFree{ { 2000, 0 } }; Check(RateFor(onlyFree, 700) == 0, "brackets: a zero-priced band reads as no price, never as free"); const std::vector 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. auto nl = ComputeTotals(58000, 1, 1500, "NL"); Check(nl.goods == 58000 && nl.shipping == 1500 && nl.total == 59500, "totals: NL"); Check(nl.vatIncluded, "totals: NL includes VAT"); Check(nl.vatCharged == 59500 - NetFromGross(59500), "totals: NL VAT covers shipping"); auto de = ComputeTotals(58000, 1, 2500, "DE"); Check(de.goods == 58000 && de.shipping == 2500 && de.total == 60500, "totals: EU"); // Export: net goods, world shipping, no VAT. auto gb = ComputeTotals(58000, 1, 5500, "GB"); Check(gb.goods == 47934 && gb.shipping == 5500 && gb.total == 53434, "totals: export"); Check(!gb.vatIncluded && gb.vatCharged == 0, "totals: export carries no VAT"); // Quantity: the export net is derived from the LINE total, not per unit — // per-unit rounding times qty would differ by a cent here, and the JS // preview mirrors this exact formula. auto gb2 = ComputeTotals(57500, 2, 5500, "GB"); Check(gb2.goods == NetFromGross(115000), "totals: qty nets the line, not the unit"); Check(gb2.goods == 95041, "totals: 2× green export net exact"); 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"); Check(!CurrencyFor("XX").has_value(), "fx: unknown country has no conversion"); if (gbp) { Check(FormatIndicative(*gbp, 920) == "≈ £920", "fx: display form"); } // A country the shop refuses gets no localised price either — the two // tables are kept consistent on purpose, so this is a real invariant and // not a coincidence of the current list. for (const std::string_view cc : SanctionedCountries()) { Check(!CurrencyFor(cc).has_value(), "fx: sanctioned destinations have no display currency", cc); } Check(!CurrencyFor("US").has_value() && !CurrencyFor("CA").has_value(), "fx: north america has no display currency either"); // The invariant covers only refusals nothing will lift. The table otherwise // runs AHEAD of the shipping list on purpose — GB keeps its GBP row while its // e-waste registrations are pending, because "what would this cost me" stays // a fair question for a country one small registration from opening, and // deleting the row to restore it weeks later would be churn. The refusal that // must not be quoted around is enforced in SellsTo, not here. Check(CurrencyFor("GB").has_value(), "fx: a temporarily-closed destination keeps its display currency"); // ── rates loader ────────────────────────────────────────────────── const Rates r = LoadRates( R"({"date":"2026-08-04","micro_per_eur":{"USD":1083400,"CAD":1489000}})"); Check(r.date == "2026-08-04", "rates: date"); Check(r.Find("USD") == 1'083'400, "rates: lookup"); 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; } return 0; }