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

@ -82,33 +82,19 @@ export bool IsEuCountry(std::string_view cc) {
return std::ranges::find(EuCountries(), cc) != EuCountries().end();
}
// Destinations this shop refuses outright.
//
// Not a carrier problem — parcels reach both fine, and the rate tables price
// them. It is an insurance boundary: liability cover for a Dutch shop is
// written "worldwide excluding USA/Canada", and a phone carrying a lithium
// battery and a replaced OS is precisely the product-liability exposure that
// exclusion exists for. A sale into either country would therefore be
// uninsured, with the buyer's own courts deciding the damages, so checkout
// declines it instead of pricing it. Declining also keeps Catcrafts from
// having marketed into those jurisdictions at all, which is the other half of
// why the answer is no rather than a surcharge.
export std::span<const std::string_view> NoSaleCountries() {
static constexpr std::array<std::string_view, 2> blocked{ "US", "CA" };
return blocked;
}
// Destinations the law forbids, as opposed to the insurance choice above.
// Destinations the law forbids.
//
// EU sanctions — Regulation 833/2014 for Russia, its Belarus mirror, and the
// North Korea embargo — prohibit exporting consumer electronics to these
// countries, by customs code and by the luxury-goods value threshold both, and
// the prohibition covers indirect routes (a forwarder, a reseller) as much as
// a direct parcel. That binds every EU seller as criminal law; there is no
// small-shop exemption and no surcharge version of compliance. A separate list
// rather than more entries in NoSaleCountries because the refusal needs
// different words: "does not" is a choice, "cannot" is the law, and each gets
// its own explanation on the terms page.
// small-shop exemption and no surcharge version of compliance.
//
// Kept apart from the shipping allow-list below even though both refuse,
// because the refusal needs different words: not shipping somewhere yet is a
// state of the paperwork, while this is a prohibition nothing about the shop
// could change. Each gets its own sentence at checkout and on the terms page.
export std::span<const std::string_view> SanctionedCountries() {
static constexpr std::array<std::string_view, 3> blocked{ "RU", "BY", "KP" };
return blocked;
@ -118,14 +104,189 @@ export bool IsSanctioned(std::string_view cc) {
return std::ranges::find(SanctionedCountries(), cc) != SanctionedCountries().end();
}
// Where this shop ships. An allow-list, and that inversion IS the design.
//
// Everything not named here is refused. That is not timidity, it is the only
// honest posture available: the rules deciding whether a phone may lawfully be
// sold into a country are national, they differ in kind rather than degree, and
// nobody has read all of them. A deny-list ships to every country nobody thought
// about; an allow-list refuses them. Only one of those failure modes is
// survivable, so the default is no, and every code below is a country somebody
// actually checked.
//
// What "checked" has to mean before a code goes in:
//
// * the destination does not reach the FOREIGN SELLER with producer duties of
// its own — packaging, e-waste, batteries. The EU does. The EEA (Iceland,
// Liechtenstein, Norway) does too, through the same directives, which is why
// no EEA country is here. The UK does by national law.
// * a phone posted from here is actually admitted: no national IMEI database
// to be registered against before a local network will attach it, and no
// customs practice of seizing handsets that lack local type approval.
// * the parcel crosses as the BUYER's import, so their own authority charges
// them VAT and duty and their own type-approval rules bind them rather than
// Catcrafts. This is the one thing the price buys: every foreign
// low-value-consignment regime found sits far below €600, so nothing routes
// through a seller-side registration scheme.
// * liability cover reaches it. The AVB is written worldwide EXCLUDING the
// United States and Canada — confirm that territory in the policy schedule
// before trusting this list, because every entry assumes it.
//
// Removed after verification, and NOT to be re-added on a hunch — both were on
// this list once, on the mistaken assumption that a domestic-supply approval
// regime never reaches a personal import:
//
// JP — using a handset without Japan's giteki (技適) mark on a Japanese network
// is a Radio Act offence carrying up to a year's detention or ¥1,000,000.
// The only exemption is for a short-stay visitor carrying a device in; it
// does not reach a resident receiving a parcel. Fairphone holds no Japanese
// certification and says outright that its devices cannot be used in
// countries requiring local homologation. Note where the liability lands:
// the offence is USE, so the person committing it is the customer. Selling
// someone a phone they break the law by switching on is worse than any
// paperwork gap on this page.
// NZ — the radio regulator defines a supplier to include "a seller", says the
// rules apply identically whether stock ships from inside New Zealand or
// reaches the market through a website, and requires a Licence to Supply
// for radio transmitters. The cure is then bolted shut: an overseas company
// cannot register on the compliance database, and unlike Australia there is
// no agent workaround. Structurally the same trap as Norway and the UK,
// living in radio law rather than waste law.
//
// Not here, and deliberately: the United States and Canada. The insurance
// exclusion was only ever half the reason and is no longer the interesting half.
// Canada is legally shut — the Fairphone (Gen. 6) holds no ISED certification,
// and Canadian law bars importing, distributing or selling uncertified radio
// apparatus, so no structure or policy opens it. The US adds per-parcel customs
// entry since de minimis ended, carrier certification that gates activation, and
// no emergency-call immunity of any kind for a device maker. Both are refused for
// regulatory reasons now, not commercial ones, and neither becomes available by
// buying a policy.
// Every entry must also ship DDU/DAP — the buyer as importer of record, paying
// their own authority at the border. That is not a commercial preference, it is
// load-bearing law in three of the five: it is what makes Hong Kong's
// personal-use import exemption apply, what keeps Catcrafts outside Singapore's
// producer definition, and what puts import tax on the consumer everywhere else.
// Appear as importer of record and two of these countries close.
export std::span<const std::string_view> ShippableCountries() {
static constexpr std::array<std::string_view, 10> open{
// Home. Verpact wants nothing under 50,000 kg of packaging, and the
// phones are already on the Dutch market when Catcrafts buys them, so no
// producer duty attaches. That second clause is load-bearing: source
// stock from another member state and Catcrafts becomes the Dutch
// importer, owing Stichting OPEN before this line is honest again.
"NL",
// Checked end to end against the federal texts. CE accepted under the
// bilateral MRA (its chapter 7 is radio equipment); the e-waste ordinance
// reaches only those importing for COMMERCIAL supply, with no
// distance-seller limb and no producer register; and the mail-order VAT
// duty is scoped to consignments cheap enough to be import-tax-exempt, so
// a phone goes through ordinary import with the buyer paying at the
// border. The packaging ordinance expected 1 Jan 2027 turns out not to
// matter — it was adopted 24 June 2026 and its fee covers GLASS only.
//
// The real watch item is elsewhere, and it is specific: the circular-
// economy revision of the environment act, in force since 1 Jan 2025,
// already empowers a disposal fee on "ausländische Online-
// Versandhandelsunternehmen" — defined as whoever offers products
// digitally and delivers to consumers in Switzerland without a Swiss
// seat or establishment. That is this shop, definitionally. It is dormant
// only because the power is discretionary and no ordinance names a phone,
// and the companion article is deliberately not yet in force. An ordinance
// extending the fee to electrical devices is the day Switzerland flips.
"CH",
// Read out of the Radiocommunications Equipment (General) Rules 2021
// rather than off a guidance page, because ACMA's own site is misleading
// here. The labelling and registration duties in s25 bind an importer who
// then SUPPLIES — neither limb is met when the consumer imports one phone
// for themselves. What does reach Catcrafts is s12: a person must not
// supply a device failing a prescribed general standard, extended to trade
// between Australia and places outside it. That is a SUBSTANTIVE standards
// duty, not a registration one — so no Australian establishment is needed,
// unlike New Zealand. Phones are outside the e-waste scheme (which covers
// TVs, printers and computers, and binds Australian corporations anyway).
// Two watch items: a mandatory small-electricals stewardship scheme is
// committed but not law, and the A$1,000 low-value-import line sits close
// enough to €600 that FX moves can cross it.
"AU",
// The best-documented jurisdiction of the set. Mobile phones sit outside
// the e-waste producer-responsibility list; user equipment needs no type
// approval (voluntary certification only); the import ordinance exempts
// equipment brought in for reasonable personal use; and there is no VAT or
// GST at all, so no foreign-seller registration can arise.
"HK",
// Both feared hooks miss on the facts. The e-waste producer duty requires
// importing INTO Singapore in furtherance of a Singapore supply business,
// which a DDU parcel is not, and there is no distance-selling limb of the
// kind UK law uses. IMDA states personal-use imports need no registration
// and sets no quantity limit. On tax the price helps: the low-value-goods
// regime caps at S$400, so a €600 phone is above it and the overseas-vendor
// rules cannot reach it at any turnover.
"SG",
// The Western Balkans four. All share the shape that matters: producer
// responsibility attaches to whoever places goods on the DOMESTIC market
// — the in-country importer, so the buyer — with none of the
// "regardless of sales channel" drafting that catches a distance seller in
// the EU, Norway, Iceland, Moldova and Bosnia. CE is accepted, no IMEI
// whitelist exists, and non-resident VAT reaches services only.
//
// RS — the strongest of them, because the answer comes from the customs
// authority itself: conformity documents are demanded only for
// certain drones, no radio-equipment conformity paper is required at
// import, and per the telecoms ministry's published position the
// Radio Equipment Rulebook does not apply to natural persons at all.
// ME — best-evidenced on tax: the VAT act states in terms that where
// transport begins outside Montenegro the IMPORTER makes the supply,
// and a tax representative is needed only for supplies made inside
// the country. EPR bylaws are still pending.
// AL — its WEEE decree still uses the pre-2012 three-limb producer
// definition, so the distance-selling limb simply is not there, and
// registration would need an Albanian tax number nobody can give a
// foreigner. Smartphones are also duty-free in the 2026 tariff.
// HARD DATE: Law 74/2025 takes effect 1 December 2026. Re-read its
// producer definition and any implementing acts before then, because
// that is when this entry could stop being true.
// XK — cleared on the law, with one operational caveat that is not legal:
// Kosovo is not a UPU member, so there is no treaty-based tracking
// guarantee or loss indemnity. On a €600 parcel that matters — ship
// it by courier rather than post. Note also that XK is a
// user-assigned code rather than official ISO 3166-1; if the carrier
// table spells Kosovo differently this entry simply never matches and
// the destination falls through to the no-carrier-rate refusal, which
// is the safe direction to fail.
"RS", "ME", "AL", "XK",
// Held back for a while on the dual-use crypto question rather than
// anything Georgian, and that question turned out to be a paperwork task
// rather than a gate — the mass-market exemption releases this phone for
// the same reason it releases every unlockable Pixel. On its own law
// Georgia is among the cleanest here: producer duties attach to whoever
// IMPORTS, with no distance-selling limb and no authorised-representative
// concept at all; the electronic communications act contains not one
// mention of IMEI; the product-safety code admits goods built to the
// standards of any EU or OECD country; and phones carry no duty, with the
// buyer paying import VAT at the border. Same caveat as Kosovo: the
// national post is slow with unreliable tracking, so send it by courier.
"GE",
};
return open;
}
export bool ShipsTo(std::string_view cc) {
return std::ranges::find(ShippableCountries(), cc) != ShippableCountries().end();
}
// ISO 3166-1 alpha-2, uppercase, like everything else here. Callers ask this
// rather than comparing against "US" themselves, so the policy has exactly one
// definition and adding a country later is a one-line change. Both lists deny:
// most callers only need "is this destination for sale", and only the checkout
// error message cares which refusal it is (IsSanctioned above).
// rather than testing the lists themselves, so the policy has exactly one
// definition and opening a country is a one-line change.
//
// Two HARD gates, in order of permanence: the law, then the shipping list. A
// destination the carrier happens not to price is a third and much softer
// refusal that lives with the rate table (Form::kNoShippingTemplate) — it says
// no price exists, not that the sale is refused, and a carrier contract can
// change it tomorrow. Most callers only need "is this destination for sale";
// only the checkout error cares which refusal it is.
export bool SellsTo(std::string_view cc) {
return !IsSanctioned(cc) &&
std::ranges::find(NoSaleCountries(), cc) == NoSaleCountries().end();
return !IsSanctioned(cc) && ShipsTo(cc);
}
// Delivery-time tiers. NOT a price concept — every rate comes from the carrier
@ -271,10 +432,17 @@ export struct CurrencyRow {
// Only currencies the ECB publishes reference rates for; anywhere else shows
// plain euros. Euro countries are deliberately absent — converting EUR to EUR
// is noise. So are USD and CAD: NoSaleCountries means no order can ever be
// charged from those countries, and quoting a visitor a friendly price in
// their own currency before refusing them at checkout is both a worse
// experience and the kind of localisation that reads as marketing there.
// is noise. So are USD and CAD, and that one is a policy choice rather than a
// gap in the ECB feed: those two are refused for regulatory reasons that no
// amount of demand will lift, so quoting a visitor a friendly price in their own
// currency before declining them is both a worse experience and the kind of
// localisation that reads as marketing into a market this shop cannot serve.
//
// The rest of this table deliberately runs AHEAD of ShippableCountries. It
// answers "what would this cost in my money", which stays a fair question for a
// country whose paperwork is merely pending — GB is one small registration from
// opening — and keeping the row spares a delete-and-restore cycle later. The
// refusal that must never be quoted around is enforced in SellsTo, not here.
export std::span<const CurrencyRow> AllCurrencies() {
static constexpr std::array<CurrencyRow, 14> rows{{
{ "GB", { "GBP", "£" } },