# F-069 — Implementer evidence ## Scope delivered Shipping method names ("Estandar", "Express 24h") and prices lived in `shipping_methods`, but the descriptive text the storefront checkout shows under each method was hardcoded in `CheckoutClient.tsx`. Admin edits to the name and price did update the catalog, but the marketing description ("Entrega 3-5 días laborables") could not be changed without a code deploy. This fix makes the description a first-class field on `shipping_methods` and edits it from the admin. ## Changes ### Migration `project/migrations/029_shipping_method_description.js` (new) ```js ALTER TABLE shipping_methods ADD COLUMN IF NOT EXISTS description text; ``` Applied via `node-pg-migrate up`. ### Backend `project/src/modules/shipping/api/shipping.routes.ts` - `methodBodySchema` accepts an optional `description` string (max 500 chars). - `POST /shipping/methods` writes the new column. - `GET /admin/shipping/methods` returns `description` per row. - `PATCH /admin/shipping/methods/:id` accepts a `description` patch. - New public `GET /shipping/methods?country=…&postalCode=…` joins `shipping_zones` (active, country + postal prefix match) and `shipping_methods` (active) and returns the catalogue the storefront checkout needs. The response includes `description` for each method. ### Admin UI `project/apps/admin/src/lib/api-client.ts` - `ShippingMethod` type gains `description: string | null`. - `createMethod` / `updateMethod` payloads accept `description`. `project/apps/admin/src/app/(dashboard)/shipping/page.tsx` - `MethodRow` shows the truncated description under the method name. - `MethodForm` gains a description input below the name; both create and update paths persist it. ### Storefront proxy `project/frontend/src/app/api/shipping/methods/route.ts` (new) Thin GET proxy that forwards query params to the backend `/shipping/methods`. ### Checkout `project/frontend/src/components/checkout/CheckoutClient.tsx` - New `ShippingMethod` interface. - New `useEffect` that fetches `/api/shipping/methods` on mount and defaults the selection to the first method. - The "Método de envío" radio list now renders one card per real method from the API: name, optional description, optional "Envío gratis aplicado" badge when `freeShippingThresholdCents` is met, and the formatted price. The hardcoded `[{id:'standard',…},{id:'express',…}]` array is gone. - The summary panel and the order payload both source the cost from the selected method (`selectedShippingMethod?.baseCostCents ?? 0`) instead of a hardcoded `899 / 499`. ## Acceptance traceability | Acceptance criterion | How it is met | | -------------------- | ------------- | | Shipping methods table has a description column | Migration 029 added `shipping_methods.description text`. | | Admin can edit description from `/shipping` | `MethodForm` has a description input; `shippingApi.createMethod` / `updateMethod` payloads include `description`; admin GET returns the field; PATCH writes it. | | Storefront `/checkout` fetches methods and shows the description under each one | `CheckoutClient` calls `fetch('/api/shipping/methods')` on mount; each radio card renders `opt.description`. | | Public endpoint returns methods with description | `GET /shipping/methods?country=ES` returns two methods, each with the description we just wrote. Verified end-to-end with curl. | | `verify.sh` is green | Exit 0. | ## Manual verification ``` # Public catalogue after the admin edits the descriptions $ curl 'http://192.168.18.93:3000/shipping/methods?country=ES' { "items": [ { "id": "c57cf1bc-…", "name": "Estandar", "baseCostCents": 599, "description": "Entrega 3-5 días laborables", "freeShippingThresholdCents": 5900 }, { "id": "1d3979fb-…", "name": "Express 24h", "baseCostCents": 999, "description": "Entrega al día siguiente" } ] } # Admin list reflects the same $ curl 'http://192.168.18.93:3004/api/admin/shipping/methods' -b /tmp/admin_cookies.txt [ same shape, both with description populated ] ``` ## Build verification - `npm run typecheck` (backend) — exit 0 - `npm run build` (backend) — exit 0 - `npm test` (backend) — 124 passed, 56 skipped - `npx tsc --noEmit` (frontend / admin) — exit 0 - Migration `node-pg-migrate up` — applied - `monolith.sh prod restart backend admin frontend` → 200 on all - `./scripts/verify.sh` — exit 0 ## Files touched ``` project/migrations/029_shipping_method_description.js (new) project/src/modules/shipping/api/shipping.routes.ts (description field + public GET) project/apps/admin/src/lib/api-client.ts (ShippingMethod.description) project/apps/admin/src/app/(dashboard)/shipping/page.tsx (MethodRow + MethodForm description) project/frontend/src/app/api/shipping/methods/route.ts (new proxy) project/frontend/src/components/checkout/CheckoutClient.tsx (fetch + render real methods) ```