markdown fix
All checks were successful
Deploy / build-deploy (push) Successful in 2m23s

This commit is contained in:
Jorijn van der Graaf 2026-08-18 11:12:24 +02:00
commit 4666c1995f
14 changed files with 876 additions and 346 deletions

View file

@ -228,23 +228,55 @@ export inline constexpr std::string_view kPayCrypto = "crypto";
// "quantity=99999999999" before any of that arithmetic runs.
export inline constexpr std::int64_t kMaxQuantity = 99;
// What a buyer in a NoSaleCountries destination is told. Stated as a standing
// fact about where the shop sells, not as an apology or an outage: someone
// there should close the tab rather than retry tomorrow or hunt for a
// workaround. The reason itself (insurance territory) is on the terms page —
// a form field is the wrong place for it.
// What a buyer outside Money::ShippableCountries is told.
//
// One definition, three renderings: this error, the note above the form, and
// the on-page total preview, so the page can never encourage an order the
// server will refuse.
export inline constexpr std::string_view kNoSaleMessage =
"Catcrafts does not sell or ship to the United States or Canada.";
// One sentence for every refusal that is not sanctions, because from the buyer's
// side they are all one fact: their country has not been cleared. Merging them is
// also the honest shape — the old split between "we are not registered there",
// "the handset would not work there" and "our insurer excludes it" described
// Catcrafts' internal reasons, not anything the buyer can act on.
//
// Names the reason as regulatory rather than commercial, because a Dutch shop
// that plainly posts worldwide would otherwise read as arbitrary, or worse as
// quietly declining someone. The truth is duller and better: selling a phone into
// a country means meeting that country's rules first, and doing it in the wrong
// order is the illegal part.
//
// No date is promised. Which country comes next depends on which one people ask
// for, and an invented timeline is worse than none — so the address is the point
// of the last clause.
//
// One definition, three renderings: this error, the note above the form, and the
// on-page total preview, so the page can never encourage an order the server
// will refuse.
export inline constexpr std::string_view kRegulatoryMessage =
"Catcrafts can't ship there. Selling a phone into a country means meeting "
"that country's own rules first — recycling schemes, radio approval, import "
"registration — and only a few are cleared so far. Email "
"orders@catcrafts.net if you would like yours looked at next.";
// The sanctions refusal, in different words on purpose: kNoSaleMessage states
// a choice, this states a prohibition. Naming the reason here rather than only
// on the terms page — unlike the insurance one — because "sanctions" is the
// whole answer: nothing about the shop could change it, and a buyer told only
// "no" would rightly ask why.
// The standing note above the buy form, and the one place the policy is stated
// POSITIVELY. With an allow-list that is the only useful phrasing: "we ship to
// these five" tells a visitor in one glance what "we cannot ship to two hundred
// others" never would, and it stops someone filling in a whole address before
// the field error tells them no.
//
// Prose rather than generated from Money::ShippableCountries because that array
// holds ISO codes and a buyer should not have to decode "NZ". The duplication is
// real, so ShouldComputeMoney asserts the array's LENGTH — add a country and that
// test fails, pointing here. Update both or neither.
export inline constexpr std::string_view kShipsToMessage =
"Catcrafts currently ships to the Netherlands, Switzerland, Serbia, "
"Montenegro, Albania, Kosovo, Georgia, Australia, Hong Kong and "
"Singapore. Other "
"countries are being worked through one at a time; email "
"orders@catcrafts.net if you would like yours looked at next.";
// The sanctions refusal, in different words on purpose: kRegulatoryMessage
// describes paperwork that could be done, this states a prohibition that cannot.
// Naming the reason here rather than only on the terms page, because "sanctions"
// is the whole answer: nothing about the shop could change it, and a buyer told
// only "no" would rightly ask why.
export inline constexpr std::string_view kSanctionsMessage =
"Catcrafts cannot sell or ship to Russia, Belarus or North Korea: "
"EU sanctions prohibit exporting consumer electronics there.";
@ -365,14 +397,15 @@ export CheckoutResult ValidateCheckout(const Fields& f) {
} else if (!LooksLikeCountryCode(country)) {
r.errors.push_back({ "country", "Country must be a two-letter code." });
} else if (Money::IsSanctioned(r.value.country)) {
// Checked before the general refusal because SellsTo denies both and
// the words differ: this one says the law forbids the sale, not that
// the shop chose not to make it.
// Sanctions are checked first because both gates deny and only the words
// differ: this one says the law forbids the sale, not that the shop has
// not got round to that country yet.
r.errors.push_back({ "country", std::string(kSanctionsMessage) });
} else if (!Money::SellsTo(r.value.country)) {
// The refusal happens here, in validation, rather than at the payment
// step: no order record, no payment link, nothing charged to undo.
r.errors.push_back({ "country", std::string(kNoSaleMessage) });
} else if (!Money::ShipsTo(r.value.country)) {
// Everything not on the shipping list. The refusal happens here, in
// validation, rather than at the payment step: no order record, no
// payment link, nothing charged to undo.
r.errors.push_back({ "country", std::string(kRegulatoryMessage) });
}
// Colour: shape only (slug-ish, bounded). Whether it names a variant that