Skip to content

feat: let any whole number from the minimum order up be ordered - #11

Merged
JasonYv merged 13 commits into
mainfrom
feature/quantity-any-above-minimum
Oct 18, 2026
Merged

JasonYv merged 13 commits into
mainfrom
feature/quantity-any-above-minimum

Conversation

@JasonYv

@JasonYv JasonYv commented Oct 9, 2026

Copy link
Copy Markdown
Contributor

Stacked on #10 (the export).

What this is

Two decisions of the owner's (design §11, decisions 11 and 12).

What a quantity must be: at least the minimum order, at most what a line may hold — and not a multiple of the minimum. The server never asked for a multiple. The quantity fields did, by stepping in units of the minimum, so a browser refused what the server would have taken; and with a ceiling of 500 a part with a minimum of 200 could be ordered as 200 or 400 and nothing else. The fields step by one now.

The rounding of a derived price stays as it is: up to the next .99. Nothing changes in the code for it; the design records it.

How it was verified

Check Result
lint, type check, build, both smoke runs, the preview check Pass
Tests inside workerd 811 of 811; one new, which orders seventeen of a part whose minimum is ten
Red and green three deliberate breakages, each noticed: a multiples rule on the server, and the old step on each page

JasonYv added 13 commits October 9, 2026 11:58
…um order

The ceiling was 10,000, a guard against scripted abuse carried over from the
previous implementation. The owner set it to 500 (design §11, decision 9),
which makes it a rule of the shop that a buyer will meet.

So it is judged where the minimum order is, in quantityIssue: a line above it
that is already in a cart cannot be ordered either, and says the figure. A
quantity field carries it as max, on a product page and in the cart, from
max_quantity in both views. The admin refuses a minimum order above it.
An inquiry is quoted by a number of the same shape as an order's: a prefix,
the date and eight characters of Crockford's alphabet. The generator moves
out of the order code so that both use it, with a test of the shape it
makes and of how one is recognised under its own prefix.
Phase 1's last piece, which the owner decided is the shop plugin's to build
(design §11, decision 10).

A form on the cart page takes who to answer and a message, and posts to
cart/inquiry. The route stores the cart as an inquiry with its lines as a
snapshot, queues the job that owes its emails, and empties the cart, in one
batch; it answers with a redirect to the cart page, which shows the
inquiry's number to the browser whose cart it was. A part with no price can
be asked about, so a product page now offers a form for a size without one.

The write is conditional on the cart still having lines and on the visitor
being under five inquiries an hour, both asked inside the statement, so two
forms at once store one inquiry and a refused one leaves the cart alone.

The seller is told by email, answerable to the buyer, and reads inquiries in
a panel of the admin: to mark, export as CSV, or delete. The email
confirming to the buyer is built and off by default, because the form has no
challenge in front of it: Mallok has nowhere to draw one on a plugin's page.
The design gains the owner's decision and §18. The security notes say what
personal data an inquiry holds, what stands in place of a challenge, and what
that does not stop.
…ependent review found

The review found the guards inside the inquiry's write sound, and these
around them:

- A cart that changed between being read and being written lost the change:
  a part added in between was in neither the inquiry nor the cart. The write
  is now conditional on the cart being exactly what is sent, and a cart that
  changed is answered with cart_changed and left alone.
- An address could carry a mailto link's own syntax, which the admin links
  as stored. It is held to a list of what an address may be made of.
- A browser let through what the server refused, and the form came back
  empty: a name of spaces, a tab in a name, a message whose line breaks the
  field counted once. The name has a pattern, white space in a one-line
  field becomes one space, a line break is counted once.
- A semicolon let a formula through the CSV export where it is the list
  separator. Every piece that could begin a cell is made text.
- A request with no mark of its visitor was not limited. They are counted
  together, as Mallok's own rate limit counts them.
- A cart refused for the limit inside the write could be told it was sent.
- The export scanned every line for every inquiry.

