feat(store): overhaul into a shop that can also install — design tokens, catalog v2, cart with WhatsApp checkout #39
Loading…
Add table
Add a link
Reference in a new issue
No description provided.
Delete branch "store-overhaul"
Deleting a branch is permanent. Although the deleted branch may continue to exist for a short time before it actually gets removed, it CANNOT be undone in most cases. Continue?
Overhauls
apps/storefrom a workshop landing page with a product strip into a shop whose differentiator is that it can also install what it sells, ships parts nationwide from Situbondo, and takes special-part orders. No payment gateway, no accounts, no server — the site stays a static export.Closes #33, #34, #35, #36, #37, #38.
What landed
globals.css— spacing/colour/radius tokens, retail type scale, symmetric rhythm, denser grid, a1024pxbreakpoint for the previously unhandled 701–1120px rangepackages/catalogschema v2 —price,brand,partNumber,category,weightGrams,inStock,shippable,installService. Clean cutover, no v1 branchapps/managementproduces v2 — six inventory columns + migration,getPublicCatalogmapping,STOREFRONT_COLUMNS, validators, actions, MCP tools, stok UIapps/storeconsumes v2 — price-led cards,formatIdr,Product/OfferJSON-LD, category-derived filterslocalStoragecart + WhatsApp checkout, and/cara-pesanVerification
@garage/catalog10/10 ·garage51/51 ·garage-store15/15 · build 19 routes · store lint clean.Driven in a browser against the static export, not just unit-tested:
<picture>2270 → photos 3072. The workshop photo is out of the hero.oli→/products?q=oli, input pre-filled, grid filtered. Filters renderSemua · Kaki-kaki · Oli, derived from catalog data.Rp 450.000,Ready, brand + part number, resolved install link,ProductJSON-LD withOffer/IDR/InStock.wa.metext matched the spec exactly, including[pasang di tempat]on only the install line.Habis+.is-out, add button disabled, availability WhatsApp link instead, and noOfferemitted for a null price.Consequences to review deliberately
Orders now trigger store rebuilds.
sellPriceandstockjoinedSTOREFRONT_COLUMNS, because price and availability are on the public site now. Every stock-decrementing order queues a build; the workflow'scancel-in-progressgroup collapses bursts.buyPrice/avgCoststill do not trigger one.sellPrice === 0maps toprice: null("belum ditentukan"), matching the existing bot-tool convention. A genuinely free item cannot be expressed.categoryis free text, and homepage tiles are derived from the distinct values present, so an operator typo creates a stray tile.Bug caught only by a browser check
AddToCartinitially calledwhatsappUrl({kind:"product"})client-side. That resolvessiteUrl(), which reads the build-onlySITE_URLand throws in the browser; React's hydration retry loop then crashed the renderer on every out-of-stock product page — whilecurlserved the page fine, so tests and a build check both passed it. Fixed by computing the URL server-side and passingaskUrlas a prop.product-browser.tsxis a client component importing the same helper. It only useskind: "general"today, which is safe, but it is a live landmine.Not addressed
pnpm --filter garage lintstill fails on 35 pre-existing biome errors in untouchedcomponents/ui/*shadcn files. It was 37 before this branch. Deliberately left: auto-formatting 177 files would bury the real diff.Review actioned —
560ba64Two reviewers ran over this: a general quality pass and a read-only security pass. Both independently landed on the same producer/validator gap, which is the strongest signal in the whole review. Every finding is fixed.
P1 — stale availability (the one that mattered)
The branch documented and tested "stock changes rebuild the store", but
requestStoreDeploywas only ever called fromlib/inventory/actions.tsandlib/mcp/tools/inventory.ts. The paths that actually move stock —orders/service.ts:127,purchases/service.ts:49,114,160,stock-movements/service.ts:38— write the column directly inside a transaction and never triggered a deploy. Verified with a grep for callsites before fixing.So adding
"stock"toSTOREFRONT_COLUMNSbought nothing for the case it was added for: selling the last unit left the live site advertisingReadyindefinitely. The branch shipped the opposite of the tradeoff it documented.All ten transaction wrappers that move stock now fire after commit — five server actions and five MCP tools. Deliberately after the transaction, not inside it: a rollback should not queue a build, and a network call has no business holding a DB transaction open.
P1 — one bad row took the whole catalog down
category: "",sellPrice < 0, andweightGrams <= 0all pass the inbound validators and persist, then failPublicCatalogSchema.parsefor the entire payload.?? "Lainnya"only caughtnull. Result:/api/public/catalog500s,next buildfails, and the storefront can never be redeployed until someone finds the offending row — while the live site stays green, so the symptom surfaces in CI far from the cause.Fixed at the producer, the single boundary that already owns the contract, rather than by tightening four validator schemas:
weight_gramsandsell_priceinvalidators.tswere also tightened to match the contract — the reviewer noticed the MCP schema already had this right and the REST path was the laxer one, which is the opposite of the usual expectation.This is now a real regression test, not a decorative one. I stashed the fix and confirmed the new test fails — and that one bad row fails three tests, because the whole payload throws:
P2 — two client bugs, both browser-verified
/cartpainted "Keranjang masih kosong" on every visit. ThelocalStorageread is inuseEffect, which runs after paint, so a customer with items was told their cart was empty for a frame. Fixed with areadyflag off the module-levelloadedvalue, which keeps the hydration-match guarantee. Proved with aMutationObserverinstalled before navigation:emptyStateEverPainted: falsewith a full cart, and the empty state still renders for a genuinely empty one.Number("") === 0andsetQtytreats<= 0as removal, so emptying the field to retype destroyed the line. Guarded at the caller, since the−button is the deliberate path to zero. Verified: clear → both lines survive; retype3→ totalRp 1.445.000; and−on a qty-1 line still removes it.P3
Newline in a product name could forge rows in the admin's WhatsApp message (including a fake total) — collapsed whitespace in the name and destination, left the note alone since newlines are legitimate there. Two tests added. Also replaced the inert
avgCosttest case (not a column, so no bug could ever break it) withcreatedAt, fixed aneslint-disablecomment in a biome repo that was silently doing nothing, and corrected the cart badge'saria-labelto sayjenis barang, since it counts lines rather than units by design.Declined
The security pass flagged that a tampered
localStoragecart can show its owner a false price. Not fixed: writing that key requires already executing script on this origin, so the only victim is the tamperer, and nothing is authorised on the strength of the cart — the admin confirms price out of band, which the UI states. Rated informational by the reviewer too.Cleared by security, with evidence
Data exposure (
buyPrice/avgCost/supplier/raw stock cannot reach the payload — three stacked controls), thewa.medeep link (oneencodeURIComponentover the whole message; the phone number is a literal), XSS via the JSON-LD block (the</script>escape is global), and a re-audit for more instances of the build-only-env-in-client-code bug — none found.Final state
@garage/catalog10/10 ·garage52/52 ·garage-store17/17 · build 19 routes · store lint clean. Management biome errors 37 → 32 (all remaining are pre-existingcomponents/uishadcn debt, untouched on purpose).