sanctions block list
All checks were successful
Deploy / build-deploy (push) Successful in 2m18s

This commit is contained in:
Jorijn van der Graaf 2026-08-15 03:22:25 +02:00
commit b0666841f6
7 changed files with 139 additions and 31 deletions

View file

@ -234,22 +234,33 @@ export inline constexpr std::int64_t kMaxQuantity = 99;
export inline constexpr std::string_view kNoSaleMessage =
"Catcrafts does not sell or ship to the United States or Canada.";
// 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.
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.";
// The two shipping refusals, worded once.
//
// Since the shop stopped carrying its own rate table, the carrier's coverage
// IS the shop's coverage: no bracket for a country means no price exists to
// charge, and a parcel above every bracket is one the carrier will not take.
// Both refuse rather than guess — a quote the shop cannot honour is worse than
// a no — and both name a way forward, because a bare "can't" makes the buyer
// guess whether to try again or give up.
// a no. The uncovered-country refusal is final: no carrier service means the
// shop does not ship there, and offering to arrange it by hand would promise
// exactly the ad-hoc export the shop decided not to do. The too-heavy refusal
// still names the quantity that WOULD fit, because that buyer has an order the
// shop can take — just not in one parcel.
//
// Kept as {cc}/{n} templates rather than format strings because they have two
// consumers: the handler fills them for its field errors, and the buy page
// hands them to the total preview verbatim to fill client-side. Same sentence
// before and after the submit, from one definition.
export inline constexpr std::string_view kNoShippingTemplate =
"No carrier rate for {cc} is available right now, so this order can't be "
"priced. Email orders@catcrafts.net and it gets arranged by hand.";
"No carrier rate for {cc} is available, so Catcrafts can't ship there.";
export inline constexpr std::string_view kTooHeavyTemplate =
"That is more than fits one parcel to {cc} — up to {n} per order. For a "
@ -257,10 +268,11 @@ export inline constexpr std::string_view kTooHeavyTemplate =
// The degenerate case: a destination whose heaviest bracket does not even carry
// one boxed unit. "Order fewer" is not advice when fewer is zero, so it gets
// its own sentence.
// its own sentence — and like the uncovered country, it is a final no: what
// the carrier can't take, the shop doesn't ship.
export inline constexpr std::string_view kTooHeavyNoneTemplate =
"A parcel this heavy can't be shipped to {cc} by any rate available. "
"Email orders@catcrafts.net.";
"A parcel this heavy can't be shipped to {cc} by any rate available, "
"so this order can't be placed.";
export std::string FillShipMessage(std::string_view tmpl, std::string_view cc,
std::int64_t n) {
@ -346,6 +358,11 @@ export CheckoutResult ValidateCheckout(const Fields& f) {
r.errors.push_back({ "country", "Pick a country — it decides shipping and VAT treatment." });
} 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.
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.