feat(F-069): completed feature

This commit is contained in:
chattie
2026-08-19 18:09:23 +02:00
parent ba2ac939c7
commit ddcf2e0c28
14 changed files with 437 additions and 50 deletions

View File

@@ -0,0 +1,124 @@
# 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)
```

View File

@@ -0,0 +1,13 @@
{
"feature_id": "F-069",
"agent": "leader",
"verdict": "APPROVED",
"summary": "All gates approved. Closing F-069.",
"evidence": [
"work/artifacts/F-069/reviewer.json verdict=APPROVED",
"work/artifacts/F-069/security.json verdict=APPROVED",
"work/artifacts/F-069/qa.json verdict=APPROVED",
"./scripts/verify.sh exit 0"
],
"timestamp": "2026-08-19T15:55:00Z"
}

View File

@@ -0,0 +1,15 @@
{
"feature_id": "F-069",
"agent": "qa",
"verdict": "APPROVED",
"summary": "End-to-end trace. Description column exists, admin can edit it, public endpoint returns it, storefront checkout fetches and renders it under each method.",
"evidence": [
"AC1 'Shipping methods table has a description column' — migration applied; SELECT shipping_methods returns the column",
"AC2 'Admin can edit description from /shipping' — MethodForm has a description input; PATCH /api/admin/shipping/methods/:id persists it; admin GET returns the value",
"AC3 'Storefront /checkout fetches methods and shows the description' — CheckoutClient fetches /api/shipping/methods on mount and renders opt.description in each radio card",
"AC4 'Public endpoint returns methods with description' — GET /shipping/methods?country=ES returns two methods with their descriptions populated",
"AC5 'verify.sh is green' — exit 0",
"Regression: backend tests 124 passed, 56 skipped; typecheck green across frontend / admin / backend; services restart 200"
],
"timestamp": "2026-08-19T15:55:00Z"
}

View File

@@ -0,0 +1,21 @@
{
"feature_id": "F-069",
"agent": "reviewer",
"verdict": "APPROVED",
"summary": "Shipping methods now carry an editable description end-to-end. Migration adds the column; backend list/patch schemas and the new public /shipping/methods endpoint read it; admin MethodForm exposes the field; storefront checkout fetches the catalogue and renders the description per method. Hardcoded array of two methods is gone.",
"evidence": [
"git diff project/migrations/029_shipping_method_description.js — new migration adding description text",
"git diff project/src/modules/shipping/api/shipping.routes.ts — methodBodySchema, POST insert, GET select, PATCH schema, new public GET /shipping/methods all include description",
"git diff project/apps/admin/src/lib/api-client.ts — ShippingMethod.description + payload types",
"git diff project/apps/admin/src/app/(dashboard)/shipping/page.tsx — MethodRow shows description, MethodForm has description input",
"git diff project/frontend/src/app/api/shipping/methods/route.ts — new proxy",
"git diff project/frontend/src/components/checkout/CheckoutClient.tsx — useEffect fetches methods, renders dynamic radio list with description",
"Migration applied: ALTER TABLE shipping_methods ADD COLUMN description text",
"curl /shipping/methods returns methods with description after PATCH",
"curl /api/admin/shipping/methods shows description on both rows",
"npm test (backend) — 124 passed, 56 skipped",
"npx tsc --noEmit (frontend / admin) — exit 0",
"./scripts/verify.sh — exit 0"
],
"timestamp": "2026-08-19T15:55:00Z"
}

View File

@@ -0,0 +1,14 @@
{
"feature_id": "F-069",
"agent": "security",
"verdict": "APPROVED",
"summary": "The description is plain text, bounded by max 500 chars in the zod schema. The admin write path is unchanged: same admin role gate as the existing method endpoints. The new public GET is read-only and filters by active=true on both zone and method.",
"evidence": [
"PATCH /api/admin/shipping/methods/:id and POST /api/admin/shipping/methods both require admin role (unchanged)",
"Public GET /shipping/methods filters sm.active = true AND sz.active = true at SQL level",
"description is z.string().max(500) — bounded, plain text",
"No new env vars, no new dependencies, no new auth surface",
"Frontend proxy is GET-only"
],
"timestamp": "2026-08-19T15:55:00Z"
}