And the tests of the two races proved less than they claimed: Promise.all
interleaves two requests about half the time here, and the other half never
reaches the guard. They now hold both requests at the write (holdWrites),
and each guard, broken, is red every time.
An email handed to Mallok keeps what a buyer typed in Mallok's own queue,
where deleting the inquiry does not reach it. The security notes, the
guidance file and the design said otherwise; they say what is true now, with
what an operator has to do about it. The design gains what the independent
review of the inquiry cart found and what changed.
An independent review showed that two requests sent with Promise.all
interleave only about half the time. The nine older tests of a race —
payments, refunds, cancellations — call functions side by side instead, and
a probe found those overlap every time: both readings before either write,
in twenty-five rounds of twenty-five.

They now run through atTheSameMoment, which gives each call a database that
notes when it is used and fails unless every call had read before any wrote.
A change that put a wait in front of a reading would have turned such a race
into a sequence and left its test passing; now it fails. The helper has
tests of its own, for the case it must refuse.

With everything that stops its second write taken away, each of the nine was
red eight times in eight.
A site export is how an operator leaves with what is theirs, and the shop
handed Mallok nothing for it: variants, prices, the stock ledger, orders and
inquiries were in no backup.

exportFiles now returns a JSON file for each, under shop/, with a manifest:
a row an object, as stored, so that an amount stays whole minor units and
nothing a buyer typed is rewritten. A variant carries its product's slug as
well as its translation group, which only this database knows. Carts, rates
and the scheduler's state stay behind, and of an inquiry the cart it came
from and the mark of who sent it.

Tables are read with *, and a test fails when a new p_shop_ table is neither
exported nor named among those left behind. A table past the row limit fails
the export by name rather than be cut short: Mallok then says there is no
backup, where a file missing rows would have passed for one.
…t do

The design gains §19. The security notes say that an export holds orders and
inquiries as stored, and that a shop can outgrow an export built in one
request.
…o race

A review showed that atTheSameMoment checked the first trip of every call
before the second of any, which is 'every reading before any write' only
for a function that reads once. It is told the number of readings now.

Two calls that never write, one after the other, look the same as two side
by side: that is refused as nothing to race for. And a call that throws
before its first wait no longer leaves the others unwaited for.
…ects a review found

- A price whose variant is gone was left out, and the manifest's count with
  it: the prices were read through a join. Every price is read now, and the
  column the export adds is named variant_sku, which no table's column is.
- Twenty thousand rows a table came, by the review's measure, to more than
  a Worker's memory, so the limit would never have named a table. It is
  five thousand rows now, and sixteen megabytes of text for the files
  together.
- Mallok's command line and admin show that the shop failed and not why,
  so the reason is logged as well.
- The shop's settings were in no export. They are in shop/settings.json.
- A line separator inside a value broke 'a row to a line' for a reader that
  splits lines the way Unicode does. It is written as an escape.

And four wrong exports passed every test: one dropping columns of an
inquiry, one dropping columns of a price, one with no order to its rows,
one holding two tables to the limit. The tests compare every table whole,
from rows put in the other way round, hold each of the nine to the limit,
and list every column of every exported table, so that a new one is looked
at before it leaves the shop.
… showed wrong

Removing the status condition does turn a race red where two different
changes of an order meet; the design said it turned none. The security
notes say what an export holds in full, and not to switch the shop off to
get a backup when its export fails.
The owner decided what a quantity must be: at least the minimum order, at
most what a line may hold, and not a multiple of the minimum (design §11,
decision 12). The server never asked for a multiple. The quantity fields
did, by stepping in units of the minimum, so a browser refused what the
server would have taken — and with a ceiling of 500 a part with a minimum
of 200 could be ordered as 200 or 400 and nothing else. They step by one.

The design also records the owner's other decision of the day: a derived
price goes on being rounded up to the next .99 (decision 11).
@JasonYv
JasonYv changed the base branch from feature/shop-export to main October 18, 2026 09:28
@JasonYv
JasonYv merged commit 4ea7e51 into main Oct 18, 2026
1 check passed
@JasonYv
JasonYv deleted the feature/quantity-any-above-minimum branch October 18, 2026 09:30
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant