Filter by Sold / Not Sold / All, on the storefront and the admin inventory #105

Closed
opened 2026-08-21 16:55:04 -05:00 by bermudalamb · 1 comment
Owner

A three-way filter — Sold, Not Sold (default), All — on the customer storefront, and the same filtering on Admin → Inventory.

"Not Sold" is not a status filter, and that is the crux

The existing status filter in itemFilters.ts is single-value equality: i.status = $n. It takes one of pending, available, reserved, sold.

Not Sold is not one status. It is available or reserved on the storefront, and available or reserved or pending in the admin. So this cannot be delivered by exposing the control that already exists — it needs either a new filter dimension or a generalisation of the one there.

Two ways to do it, and the choice should be deliberate:

Generalise status to accept several values. ?status=available,reserved instead of one. One concept instead of two, the admin's existing single-select keeps working unchanged as a list of one, and buildItemFilterSql swaps = $n for = ANY($n). The three-way control becomes a preset over that.

Add a separate sold dimension. ?sold=yes|no|all alongside the existing status filter. Simpler to reason about on its own, but the storefront then has two controls that overlap and can contradict each other — ?status=sold&sold=no has to mean something.

The first looks better, but it touches the shared parser used by both routes, so it is worth agreeing before anyone starts.

It changes what the storefront shows by default

Worth being explicit, because this is a product decision rather than a technical one.

Today the storefront lists sold items and ItemCard gives them a black SOLD ribbon (Badge.Ribbon). That is deliberate: on a catalogue where every piece is one of a kind, visible sold items are evidence the shop sells things, and they carry the "this one is gone forever" story the rest of the copy leans on.

Defaulting to Not Sold removes that from the default view. That may well be what you want — a shopping view full of things nobody can buy is its own problem — but it is a change in character, not just a new control, and it should be chosen rather than arrived at.

"All" means different things in the two places

