/* 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 compiled-in catalogue and the schema.org identity graph. Content is // code now; these assertions are the contract the shop pages rely on, // checked against the actual shipped data. 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); } // ── a priced product, built by hand ─────────────────────────────────── // Since 2026-09-05 the catalogue lists the donation item alone: the goods that // opened the shop were withdrawn. The renderer's contract for priced goods — // the ProductGroup record, the merchant fields, the carrier-table filter, the // spec lede, the preview blob — must not lapse while nothing exercises it, so // this fixture stands in for the next listing. It carries every optional field // a real one would: three colours at distinct prices (so from-price and default // selection are decidable), a boxed weight that lands in the fixture ladders' // cheap band, a brand for the spec lede, a photo path and a warranty text. The // stickers are supplier + €60.50, the same rule a real listing follows. Product Fixture() { Product pr; pr.slug = "fixture-handheld"; pr.name = "Fixture Handheld"; pr.brand = "Acme"; pr.tagline = "A stand-in priced product for the renderer contract."; pr.status = "coming-soon"; pr.shipWeightGrams = 700; pr.image = "/fixture-handheld.jpg"; pr.summary = "Stands in for a priced listing."; pr.warranty = "Two years from Catcrafts, worldwide, one counter: every claim goes to " "Catcrafts, whatever turns out to be broken. The full terms are on the " "terms page."; pr.variants = { { "green", "Forest Green", 51330 + 6050 }, { "black", "Black", 51930 + 6050 }, { "white", "White", 60488 + 6050 }, }; pr.specs = { { "Display", "6.31″ OLED" }, { "Memory", "8 GB RAM" }, }; pr.priceInclMinor = pr.CheapestVariant()->priceInclMinor; return pr; } // ── the compiled-in catalogue ───────────────────────────────────────── // What is asserted against the shipped data is the SHAPE every listing must // have, over whatever is listed, plus the donation item's own contract. No // slug, price or model is written here: a listing coming or going is a content // change, not a test edit. void CatalogueContract() { using namespace Catcrafts::Money; const auto& products = Content::Products(); Check(!products.empty(), "content: the catalogue is not empty"); // The donation item: exactly one, LAST (it is the odd entry out, not what // the shop is about), and available — it is what the shop is open FOR // whatever the goods are doing. Buyer names the amount, so no price, no // variants, no weight — and the Buyable() price check is waived for // exactly this shape. std::size_t donations = 0; for (const Product& p : products) donations += p.donation ? 1 : 0; Check(donations == 1, "content: exactly one donation item"); const Product& don = products.back(); Check(don.donation && don.slug == "donation", "content: the donation item comes last"); Check(don.Buyable() && !don.ComingSoon(), "content: the donation item is on sale"); Check(don.priceInclMinor == 0 && don.variants.empty() && don.shipWeightGrams == 0, "content: a donation has no price, no colours and no parcel"); Check(don.warranty.empty() && don.specs.empty(), "content: a donation carries no spec sheet and no warranty"); // Its page: a donation form (amount + optional email), no price line, no // product JSON-LD — an offer with no amount is a claim shopping crawlers // can only misread — and no goods sections. { const auto dp = Views::RenderProduct(don, Rates{}); const std::string_view html = dp.main.View(); Check(html.find("name=\"amount\"") != std::string_view::npos, "donation page: the form asks for an amount"); Check(html.find("name=\"street\"") == std::string_view::npos && html.find("name=\"country\"") == std::string_view::npos, "donation page: no address is asked for — nothing ships"); Check(html.find("field__req") == std::string_view::npos || html.find("Email 0, "content: a priced product has a price", pr.slug); if (!pr.variants.empty()) { // The from-price is the cheapest variant, derived so the two can // never disagree, and no colour may be listed unpriced. Check(pr.CheapestVariant() && pr.CheapestVariant()->priceInclMinor == pr.priceInclMinor, "content: from-price is the cheapest variant", pr.slug); for (const Variant& v : pr.variants) { Check(v.priceInclMinor > 0 && !v.slug.empty() && !v.label.empty(), "content: every colour is priced and named", pr.slug + "/" + v.slug); } } // The boxed weight of one outgoing parcel. Not decoration: this single // integer picks the carrier's weight bracket, so it decides the // shipping cents added to every order AND the per-country quantity // ceiling. Zero would quote a rate the carrier does not honour. Check(pr.shipWeightGrams > 0, "content: one boxed unit has a weight", pr.slug); Check(pr.warranty.find("TODO") == std::string::npos && pr.warranty.size() > 100, "content: warranty is written, not a placeholder", pr.slug); Check(pr.safetyNote.find("not yet verified") == std::string::npos, "content: no stale unverified-emergency-calling caveat", pr.slug); // Published, not merely committed: the photo the page, the link preview // and the schema record name has to exist in the tree, or nobody sees it. if (!pr.image.empty()) { Check(std::filesystem::exists("images" + pr.image), "content: the photo exists in the repo", pr.image); } // Its record parses with our own JSON parser and is sold by the // registered organisation — the join the identity graph depends on. const auto pp = Views::RenderProduct(pr, Rates{}); Check(Json::Parse(pp.meta.jsonLd).has_value(), "content: the product record parses", pr.slug); Check(pp.meta.jsonLd.find("https://catcrafts.net/#organization") != std::string::npos, "content: the offer is sold by the organization node", pr.slug); } // The spec lede is "a stock , unmodified", so a spec sheet with no // brand renders a sentence with a hole in it. Cheaper to assert than to // branch around in the renderer. for (const Product& any : products) { Check(any.specs.empty() || !any.brand.empty(), "content: a product with a spec sheet names its manufacturer", any.slug); } Check(!Content::Projects().empty(), "content: projects present"); Check(Content::LegalPages().size() == 3, "content: three legal pages"); Check(Content::AboutPage().sections.size() >= 3 && !Content::AboutPage().sections[0].body.empty() && Content::AboutPage().sections[0].body[0].find("Jorijn van der Graaf") != std::string::npos, "content: about page names the founder"); Check(!Content::Demos().empty(), "content: demos present"); } // ── the product page, against the fixture ───────────────────────────── // The renderer's contract for a priced listing, pinned on the hand-built // product above so it holds while the catalogue lists none. void RendererContract() { using namespace Catcrafts::Money; const Product pr = Fixture(); Check(pr.FindVariant("mauve") == nullptr, "fixture: unknown colour is null"); Check(pr.CheapestVariant() && pr.CheapestVariant()->slug == "green", "fixture: cheapest is green"); // The pricing rule as the user states it: after shipping (a pass-through) // and VAT, every unit sold walks away with €50.00 — however the supplier // moves. net(retail) - net(supplier) must be exactly 5000 minor, in the // same arithmetic the invoice and checkout use. for (const auto& [slug, supplier] : std::initializer_list>{ { "green", 51330 }, { "black", 51930 }, { "white", 60488 } }) { const Variant* v = pr.FindVariant(slug); Check(v && NetFromGross(v->priceInclMinor) - NetFromGross(supplier) == 5000, "fixture: variant nets the supplier price plus exactly €50", slug); } { // A ladder straddling the boxed weight. 700 g fits both bands, and // RateFor takes the CHEAPEST band that can carry it — 895, not the // tighter-looking 5500 — while the ceiling comes off the heaviest band: // 10000 / 700 = 14 units to a parcel. const std::vector ladder{ { 2000, 895 }, { 10000, 5500 } }; Check(RateFor(ladder, pr.shipWeightGrams) == 895, "fixture: the shipped weight lands in the cheap carrier bracket"); Check(MaxUnitsFor(ladder, pr.shipWeightGrams) == 14, "fixture: and caps one parcel at fourteen units"); } // The product page's schema.org record must parse with our own JSON parser // and carry one variant Product per colour, each with its ONE offer — built // from the same integers the checkout charges. { // The listing's shipping block is carrier data, so the render needs a // table. Three of these four are priced on purpose and must still not // be advertised: US is refused by policy, DE and GB on their missing // producer registrations. The carrier will happily quote all three, // which is exactly why the filter is worth asserting — CH is the only // one here besides home that sells. const std::vector feedTable{ { "NL", { { 2000, 895 } } }, { "CH", { { 2000, 2450 } } }, { "DE", { { 2000, 995 } } }, { "GB", { { 2000, 3300 } } }, { "US", { { 2000, 1794 } } }, }; auto pp = Views::RenderProduct(pr, Rates{}, feedTable); auto ld = Json::Parse(pp.meta.jsonLd); bool variantsOk = false; if (ld && ld->IsObject()) { if (const Json::Value* v = ld->Find("hasVariant"); v && v->IsArray() && v->array.size() == pr.variants.size()) { variantsOk = true; for (const Json::Value& node : v->array) { const Json::Value* o = node.Find("offers"); variantsOk = variantsOk && node.Str("@type") == "Product" && o && o->IsObject(); } } } Check(ld && ld->IsObject() && ld->Str("@type") == "ProductGroup" && variantsOk, "schema: product JSON-LD parses, one variant per colour"); Check(pp.meta.jsonLd.find("\"brand\":{\"@type\":\"Brand\",\"name\":\"Acme\"}") != std::string::npos, "schema: the group carries the hardware brand"); // Merchant-grade fields: shipping, returns, sku, group id — what // Merchant Center's website-crawl feed reads (productGroupID is its // item_group_id). Check(pp.meta.jsonLd.find("OfferShippingDetails") != std::string::npos && pp.meta.jsonLd.find("MerchantReturnPolicy") != std::string::npos && pp.meta.jsonLd.find("\"sku\":\"fixture-handheld-green\"") != std::string::npos && pp.meta.jsonLd.find("\"productGroupID\"") != std::string::npos, "schema: variants carry shipping, returns, sku and group id"); // The published rates ARE the carrier's, at one unit's weight. Check(pp.meta.jsonLd.find("\"8.95\"") != std::string::npos && pp.meta.jsonLd.find("\"24.50\"") != std::string::npos, "schema: shipping rates come from the carrier table"); // And nothing the shop refuses is advertised, whatever the carrier // quotes for it — by rate, so a filter that dropped the country code // but kept the price would still be caught. Check(pp.meta.jsonLd.find("\"9.95\"") == std::string::npos && pp.meta.jsonLd.find("\"33.00\"") == std::string::npos && pp.meta.jsonLd.find("\"17.94\"") == std::string::npos, "schema: refused destinations are never advertised"); Check(pp.meta.jsonLd.find("\"US\"") == std::string::npos, "schema: a refused destination is never advertised, priced or not"); Check(pp.meta.jsonLd.find("https://catcrafts.net/#organization") != std::string::npos, "schema: offers are sold by the organization node"); // The photo travels three ways: the page, the link preview, the record. Check(pp.main.View().find("product__photo") != std::string_view::npos, "product page: the photo renders"); Check(pp.meta.ogImage == pr.image, "product page: the link preview names the photo"); Check(pp.meta.jsonLd.find("\"image\":\"https://catcrafts.net/fixture-handheld.jpg\"") != std::string::npos, "product page: and the schema record carries it absolute"); // The spec lede is generated from the brand, so it introduces THIS // sheet — and claims nothing about any particular model or software. Check(pp.main.View().find("a stock Acme, unmodified") != std::string_view::npos, "product page: the spec lede names the brand, not a model"); Check(pp.main.View().find("postmarketOS") == std::string_view::npos, "product page: the renderer names no software of its own"); Check(pp.main.View().find("

Warranty

") != std::string_view::npos, "product page: the warranty section renders where a text exists"); // No table: no shipping claim. The listing loses the merchant block // rather than inventing a rate — the whole point of dropping the zone // fallback. auto bare = Views::RenderProduct(pr, Rates{}); Check(bare.meta.jsonLd.find("OfferShippingDetails") == std::string::npos && bare.meta.jsonLd.find("MerchantReturnPolicy") == std::string::npos, "schema: with no carrier table the offer publishes no shipping"); Check(Json::Parse(bare.meta.jsonLd).has_value() && bare.meta.jsonLd.find("\"productGroupID\"") != std::string::npos, "schema: and the rest of the record still parses"); // Coming-soon publishes the price and closes the form. Check(bare.meta.jsonLd.find("schema.org/PreOrder") != std::string::npos, "schema: coming-soon maps to PreOrder availability"); Check(bare.main.View().find("{}); auto ld = Json::Parse(home.meta.jsonLd); const Json::Value* org = nullptr; const Json::Value* site = nullptr; if (ld && ld->IsObject()) { if (const Json::Value* g = ld->Find("@graph"); g && g->IsArray()) { for (const Json::Value& node : g->array) { if (node.Str("@type") == "Organization") org = &node; if (node.Str("@type") == "WebSite") site = &node; } } } Check(org && site, "schema: home graph parses, carries Organization and WebSite"); if (org && site) { Check(org->Str("@id") == kOrgId, "schema: organization node is identified"); Check(site->Str("@id") == "https://catcrafts.net/#website", "schema: website node is identified"); // The join that makes two nodes one entity rather than two. const Json::Value* publisher = site->Find("publisher"); Check(publisher && publisher->IsObject() && publisher->Str("@id") == kOrgId, "schema: website is published by the organization node"); // The navigational query "catcrafts" is answered from the site // entity, so the spellings people type belong on it. const Json::Value* alt = site->Find("alternateName"); Check(alt && alt->IsArray() && !alt->array.empty(), "schema: website carries the spellings people type"); // Registry numbers, typed. These are the part no name-twin can // produce — each one is checkable against a public register, // which also means a wrong value is worse than no value. bool kvk = false, vat = false, eori = false; if (const Json::Value* ids = org->Find("identifier"); ids && ids->IsArray()) { for (const Json::Value& id : ids->array) { if (id.Str("propertyID") == "KVK") kvk = id.Str("value") == "78437059"; if (id.Str("propertyID") == "VAT") vat = id.Str("value") == "NL003329281B38"; if (id.Str("propertyID") == "EORI") eori = id.Str("value") == "NL1900095326"; } } Check(kvk && vat && eori, "schema: KVK, VAT and EORI present and exact"); // The name-twin guard. "Cat Crafts" with a space is the generic // craft phrase owned by everyone else; claiming it as an alternate // name argues for merging this entity into the corpus it needs to // stay distinct from. Check(home.meta.jsonLd.find("Cat Crafts") == std::string::npos, "schema: the spaced generic is not claimed as a brand name"); } // Cross-page joins: both must name the SAME @id the home page defines. auto about = Views::RenderAbout(Content::AboutPage()); Check(about.meta.jsonLd.find(kOrgId) != std::string::npos, "schema: about joins the founder to the organization node"); { // The offer side of the join, on the fixture: the catalogue may list // no priced product (the donation publishes no offer at all), and the // join must hold for the next one regardless. auto pp = Views::RenderProduct(Fixture(), Rates{}); Check(pp.meta.jsonLd.find(kOrgId) != std::string::npos, "schema: offers are sold by the organization node"); } // The person join, same mechanism in the other direction: home's // founder and about's mainEntity must name one Person node, or "who // founded Catcrafts" splits into two same-named strangers too. constexpr std::string_view kPersonId = "https://catcrafts.net/about#person"; Check(home.meta.jsonLd.find(kPersonId) != std::string::npos && about.meta.jsonLd.find(kPersonId) != std::string::npos, "schema: founder and about name one Person node"); } // ── the sale gates, built by hand ───────────────────────────────────── // The shipped catalogue holds each status at most once (today: the donation // alone), so asserting against it can only ever exercise one arm of each // guard. These products exist to reach the others. // // Buyable() is the ONLY server-side gate on POST /checkout, and it is a // conjunction: status AND a price. ComingSoon() is separately what decides // whether a visitor is told "the shop has not opened yet" or "temporarily // unavailable", so the two are pinned apart rather than as one either/or. void SaleGates() { Product priced; priced.status = "available"; priced.priceInclMinor = 56330; Check(priced.Buyable() && !priced.ComingSoon(), "status: available with a price is the one buyable state"); // The half the catalogue can never exercise. An "available" product with // no price is what a from-price sync that failed to run leaves behind // (Content::Products derives priceInclMinor from CheapestVariant); if // this arm of the conjunction regressed, checkout would mint a real order // record and a live payment link for €0. Product unpriced; unpriced.status = "available"; unpriced.priceInclMinor = 0; Check(!unpriced.Buyable(), "status: an unpriced product cannot be bought"); // The donation arm of the same conjunction: no price by definition, yet // buyable — and ONLY because the flag says the buyer names the amount. // The status half still gates it like anything else. Product gift; gift.status = "available"; gift.donation = true; Check(gift.Buyable(), "status: an available donation needs no price"); gift.status = "unavailable"; Check(!gift.Buyable(), "status: a withdrawn donation is not buyable either"); Product withdrawn; withdrawn.status = "unavailable"; withdrawn.priceInclMinor = 56330; Check(!withdrawn.Buyable() && !withdrawn.ComingSoon(), "status: unavailable is neither for sale nor coming soon"); Product soon; soon.status = "coming-soon"; soon.priceInclMinor = 56330; Check(soon.ComingSoon() && !soon.Buyable(), "status: coming-soon publishes a price without opening orders"); // A product with no colours is a supported catalogue shape — priceInclMinor // is then simply the price. The null return is the only thing standing // between that shape and the two call sites that dereference the result // (the checkout form's default selection, and the checkout handler). Product plain; plain.priceInclMinor = 56330; Check(plain.CheapestVariant() == nullptr, "variants: no colours means no cheapest colour"); Check(plain.FindVariant("green") == nullptr && plain.FindVariant("") == nullptr, "variants: nothing is ever found in an empty colour list"); // The loop compares with a strict <, so a tie keeps the listed order. // That is what stops the advertised "from" price and the form's default // selection from naming different colours when two cost the same. Product tied; tied.priceInclMinor = 56330; tied.variants = { { "green", "Forest Green", 56330 }, { "black", "Black", 56330 }, }; Check(tied.CheapestVariant() && tied.CheapestVariant()->slug == "green", "variants: equally priced colours keep the listed order"); } // ── the product page's live-total blob ──────────────────────────────── // RenderCheckoutForm renders only for a Buyable priced product, and the // shipped catalogue lists none, so nothing in it renders the form: the suites // that read this attribute over HTTP sit behind a ShopOpen() gate. Flip the // fixture to "available" and read the markup here instead — the refusal lists // in that blob are what make the on-page total decline in exactly the places // checkout declines, and a page that quotes a total for a sanctioned or // no-sale destination invites an order that must then be refused. void CheckoutPreviewData() { Product pr = Fixture(); pr.status = "available"; Check(pr.Buyable(), "checkout: the flipped copy is buyable, so the form renders"); const std::vector feedTable{ { "NL", { { 2000, 895 } } }, { "DE", { { 2000, 995 } } }, }; const auto page = Views::RenderProduct(pr, Rates{}, feedTable, {}, {}, true); const std::string_view html = page.main.View(); // The blob is a JSON document inside an HTML attribute, so every quote // arrives escaped — matching the escaped form is matching what the // browser actually parses back out. Check(html.find(""s":["RU","BY","KP"]") != std::string_view::npos, "checkout: the preview carries the sanctions list verbatim"); // `w` is the shipping ALLOW-list: the preview refuses a country by its // ABSENCE here, which is why the payload stays five codes long instead of // enumerating the two hundred that are closed. Check(html.find(""w":["NL","CH","AU"," ""HK","SG","RS","ME"," ""AL","XK","GE"]") != std::string_view::npos, "checkout: the preview carries the shipping allow-list verbatim"); Check(html.find(""US"") == std::string_view::npos, "checkout: no deny-list survives in the preview payload"); // Both refusals ship their own sentence, or the preview would word a decline // differently from the submit that follows it. Check(html.find(""sm"") != std::string_view::npos && html.find(""rm"") != std::string_view::npos, "checkout: the preview carries a message for each refusal"); // The unit weight, which is what selects a bracket out of the carrier // table the same blob carries. Check(html.find(""g":700") != std::string_view::npos, "checkout: the preview knows one unit's shipping weight"); // Per-colour unit prices: the preview multiplies these, so they are the // same integers the checkout charges or the two disagree on screen. Check(html.find(""green":57380") != std::string_view::npos && html.find(""black":57980") != std::string_view::npos && html.find(""white":66538") != std::string_view::npos, "checkout: every colour is priced in the preview blob"); } // ── which euro amount a localised price converts ────────────────────── // The headline price rides along as one pre-formatted attribute per // currency, and the basis differs by EU membership: a member's currency // converts the VAT-inclusive price the buyer pays, everyone else's converts // the ex-VAT export price. Invert that test and a Swedish visitor sees a // figure 21% below what their card is charged, or a British one 21% above // the export price — both while every existing assertion (which only checks // that the attributes exist) still passes. void LocalisedPriceBasis() { // €121.00 inclusive: net = 12100 * 10000 / 12100 = 10000 exactly, so the // two bases are a clean €121 and €100 with no rounding to reason about. Product pr; pr.slug = "basis-probe"; pr.name = "Basis probe"; pr.status = "coming-soon"; pr.priceInclMinor = 12100; // 1 EUR = 1 unit in both currencies, so the printed number can only // report which euro amount the conversion started from. Rates rates; rates.date = "2026-01-01"; rates.microPerEur.emplace_back("SEK", 1'000'000); rates.microPerEur.emplace_back("GBP", 1'000'000); const auto page = Views::RenderProduct(pr, rates); const std::string_view html = page.main.View(); // SE is an EU member: base 12100, converted whole and half-up -> // (12100 * 1e6 + 5e7) / 1e8 = 121. Check(html.find(R"(data-sek="~kr 121")") != std::string_view::npos, "price: an EU member's currency converts the VAT-inclusive price"); // GB is not: base 10000 -> 100. Check(html.find(R"(data-gbp="~£100")") != std::string_view::npos, "price: a non-EU currency converts the ex-VAT export price"); // The euro fallback that no-JS visitors and crawlers read is the export // price, in the same units the two above were derived from. Check(html.find(R"(data-world="€100")") != std::string_view::npos, "price: the export euro price is the world default"); } // ── the URLs this site advertises as its own ────────────────────────── // The sitemap is the list of URLs the site asks crawlers to index; the nav // is what a visitor clicks. Both are hand-maintained lists, so a renamed // legal slug or a retired path left behind advertises a 404 — or a 301 — as // canonical, silently and forever. Nothing else cross-checks them: the route // suite walks a literal list of its own rather than SitemapPaths(). void AdvertisedUrls() { Views::SiteContent site; site.legal = Content::LegalPages(); for (const std::string_view path : SitemapPaths()) { const Route r = ParseRoute(path); Check(r.kind != RouteKind::NotFound, "sitemap: every advertised path resolves to a page", path); // A route carrying a canonical target is a redirect whatever it // renders, and asking a crawler to index a redirect is asking it to // index a non-canonical URL. Check(r.canonicalRedirect.empty(), "sitemap: no advertised path is itself a redirect", path); // Parsing only proves the URL is SHAPED like a legal page; the // dispatcher's lookup is what decides whether it renders one. if (r.kind == RouteKind::Legal) { Check(site.FindLegal(r.slug) != nullptr, "sitemap: every advertised legal slug names a real page", path); } } for (const NavItem& item : NavItems()) { Check(ParseRoute(item.href).kind == item.kind, "nav: every nav entry parses to the route it claims", item.href); } } } // namespace int main() { CatalogueContract(); RendererContract(); IdentityGraph(); SaleGates(); CheckoutPreviewData(); LocalisedPriceBasis(); AdvertisedUrls(); if (failures != 0) { std::println(std::cerr, "{} check(s) failed", failures); return 1; } return 0; }