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

5.9 KiB

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.VOGELA.Vogel, not A.vogel).
    • Handle Spanish accented characters (NUTRICIÓNNutrición).
    • Handle multi-word names (EL GRANERO INTEGRALEl 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

/**
 * 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.