Files
mercadodevida/work/artifacts/F-136/implementer.md
2026-08-21 21:10:29 +02:00

122 lines
5.3 KiB
Markdown

# F-136 — Implementer notes: Brand names Title Case + SEO title auto-fill
## Cambios
### `project/src/shared/text.ts` (nuevo)
Helper puro `toTitleCase(input: string): string`:
- Divide el string en palabras usando como separadores espacio, guión, punto, ampersand y slash.
- Para cada palabra: primera letra mayúscula, resto minúscula (con `toLocaleUpperCase('es-ES')` / `toLocaleLowerCase('es-ES')` para manejar acentos correctamente).
- Mantiene `y`, `e`, `o`, `u` en minúscula cuando NO son la primera palabra (tipografía española estándar).
- **Idempotente sobre mixed-case**: si la entrada no es ALL-CAPS, la devuelve sin tocar (así `BioSana` se queda como `BioSana`, y aplicar dos veces da el mismo resultado).
- Sin dependencias externas. Sin estado. Pura.
### `project/src/shared/tests/text.test.ts` (nuevo)
15 tests vitest que cubren:
- String vacío → vacío.
- Single word ALL CAPS (`BIOCOP``Biocop`).
- Multi-word con espacios (`EL GRANERO INTEGRAL``El Granero Integral`).
- Punto preservado (`A.VOGEL``A.Vogel`).
- Guion preservado (`DAS-BROT``Das-Brot`).
- Ampersand preservado (`TEA & INFUSIONS``Tea & Infusions`).
- Acentos españoles (`COMPLEMENTOS Y NUTRICIÓN``Complementos y Nutrición`).
- Idempotencia sobre Title Case ya aplicado.
- Mixed-case sin tocar (`BioSana``BioSana`).
- Dígitos intactos (`500 KILOS``500 Kilos`).
- Separadores múltiples consecutivos.
- String de solo separadores.
- Single-letter conjunctions lowercase cuando no son primera palabra.
- Single-letter conjunctions mayúscula cuando SÍ son primera palabra.
- Single-letter words no-conjunción (`A SIDE B``A Side B`).
Resultado: **15/15 passed**.
### `project/migrations/042_brand_title_case.js` (nuevo)
Migración de datos node-pg-migrate:
- Selecciona todas las filas de `brands_brands`.
- Para cada fila calcula `titleCased = toTitleCase(name)` y `newSeoTitle = (seo_title || titleCased)`.
- Solo ejecuta UPDATE si el nombre cambió o `seo_title` estaba vacío.
- No toca slugs (ya están en lowercase vía `slugify()`).
- Loggea cuántos rows actualizó de los totales.
Inline la lógica de `toTitleCase` (en vez de importar de `src/shared/text.ts`) para que la migración sea autocontenida y no requiera `npm run build` antes de correr. Los 15 unit tests cubren la lógica y detectarían cualquier drift entre el helper compartido y la versión inline.
## Resultado de la migración
```
F-136: updated 28 brand row(s) (of 33 total)
```
| Métrica | Antes | Después |
|---|---|---|
| Total brands | 33 | 33 |
| ALL-CAPS | 28 | **0** |
| sin `seo_title` | 28 | **0** |
| Slugs modificados | — | **0** (lowercase se preserva) |
Re-ejecución es no-op: `node-pg-migrate up` reporta `No migrations to run!` (porque la migración ya está en `pgmigrations`).
## Ejemplos antes/después
| Antes | Después |
|---|---|
| `A.VOGEL` | `A.Vogel` |
| `BIOCOP` | `Biocop` |
| `COMPLEMENTOS Y NUTRICIÓN` | `Complementos y Nutrición` |
| `EL GRANERO INTEGRAL` | `El Granero Integral` |
| `LA FINESTRA SUL CIELO` | `La Finestra Sul Cielo` |
| `DULCES LISSEN` | `Dulces Lissen` |
| `BioSana` (mixed) | `BioSana` (sin tocar) |
Todos los `seo_title` se backfillearon con el nuevo nombre en Title Case.
## Archivos
- `project/src/shared/text.ts` (nuevo)
- `project/src/shared/tests/text.test.ts` (nuevo)
- `project/migrations/042_brand_title_case.js` (nuevo)
- `pgmigrations` table: nueva fila `042_brand_title_case` con `run_on` timestamp
## Evidencia
```
$ npx tsc --noEmit
(exit 0)
$ npx eslint src/shared/text.ts src/shared/tests/text.test.ts migrations/042_brand_title_case.js
(exit 0)
$ npx vitest run src/shared/tests/text.test.ts
✓ src/shared/tests/text.test.ts (15 tests) 2ms
Test Files 1 passed (1)
Tests 15 passed (15)
$ node --env-file-if-exists=.env node_modules/node-pg-migrate/bin/node-pg-migrate.js up --migrations-dir migrations
F-136: updated 28 brand row(s) (of 33 total)
Migrations complete!
$ psql -c "SELECT name, seo_title FROM brands_brands ORDER BY name LIMIT 35;"
(muestra todas las marcas en Title Case con seo_title poblado)
$ node-pg-migrate.js up (re-run)
No migrations to run! ← idempotencia confirmada
```
## Sin cambios en frontend
El form de admin `brands/page.tsx` ya tenía `autoSeoTitle(name) → name` y `handleNameChange` que lo invoca cuando `!seoTitleManual`. Eso significa que las marcas NUEVAS ya reciben `seo_title = name` automáticamente. Para las marcas existentes (que era el problema real), la migración lo arregla en DB.
No fue necesario tocar código del admin.
## Notas
- El operador puede verificar el resultado con `psql -c "SELECT name, slug, seo_title FROM brands_brands ORDER BY name"` o navegando a `/brands` en el admin.
- La columna `slug` no se tocó porque `slugify()` lowercasea; los slugs `a-vogel`, `biocop`, etc. ya estaban correctos desde el import inicial.
- Si en el futuro el operador pide lo mismo para categorías, el mismo helper `toTitleCase` se puede reutilizar en una nueva migración (las traducciones al español ya están en `legacy-catalog.ts` desde F-116).
- La migración es destructiva en sentido figurado: pasa `A.VOGEL``A.Vogel`. No hay forma de rollback automático porque el original está perdido. Si el operador pide revertir, sería con un script ad-hoc que lea el log de git para reconstruir.