Product name generation
This page describes the catalog name formula, the fields it reads, when the server regenerates the name, and the errors and UI messages around it.
Formula
{presentation} con {content} {unit} de {categoryName} {model} {color} {size} {flavor}
| Rule | Detail |
|---|---|
| Literals | con and de are always emitted. |
| Content and unit | Emitted only when both are present (content not null/undefined and unit non-empty). |
| Empty parts | Skipped. |
| Separator | Parts are joined by a single space. |
| Category | The category name (Tipo de Producto). Category names are stored in lowercase (modules/inventory/src/categories/application/use-cases/Create.ts). |
Examples
| Input | Output |
|---|---|
presentation Caja, content 5, unit Kg, category Arroz, model Premium | Caja con 5 Kg de Arroz Premium |
presentation Botella, content 750, unit ML, category Perfume, model X1, color Rojo, size M, flavor Fresa | Botella con 750 ML de Perfume X1 Rojo M Fresa |
presentation Caja, content 5, unit null, category Arroz, model Premium | Caja con de Arroz Premium |
The full parity table is in modules/inventory/tests/products/domain/services/NameGenerator.test.ts and apps/web/utils/products/__tests__/product-name.spec.ts.
Implementations
| Side | Function | File |
|---|---|---|
| Server (source of truth) | generateProductName | modules/inventory/src/products/domain/services/NameGenerator.ts |
| Client mirror | buildCatalogProductName | apps/web/utils/products/product-name.ts |
The client mirror reads form keys (presentacion, contenido, unidad, tipo, modelo, color, talla, sabor) and treats contenido === '' as absent.
Fields
| Server key | Form key | Required in catalog | Label in errors |
|---|---|---|---|
presentation | presentacion | yes | Presentación |
content | contenido | yes (≥ 1) | Contenido |
unit | unidad | yes | Unidad |
categoryId | tipo | yes | Tipo de producto |
model | modelo | yes | Modelo |
color | color | no | — |
size | talla | no | — |
flavor | sabor | no | — |
Defined in modules/inventory/src/products/domain/services/CatalogNameFields.ts.
When the name is generated
Catalog mode is resolved per request by ProductController._resolveCatalogMode (modules/inventory/src/products/infrastructure/controllers/Product.ts), with partner inheritance for VENDOR organizations.
STOCK products
| Entry point | Catalog OFF | Catalog ON |
|---|---|---|
Web create (POST /api/products → CreateProductUseCase) | Name as sent | Required fields validated; name always generated; client name ignored |
Web bulk create (POST /api/products/bulk) and gateway POST /products (CreateProductsManyUseCase) | Name as sent | Required fields validated on every STOCK row; name always generated |
Web update (PATCH /api/products/:id → UpdateProductUseCase) | Name as sent | Regenerated only when a name field changed; otherwise the stored name is kept and the client name ignored |
Gateway update (PUT /products/:id → updateApi) | Name as sent | Name as sent (catalogMode: false is always passed) |
A product with no type is treated as STOCK on create (isStockType in modules/inventory/src/products/domain/enums/Type.ts). On update, the check is product.type === 'STOCK'.
Name-field change detection (update)
nameFieldsChanged(current, effective) in CatalogNameFields.ts:
| Rule | Detail |
|---|---|
| Compared fields | Server NAME_FIELDS |
| Equivalent values | '', null, undefined |
| Strings | Trimmed |
content, categoryId | Compared as numbers |
Category name resolution (update)
When the name is regenerated, the category name comes from the first non-empty value of:
- The name of the
categoryIdsent in the body. - The stored category name, if the category did not change.
resolveCategoryName(categoryId), an RLS-scoped lookup (findCategoryNames).
KIT products
| Entry point | Catalog ON |
|---|---|
| Create, bulk create | Name generated only when the sent name is empty |
| Web update | Name generated only when the body has no name |
| Required fields | Not validated |
The web edit page always sends the name for KIT products.
Errors
MissingCatalogFieldsError
File: modules/inventory/src/products/domain/errors/MissingCatalogFields.ts. HTTP status 422. Response body: { "error": "<message>" }.
| Case | Message format |
|---|---|
| Single product | Campos obligatorios con catálogo activo: <labels> |
| Bulk | Campos obligatorios con catálogo activo: SKU <sku>: <labels>; Fila <n>: <labels> |
Labels follow declaration order and are joined by , . In bulk messages a row is identified by SKU <sku> when it has a SKU, otherwise by Fila <n> (1-based).
Example: Campos obligatorios con catálogo activo: Unidad, Modelo.
UI
Create page (apps/web/pages/[client]/products/new.vue)
| Element | Behavior |
|---|---|
| Name input | Replaced by a read-only preview (catalog ON + STOCK) |
| Helper text | Generado automáticamente según los datos del producto |
| Submit without a generated name | Error Los campos de catálogo son requeridos para generar el nombre |
Edit page (apps/web/pages/[client]/products/[id].vue)
| Element | Behavior |
|---|---|
| Name input | Read-only; shows the stored name until the save succeeds and the product is refetched |
| Confirmation trigger | Catalog ON + STOCK + org:products:edit + at least one name field changed |
| Confirmation dialog | Title ¿Estás seguro?, confirm button Actualizar |
Dialog message:
Esta acción actualizará el nombre del producto.
Nombre actual: <stored name>
Nuevo nombre: <buildCatalogProductName(form)>
Related
- Field rules: Product field access
- Why the server owns the name: Catalog mode and permissions
- Why category lookups are RLS-scoped: RLS and raw queries