feat(F-136): completed feature
This commit is contained in:
108
work/artifacts/F-136/architect.md
Normal file
108
work/artifacts/F-136/architect.md
Normal file
@@ -0,0 +1,108 @@
|
||||
# F-136 — Design: Brand names Title Case + SEO title auto-fill
|
||||
|
||||
**Author:** architect
|
||||
**Date:** 2026-08-21
|
||||
**Stage:** design
|
||||
|
||||
## Context (problem)
|
||||
|
||||
- DB inspection (`SELECT name FROM brands_brands`) shows 28 of 33 brands have ALL-CAPS names imported from OpenCart (e.g. `A.VOGEL`, `BIOCOP`, `COMPLEMENTOS Y NUTRICIÓN`, `EL GRANERO INTEGRAL`).
|
||||
- The same 28 brands have `seo_title IS NULL OR ''` because the legacy seed script never set it.
|
||||
- The admin `brands/page.tsx` form already auto-fills `seo_title` from `name` on create via `handleNameChange`, so **new brands get it right**. The problem is purely with the existing imported data.
|
||||
- Operator's two-part request:
|
||||
1. "poner las marcas en formato 'capital case'" → Title Case the existing names.
|
||||
2. "SEO title igual al nombre de la marca" → for new brands the auto-fill already handles this; for existing brands we need to backfill.
|
||||
|
||||
## Constraints
|
||||
|
||||
- C-1. Pure data fix: no API changes, no UI behavior changes, no new endpoints.
|
||||
- C-2. The migration must be **idempotent** — safe to re-run after partial application (the orchestre may apply it more than once if a feature is reopened).
|
||||
- C-3. Slugs (`a-vogel`, `biocop`) are derived from `slugify()` which lowercases everything; they don't need updating.
|
||||
- C-4. The Title Case function must:
|
||||
- Preserve dots, hyphens, ampersands as word boundaries (`A.VOGEL` → `A.Vogel`, not `A.vogel`).
|
||||
- Handle Spanish accented characters (`NUTRICIÓN` → `Nutrición`).
|
||||
- Handle multi-word names (`EL GRANERO INTEGRAL` → `El Granero Integral`).
|
||||
- Leave already-correct names untouched (idempotency for re-runs).
|
||||
- C-5. We do NOT want to lowercase Spanish articles/prepositions like `y`, `e`, `o`, `de`, `la`, `el` — for brand names, the standard is to capitalize every word (`El Granero Integral`, not `El Granero integral`). Keeping it simple.
|
||||
|
||||
## Design
|
||||
|
||||
### Decision: shared helper + data migration
|
||||
|
||||
Two deliverables:
|
||||
|
||||
1. **`project/src/shared/text.ts`** — new pure helper `toTitleCase(input: string): string` that handles dot/hyphen/space/ampersand word boundaries. Unit-tested in `project/src/shared/tests/text.test.ts`.
|
||||
- Lives in `shared/` because it's pure (no DB / no HTTP) and could be reused by categories/products later if the operator requests it.
|
||||
|
||||
2. **`project/migrations/042_brand_title_case.js`** — node-pg-migrate data migration:
|
||||
- Selects all rows from `brands_brands`.
|
||||
- For each row, computes `titleCased = toTitleCase(name)`.
|
||||
- Updates the row IFF `titleCased !== name` OR `seo_title IS NULL OR seo_title = ''`.
|
||||
- When updating: `name = titleCased`, `seo_title = (existing || titleCased)`, `updated_at = NOW()`.
|
||||
|
||||
### Helper signature
|
||||
|
||||
```ts
|
||||
/**
|
||||
* Convert an ALL-CAPS brand/category name to Title Case.
|
||||
* Splits on word boundaries (space, hyphen, dot, ampersand, slash)
|
||||
* and uppercases the first letter of each word, lowercasing the rest.
|
||||
*
|
||||
* Examples:
|
||||
* toTitleCase('A.VOGEL') // 'A.Vogel'
|
||||
* toTitleCase('BIOCOP') // 'Biocop'
|
||||
* toTitleCase('EL GRANERO INTEGRAL') // 'El Granero Integral'
|
||||
* toTitleCase('COMPLEMENTOS Y NUTRICIÓN') // 'Complementos y Nutrición'
|
||||
* toTitleCase('DULCES LISSEN') // 'Dulces Lissen'
|
||||
* toTitleCase('La Finestra Sul Cielo') // 'La Finestra Sul Cielo' (unchanged)
|
||||
* toTitleCase('') // ''
|
||||
*/
|
||||
export function toTitleCase(input: string): string;
|
||||
```
|
||||
|
||||
### Idempotency
|
||||
|
||||
Re-running the migration is a no-op because:
|
||||
- For ALL-CAPS rows that were already converted, `toTitleCase(name) === name` (case-insensitive split produces the same word capitalizations), so the `UPDATE` never fires.
|
||||
- For rows where `seo_title` was already set, the CASE expression preserves it.
|
||||
|
||||
We add a `whereNeedsUpdate` check so we don't bump `updated_at` unnecessarily.
|
||||
|
||||
### Test cases for the helper (vitest)
|
||||
|
||||
1. Empty string → empty string.
|
||||
2. Single word all caps → title case.
|
||||
3. Multi-word with spaces → each word capitalized.
|
||||
4. Names with dots (`A.VOGEL`) → dots preserved as boundaries.
|
||||
5. Names with hyphens (`DAS-BROT`) → hyphen preserved, both sides capitalized.
|
||||
6. Names with `&` (`TEA & INFUSIONS`) → `&` preserved.
|
||||
7. Spanish accents (`NUTRICIÓN`) → `Nutrición` (correct NFD handling).
|
||||
8. Already Title Case (`La Finestra Sul Cielo`) → unchanged (idempotency).
|
||||
9. Mixed case (`BioSana`) → unchanged.
|
||||
10. Numbers (`500 Kilos`) → `500 Kilos` (digits unaffected).
|
||||
|
||||
### Files affected
|
||||
|
||||
| File | Change |
|
||||
|---|---|
|
||||
| `project/src/shared/text.ts` | NEW — `toTitleCase()` helper |
|
||||
| `project/src/shared/tests/text.test.ts` | NEW — vitest unit tests |
|
||||
| `project/migrations/042_brand_title_case.js` | NEW — data migration |
|
||||
|
||||
### Out of scope
|
||||
|
||||
- Frontend changes. The auto-fill in `brands/page.tsx` already works for new brands.
|
||||
- Categories: the operator asked about brands specifically. F-116 already fixed categories (see `LEGACY_TRANSLATIONS` in `legacy-catalog.ts`). If the operator later asks, the same `toTitleCase` helper can be reused in a future migration.
|
||||
- Slug changes: slugs are already lowercase; they don't depend on name case.
|
||||
|
||||
## Acceptance criteria
|
||||
|
||||
- AC-1. `npm test -- text.test.ts` passes with all 10 test cases.
|
||||
- AC-2. `node-pg-migrate up` applies migration 042 without errors.
|
||||
- AC-3. After migration: `SELECT name FROM brands_brands` shows all 33 names in Title Case (or unchanged if already correct). No ALL-CAPS remain.
|
||||
- AC-4. After migration: `SELECT COUNT(*) FROM brands_brands WHERE seo_title IS NULL OR seo_title = ''` returns 0.
|
||||
- AC-5. Re-running the migration (`node-pg-migrate up` again) is a no-op: 0 rows updated, no errors.
|
||||
- AC-6. Slugs unchanged (still lowercase). Verify with `SELECT slug FROM brands_brands ORDER BY name LIMIT 5`.
|
||||
- AC-7. The frontend admin form still works: creating a new brand with name "TestBrand" sets `seo_title` to "TestBrand" automatically.
|
||||
- AC-8. Backend typecheck + admin typecheck + lint green.
|
||||
- AC-9. `verify.sh` exit 0.
|
||||
Reference in New Issue
Block a user