On the storefront, All can only ever mean available + reserved + sold. Pending items are excluded from every public read unconditionally (#90), and that must not be weakened by a filter that says "All" — a label promising more than it delivers is the shape this codebase keeps designing against.

In the admin, All genuinely means all four including pending.

Same word, two meanings. Either the storefront's label should say something narrower, or the difference gets documented where both are defined.

The admin already has a status control

InventoryFilters.tsx has a status dropdown offering Pending, Available, Reserved and Sold. Adding a second three-way control beside it gives two overlapping ways to express the same thing. Worth deciding whether the new control replaces that dropdown, sits beside it as a preset, or whether the admin simply keeps what it has and only the storefront gains the new one.

URL round-tripping

Storefront filter state lives in the URL, and filtersFromSearchParams treats an absent parameter as "no filter". With Not Sold as the default, absence has to mean not sold rather than everything — which means every currently-shared storefront link quietly changes meaning after this ships. Not a reason to avoid it, but the default belongs in one place rather than being implied by omission in several.

Note also that the same parser deliberately refuses pending for the storefront, with a comment saying so. Whatever shape this takes must not accidentally reopen that.

Verification

Integration tests on the parser and the SQL: not-sold excludes sold, sold excludes the rest, all includes everything the caller is allowed to see, and the storefront still cannot reach pending under any combination. End-to-end that the default view hides sold items and that switching to All brings them back with their ribbon.

Severity

Low — a feature, nothing broken. The parts worth care are the default changing what customers see, and "All" not being allowed to mean all on the public side.

A three-way filter — **Sold**, **Not Sold** (default), **All** — on the customer storefront, and the same filtering on Admin → Inventory. ## "Not Sold" is not a status filter, and that is the crux The existing `status` filter in `itemFilters.ts` is single-value equality: `i.status = $n`. It takes one of `pending`, `available`, `reserved`, `sold`. **Not Sold is not one status.** It is *available or reserved* on the storefront, and *available or reserved or pending* in the admin. So this cannot be delivered by exposing the control that already exists — it needs either a new filter dimension or a generalisation of the one there. Two ways to do it, and the choice should be deliberate: **Generalise `status` to accept several values.** `?status=available,reserved` instead of one. One concept instead of two, the admin's existing single-select keeps working unchanged as a list of one, and `buildItemFilterSql` swaps `= $n` for `= ANY($n)`. The three-way control becomes a preset over that. **Add a separate `sold` dimension.** `?sold=yes|no|all` alongside the existing status filter. Simpler to reason about on its own, but the storefront then has two controls that overlap and can contradict each other — `?status=sold&sold=no` has to mean something. The first looks better, but it touches the shared parser used by both routes, so it is worth agreeing before anyone starts. ## It changes what the storefront shows by default Worth being explicit, because this is a product decision rather than a technical one. Today the storefront lists sold items and `ItemCard` gives them a black **SOLD** ribbon (`Badge.Ribbon`). That is deliberate: on a catalogue where every piece is one of a kind, visible sold items are evidence the shop sells things, and they carry the "this one is gone forever" story the rest of the copy leans on. Defaulting to **Not Sold** removes that from the default view. That may well be what you want — a shopping view full of things nobody can buy is its own problem — but it is a change in character, not just a new control, and it should be chosen rather than arrived at. ## "All" means different things in the two places On the storefront, **All** can only ever mean available + reserved + sold. Pending items are excluded from every public read unconditionally (#90), and that must not be weakened by a filter that says "All" — a label promising more than it delivers is the shape this codebase keeps designing against. In the admin, **All** genuinely means all four including pending. Same word, two meanings. Either the storefront's label should say something narrower, or the difference gets documented where both are defined. ## The admin already has a status control `InventoryFilters.tsx` has a status dropdown offering Pending, Available, Reserved and Sold. Adding a second three-way control beside it gives two overlapping ways to express the same thing. Worth deciding whether the new control **replaces** that dropdown, sits beside it as a preset, or whether the admin simply keeps what it has and only the storefront gains the new one. ## URL round-tripping Storefront filter state lives in the URL, and `filtersFromSearchParams` treats an absent parameter as "no filter". With **Not Sold** as the default, absence has to mean *not sold* rather than *everything* — which means every currently-shared storefront link quietly changes meaning after this ships. Not a reason to avoid it, but the default belongs in one place rather than being implied by omission in several. Note also that the same parser deliberately refuses `pending` for the storefront, with a comment saying so. Whatever shape this takes must not accidentally reopen that. ## Verification Integration tests on the parser and the SQL: not-sold excludes sold, sold excludes the rest, all includes everything the caller is allowed to see, and the storefront still cannot reach pending under any combination. End-to-end that the default view hides sold items and that switching to All brings them back with their ribbon. ## Severity Low — a feature, nothing broken. The parts worth care are the default changing what customers see, and "All" not being allowed to mean all on the public side.
bermudalamb self-assigned this 2026-08-21 17:50:35 -05:00
bermudalamb added this to the Make the password-reset email editable from Admin project 2026-08-21 17:50:45 -05:00
Author
Owner

Three decisions taken 2026-08-22, answering the questions this issue left open.

Filter shape: generalise status to multi-value. ?status=available,reserved rather than a second sold dimension. buildItemFilterSql swaps = $n for = ANY($n), the admin's existing single-select keeps working as a list of one, and the three-way control becomes a preset over it. One dimension means there is no way to write a contradiction like ?status=sold&sold=no.

Storefront default: Not Sold. A shopping view stops being full of things nobody can buy. Accepted with its two costs, both named in the issue above: the black SOLD ribbons leave the default view on a catalogue where they were evidence the shop sells things, and every storefront link shared so far quietly changes meaning once the default is no longer "everything". The default will live in one named place rather than being implied by the absence of a parameter.

Admin: the three-way control replaces the status dropdown. One control instead of two overlapping ones.

The cost of that third decision is worth writing down rather than discovering later. The dropdown can currently isolate a single status, and the three-way control cannot: after this, there is no way to view only Reserved, or only Pending. The pending workflow from #90 is the one most likely to want that, since finding items awaiting publication is a real admin task and "Not Sold" will fold pending in with available and reserved. If that turns out to matter, the fix is to put single-status isolation back alongside the preset rather than to undo the preset, and it should be its own issue at that point.

All still means different things in the two places, exactly as this issue set out: available + reserved + sold on the storefront, all four including pending in the admin. Pending stays unreachable from every public read regardless of filter, and the tests will assert that directly rather than trusting the parser to keep refusing it.

Three decisions taken 2026-08-22, answering the questions this issue left open. **Filter shape: generalise `status` to multi-value.** `?status=available,reserved` rather than a second `sold` dimension. `buildItemFilterSql` swaps `= $n` for `= ANY($n)`, the admin's existing single-select keeps working as a list of one, and the three-way control becomes a preset over it. One dimension means there is no way to write a contradiction like `?status=sold&sold=no`. **Storefront default: Not Sold.** A shopping view stops being full of things nobody can buy. Accepted with its two costs, both named in the issue above: the black SOLD ribbons leave the default view on a catalogue where they were evidence the shop sells things, and every storefront link shared so far quietly changes meaning once the default is no longer "everything". The default will live in one named place rather than being implied by the absence of a parameter. **Admin: the three-way control replaces the status dropdown.** One control instead of two overlapping ones. The cost of that third decision is worth writing down rather than discovering later. The dropdown can currently isolate a single status, and the three-way control cannot: after this, there is no way to view only Reserved, or only Pending. The pending workflow from #90 is the one most likely to want that, since finding items awaiting publication is a real admin task and "Not Sold" will fold pending in with available and reserved. If that turns out to matter, the fix is to put single-status isolation back alongside the preset rather than to undo the preset, and it should be its own issue at that point. `All` still means different things in the two places, exactly as this issue set out: available + reserved + sold on the storefront, all four including pending in the admin. Pending stays unreachable from every public read regardless of filter, and the tests will assert that directly rather than trusting the parser to keep refusing it.
bermudalamb added reference feature/105-sold-filter 2026-08-22 12:56:09 -05:00
bermudalamb moved this to In Progress in Make the password-reset email editable from Admin on 2026-08-22 12:57:09 -05:00
bermudalamb moved this to Review in Make the password-reset email editable from Admin on 2026-08-22 13:07:02 -05:00
bermudalamb moved this to Ready for Release in Make the password-reset email editable from Admin on 2026-08-23 07:22:52 -05:00
Sign in to join this conversation.
No labels
1 Participants
Notifications
Due Date
No due date set.
Dependencies

No dependencies set.

Reference: bermudalamb/redefined-designs#105