Skip to main content

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.

Doc corrected 2026-07-19 (KVICK.BIKE.Swicked_Trades code session verified against source)

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 immediately
  • consignments — 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/sell
  • POST /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):

EndpointWhat it doesUsed by
GET /api/tradein/listed?limit=Reads the currently-listed trade-ins for the ledger's 'listed' modeTrades tab first-load
GET /api/tradein/search?q=&limit=Free-text search over historic trade-in evaluationsThe tab's search bar
GET /api/tradein/by-customer/:idHistoric trade-ins for one customerCustomer-history flip on the ledger
GET /api/bikes/brands?q=DISTINCT over customer_bikes.brand for the build-bike modal's datalistbuild-bike modal
GET /api/bikes/models?brand=DISTINCT models for the picked brandbuild-bike modal
POST /api/customers/:id/bikesAdds 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:

CodebasePersistenceFailure 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. Registers screen.trades permission 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. Grants screen.trades to five roles.
  • src/modules/trades/api.tscontains zero INSERT statements. Every route answers from a fixture.
  • client.tsmockAction() is wired to tr-save, tr-approve, tr-reject (client.ts:507–509) so no click can silently fake a write.
  • view.ts:63 — Mock-up badge, tr-mock class, title explaining 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/catalog endpoint (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/brands and /api/bikes/models?brand=X endpoints (DISTINCT queries over the live customer_bikes rows; alphabetical; the shop's catalog grows organically as bikes get registered).
  • Added#build-bike-modal body-level modal, exposed as window.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); onSaved callback 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

  1. Operator types in the search bar at the top of the left zone → searches historic trade_in_evaluations only (multi-source historic + customers per v0.6.71).
  2. 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)

  1. Operator clicks + Customer in the till header → standard customer picker (shared with Sales / Service).
  2. Customer attaches → the left ledger flips to "<Customer> — Bikes on file" listing every bike the shop knows the customer owns (from customer_bikes). Columns: Bike · Size · Colour · Serial.
  3. 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).
  4. Click a listed bikeprefillBike(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).
  5. 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 to customer_bikes AND prefills the till in one motion.
  6. 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 — updateActionState dutifully 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 persistencenot built. Save is stubbed; nothing writes to a trade_ins table. 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

See also