/* 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 shop front: prices rendered from the same integers the checkout // charges, the currency conversions the shop card carries, and the copy // around them. Runs in both shop states — coming-soon asserts the closed // shape, launch re-arms the open one with no edit here. // // The goods half addresses whatever priced product the compiled catalogue // lists first — its slug, its variants and its photo are read off the record, // and every expected figure is formatted with the same Money routines the // renderer uses. While the catalogue lists no goods (as since 2026-09-05) that // half prints a note and is skipped; the donation checks run regardless. import std; import Catcrafts.Shared; import Catcrafts.E2eHarness; using namespace Catcrafts; using namespace Catcrafts::E2e; namespace { void PricedProduct(TestServer& srv, const Product& pr) { const std::string shop = "/shop/" + pr.slug; const std::string incl = Money::FormatEuro(pr.priceInclMinor); const std::string net = Money::FormatEuro(Money::NetFromGross(pr.priceInclMinor)); // The price is rendered from the same integers the checkout charges, with // the derived ex-VAT twin alongside — asserting both pins the arithmetic. srv.BodyHas(shop, incl, "product page shows the (from-)price"); srv.BodyHas(shop, net, "product page shows the derived ex-VAT price"); srv.BodyHas("/shop", incl, "shop card shows the (from-)price"); // "from" is a claim that other colours cost more, so it appears exactly // when there is more than one colour to choose from. if (pr.variants.size() > 1) { srv.BodyHas(shop, ">from<", "product page marks the price as a from-price"); Check(pr.CheapestVariant() && pr.CheapestVariant()->priceInclMinor == pr.priceInclMinor, "the advertised from-price is the cheapest colour's"); } else { srv.BodyLacks(shop, ">from<", "a single-price product is not a from-price"); } // Every colour is priced in the selector, label and euro amount together, // and the form carries the exact data blob the preview computes from. if (srv.ShopOpen(pr.slug)) { for (const Variant& v : pr.variants) { srv.BodyHas(shop, v.label + " " + Money::FormatEuro(v.priceInclMinor), "colour selector prices " + v.slug); } srv.BodyHas(shop, "data-cc=", "form embeds the pricing blob"); srv.BodyHas(shop, "id=\"cc-total\"", "live total element present"); } else { srv.BodyHas(shop, "Coming soon", "coming-soon notice on the buy panel"); srv.BodyHas("/shop", "coming soon", "shop card carries the coming-soon badge"); srv.BodyLacks(shop, "Donate", "donation form submits to payment"); srv.BodyLacks("/shop/donation", "name=\"street\"", "donation form asks no address — nothing ships"); srv.BodyLacks("/shop/donation", "name=\"quantity\"", "donation form has no quantity — one gift, one line"); srv.BodyHas("/shop/donation", "No VAT is charged on a donation", "donation page states the VAT treatment"); srv.BodyHas("/shop/donation", "aggregate total", "donation page states how it appears on the financials page"); // Both rails are configured in this harness, so the donation offers the // same payment choice checkout does. srv.BodyHas("/shop/donation", "value=\"crypto\"", "donation form offers crypto"); // No price, no conversions, no preview: the donation page ships NO // executable script at all — stricter than the shop pages' one-script rule. srv.BodyLacks("/shop/donation", "