/* 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 invoice builder and the order confirmation email — the two documents a // paying customer actually receives, including the VAT treatment on each and // the header-injection guard on the address that goes into the envelope. import std; import Catcrafts.Shared; import Catcrafts.Server; 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); } // The euro amount on the table row starting with `prefix`, in minor units, or // -1 when the row is missing. Reads the rendered document, not the builder's // internals: FormatEuro prints "€938.21", or "€580" when the cents are zero. std::int64_t RowMinor(const std::string& md, std::string_view prefix) { const std::size_t at = md.find(prefix); if (at == std::string::npos) return -1; std::size_t i = at + prefix.size(); std::int64_t euros = 0; bool any = false; for (; i < md.size() && md[i] >= '0' && md[i] <= '9'; ++i) { euros = euros * 10 + (md[i] - '0'); any = true; } if (!any) return -1; std::int64_t cents = 0; if (i + 2 < md.size() && md[i] == '.') { cents = (md[i + 1] - '0') * 10 + (md[i + 2] - '0'); } return euros * 100 + cents; } } // namespace int main() { Server::OrderRecord o; o.token = "0123456789abcdef0123456789abcdef"; o.reference = "CC-TEST01"; o.invoiceNumber = "f57c6512-f012-4b91-adb3-077876480178-7"; o.invoicedAt = "2026-08-05T10:00:00Z"; o.createdAt = "2026-08-05T09:55:00Z"; o.paidVia = "ideal"; o.buyer = { "b@example.org", "Ada Lovelace", "Main St 1", "1234AB", "Delft", "NL" }; o.quantity = 2; o.unitMinor = 56330; o.goodsMinor = 112660; o.shippingMinor = 863; o.totalMinor = 113523; o.vatIncluded = true; const std::string eu = Server::BuildInvoiceMarkdown(o, "Fairphone 6", "Forest Green"); Check(eu.find("# Invoice f57c6512-f012-4b91-adb3-077876480178-7") != std::string::npos, "invoice: number heading"); Check(eu.find("* Customer number: f57c6512-f012-4b91-adb3-077876480178") != std::string::npos, "invoice: customer series shown separately"); Check(eu.find("* Invoice number: 7") != std::string::npos, "invoice: sequence within the series"); Check(eu.find("Chico Mendesring 256") != std::string::npos, "invoice: seller address"); Check(eu.find("3315NN Dordrecht") != std::string::npos, "invoice: seller city"); Check(eu.find("KVK 78437059") != std::string::npos, "invoice: KVK"); Check(eu.find("NL003329281B38") != std::string::npos, "invoice: VAT id"); Check(eu.find("CC-TEST01") != std::string::npos, "invoice: order reference"); Check(eu.find("Ada Lovelace") != std::string::npos, "invoice: buyer name"); Check(eu.find("Fairphone 6 — Forest Green") != std::string::npos, "invoice: item names the colour"); Check(eu.find("VAT 21% (NL)") != std::string::npos, "invoice: EU VAT line"); Check(eu.find("€1135.23") != std::string::npos, "invoice: EU total"); Check(eu.find("zero-rated") == std::string::npos, "invoice: EU is not an export"); // Every cent of the amounts table, pinned. This is the document a Dutch // buyer, an accountant and the Belastingdienst read, so a rounding change // in Money::NetFromGross must break a test rather than ship a wrong VAT // figure. Derived by hand from net = (gross*10000 + 6050) / 12100: // goods 112660 -> (1'126'600'000 + 6050) / 12100 = 93107 -> €931.07 // total 113523 -> (1'135'230'000 + 6050) / 12100 = 93821 -> €938.21 // VAT = 113523 - 93821 = 19702 -> €197.02 // shipping = 93821 - 93107 = 714 -> €7.14 // The shipping line is the REMAINDER of the subtotal, not a rounding of // its own — that is what makes the columns add up. Rounded independently // it would print €7.13 ((8'630'000 + 6050) / 12100 = 713) and sit a cent // below the subtotal, which is why the remainder rule exists: shipping // absorbs the cent so a signed tax document cannot disagree with itself. Check(eu.find("| Fairphone 6 — Forest Green | 2 | €931.07 |\n") != std::string::npos, "invoice: EU item line is net, not the gross the buyer paid"); Check(eu.find("| Shipping | 1 | €7.14 |\n") != std::string::npos, "invoice: EU shipping line is the subtotal remainder"); Check(eu.find("| Subtotal (ex VAT) | | €938.21 |\n") != std::string::npos, "invoice: EU subtotal is the net of the gross total"); Check(eu.find("| VAT 21% (NL) | | €197.02 |\n") != std::string::npos, "invoice: EU VAT line is the amount actually remitted"); Check(eu.find("| **Total (incl. VAT)** | | **€1135.23** |\n") != std::string::npos, "invoice: EU gross total is what was charged"); // The property the pinned cents above are one instance of, swept across // the realistic price grid: the three retail prices × every quantity a // parcel can carry × the range a shipping rate lives in. Before the // remainder rule, roughly a quarter of these combinations printed lines // one cent apart from their own subtotal (three independent half-up // roundings; two errors uniform on [-½,½) cross a boundary with // probability ¼). Rendered and re-parsed rather than recomputed, so what // is being held is the document itself: // item + shipping == subtotal (the remainder rule, by construction) // subtotal + VAT == total (what the buyer paid, to the cent) // |shipping - NetFromGross(shipping gross)| <= 1 (the cent stops here) { Server::OrderRecord s = o; std::string broke; for (const std::int64_t unit : { 57380, 57980, 66538 }) { for (std::int64_t qty = 1; qty <= 28; ++qty) { for (std::int64_t ship = 400; ship <= 6000; ship += 97) { s.quantity = qty; s.unitMinor = unit; s.goodsMinor = unit * qty; s.shippingMinor = ship; s.totalMinor = s.goodsMinor + ship; const std::string md = Server::BuildInvoiceMarkdown(s, "P", ""); const std::int64_t item = RowMinor(md, std::format("| P | {} | €", qty)); const std::int64_t shipping = RowMinor(md, "| Shipping | 1 | €"); const std::int64_t sub = RowMinor(md, "| Subtotal (ex VAT) | | €"); const std::int64_t vat = RowMinor(md, "| VAT 21% (NL) | | €"); const std::int64_t total = RowMinor(md, "| **Total (incl. VAT)** | | **€"); const bool ok = item >= 0 && shipping >= 0 && sub >= 0 && vat >= 0 && total == s.totalMinor && item + shipping == sub && sub + vat == total && shipping - Money::NetFromGross(ship) <= 1 && Money::NetFromGross(ship) - shipping <= 1; if (!ok && broke.empty()) { broke = std::format("unit {} qty {} ship {}: {} + {} vs {}", unit, qty, ship, item, shipping, sub); } } } } Check(broke.empty(), "invoice: EU columns add up across the whole price grid", broke); } o.vatIncluded = false; o.buyer.country = "GB"; o.goodsMinor = 93107; o.shippingMinor = 2395; o.totalMinor = 95502; const std::string ex = Server::BuildInvoiceMarkdown(o, "Fairphone 6", "Forest Green"); Check(ex.find("VAT 0%") != std::string::npos, "invoice: export VAT 0%"); Check(ex.find("art. 146") != std::string::npos, "invoice: export legal basis"); Check(ex.find("€955.02") != std::string::npos, "invoice: export total"); // The mirror image of the EU table: a zero-rated export carries no VAT to // strip, so every line is the gross that was charged and NetFromGross must // never touch it. 93107 stays €931.07 (netting it again would print // €769.48) and 2395 stays €23.95 (€19.79 netted) — the two branches // swapping their treatment is exactly the accident these pin down. Check(ex.find("| Fairphone 6 — Forest Green | 2 | €931.07 |\n") != std::string::npos, "invoice: export item line stays gross"); Check(ex.find("| Shipping | 1 | €23.95 |\n") != std::string::npos, "invoice: export shipping line stays gross"); Check(ex.find("| **Total** | | **€955.02** |\n") != std::string::npos, "invoice: export total carries no VAT label"); // ── the order confirmation email ────────────────────────────────── // Same order, EU shape again; the attachment stands in for the // clearsigned invoice — the builder must carry it verbatim. o.vatIncluded = true; o.buyer.country = "NL"; o.goodsMinor = 112660; o.shippingMinor = 863; o.totalMinor = 113523; const std::string mail = Server::BuildOrderConfirmationEmail( o, "Fairphone 6", "Forest Green", "Catcrafts ", "https://catcrafts.net/order/0123456789abcdef0123456789abcdef", "SIGNED-INVOICE-STAND-IN\n", "Fri, 08 Aug 2026 10:00:00 +0000"); Check(mail.find("From: Catcrafts \n") != std::string::npos, "email: From header"); Check(mail.find("To: b@example.org\n") != std::string::npos, "email: To header"); Check(mail.find("Subject: Catcrafts order CC-TEST01 confirmed\n") != std::string::npos, "email: subject carries the reference"); Check(mail.find("Date: Fri, 08 Aug 2026 10:00:00 +0000\n") != std::string::npos, "email: date header"); Check(mail.find("Message-ID: <0123456789abcdef0123456789abcdef@catcrafts.net>\n") != std::string::npos, "email: message id from the token"); Check(mail.find("MIME-Version: 1.0\n") != std::string::npos, "email: mime version"); Check(mail.find("multipart/mixed") != std::string::npos, "email: multipart"); Check(mail.find("Fairphone 6 — Forest Green × 2") != std::string::npos, "email: item names colour and quantity"); Check(mail.find("€1135.23") != std::string::npos, "email: total"); Check(mail.find("incl. 21% NL VAT") != std::string::npos, "email: EU VAT wording"); Check(mail.find("* Paid via: ideal\n") != std::string::npos, "email: payment method"); Check(mail.find("https://catcrafts.net/order/0123456789abcdef0123456789abcdef") != std::string::npos, "email: order page link"); Check(mail.find("filename=\"catcrafts-invoice-" "f57c6512-f012-4b91-adb3-077876480178-7.md\"") != std::string::npos, "email: attachment filename is the invoice number"); Check(mail.find("SIGNED-INVOICE-STAND-IN\n") != std::string::npos, "email: attachment body verbatim"); Check(mail.find("--=_cc_0123456789abcdef0123456789abcdef--\n") != std::string::npos, "email: multipart closes"); Check(mail.find("KVK 78437059") != std::string::npos, "email: footer identity"); // The export wording mirrors the invoice's VAT treatment. o.vatIncluded = false; o.buyer.country = "GB"; o.totalMinor = 95502; const std::string exMail = Server::BuildOrderConfirmationEmail( o, "Fairphone 6", "Forest Green", "Catcrafts ", "https://catcrafts.net/order/x", "S\n", "Fri, 08 Aug 2026 10:00:00 +0000"); Check(exMail.find("zero-rated export") != std::string::npos, "email: export VAT wording"); Check(exMail.find("€955.02") != std::string::npos, "email: export total"); // A single unit does not advertise a quantity. o.quantity = 1; const std::string one = Server::BuildOrderConfirmationEmail( o, "Fairphone 6", "Forest Green", "Catcrafts ", "https://catcrafts.net/order/x", "S\n", "Fri, 08 Aug 2026 10:00:00 +0000"); Check(one.find("Forest Green ×") == std::string::npos, "email: qty 1 stays silent"); // The last line of defence: an address that could smuggle a header // yields NO message at all, however it got into the record. o.buyer.email = "a@b.example\nBcc: leak@evil.example"; Check(Server::BuildOrderConfirmationEmail( o, "F", "", "x", "u", "S", "D").empty(), "email: header-injecting address yields no message"); // The bare newline is only the loudest of the shapes that would widen the // envelope. Under `msmtp -t` the To: header IS the recipient list, so // every address Form::LooksLikeEmail rejects must yield NO message — // a comma is the cheapest extra-recipient smuggle of the lot, and it is // barred only because that shared form validator happens to bar it. // Pinning the coupling here means a future loosening of LooksLikeEmail // (a legitimate-looking change to a form helper) cannot quietly re-open // the envelope, and each of these carries a buyer's name and address. // "…, evil@…" comma, plus a second '@' // "…> , , \nBcc: leak@evil.example" }); // Refused WHOLE, not sanitised: the guard returns before gMail is // assigned, so the command does not install either. A half-applied config // would be the dangerous outcome — a mailer that runs with a bad From. Check(!Server::MailConfigured(), "mail: a From with a line break rejects the whole config"); Check(Server::MailFrom().empty(), "mail: a rejected From is never installed", Server::MailFrom()); // kSellerName + kSellerSite, so an operator who sets MAIL_COMMAND and // forgets MAIL_FROM still sends from an address that exists. Server::ConfigureMail(Server::MailConfig{ "true", "" }); Check(Server::MailConfigured(), "mail: a clean config installs the command"); Check(Server::MailFrom() == "Catcrafts ", "mail: empty MAIL_FROM defaults to the shop inbox", Server::MailFrom()); if (failures != 0) { std::println(std::cerr, "{} check(s) failed", failures); return 1; } return 0; }