Slice 8 — Trade-In & Consignment
Used bikes coming into the shop, either as trade-in (immediate ownership transfer for credit) or consignment (shop sells on behalf of original owner, takes commission).
Status: Intake UI built; no persistence. The Trade-in intake surface (search / customer-attach / bikes-on-file ledger / build-bike modal / intake form) shipped in the monolith over v0.6.69 → v0.6.77 and reached a functional read + form state, but the write path was never wired — Save is a placeholder that console.logs the intake shape and shows a "Save endpoint ships next slice" confirm. #ti-approve-btn and #ti-reject-btn are in the markup and get enabled/disabled by updateActionState, but no click handlers are bound to them — dead controls. No trade-in has ever been persisted through this surface in the monolith. Consignment schema is drafted; nothing is wired.
An earlier version of this slice claimed the trade-in workflow was "live and shipping at Swicked" and listed a set of REST endpoints (GET/POST/PUT /api/trades…) that do not exist. Verified against Swicked-Cycles/public/index.html: no /api/trades route was ever added; the reads point at /api/tradein/listed and /api/tradein/search; the Save handler is stubbed at line 29932 (comment reads "wire to /api/tradein once endpoint lands"); Approve / Reject have no click handlers. The .till-left / .till-right chassis, the customer-attach flow, the bikes-on-file ledger, the build-bike modal, and the read endpoints are real; the persistence layer is not. Corrected here rather than deleted so the failure mode is on record.
Scope
trade_ins,consignments(separate tables; different lifecycles)- Intake form (photos, condition assessment, asking price, owner info)
- Valuation aid (similar bikes sold recently)
- Listing on the public site
- Sale flow (creates a
transactions) - Consignment payout (separate transaction back to original owner)
Schema
trade_ins— owner is the customer being credited; bike becomes shop inventory immediatelyconsignments— owner retains title; shop holds and sells; commission applied at sale
Both reference customer_bikes for the bike record.
Endpoints — as-planned vs as-shipped
As-planned (v0.1 draft — none of these were built):
GET /api/trades·POST /api/trades·PUT /api/trades/:id·POST /api/trades/:id/list·POST /api/trades/:id/sellPOST /api/consignments·POST /api/consignments/:id/sell·POST /api/consignments/:id/payout
None of the eight ever landed in code. Kept here as the intake spec for the eventual rebuild.
As-shipped (monolith reads only):
| Endpoint | What it does | Used by |
|---|---|---|
GET /api/tradein/listed?limit= | Reads the currently-listed trade-ins for the ledger's 'listed' mode | Trades tab first-load |
GET /api/tradein/search?q=&limit= | Free-text search over historic trade-in evaluations | The tab's search bar |
GET /api/tradein/by-customer/:id | Historic trade-ins for one customer | Customer-history flip on the ledger |
GET /api/bikes/brands?q= | DISTINCT over customer_bikes.brand for the build-bike modal's datalist | build-bike modal |
GET /api/bikes/models?brand= | DISTINCT models for the picked brand | build-bike modal |
POST /api/customers/:id/bikes | Adds a bike to the customer's file (existing Customers endpoint, reused) | build-bike modal's Save |
No POST /api/tradein (or /api/trades) exists. The Save button is stubbed — see the Status block at the top of this page.
Four tables that have always been empty. The monolith's 008_tradein_consignment.sql schema defined trade_in_evaluations, consignor_terms, consignment_listings, and consignor_payouts. Nothing anywhere — app, converter, or any SQL script — has EVER inserted into any of them. The monolith's Trades ledger has therefore always rendered its empty-state; there is no live data to migrate.
kvick-bike Trades module — deliberate mock-up (mig 051)
The kvick-bike Trades module is a mock-up scaffold. It renders the Search / Ledger / Till anatomy against a fixture and has zero write path — but it does so deliberately and says so on screen, which is the important distinction from the monolith.
The distinction — both persist nothing, but they fail differently:
| Codebase | Persistence | Failure mode |
|---|---|---|
Monolith (Swicked-Cycles/public/index.html) | None (Save stubbed, Approve/Reject unwired) | Silent — indistinguishable from working until you go looking for the record that was never written |
kvick-bike (src/modules/trades/) | None (fixture-only, no INSERT anywhere) | Deliberate + self-declared — a "Mock-up · sample data" badge in the header (view.ts:63), and every action button routes through mockAction() which paints an amber strip reading "…is not wired yet. This screen is a layout mock-up — nothing is saved and no record is created." (client.ts:513–514) |
The kvick-bike module's substrate proves the design intent:
migrations/051_trades.sql— MOCK-UP SCAFFOLD. Registersscreen.tradespermission only; no tables. The four monolith tables (trade_in_evaluations,consignor_terms,consignment_listings,consignor_payouts) are deferred until the write flows are specified and built. Creating them now would recreate the monolith's four empty tables nothing reads state. Grantsscreen.tradesto five roles.src/modules/trades/api.ts— contains zeroINSERTstatements. Every route answers from a fixture.client.ts—mockAction()is wired totr-save,tr-approve,tr-reject(client.ts:507–509) so no click can silently fake a write.view.ts:63— Mock-up badge,tr-mockclass,titleexplaining the fixture rows.
Ten Trades entries remain correctly is_programmed=0 — the Codex hasn't marked any as built because nothing is.
Rebuild note. When the write flows are specified and the real Trades module ships in kvick-bike, mockAction() and the Mock-up badge come out; the four table names above land in a fresh migration; api.ts gets real INSERTs. Until then, the mock-up is the reference for screen shape only.
Trades tab — built end-to-end (v0.6.69 → v0.6.77)
Trades is now a live working surface that mirrors the cross-tab .till-left / .till-right shape. Status changes from earlier draft: the "Bike Index lookup" path was dropped (Bike Index is a stolen-bike registry, not an OEM catalog; can't deliver spec data and the registry records were noisy). Replaced with a shop-grown brand/model catalog built from the customer-bike intake stream, plus a build-bike modal that adds new brand/model values to the catalog on the fly.
Architecture changes from v0.1 draft
- Removed —
/api/tradein/catalogendpoint (Bike Index proxy), 24 h cache, search-bar mode toggle (Historic ↔ Bike Index), catalog rendering branch in the result dropdown, "Look up" chip strip (Google / Pinkbike / Bike Index source links). - Added —
/api/bikes/brandsand/api/bikes/models?brand=Xendpoints (DISTINCT queries over the livecustomer_bikesrows; alphabetical; the shop's catalog grows organically as bikes get registered). - Added —
#build-bike-modalbody-level modal, exposed aswindow.helmOpenBuildBike({customerId, customerName, onSaved}). Brand + model comboboxes backed by<datalist>from the two endpoints; free-text typing in either field adds that value to the shop's catalog the next time the dropdowns load. Year, frame size, colour, serial, notes round out the form. Save POSTs/api/customers/:id/bikes(existing endpoint);onSavedcallback fires with the new bike so callers can prefill their till + refresh their bikes-on-file list. Same modal is reused from Service drop-off intake (see slice 4 → Bike field is a picker).
Workflow today
Trades tab follows the unified .till-left / .till-right two-zone pattern (Sales, Service, Products, Customers all share it). The Trades surface is the fifth working tab to carry the same anatomy.
Workflow A — search by historic trade-in
- Operator types in the search bar at the top of the left zone → searches historic
trade_in_evaluationsonly (multi-source historic + customers per v0.6.71). - Click a result → past trade-in detail opens in the till; operator can re-list, adjust pricing, or use it as a comparable for a new intake.
Workflow B — customer-first (the canonical path for new intakes; v0.6.77)
- Operator clicks
+ Customerin the till header → standard customer picker (shared with Sales / Service). - Customer attaches → the left ledger flips to "
<Customer>— Bikes on file" listing every bike the shop knows the customer owns (fromcustomer_bikes). Columns: Bike · Size · Colour · Serial. setLedgerTableMode('bikes' | 'listed')swaps the ledger thead between the two views; the till body itself stays purely the intake form (no bike-picker INSIDE the till — that was the v0.6.69 → v0.6.77 simplification).- Click a listed bike →
prefillBike(id)fires → till intake fields (brand, model, year, frame size, colour, serial) pre-populate with the picked bike's identity. Operator fills in condition notes, asking price, photos, disposition (Trade-in vs Consignment), commission split (Consignment). - Or click
+ Build new bike(button in the ledger header, visible only when a customer is attached) → opens the build-bike modal → save adds the bike tocustomer_bikesAND prefills the till in one motion. - Save — stubbed: the handler logs the intake payload to the console and shows a "Save endpoint ships next slice" confirm; nothing persists. Approve (
#ti-approve-btn) and Reject (#ti-reject-btn) are in the markup with no click handlers bound at all —updateActionStatedutifully enables + disables them but they don't do anything when clicked.
Workflow C — walk-in / unknown bike
+ New customer in the till footer → minimal customer-create → bike auto-attaches via build-bike modal → continue as Workflow B.
Detach (× on the customer pill, or load a different past trade) → ledger flips back to 'listed' mode and reloads the currently-listed trades.
Endpoints live today
See the Endpoints — as-planned vs as-shipped table above. The five live routes are all reads or reuses of the existing /api/customers/:id/bikes write path from the Customers module. No /api/tradein write path exists; no /api/consignments routes exist. The disposition picker (Trade-in vs Consignment) is a UI radio the operator can set, but neither branch persists.
Contentious: the v0.6.386 Trade-in Edit Modal added a small later arc that retired an earlier contenteditable chain on the intake form — the arc between v0.6.77 and v0.6.386 mostly refined the intake UI without landing persistence.
Removed: per-screen color themes / 🎨 picker (v0.6.79 → v0.6.82)
A brief sub-arc: v0.6.79 added per-screen accent themes + a paint-can picker in Adjust mode (operators could colour their own tabs). v0.6.80 reverted that — too noisy. v0.6.81 / v0.6.82 settled on ROYGBIV accents in nav order (Owner red → Sales orange → Service yellow → Products green → Customers blue → eComm indigo → Trades violet) on the page header line + active tab — same idea, narrower scope, not operator-customisable. The Service tab's yellow ROYGBIV slot is also what the "Service" parked-sale badge uses on the Sales ledger (see slice 5).
Status now
- Trade-in intake UI — built and reachable; the operator can walk the whole flow up to Save
- Trade-in persistence — not built. Save is stubbed; nothing writes to a
trade_instable. Approve / Reject are dead controls - Build-bike modal — live; the catalog grows as operators use it (Customers-side endpoint, reused)
- Bikes-on-file ledger view — live (reads
customer_bikes) - Historic trade-in search — live (
/api/tradein/search+/api/tradein/listed) - Consignment workflow — schema drafted; intake + sale + commission + payout not built
- Public-site listing flow — not built (depends on the public-site Worker)
Rebuild note for the kvick-bike port. When the Trades module ships in kvick-bike, the intake UI is the design reference — but the write path, the trade_ins + consignments tables, and the Approve / Reject handlers all need to be built for the first time, not ported. The monolith's intake form is where the operator experience settled; the shape of the eventual POST /api/tradein payload can be read straight off the console.log('[trade-in intake]', payload) line at Swicked-Cycles/public/index.html:29940.
What's not yet built
Acceptance criteria
- Operator can intake a trade with photos, valuation, owner credit
- Consignment intake records owner contact + percentage agreement
- Listings flow to the public site (slice that depends on the public-site Worker)
- Sale flow handles both trade and consignment correctly (consignment computes commission)
- Owner payout for consignment is a tracked transaction, not just a manual cheque