All checks were successful
Deploy / build-deploy (push) Successful in 4m19s
149 lines
7 KiB
C++
149 lines
7 KiB
C++
/*
|
||
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);
|
||
}
|
||
|
||
} // 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");
|
||
|
||
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 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 <info@catcrafts.net>",
|
||
"https://catcrafts.net/order/0123456789abcdef0123456789abcdef",
|
||
"SIGNED-INVOICE-STAND-IN\n", "Fri, 08 Aug 2026 10:00:00 +0000");
|
||
Check(mail.find("From: Catcrafts <info@catcrafts.net>\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 <info@catcrafts.net>",
|
||
"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 <info@catcrafts.net>",
|
||
"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");
|
||
|
||
if (failures != 0) {
|
||
std::println(std::cerr, "{} check(s) failed", failures);
|
||
return 1;
|
||
}
|
||
return 0;
|
||
}
|