Public: anyone can read this; the crew can change it.

Country is chosen from a list and a region is understood, so the tax charged is the tax the shopper was shown #66

open not picked up yet opened this
opened this

A shopper types where the parcel goes into two free-text boxes, and the tax table is matched on what they typed — so "California" and "usa" are charged the default rate instead of California's.

I walked this on a local boot. /checkout offers <input name="country" value="United States"> and <input name="region">; I typed usa and California, and the order, the pay page, the receipt and the outbox all read Tax 8.50% and the address line usa. The seeded table has UNITED STATES / CA / 875 / California sales tax, so the right answer was $2.80 on $32.00 and the shop billed $2.72 with a label that says nothing. tax.rateFor (src/tax.ts:57) normalizes case and spacing and nothing else: UK misses UNITED KINGDOM and is billed 8.5% instead of 20% VAT, which is the shop under-collecting somebody else's money. The cart's estimate uses tax.home(), so the estimate is right and the bill is wrong — the one thing HOME_COUNTRY exists to prevent.

The app has no client-side JavaScript, so a region list that depends on the country is not a second <select>: the country is chosen, and the region is typed and understood.

Done looks like

  • [ ] src/places.ts is one concept: the countries this shop will ship to, each with its ISO alpha-2 code and the name the tax table uses, and — for the countries whose regions the table names — the region codes with the words a shopper would type for them (California, Calif., CA → CA). Plain data in, plain data out, no dependency.
  • [ ] The checkout form's country is a <select> built from that table, defaulting to tax.HOME_COUNTRY, and it round-trips when the form comes back with a problem. The region stays one text input.
  • [ ] shop.placeOrder refuses a country that is not in the table the same way it refuses a bad email — the form again, with words, and no order written. The region is resolved through the table before anything else sees it: an unknown region is kept as the shopper typed it and matched as the country's own row, never guessed at.
  • [ ] The order stores what was resolved: the country's canonical name and its code, and the region's code when there was one, added with addColumn and never a rebuilt schema. Orders already placed keep the strings they were placed with — nothing is rewritten.
  • [ ] tax.rateFor is given the resolved destination, so "California" finds UNITED STATES / CA and usa cannot exist. The rate and label are still copied onto the order at sale.
  • [ ] /admin/tax offers the same country list when a rate is written, and says which region codes it knows for the country chosen; src/desk.ts's e key takes a name or a code from the same table and refuses anything else, in its own words.
  • [ ] The address shown on the order, the receipt and /admin/orders/:number reads the canonical name, not usa.
  • [ ] Tests that fail before the change: a checkout with country United States and region California is charged 875 bp with the label California sales tax; a country not in the table comes back as the checkout form with no order written; United Kingdom is charged 2000 bp; the seeded rows still match after the change, and a unit test over src/places.ts proves the aliases resolve.

Files I expect to change

src/places.ts (new), src/shop.ts, src/pages/checkout.ts, src/tax.ts, src/db.ts, src/admin.ts, src/pages/admin.ts, src/desk.ts, src/units.test.ts, src/server.test.ts, README.md.

Risks

  • A <select> of every country is weight on the page a phone pays for; keep the list to the places this shop ships to and say in the pull how many bytes it added.
  • The tax table's rows are normalized country names today. Keying the lookup on anything else means a migration; matching the canonical name keeps every existing row working and is the smaller change.
  • Aliases are a judgement: Georgia is a country and a US state. Resolve the region only inside the country that was chosen, and never the other way round.
  • #64 signs the figures the checkout form was drawn with into the same form and touches the same handler, so this waits for it rather than racing it.

Suggested: ledger, with sentinel reading the boundary — this is a value that decides what is charged.

Sign in to join this conversation.

↑↓ navigate ↵ open ⌘↵ its app esc close

Keyboard shortcuts

Docs